Astro has a reputation for being SEO-friendly, and that reputation is earned. Most framework SEO problems stem from the same root cause: pages that ship empty HTML and rely on JavaScript to fill in content. Astro goes the other direction. Its default output is fully rendered static HTML, and JavaScript only enters the picture where you explicitly ask for it. That changes a lot for search engines and AI crawlers alike.
This guide covers everything that matters for Astro SEO: why the architecture works in your favour, how to handle metadata correctly, the official sitemap integration, content collections, JSON-LD, image optimisation, RSS, and the one gotcha that catches people off guard.
Why Astro is SEO-friendly by default
Astro outputs static HTML at build time with zero JavaScript shipped to the browser unless you opt in. When a crawler fetches an Astro page, it gets the full rendered content in the first HTTP response, with no JavaScript execution required.
This is the opposite of how most React or Angular apps behave. A typical single-page application sends a near-empty HTML shell and renders content client-side. Crawlers that don’t execute JavaScript (including many AI crawlers) see almost nothing. Astro avoids this by design. The platform-level SEO implications of JavaScript rendering disappear when your content is already in the HTML.
The result: reliable indexing, fast time-to-first-byte, and content that’s available to any crawler that can make an HTTP request.
The islands architecture
Astro’s islands architecture is the mechanism that keeps pages SEO-safe even when you add interactivity. The model works like this: the page is static HTML by default, and interactive components are individual “islands” that hydrate selectively.
You control hydration per component using a client:* directive:
client:loadhydrates immediately on page loadclient:idlehydrates when the browser is idleclient:visiblehydrates when the component scrolls into viewclient:onlyrenders exclusively in the browser, skipping server rendering entirely
The last one is the SEO gotcha. A component marked client:only produces no HTML on the server. If you put primary page content, headings, or important text inside a client:only component, crawlers won’t see it. Use client:only for genuinely interactive UI (autocomplete, live charts, video players) and keep content in standard Astro components or Markdown.
Everything outside of client:only islands is rendered to HTML at build time or on the server, which means it’s available in the initial response.
Setting title, description, and canonical in a layout
Astro doesn’t have a metadata API the way Next.js does. The standard pattern is a shared layout component that accepts title, description, and canonical as props from each page’s frontmatter.
---
// src/layouts/BaseLayout.astro
const { title, description, canonical } = Astro.props;
---
<!DOCTYPE html>
<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} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:type" content="website" />
</head>
<body>
<slot />
</body>
</html>
Each page then passes its values through frontmatter:
---
// src/pages/some-page.astro
import BaseLayout from '../layouts/BaseLayout.astro';
const title = "Your Page Title";
const description = "150-160 character description for search results.";
const canonical = "https://example.com/some-page/";
---
<BaseLayout title={title} description={description} canonical={canonical}>
<!-- page content -->
</BaseLayout>
Every page gets a unique, accurate title and description, and the canonical tag prevents duplicate content issues. This pattern scales cleanly across hundreds of pages. Fokal.com itself runs on Astro and uses this layout approach for its content pages.
The @astrojs/sitemap integration
Astro has an official sitemap integration that generates your sitemap automatically at build time.
Install and configure it in astro.config.mjs:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [
sitemap(),
],
});
With site set, the integration crawls your built pages and outputs a sitemap-index.xml at the root. For large sites it also shards into numbered sitemaps automatically. You can exclude pages using the filter option, and customise changefreq and priority values if you need them.
Submit the sitemap URL to Google Search Console once deployed. After that, it updates on every build.
Content collections for structured content
Content collections are Astro’s built-in system for managing Markdown, MDX, and JSON content with type safety. They’re worth understanding from an SEO perspective because they enforce frontmatter schemas at build time, which means you can’t accidentally deploy a blog post without a title or description.
Define a collection in src/content/config.ts:
import { defineCollection, z } from 'astro:content';
export const collections = {
blog: defineCollection({
schema: z.object({
title: z.string(),
description: z.string().max(160),
publishedDate: z.string(),
image: z.string().url().optional(),
}),
}),
};
Astro throws a build error if any post in the collection is missing a required field. For a content-heavy site, this is one of the most practical SEO safeguards available, because metadata gaps that would normally only surface in a crawl audit get caught before deployment.
Query your collection in pages using getCollection('blog') and build dynamic routes with getStaticPaths.
Adding JSON-LD structured data
Astro has no built-in schema injection, but adding JSON-LD is straightforward. Drop a <script type="application/ld+json"> block in your layout or directly in the <head> of specific page types.
For a blog post layout:
---
const { title, description, publishedDate, canonical, authorName } = Astro.props;
const schema = {
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": description,
"datePublished": publishedDate,
"url": canonical,
"author": {
"@type": "Person",
"name": authorName
}
};
---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
The set:html directive tells Astro to render the string as raw HTML rather than escaping it. For product pages, use @type: "Product". For FAQ sections, use @type: "FAQPage". The schema markup guide covers which types matter most and how they affect search features.
RSS
Astro has a built-in RSS helper at @astrojs/rss. Add it to a dedicated endpoint file:
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'Recent posts',
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
pubDate: new Date(post.data.publishedDate),
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
});
}
RSS feeds help news aggregators and some AI tools discover and attribute your content. They’re low effort to set up in Astro and worth including on any blog or documentation site.
Image optimisation
Astro’s built-in <Image> component from astro:assets handles format conversion and responsive image generation at build time. It outputs modern formats (WebP, AVIF) and generates srcset attributes automatically.
---
import { Image } from 'astro:assets';
import heroImg from '../assets/hero.jpg';
---
<Image src={heroImg} alt="Descriptive alt text" width={1200} height={630} />
Core Web Vitals, particularly Largest Contentful Paint, are directly affected by how images load. Astro’s image handling removes most of the manual work: you get lazy loading by default, correct dimensions to prevent layout shift, and compressed output without a separate CDN or transform service.
Always write descriptive alt attributes. Alt text is how crawlers understand image content, and it’s one of the signals AI systems use when processing pages for potential citations.
Astro SEO and AI citations
AI search tools (ChatGPT, Perplexity, Google AI Overviews) increasingly draw on indexed web content to answer queries. The same property that makes Astro good for Google, clean static HTML with structured content, makes it good for AI citation too.
AI crawlers often skip JavaScript execution entirely. A page that depends on client-side rendering for its main content is invisible to these systems regardless of how well it ranks in traditional search. Astro pages don’t have this problem.
A few practices that specifically help with AI citation:
- Use clear H2/H3 headings that answer specific questions directly. AI systems parse heading structure to understand what a passage is about.
- Add FAQ schema for pages that answer common questions. Google AI Overviews and similar features actively use structured data to surface answer blocks.
- Keep paragraphs focused. A dense wall of text is harder to extract a clean citation from than a well-structured passage with a clear point per paragraph.
- Publish an RSS feed and link to it from your site header. Some AI discovery pipelines follow RSS.
The AI search visibility guide at /ai-seo/ covers the broader picture of how to get cited in AI answers, not just indexed by Google.
Comparing Astro to other frameworks for SEO
| Framework | Default output | JS on initial load | SEO effort |
|---|---|---|---|
| Astro | Static HTML | Zero (unless opted in) | Low |
| Next.js (SSG/SSR) | Rendered HTML | React runtime | Low to medium |
| React (CRA/Vite SPA) | Near-empty shell | Full bundle | High |
| Angular (CSR default) | Near-empty shell | Full bundle | High |
Next.js and React can both achieve good SEO outcomes, but they require deliberate choices (SSR, SSG, Metadata API) that developers sometimes skip. Astro makes the SEO-safe path the default, so you have to explicitly opt out to break indexability.
What to do next
If you’re starting a new content site or marketing site and SEO matters, Astro is one of the strongest choices available. The defaults work in your favour without configuration. For an existing Astro site, run through the checklist below to confirm you haven’t left gaps.
The platform SEO overview covers how different frameworks compare across the full spectrum of SEO requirements, which is useful context if you’re evaluating Astro against alternatives.
Astro SEO checklist
- Each page passes a unique
title,description, andcanonicalto the layout -
siteis set inastro.config.mjsand@astrojs/sitemapis installed - Sitemap is submitted to Google Search Console
- Content collections have a Zod schema that enforces
titleanddescription - No primary content is inside
client:onlycomponents - JSON-LD added to article, product, or FAQ page layouts as appropriate
-
<Image>component used for all significant images, with descriptive alt text - RSS feed generated and linked from the site header
- Open Graph tags in the layout
<head>for social sharing