Astro SEO
Astro is one of the strongest frameworks for SEO — pages prerender to static HTML by default and ship JS only where you ask for it, with strong Core Web Vitals. But defaults aren't guarantees, and it doesn't write your meta tags, sitemap, or canonicals for you. Here's how I run my own Astro site, and how to get the architecture right.
Astro is one of the strongest framework choices for SEO because it prerenders to static HTML by default — your content is in the raw HTML on first crawl, with no rendering queue to wait on for that route. Islands hydrate only the components you mark with a client directive, so most of the page ships without its own hydration JS (Astro can still add page scripts and router JS elsewhere) — none of this guarantees crawlability, indexing, or rankings on its own, so verify the deployed routes. Astro sites also post strong Core Web Vitals numbers. The catch: Astro generates clean HTML but doesn't write your meta tags, canonicals, sitemap, or structured data — those are deliberate build steps, and sitemap discovery targets statically generated routes, so runtime-only URLs need explicit handling. Server Islands (which require an adapter) and View Transitions are SEO-safe when you understand what crawlers actually fetch. I run patrickstox.com on Astro, so this is the stack I actually live in.
TL;DR — Astro is a great choice for SEO. By default it builds your pages into plain HTML files ahead of time, so when Google (or any bot) shows up, your content is already there — no waiting for JavaScript to run. It’s also very fast. The one thing to know: Astro gives you clean HTML, but it does not automatically add your page titles, sitemap, or canonical tags. You set those up yourself (it’s easy).
What “Astro SEO” means
Astro is a web framework for building websites. Its big idea is “less JavaScript.” Where frameworks like React or Next.js often build the page in your browser using JavaScript, Astro builds your pages into plain HTML files at build time and ships almost no JavaScript unless a part of the page actually needs it. Evidence for this claim Astro prerenders pages as static HTML by default and only sends client JavaScript for explicitly hydrated components. Scope: Astro default static output and islands architecture. Confidence: high · Verified: Astro: Why Astro
That single difference is why Astro is so friendly to search engines. When a search engine crawls a page, it wants to read your content. With a JavaScript-heavy site, the content sometimes isn’t in the page yet — the bot has to run the JavaScript first, and that can be delayed. With Astro, the content is already in the HTML the moment the page loads. There’s nothing to wait for.
I run my own site, patrickstox.com, on Astro — so this isn’t theory for me. It’s the stack I actually build on.
Why Astro is good for SEO
- Content is in the HTML right away. No rendering delay, no missing content.
- It’s fast. Astro sites are lightweight and tend to score very well on Google’s page-experience metrics (Core Web Vitals).
- Each page is its own real URL. No fancy single-page-app routing that can confuse crawlers.
- JavaScript only loads where it’s needed. A photo gallery or a search box can be interactive without slowing down the rest of the page.
What Astro does NOT do for you
This trips people up. “Astro is SEO-optimized” is only half true. Astro gives you a clean foundation, but it does not automatically:
- Write your page titles and meta descriptions.
- Generate a sitemap (you add a free official plugin for that). Evidence for this claim Astro's official sitemap integration generates sitemap files from statically generated routes. Scope: Astro @astrojs/sitemap integration. Confidence: high · Verified: Astro: Sitemap integration
- Add canonical tags (which tell Google the “official” version of a page).
- Add structured data (the code that powers rich results).
- Create a robots.txt file.
None of this is hard — it’s just on you to set it up. Think of Astro as a great kitchen: the appliances are excellent, but you still have to cook.
The simple starter checklist
- Add a sitemap with the official
@astrojs/sitemapplugin. - Set a
title,description, andcanonicalURL on every page (usually from one shared layout file). - Use Astro’s built-in
<Image />component for photos — it makes them load fast and prevents the page from jumping around. - Put a
robots.txtfile in yourpublic/folder.
Want the deep version — Server Islands, View Transitions, static vs server rendering, and the exact mistakes to avoid? Switch to the Advanced tab. For the bigger picture of how search engines handle JavaScript, see JavaScript SEO.
Evidence for this claim Astro prerenders pages as static HTML by default and only sends client JavaScript for explicitly hydrated components. Scope: Astro default static output and islands architecture. Confidence: high · Verified: Astro: Why AstroTL;DR — Astro is architecturally well set up for SEO: pages and endpoints are prerendered to static HTML by default, so there’s no render-queue wave to wait on for that content — but that’s a default, not a universal guarantee, and static/server HTML doesn’t by itself prove crawlability, indexing, rankings, or Core Web Vitals for your production URLs. Islands architecture hydrates only the components you mark with a
client:*directive — everything else ships HTML without its own hydration JS, though page scripts, other islands, and router enhancements can still add JavaScript elsewhere on the page. Astro doesn’t auto-generate meta tags, canonicals, sitemaps, or structured data — wire those up explicitly, ideally validated by Content Collections + Zod. Server Islands (which require an adapter) serve the static shell with fallback content in the initial document and fetch the deferred content independently afterward — verify what a given crawler actually retrieves rather than assuming. View Transitions usehistory.pushStateand are SEO-safe; Google crawls the underlying MPA pages normally. I run patrickstox.com on Astro, and the features below are the ones I actually use — validated against the deployed site, not just local dev.
Why Astro sidesteps the JavaScript rendering problem entirely
The whole reason JavaScript SEO is hard is the second wave. Google fetches your raw HTML first, then queues the page for rendering in a headless Chromium later — and that queue is the risk. Google’s own docs describe it: “Googlebot queues all pages with a 200 HTTP status code for rendering unless a robots meta tag tells Google not to index the page. The page may stay on this queue for a few seconds, but it can take longer than that.” For a client-rendered SPA, your content doesn’t exist until that render wave runs.
Astro’s default output mode is static: pages and endpoints are prerendered to a complete HTML file at build time. So for a route running in that default, the raw HTML is the rendered page. Evidence for this claim Astro uses static output and prerenders routes at build time by default. Scope: Astro default output mode; routes can opt out of prerendering. Confidence: high · Verified: Astro: On-demand rendering There’s no second wave to wait on for that route, because there’s nothing left to execute. Googlebot sees the full content on the first fetch. As Joost de Valk (Yoast’s founder) puts it, “From an SEO perspective, static HTML on a CDN is a better starting point than most CMSes will ever give you.”
That’s the default, though — not a universal property of every route. Set
output: 'server' and the default flips to on-demand rendering (more on that below);
even in a static-default project, an adapter lets an individual route opt out with
export const prerender = false. None of this is guaranteed by architecture alone:
static or server HTML, islands, and adapters don’t by themselves guarantee
crawlability, indexing, rankings, or Core Web Vitals — those depend on the deployed
route and the specific crawler, so verify them rather than assuming the framework
handles it.
This also helps with the crawlers that can’t render at all. Google notes plainly that “not all bots can run JavaScript” — and that’s the 2026 reality for most AI crawlers and many third-party tools. Astro’s HTML-first output is readable by every one of them where it’s actually prerendered, not just Googlebot. (This is the same point I make in SEO for a headless CMS: the rendering mode is the product.)
Islands architecture: JS only where you ask for it
Astro renders your components to HTML and, in its words, ships “just HTML & CSS,
stripping out all client-side JavaScript automatically.” Interactivity is opt-in. You
mark a component with a client:* directive — client:load, client:idle, or
client:visible — and only that island hydrates with JavaScript. Everything else
stays static HTML. Evidence for this claim Astro client directives selectively hydrate interactive islands while other components remain static HTML. Scope: Astro islands and client directives. Confidence: high · Verified: Astro: Islands
For SEO this is close to ideal. The content Googlebot needs to index is plain HTML, and
your interactive widgets don’t drag it down. client:visible is especially useful: a
below-the-fold component doesn’t even begin hydrating until it scrolls into view, so it
never blocks your LCP. The concept comes from Jason Miller (creator of Preact), who
described “rendering HTML pages on the server, and inject[ing] placeholders or slots
around highly dynamic regions” for selective hydration.
Two nuances worth being precise about, because competitor coverage tends to blur them:
client:onlyis a different animal. Unlikeclient:load/client:idle/client:visible, aclient:onlycomponent skips server rendering entirely — it produces no HTML on the server at all. Anything indexable placed only inside aclient:onlycomponent isn’t in the document Googlebot fetches; it only exists after the browser hydrates it. Don’t put primary content there.- This is selective hydration, not resumability. Astro re-runs each island’s client code from scratch in the browser; it doesn’t resume execution state that was serialized on the server the way a resumability model (Qwik’s, for example) does. The two get conflated in writeups — they’re not the same mechanism.
And a non-hydrated component isn’t the whole JavaScript picture on a page. “Zero JS” describes
one component without a client:* directive — Astro can still ship page-level <script>
tags, the View Transitions router, and other islands elsewhere on the same page. Describe what
ships per component/route, not as a blanket claim about the page.
What Astro does NOT do automatically
Astro generates clean semantic HTML — and nothing else, SEO-wise. There is no metadata, no canonical, no sitemap, no structured data out of the box. The myth that “Astro is automatically SEO-optimized” is exactly that. You own:
- Meta tags (title, description, Open Graph, Twitter)
- Canonical URLs
- The sitemap (via the official integration)
- Structured data / JSON-LD
- robots.txt
Astro is the best foundation I’ve used, but it’s a foundation, not a finished house.
The sitemap: @astrojs/sitemap
Install it with npx astro add sitemap. It crawls your statically-generated routes and
emits a sitemap-index.xml plus chunked sitemap-0.xml files at build time. Two things
bite people:
- You must set
site:inastro.config.mjs. Without it, the integration silently generates nothing. This is the single most common “where’s my sitemap?” cause. - You must add the sitemap line to
robots.txtyourself. Astro doesn’t do it.
For control, filter() excludes routes (preview/draft pages — exactly what I use it for
on this site), serialize() lets you set lastmod/changefreq/priority, and the
i18n option emits hreflang entries in the sitemap. This is what the current
@astrojs/sitemap docs specify — confirm against your installed version if you’re on an
older release, integration behavior has changed across major versions before.
The scope that trips people up: the integration’s discovery targets statically
generated routes. If any of your URLs only exist at runtime — server-rendered (output: 'server') routes, or routes generated on demand rather than at build time — don’t
assume they’re in the sitemap. Add them explicitly with customPages, then actually
open sitemap-index.xml after a build to confirm they’re there. Don’t take “the
integration handles it” on faith for anything that isn’t a build-time static route.
Meta tags and canonicals: the BaseLayout pattern
Astro has no special <Head> component — you control <head> directly in .astro
files. The standard pattern (and what I do) is a single BaseLayout.astro that takes
title, description, and canonicalURL as props and writes the head. Set the canonical
explicitly on every page and keep it consistent with og:url. You don’t need a library,
but the community astro-seo package (npm) is a convenient one-component wrapper for
title/description/OG/Twitter/canonical if you want it.
Content Collections as an SEO safety net
This is Astro’s underrated SEO feature. Content Collections are a type-safe content
layer for your Markdown/MDX/JSON, with Zod schema validation. That means you can make
title and description required fields — and if a page is missing one, the build
fails. You can’t accidentally ship a page with no title. Query functions
(getCollection(), getEntry()) generate static pages at build time, so the output is
plain HTML when deployed. And because MDX keeps raw markdown as the source of truth, those
files are also clean source content for AI crawlers and llms.txt-style patterns. This very
site is built on Content Collections with Zod-validated frontmatter.
astro:assets: images done right (with one trap)
The <Image /> component automatically converts to WebP, infers dimensions to “avoid
Cumulative Layout Shift (CLS),” sets loading="lazy" by default, and requires alt
— a missing alt is a compile error. <Picture /> extends this with AVIF/WebP/fallback
<source> elements.
The trap: the automatic loading="lazy" is wrong for your LCP image (usually the
hero). Lazy-loading your most important image delays it. For above-the-fold images,
override with loading="eager" and fetchpriority="high". Remote images need explicit
width and height.
Server Islands: what crawlers actually see
Server Islands (Astro 4.12+) are the feature most competitor guides get wrong. With
server:defer, a component renders on the server independently of the main page. The
static shell is served immediately; per Astro’s docs, “Your page will be rendered
immediately with any specified fallback content as a placeholder. Then, the component’s
own contents are fetched on the client and displayed when available.”
Two things to be exact about. First, Server Islands require an adapter — they’re an on-demand feature, not something a purely static build produces on its own. Second, the sequence is: the initial document ships with whatever you configured as fallback content, and the island’s real content is a separate, independent request fetched after the page loads, over its own endpoint. That’s a hard floor on what the first document contains — I wouldn’t generalize past that about what every specific crawler does next without testing your own deployed URLs directly.
The SEO consequence is concrete: the static HTML a crawler reads on that first fetch contains your fallback content, not the deferred island content. That’s perfect for what Server Islands are for — personalized, session-specific stuff (login state, cart count, recommendations) that shouldn’t be cached or indexed anyway. It is wrong for primary indexable content. Put anything that needs to rank in the main Astro template, and let Server Islands handle the dynamic bits around it.
Output modes: static vs server, and the per-route override
Astro’s default output mode is static — pages and endpoints prerender to HTML at build
time. Set output: 'server' in astro.config.mjs and the default flips to on-demand
rendering: pages render per request (with an adapter), useful for auth, real-time data, or
personalization beyond what Server Islands cover. Either way, you can override the default
per route: in a static-default project, export const prerender = false opts a route into
on-demand rendering; in a server-default project, export const prerender = true opts a
route back into build-time prerendering. So “my site is static” or “my site is SSR” is
rarely true for every route — check the per-route setting, not just the top-level config.
From a pure SEO standpoint, prerendered and on-demand HTML are equivalent — both deliver complete HTML to a crawler’s first request, once you’ve actually confirmed the route returns a 200 with full markup. On-demand routes can stream their HTML, and slow data or network conditions can delay later chunks, so a streamed response isn’t automatic proof that every piece of content arrived — check it, don’t assume it. The real difference between the two modes is operational: prerendered content is fixed until the next build (or a separately configured runtime refresh) and serves straight from the CDN edge; on-demand content is always current but depends on your adapter and runtime being healthy in production. Don’t treat this as an SEO decision — choose on data freshness and ops, then validate the deployed routes, statuses, redirects, and response headers rather than extrapolating from what worked in local dev.
View Transitions: SEO-safe, despite the SPA feel
Astro’s <ClientRouter /> (formerly <ViewTransitions />) gives SPA-like soft navigation
using the browser’s View Transitions API and the History API. The key fact: it navigates
with history.pushState, which is exactly what Google recommends for client-side
navigation — Google warns that fragment-based (#hash) URLs are something it “can’t
reliably resolve.” Crucially, View Transitions are a browser-side enhancement. When
Googlebot crawls, it requests each URL and gets a normal, complete HTML page — the MPA
underneath is untouched. The transitions only affect what a human sees in the browser.
So no, View Transitions do not turn your Astro site into an SPA, and they don’t break SEO. A worth-noting gap: Astro’s View Transitions docs have no SEO section, which is probably why the “it breaks SEO” myth persists. To verify on your own site, fetch a few URLs directly and confirm each returns full HTML — don’t take it on faith, check your own deployment.
Common Astro SEO mistakes
- Assuming Astro handles SEO for you. It handles HTML. Meta, canonicals, sitemap, schema are yours.
- Forgetting
site:in the config — your sitemap silently doesn’t generate. - Not adding the sitemap to robots.txt — Astro won’t do it.
- Lazy-loading the LCP image — override the hero with
eager+fetchpriority. - Putting indexable content in a Server Island — crawlers see the fallback, not the content, and Server Islands require an adapter in the first place.
- Chasing a perfect Lighthouse score and stopping there. Speed is a ranking signal, not the ranking signal. A fast empty page doesn’t rank — content, links, and E-E-A-T still do the heavy lifting.
- Putting indexable content only inside a
client:onlycomponent. Unlike the otherclient:*directives,client:onlyskips server rendering entirely — there’s no HTML for that component until the browser hydrates it. - Treating “Astro is static/fast” as an outcome guarantee. Static or on-demand HTML, islands, and adapters are mechanisms — they don’t by themselves guarantee crawlability, indexing, rankings, or Core Web Vitals. Validate the deployed route, not the architecture diagram.
Where this fits in the cluster
Astro is one specific, unusually SEO-friendly answer to the questions JavaScript SEO raises about rendering, and a popular frontend for headless CMS setups. The performance side connects directly to Core Web Vitals in the web-performance cluster, and the “is my content actually in the HTML?” testing discipline is the same one from the crawling and indexing clusters.
Astro SEO anti-patterns
Concrete mistakes I actually see on Astro sites — not hypothetical ones. Each one is prevention, not diagnosis: catch it before it ships.
Treating Astro as “SEO-optimized” out of the box
Astro gives you fast, clean static HTML, and that’s genuinely a big head start — but it
is not the same as meta tags, canonicals, a sitemap, or structured data. Why it’s
wrong: teams ship pages with no <title> variation, no canonical, and no sitemap
because “Astro handles SEO,” then wonder why nothing’s indexed as expected. What to do
instead: wire up BaseLayout.astro props for title/description/canonical on day one,
add @astrojs/sitemap, and treat those as required build steps, not defaults.
Forgetting site: in astro.config.mjs
This is the single most common “why is my sitemap empty” report. Why it’s wrong:
@astrojs/sitemap needs an absolute site URL to build absolute <loc> entries — without
site: set, the integration silently produces nothing (no error, no warning). What to
do instead: set site: in astro.config.mjs before you install the sitemap
integration, and check that sitemap-index.xml actually has URLs in it after your next
build.
Leaving the sitemap out of robots.txt
Installing @astrojs/sitemap doesn’t add a Sitemap: line to robots.txt — that’s a
separate manual step people assume is automatic. Why it’s wrong: search engines can
still find the sitemap if you submit it in Search Console, but you lose the passive
discovery path other bots (and Bing/IndexNow-adjacent crawlers) rely on. What to do
instead: add Sitemap: https://yoursite.com/sitemap-index.xml to your robots.txt in
public/ and confirm it resolves after deploy.
Leaving the default loading="lazy" on the hero image
Astro’s <Image /> component lazy-loads by default, which is the right call for
below-the-fold images and the wrong call for the one image that usually is your LCP
element. Why it’s wrong: lazy-loading the hero delays the browser from even starting
that image request, which directly hurts your Largest Contentful Paint score. What to
do instead: override the hero/above-the-fold image explicitly with loading="eager"
and fetchpriority="high", and leave every other image on the lazy default.
Putting indexable content inside a Server Island
server:defer is built for personalized, session-specific content — cart counts, login
state, recommendations — not for anything you want ranked. Why it’s wrong: the static
HTML a crawler reads contains the fallback content specified for the island, not what
gets fetched client-side after the page loads, so any primary content placed there is
invisible to search engines on first crawl. What to do instead: keep ranking content
in the main Astro template and reserve Server Islands strictly for the dynamic,
personalized bits that shouldn’t be indexed anyway.
Assuming a fast Lighthouse score is the finish line
Astro sites post strong Core Web Vitals almost by default, and it’s tempting to stop there. Why it’s wrong: speed is one ranking input among many — a fast, empty, or thin page still doesn’t outrank a slower page with better content, links, and topical depth. What to do instead: treat performance as table stakes you get for free with Astro, then spend the actual optimization effort on content quality, internal linking, and the structured-data/meta work Astro doesn’t do for you.
Assuming “static by default” is true for every route
Astro’s static output mode is the default, but it’s a default, not a universal property —
output: 'server' flips it, and prerender can be set per route in either direction.
Why it’s wrong: teams describe their whole site as “static” or “SSR” from the
top-level config and never check individual routes, then get surprised when one route
behaves differently in production. What to do instead: check the per-route
prerender setting for anything you’re reasoning about, and validate the actual response
(status code, full markup, redirect behavior) on the deployed URL rather than the config
file.
Putting content only inside a client:only component
client:only is not the same as client:load/client:idle/client:visible — it skips
server rendering entirely. Why it’s wrong: a component using client:only produces no
HTML on the server, so anything indexable placed only there is invisible to a crawler
reading the raw response, and it’s easy to reach for client:only for “simplicity”
without realizing the SEO cost. What to do instead: render primary content in a
server-rendered component or the page template; reserve client:only for
interaction-only widgets with nothing indexable inside them.
AI summary
A condensed take on the Advanced version:
- Astro’s default output mode prerenders to static HTML at build time — for a route in
that default, the raw HTML is the finished page, so Google’s render-queue (the “second
wave”) problem doesn’t apply to it. This is a default, not a universal property:
output: 'server'flips it, andprerendercan override per route in either direction. - Islands architecture hydrates only components marked with
client:*; a component without one ships HTML without its own hydration JS (though Astro can still add page scripts, other islands, and router JS elsewhere).client:onlyis the exception — it skips server rendering entirely, so indexable content shouldn’t live only there. This is selective hydration, not resumability.client:visiblekeeps below-the-fold JS from blocking LCP. - Astro auto-generates nothing SEO-wise — meta tags, canonicals, sitemap, structured data, and robots.txt are all deliberate build steps. “Astro is auto-SEO-optimized” is a myth.
@astrojs/sitemapdiscovers statically generated routes — but you must setsite:in the config (or it silently does nothing), add the sitemap line to robots.txt yourself, and explicitly add any runtime-only URLs viacustomPages.- Content Collections + Zod can make
title/descriptionrequired, failing the build if a page is missing them — an SEO safety net. astro:assets<Image>converts to WebP, sets dimensions (prevents CLS), lazy-loads by default, and requiresalt. Override the LCP image withloading="eager"+fetchpriority="high".- Server Islands (
server:defer) require an adapter, serve the static shell + fallback instantly in the initial document, then fetch the island independently afterward. Crawlers reading that first document see the fallback, not the island — never put indexable content there. - Static and on-demand (server) output are SEO-equivalent once verified — both deliver full HTML to crawlers on first request, but on-demand routes can stream, so confirm the response actually arrives complete; choose the mode on data freshness and ops, not SEO.
- View Transitions (
<ClientRouter />) usehistory.pushStateand are SEO-safe; Google crawls the underlying MPA pages normally. They’re a browser-side enhancement, not an SPA conversion. - None of the above is an outcome guarantee — static/server HTML, islands, and adapters are mechanisms, not proof of crawlability, indexing, rankings, or Core Web Vitals. Validate the deployed route.
- Astro posts strong Core Web Vitals numbers in its own 2023 benchmark — over 50% of Astro sites passed Google’s CWV assessment, well above the industry baseline at the time; treat it as a dated data point, not a live guarantee.
Official documentation
Primary-source documentation from Astro and the search engines.
Astro
- Islands Architecture — how Astro strips client-side JS and hydrates only interactive components.
- Template Directives Reference —
client:load/idle/visible/onlyandserver:defer, including whatclient:onlyskips. - Image Optimization (astro:assets) — the
<Image>/<Picture>components, WebP, dimensions, and CLS. - @astrojs/sitemap — automatic sitemap generation,
filter,serialize, andi18n. - Server Islands —
server:defer, the adapter requirement, fallback content, and deferred client fetching. - On-Demand Rendering — the
static/serveroutput modes, per-routeprerender, and HTML streaming. - Routing Reference — how pages and endpoints prerender by default.
- Astro Runtime API Reference —
Response/redirect handling and default status codes. - Content Collections — type-safe content with Zod schema validation.
- View Transitions —
<ClientRouter />and the History API navigation.
- Understand JavaScript SEO Basics — the render queue, crawlable links, the History API, and the “not all bots run JavaScript” caveat.
- In-Depth Guide to How Google Search Works — crawl → render → index, and where SSG removes the render step.
Bing / Microsoft
- IndexNow / indexnow.org — the push protocol that pairs well with a static Astro deploy (wire it to your publish step so Bing and Yandex hear about new pages immediately).
Quotes from the source
On-the-record statements from Astro’s docs, Google, and named practitioners. Each search-engine and docs link is a deep link that jumps to the quoted passage on the source page.
Google — the render queue Astro sidesteps
- “Googlebot queues all pages with a 200 HTTP status code for rendering unless a robots meta tag tells Google not to index the page. The page may stay on this queue for a few seconds, but it can take longer than that.” Jump to quote
- “not all bots can run JavaScript” — why HTML-first output helps beyond Googlebot. Jump to quote
Astro docs — islands, images, sitemaps, Server Islands
- “just HTML & CSS, stripping out all client-side JavaScript automatically.” — on islands architecture. Jump to quote
- “infers image dimensions to avoid Cumulative Layout Shift (CLS).” — on the Image component. Jump to quote
- “Your page will be rendered immediately with any specified fallback content as a placeholder. Then, the component’s own contents are fetched on the client and displayed when available.” — on Server Islands. Jump to quote
Jason Miller (creator of Preact, coined “islands architecture”)
- Selective hydration works by “rendering HTML pages on the server, and inject[ing] placeholders or slots around highly dynamic regions.” — cited in Astro Docs: Islands Architecture
Joost de Valk (founder of Yoast SEO)
- “From an SEO perspective, static HTML on a CDN is a better starting point than most CMSes will ever give you.” — Joost.blog: Astro SEO Complete Guide
Astro SEO checklist
A pass to confirm an Astro site is actually set up for search — not just sitting on a good foundation:
-
site:is set inastro.config.mjs(sitemap silently won’t generate without it). -
@astrojs/sitemapis installed and the sitemap reference is added torobots.txtmanually. - A
robots.txtexists inpublic/and doesn’t block anything you want indexed. - Every page sets a unique
titleanddescription(ideally via a sharedBaseLayout.astro). - A self-referencing canonical is on every page and matches
og:url. - Content Collections use a Zod schema that makes
title/descriptionrequired (build fails if missing). - Images use
<Image />/<Picture />; all havealt(a missing one is a compile error anyway). - The LCP/hero image overrides the default lazy-load with
loading="eager"andfetchpriority="high". - No indexable content lives in a Server Island (
server:defer) — crawlers see the fallback, and Server Islands need an adapter to work at all. - No indexable content lives only inside a
client:onlycomponent — it skips server rendering, so there’s no HTML for it until the browser hydrates. - For any route using
output: 'server'or a per-routeprerender = false, confirm runtime-only URLs are added to the sitemap explicitly (customPages) — automatic sitemap discovery targets statically generated routes. - Structured data (JSON-LD) is in the server-rendered
<head>. - If
<ClientRouter />is enabled, spot-check that each URL still returns full HTML on a direct fetch. - On-demand/server routes: fetch a live production URL directly and confirm the status code, redirect behavior, and full response HTML — don’t extrapolate from local dev, since adapter/runtime behavior can differ.
The mental models
1. The raw HTML is the finished page — for that route’s default.
Astro’s static output mode means there’s no render wave to wait on for a prerendered
route — what Googlebot fetches is what gets indexed. That’s a per-route default, not a
site-wide guarantee (output: 'server' and per-route prerender can change it), so
check the route, not just the top-level config. When it applies, View Source is the
truth (the opposite of a CSR SPA) — but confirm it applies before treating it as fact.
2. Foundation, not finish, and not an outcome guarantee. Astro gives you clean HTML and strong performance mechanics for free. Everything that signals meaning to search engines — meta, canonical, sitemap, schema — is a deliberate step you add. “Good architecture” ≠ “done,” and neither one is proof of crawlability, indexing, or rankings — those still have to be validated on the deployed site.
3. Islands are additive — except client:only.
Interactivity sits on top of HTML, never under it, for client:load/client:idle/
client:visible: those directives add JS to one component without removing content from
the crawlable base. client:only breaks that pattern — it skips server rendering
entirely, so it’s not additive, it’s a genuine gap in the static HTML unless you plan for
it. And island hydration is selective, not resumability — don’t conflate the two.
4. The static shell is what crawlers see. For Server Islands, the crawler reads the fallback in the static shell, not the deferred content. Decision rule: indexable content goes in the main template; personalized/dynamic content goes in the island.
5. Browser enhancement ≠ structural change.
View Transitions change the browser experience (soft navigation via history.pushState)
but not the crawl experience (each URL is still a full HTML page). Enhancements that
leave the underlying MPA intact are SEO-safe.
6. Validate at build time. Content Collections + Zod turn “remember to add a title” into “the build won’t ship without one.” Push SEO requirements into the type system and they stop being things you can forget.
Astro SEO — cheat sheet
What’s automatic vs. what’s on you
| Concern | Astro does it? | What you do |
|---|---|---|
| Static HTML output | ✅ Default (static mode) | Nothing — it’s the default, but check per-route prerender |
| Non-hydrated components ship no component JS | ✅ Islands (except client:only) | Use client:* only where needed; keep indexable content out of client:only |
| Image WebP + dimensions + lazy | ✅ <Image> | Override LCP image to eager |
alt enforcement | ✅ Compile error if missing | Write good alt text |
| Sitemap | ⚠️ Plugin, static routes only | astro add sitemap + set site: + customPages for runtime-only URLs |
| Sitemap in robots.txt | ❌ | Add the line manually |
| Meta tags / canonical | ❌ | BaseLayout.astro props |
| Structured data (JSON-LD) | ❌ | Add to <head> |
| robots.txt | ❌ | File in public/ |
| Outcome guarantee (crawlability/rankings/CWV) | ❌ — mechanisms only | Validate the deployed route yourself |
Output modes
| Mode | Config | SEO (once verified) | Use for |
|---|---|---|---|
| static (default) | — | ✅ Full HTML on first request | Most content; served from CDN edge |
| server | output: 'server' | ✅ Equal to static for crawlers | Auth, real-time, personalization |
| Per-route override | export const prerender = false (static default) or = true (server default) | ✅ | Mixing prerendered + on-demand routes |
Islands directives
client:load— hydrate immediately.client:idle— hydrate when the browser is idle.client:visible— hydrate when scrolled into view (best for below-the-fold; protects LCP).client:only— skips server rendering entirely. No HTML for this component until the browser hydrates it; not additive like the others.
Fast rules
site:missing → no sitemap (silent).- Indexable content in a Server Island → crawler sees the fallback; Server Islands need an adapter.
- Indexable content only inside
client:only→ no HTML for it, period. - Sitemap covers static routes → add runtime-only URLs via
customPages. - View Transitions use
history.pushState→ SEO-safe, MPA underneath. - Static/server output and islands are mechanisms, not guarantees — validate the deployed route, status code, and full HTML before claiming an outcome.
- A fast empty page still doesn’t rank — speed is a signal, not the signal.
Audit Astro’s built HTML
Run this after astro build. It checks the artifact crawlers receive, not the source
component tree:
find dist -name '*.html' -type f | while IFS= read -r file; do
canonicals=$(grep -Eio '<link[^>]+rel=["'"']canonical["'"'][^>]*>' "$file" | wc -l | tr -d ' ')
titles=$(grep -Eio '<title>[^<]*</title>' "$file" | wc -l | tr -d ' ')
if [ "$canonicals" -ne 1 ] || [ "$titles" -ne 1 ]; then
printf '%s\ttitles=%s\tcanonicals=%s\n' "$file" "$titles" "$canonicals"
fi
doneAn empty result means every generated HTML file has exactly one title and canonical;
it does not validate whether their values are correct, so sample those separately. This
only checks the build artifact — it says nothing about on-demand (output: 'server')
routes or Server Islands, which don’t exist as static files.
Spot-check live routes in production
For anything that renders on demand — output: 'server' routes, per-route prerender = false, or Server Islands — the build-output check above doesn’t apply. Check the actual
deployed response instead:
# Replace with your real URLs
for url in "https://example.com/" "https://example.com/some-server-route/"; do
echo "== $url =="
curl -sS -D - -o /dev/null "$url" | grep -Ei '^(HTTP|location|cache-control):'
doneLook for the status code you expect (200 for a live page, a real redirect code if it
redirects — don’t assume 302 vs 301 without checking), and confirm curl -sS "$url"
returns full markup, not a fallback shell, if you’re checking a page that includes a
Server Island. Do this against production, not astro dev — adapter and runtime
behavior can differ from local.
Patrick's relevant free tools
- SEO Incident Simulator — Practice thirty deterministic technical SEO incident investigations — indexability, crawl controls, redirects, sitemaps, markup, caching, DNS, bot verification, rendering, hreflang, and faceted navigation — with clearly labeled fixture evidence and Find → Fix → Verify handoffs.
- Raw vs. Rendered HTML Checker — See what's in your page's initial HTML versus after JavaScript runs — headless-Chrome rendering only when the page actually needs it, a rendering-strategy verdict (SSR / prerendered / CSR / hybrid), ~15 calibrated JavaScript-SEO checks (noindex, canonicals, robots.txt blocking, links, soft 404s), a side-by-side raw-vs-rendered diff, and shareable reports.
- Page Speed Test & Core Web Vitals Checker — One sentence tells you whether a page passes Core Web Vitals and what to fix first — real Chrome user data (CrUX) with a Lighthouse lab fallback, mobile and desktop side by side, loudly-labeled data sources, and prioritized diagnostic fixes. Plus a bulk origin scorecard with CSV export.
Tools for an Astro site
@astrojs/sitemap— the official sitemap integration (astro add sitemap). Don’t forgetsite:in the config.astro-seo(npm) — optional community component that bundles title, description, Open Graph, Twitter cards, and canonical into one tag.astro-seo-schema(npm) — typed JSON-LD structured-data helper for Astro.- astro:assets
<Image>/<Picture>— built-in image optimization (WebP/AVIF, dimensions, lazy-load,altenforcement). - URL Inspection (Google Search Console) — confirm your content is in the crawled HTML (with Astro it should be in View Source already — a quick sanity check).
- A JS-rendering crawler — Ahrefs Site Audit or Screaming Frog to verify raw vs. rendered parity across the site (on prerendered routes they should match — confirm it, don’t assume it, and check separately for any on-demand or Server Island routes).
- IndexNow — pair with your deploy/publish step so Bing and Yandex hear about new static pages immediately.
Test yourself: Astro SEO
Five quick questions on how Astro’s architecture affects SEO. Pick an answer for each, then check.
Resources worth your time
My related writing
- JavaScript SEO: A Definitive Guide — the rendering fundamentals behind why Astro’s HTML-first output is such an advantage.
- The Beginner’s Guide to Technical SEO — where framework choice fits in the bigger picture.
My speaking
- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking — the pipeline Astro’s SSG default short-circuits. (Standing disclaimer: “This is my understanding of systems… not going to be 100% complete or accurate.”)
From around the industry
- Astro Docs: Islands Architecture — the canonical explanation of how Astro strips client-side JS.
- Astro Docs: @astrojs/sitemap — official setup, the
site:requirement, andfilter/serialize/i18noptions. - Astro Docs: Server Islands —
server:defer, fallback content, and the client-fetch behavior crawlers don’t see. - Joost de Valk: Astro SEO Complete Guide — the most authoritative practitioner guide, by Yoast’s founder; strong on modern AI-ready patterns and IndexNow.
- Google Search Central: Understand JavaScript SEO Basics — the render queue, crawlable links, and the History API guidance Astro’s View Transitions follow.
- Astro: 2023 Web Framework Performance Report — the Core Web Vitals data comparing Astro to other frameworks.
- Search Engine Journal: Core Web Vitals, WordPress and Astro — independent coverage of the Astro vs WordPress performance comparison.
Stats worth citing
All figures below come from Astro’s 2023 Web Framework Performance Report (and SEJ’s coverage of it); treat them as 2023-era benchmarks rather than live numbers.
- Over 50% of Astro sites pass Google’s Core Web Vitals Assessment — above the industry average of ~40.5%, and Astro and SvelteKit were the only major frameworks beating that baseline (Next.js ~25%, Nuxt ~20%). Source
- INP pass rate of 68.8% for Astro — credited to the MPA architecture (no JS-driven navigation), which keeps the main thread free. Source
- Median page weight of 1.65 MB — the lightest in the dataset. Source
- LCP: Astro ~0.44s vs WordPress ~0.81s — roughly 46% faster, in the report’s comparison. Coverage
Astro SEO
Astro SEO is the practice of optimizing sites built with the Astro web framework for search. Astro prerenders to static HTML by default, so content is in the raw HTML on first crawl for that route — no rendering queue — which makes it a strong starting point for SEO, though the default can be overridden per route and doesn't guarantee crawlability or rankings on its own.
Related: JavaScript SEO, Headless CMS SEO
Astro SEO
Astro SEO is the set of technical and on-page practices for optimizing websites built with the Astro web framework for crawlability, indexability, and performance. What makes Astro distinctive is its architecture: its default output mode prerenders pages and endpoints to static HTML at build time. That’s a default, not a universal property — output: 'server' flips it, and individual routes can override the default with prerender. Where it applies, a crawler sees your full content in the raw HTML on the first fetch — there’s no rendering queue to wait on, the way there is with client-rendered React or Vue apps.
Astro’s “islands architecture” hydrates JavaScript only for components marked with a client:* directive; a component without one contributes HTML without its own hydration JS, though Astro can still ship page scripts, other islands, and router enhancements elsewhere on the page — so “zero JS” describes a component, not a blanket property of the page. (client:only is the exception: it skips server rendering entirely.) Each page is its own URL in a multi-page-app (MPA) structure, so there’s no SPA routing to hijack discovery. Optional features layer cleanly on top: the @astrojs/sitemap integration generates sitemaps from statically generated routes (runtime-only URLs need explicit customPages), the astro:assets <Image> component produces WebP and sets dimensions to avoid layout shift, and View Transitions add SPA-like navigation using the History API while leaving each page independently crawlable.
The catch is that “good architecture” is not the same as “done” — or a guarantee. Astro generates clean HTML, but it does not write your meta tags, canonical tags, sitemap, or structured data for you, and static or server output, islands, and adapters don’t by themselves prove crawlability, indexing, rankings, or Core Web Vitals — validate the deployed route. Astro is the best possible foundation, not a finished SEO solution.
Related: JavaScript SEO, Headless CMS SEO
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Revision history
Compare the published article with an archived editorial snapshot. Added and removed words are shown only after you open a comparison.
Updated Jul 17, 2026.
Editorial summary and recorded change details.Summary
Qualified Astro's rendering advantages and documented the route-level conditions that determine what crawlers actually see, following an editorial review against current Astro docs.
Change details
-
Distinguished Astro's static default from server-rendered and per-route overrides (prerender true/false), with guidance to verify deployed responses instead of treating framework defaults as guarantees.
-
Clarified selective hydration, client:only's server-rendering skip, and the distinction between islands and resumability; noted that page scripts and router JS can ship outside islands, so 'zero JS' is per-component, not page-wide.
-
Expanded sitemap and Server Island guidance to cover runtime-only routes (customPages), the adapter requirement for Server Islands, and production spot-checks for status codes and full HTML on on-demand routes.
-
Added an explicit no-outcome-guarantee framing throughout (static/server output, islands, and adapters are mechanisms, not proof of crawlability, indexing, rankings, or Core Web Vitals) and two new anti-patterns covering per-route assumptions and client:only misuse.