Astro SEO: fast by default, and the layout work that remains
You picked Astro for speed and control. Good choice. Here is what Astro gives you for SEO, what it does not, and the layout pattern that closes the gaps.
By Théophile Louvart, founder of Porteur · Updated 14 September 2026 · Markdown
What Astro gives you for SEO
Astro renders static HTML by default. Crawlers see your content without running JavaScript. That helps indexing.
It ships zero JavaScript by default. You opt in per island. That keeps pages light and fast when you avoid heavy client code.
It has built-in image optimisation. You can output responsive images with width, quality and format set at build time. That helps LCP and CLS.
It has content collections. You can type front matter and enforce fields across posts or docs. That keeps metadata consistent.
There is an official sitemap integration: @astrojs/sitemap. You add it to your config. It builds your XML sitemap at build time.
What Astro does not ship
Astro has no built-in SEO layer. There is no automatic title or description system. You write them in your layout head.
There is no automatic canonical tag. You add a canonical link in your layout. It should point to the preferred URL for each page.
There is no sitemap unless you add @astrojs/sitemap. Install it and set your site URL. Without it, Search Console has to find pages by crawl alone.
There is no default robots.txt. You place a robots.txt file in public if you need one. Check the platform's current settings if in doubt.
The base layout for titles and descriptions
Put your SEO logic in one layout. Every page uses it. You pass a title and a description in front matter or props.
---
// src/layouts/BaseLayout.astro
export interface Props {
title?: string
description?: string
canonicalPath?: string // e.g. '/pricing'
noindex?: boolean
ogImage?: string
}
const {
title: pageTitle,
description: pageDescription,
canonicalPath,
noindex = false,
ogImage
} = Astro.props as Props
const site = Astro.site?.toString().replace(/\/$/, '') || '' // from astro.config.mjs
// Compose the full title: 'Page Title | Your Product'
// Keep titles around 60 characters
const siteName = 'Your Product'
const title = pageTitle ? `${pageTitle} | ${siteName}` : siteName
// Meta description around 120 to 155 characters
const description = pageDescription || 'Short description of your product or page.'
const canonical = canonicalPath ? `${site}${canonicalPath}` : `${site}${Astro.url.pathname}`
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
{/* Open Graph and Twitter for social cards */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
{ogImage && <meta property="og:image" content={ogImage} />}
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<slot />
</body>
</html>
Set Astro.site in astro.config.mjs. The sitemap integration needs it. Your canonical tag uses it too.
// astro.config.mjs
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'
export default defineConfig({
site: 'https://yourproduct.com',
integrations: [sitemap()],
})
Use the layout in pages. You pass page-specific fields. Keep the pattern the same across the site.
---
// src/pages/pricing.astro
import BaseLayout from '../layouts/BaseLayout.astro'
---
<BaseLayout
title="Pricing"
description="Pick a plan. Start in minutes."
canonicalPath="/pricing"
ogImage="https://yourproduct.com/og/pricing.png"
>
<main>
<h1>Pricing</h1>
<!-- Content -->
</main>
</BaseLayout>
A repeatable pattern for pages and posts
You want the same fields for every page type. Titles, descriptions, canonical, publish date, modified date, author for posts. One place to render them.
---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro'
export interface Frontmatter {
title: string
description: string
pubDate: string // ISO
updatedDate?: string // ISO
author?: string
tags?: string[]
ogImage?: string
}
const { frontmatter } = Astro.props as { frontmatter: Frontmatter }
---
<BaseLayout
title={frontmatter.title}
description={frontmatter.description}
canonicalPath={Astro.url.pathname}
ogImage={frontmatter.ogImage}
>
<article>
<header>
<h1>{frontmatter.title}</h1>
<p>
<time dateTime={frontmatter.pubDate}>{new Date(frontmatter.pubDate).toLocaleDateString()}</time>
{frontmatter.updatedDate && (
<span> · Updated <time dateTime={frontmatter.updatedDate}>{new Date(frontmatter.updatedDate).toLocaleDateString()}</time></span>
)}
</p>
</header>
<slot />
</article>
</BaseLayout>
A fixed page like /about can use BaseLayout direct. A blog post uses PostLayout. The head stays consistent, the content differs by slot content.
Content collections keep metadata typed
Use content collections for posts, guides or changelogs. You define a schema. Astro validates front matter at build time. Bad entries fail the build, which is good for SEO hygiene.
// src/content/config.ts
import { defineCollection, z } from 'astro:content'
const posts = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().max(155),
pubDate: z.string(),
updatedDate: z.string().optional(),
author: z.string().default('Team'),
tags: z.array(z.string()).default([]),
ogImage: z.string().url().optional(),
}),
})
export const collections = { posts }
---
// src/content/posts/getting-started.md
// Front matter must match the schema
Title: Getting started with Your Product
Description: A simple guide to set up Your Product and ship your first task.
PubDate: 2026-07-12
UpdatedDate: 2026-09-13
Tags: ["setup", "guide"]
OgImage: https://yourproduct.com/og/guides/getting-started.png
---
## First steps
Content here.
Render posts with a route like /guides/[slug].astro. Pull entries from collections and pass their front matter to PostLayout. Every post gets a valid title and description by design.
Sitemap and canonical URLs
Add @astrojs/sitemap. Set site in the config. The integration crawls your routes at build time and writes sitemap.xml. Submit that in Search Console.
Install the integration
npm i @astrojs/sitemap
Enable it in astro.config.mjs
import sitemap from '@astrojs/sitemap' and add sitemap() to integrations. Set site: 'https://yourproduct.com'.
Exclude routes you do not want indexed
Pass options like exclude: ['/drafts/**'] if needed.
Build and check the output
Run the build. Open dist/sitemap.xml. Check URLs, lastmod and priority if you set them.
Submit in Search Console
In Search Console, open Sitemaps, add https://yourproduct.com/sitemap.xml, and watch the status.
Set one canonical per page. Point to the preferred URL, with or without trailing slash, and the right casing. Avoid query strings in canonicals unless the parameter defines content.
// In BaseLayout.astro, already shown
<link rel="canonical" href={canonical} />
Example: your /guides/getting-started page must canonicalise to https://yourproduct.com/guides/getting-started. Avoid duplicates at /guides/getting-started/ or with ?ref parameters. Redirect stray variants at the host level too.
Robots and indexing controls
Serve a robots.txt from public/robots.txt. Disallow drafts, private routes or staging hosts. Keep the production site crawlable. Add a reference to your sitemap URL.
# public/robots.txt
User-agent: *
Disallow: /drafts/
Allow: /
Sitemap: https://yourproduct.com/sitemap.xml
For specific pages, add a robots meta. Use noindex on thin or temporary pages. Example: /changelog/2021-migration-test should not index. Pass noindex to the layout on that page only.
Images and speed on Astro
Use Astro’s image component for hero images. Optimise width, format and quality. Aim for Largest Contentful Paint under 2.5 seconds as of 2026.
---
import { Image } from 'astro:assets'
import hero from '../assets/hero.png'
---
<Image src={hero} alt="Dashboard screenshot" widths={[512, 768, 1024, 1280]} sizes="(max-width: 768px) 100vw, 1280px" format="webp" quality={70} loading="eager" />
Prevent layout shift. Set explicit width and height where you can. Keep Cumulative Layout Shift under 0.1. Avoid CSS that moves content after load.
Keep client JavaScript small. Hydrate only what you need. Target Interaction to Next Paint under 200 ms. Heavy islands and third-party scripts will slow it down.
Test with PageSpeed Insights. Field data shows Core Web Vitals from the last 28 days when there is traffic. Lighthouse in the same tool is one lab run and can vary.
Structured data: JSON-LD in your layouts
Add JSON-LD in the head for rich results. Start with Organization on the home page, then Article for posts. Keep it in the layout so it stays consistent.
---
// In PostLayout.astro head section, after meta tags
const articleLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: frontmatter.title,
description: frontmatter.description,
datePublished: frontmatter.pubDate,
dateModified: frontmatter.updatedDate || frontmatter.pubDate,
author: frontmatter.author ? { '@type': 'Person', name: frontmatter.author } : undefined,
mainEntityOfPage: canonical,
}
---
<script type="application/ld+json">{JSON.stringify(articleLd)}</script>
Validate with the Rich Results Test. Fix missing required fields. Example: an Article needs a headline and a datePublished to qualify for some features as of 2026. If you are not sure, keep the schema small and accurate.
The Astro SEO checklist
- Set site in astro.config.mjs to your HTTPS root
- Install and configure @astrojs/sitemap, then submit sitemap.xml in Search Console
- Create a BaseLayout that sets title, meta description, canonical and social cards
- Pass canonicalPath or compute it from Astro.url.pathname in layouts
- Add robots.txt in public with a link to the sitemap and no Disallow for live routes
- Add noindex on drafts and thin pages only
- Use content collections with a schema that caps description at 155 characters
- Render JSON-LD for Organization on / and Article on posts
- Use astro:assets to optimise hero and product images, set sizes and formats
- Avoid client-only rendering of primary content
- Hydrate only the components that need interactivity
- Keep LCP under 2.5 s, CLS under 0.1 and INP under 200 ms as of 2026
- Add internal links to key pages from the home page and from related content
- Test titles are within about 60 characters and read well on mobile
- Use trailing slashes or not, but be consistent, redirect the other form and set canonicals
- Check PageSpeed Insights for field data, then fix slow URLs first
A fixed page like /pricing should load fast, with a clear H1, a concise title, a description around 120 to 155 characters and a canonical to the clean URL. A blog post should show dates, an author, next steps and valid Article JSON-LD. Both should sit in your sitemap and be linked from somewhere useful.
Questions
Yes. Astro renders static HTML and ships no JavaScript by default. Crawlers can read your content fast. You still need to add metadata, canonicals, a sitemap and structured data in your layouts.
There is no all-in-one SEO plugin. Use @astrojs/sitemap for the sitemap. Put titles, descriptions, canonical and robots meta in your base layout. Add JSON-LD by hand. This gives you full control.
Install @astrojs/sitemap, set site in astro.config.mjs and build. The integration writes sitemap.xml. Submit it in Search Console. You can exclude routes you do not want listed.
In your layout’s head. Pass title and description from pages or content collections. Keep the pattern in one BaseLayout so you do not repeat yourself across pages.
Compute them from Astro.site and the page path. Set a link rel="canonical" in your layout. Keep the format consistent across the site and redirect stray variants on the server.
Large images, heavy client islands and third-party scripts. Optimise images with astro:assets. Hydrate only what you need. Watch Core Web Vitals. Test and trim any marketing scripts that block input.
Sources
Check my site, free
Paste your home page URL to see where your Astro site stands: the free check reads your site, the searches around it and rivals on them in about thirty seconds and shows three findings whole.
- Free check, no card
- Read-only, your own accounts
- Readable by your agent
Read next
- GuideOn-page SEO checklist: the twelve things on the page
- GuideTechnical SEO checklist for a small site
- GuideThe Sitemaps report: what Success, Has errors and Couldn’t fetch mean
- GuideCanonical tags: what they do and the mistakes that cost rankings
- GuideCore Web Vitals for a product site: the three numbers
- GuideImage optimisation for page speed: the hero image and everything below it
- GuideDocusaurus SEO: docs that rank and versions that do not fight
- GuideHugo SEO: what the templates must do, since nothing is automatic