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.

First published: Jun 26, 2026 · Last updated: Jul 17, 2026 · Advanced
demand #5 in JavaScript Frameworks#30 in Platform SEO#193 in Technical SEO#267 on the site

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 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 crawlabilityCrawlability is how well search engine crawlers can discover, access, and fetch a site's pages. A crawlability issue is any technical condition — blocked access, broken links, server failures, or bloated URL inventory — that stops pages from reaching the index., indexingStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed., rankings, or Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. for your production URLs. Islands architecture hydrates only the components you mark with a client:* directive — everything else ships HTML without its own hydrationTurning HTML, CSS, and JavaScript into the final visual page and DOM. JS, though page scripts, other islands, and router enhancements can still add JavaScript elsewhere on the page. Astro doesn’t auto-generate meta tagsMeta tags are HTML elements in a page's head that pass metadata about the page to search engines and browsers. For SEO only a few matter — the title element, the meta description, and the robots meta tag — while meta keywords and most others are ignored., canonicals, sitemapsA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing., or structured dataStructured data is a standardized way of labeling page content (using the schema.org vocabulary in JSON-LD, Microdata, or RDFa) so search engines can understand its meaning. It's not a direct ranking factor — its value is rich results and entity understanding. — 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 crawlerA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. actually retrieves rather than assuming. View Transitions use history.pushState and 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 SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. is hard is the second wave. Google fetches your raw HTML first, then queues the page for renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. in a headless Chromium later — and that queue is the risk. Google’s own docs describe it: GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. queues all pages with a 200 HTTP status codeAn HTTP status code is the three-digit number a server returns with every response to tell a browser or crawler what happened to its request — success, redirect, client error, or server error. For SEO the code matters as much as the content: it tells Google and Bing whether to index a page, follow a redirect, retry later, or drop the URL from the index. for rendering unless a robots meta tagThe robots meta tag is an HTML element in a page's head — <meta name=\"robots\" content=\"noindex\"> — that tells search engines how to index and serve that page. It's crawl-then-obey: a page blocked in robots.txt is never fetched, so the tag is never seen. 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 crawlabilityCrawlability is how well search engine crawlers can discover, access, and fetch a site's pages. A crawlability issue is any technical condition — blocked access, broken links, server failures, or bloated URL inventory — that stops pages from reaching the index., indexingStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed., rankings, or Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. — those depend on the deployed route and the specific crawlerA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index., 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 CMSA headless CMS decouples content storage and editing (the backend) from how that content is rendered and delivered (the frontend), serving content over an API instead of a built-in templated 'head'. Its SEO outcomes come almost entirely from how the separate frontend renders pages.: 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 LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good.. 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 hydrationActivating server-rendered HTML in the browser by attaching JavaScript handlers..

Two nuances worth being precise about, because competitor coverage tends to blur them:

  • client:only is a different animal. Unlike client:load/client:idle/client:visible, a client:only component skips server rendering entirely — it produces no HTML on the server at all. Anything indexable placed only inside a client:only component 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 HTMLSemantic HTML is the practice of using elements like <main>, <article>, <section>, <nav>, <header>, and <aside> to describe what content is, not just how it looks. It helps search engines and assistive tech identify a page's main content more reliably, but it isn't a ranking factor on its own. — and nothing else, SEO-wise. There is no metadata, no canonical, no sitemapA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing., no structured data out of the box. The myth that “Astro is automatically SEO-optimized” is exactly that. You own:

  • Meta tagsMeta tags are HTML elements in a page's head that pass metadata about the page to search engines and browsers. For SEO only a few matter — the title element, the meta description, and the robots meta tag — while meta keywords and most others are ignored. (title, description, Open GraphOpen Graph (OG) tags are `<meta>` elements in a page's head, defined by the Open Graph protocol (ogp.me, created by Facebook), that describe a page as a shareable object — its title, description, image, URL, and type. They control how a link preview card looks when the page is shared on Facebook, LinkedIn, Slack, Discord, WhatsApp, and iMessage. They are not a direct Google ranking factor, though Google reads og:title, og:image, and og:site_name as inputs to how a result appears., Twitter)
  • Canonical URLs
  • The sitemap (via the official integration)
  • Structured data / JSON-LDJSON-LD (JavaScript Object Notation for Linked Data) is a script-based structured data format, typically paired with the schema.org vocabulary to describe page content for search engines and AI systems. Google recommends it over Microdata and RDFa because it's the easiest format to implement and maintain at scale — but all three work, and structured data isn't a ranking signal.
  • robots.txtA plain-text file at the root of a host that tells crawlers which URLs they may and may not request. It controls crawling, not indexing — a blocked URL can still be indexed if it's linked from elsewhere.

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: in astro.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.txt yourself. 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 hreflangHreflang is an annotation (in HTML, HTTP headers, or XML sitemaps) that tells search engines which language and optional region a page targets, and which alternate versions exist. It only works when every page in the cluster references all the others. 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 crawlersAI crawlers are bots from AI companies that fetch web pages to train language models, build AI-search indexes, or answer live user questions. They come in three categories, each with its own user-agent tokens and its own robots.txt controls. and llms.txtllms.txt is a proposed (not adopted) Markdown file at /llms.txt that gives AI systems a curated map of a site's most important pages. Proposed by Jeremy Howard in 2024, it's read mostly by coding agents like Claude Code — not search crawlers — and Google ignores it.-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 (CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good.),” 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, redirectsA redirect sends browsers and crawlers from a requested URL to a different one. An HTTP redirect specifically is a 3xx status code paired with a Location header; meta refresh and JavaScript redirects achieve a similar navigation without being a 3xx response themselves. Permanent redirects (301/308) are Google's signal the target should be canonical; temporary ones (302/303/307) aren't., 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

  1. Assuming Astro handles SEO for you. It handles HTML. Meta, canonicals, sitemap, schema are yours.
  2. Forgetting site: in the config — your sitemap silently doesn’t generate.
  3. Not adding the sitemap to robots.txt — Astro won’t do it.
  4. Lazy-loading the LCP image — override the hero with eager + fetchpriority.
  5. Putting indexable content in a Server Island — crawlers see the fallback, not the content, and Server Islands require an adapter in the first place.
  6. Chasing a perfect LighthouseLighthouse is Google's free, open-source tool that audits a page under simulated lab conditions and scores it 0–100 across Performance, Accessibility, Best Practices, and SEO. It's lab data — useful for debugging, not a ranking signal. 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.
  7. Putting indexable content only inside a client:only component. Unlike the other client:* directives, client:only skips server rendering entirely — there’s no HTML for that component until the browser hydrates it.
  8. 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 VitalsWeb Vitals is Google's initiative (launched May 2020) for unified page-experience quality signals. Core Web Vitals — LCP, INP, and CLS — are the subset used in ranking; the rest (TTFB, FCP, TBT, Speed Index) are diagnostic, not ranking factors.. 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 SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. raises about rendering, and a popular frontend for headless CMSA headless CMS decouples content storage and editing (the backend) from how that content is rendered and delivered (the frontend), serving content over an API instead of a built-in templated 'head'. Its SEO outcomes come almost entirely from how the separate frontend renders pages. 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 crawlingCrawling is how search engines use automated bots (like Googlebot and Bingbot) to discover URLs and download pages. A page has to be crawlable to be indexed, but crawling on its own isn't a ranking factor. and indexing clusters.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.