Astro SEO: How This Site Is Built for Search
Astro SEO is mostly about what Astro gives you for free, which is complete static HTML, and what it leaves to you: titles, canonicals, sitemaps, structured data and content. Astro renders pages to HTML at build time with no client-side runtime unless you add one, so Google, Bing and AI crawlers that do not execute JavaScript all receive the full page. The rest is layout work, and this page shows how we did it.
ambeltek.com runs on Astro 5. At the time of writing, src/content/blog holds 162 posts, and the latest build wrote 191 HTML pages, counting the service, location and topic pages. Everything below describes this site’s actual source code: the config, the base layout and the blog template. It is written for developers and technical marketers choosing or configuring Astro, and it includes the mistakes we made along the way. It makes no claims about traffic results.
On this page
- Is Astro good for SEO?
- Static HTML for crawlers and AI bots
- Sitemaps with @astrojs/sitemap
- Titles, canonicals and Open Graph in one layout
- JSON-LD: one graph per page
- Images, view transitions and other caveats
- Is Astro better than WordPress for SEO?
- Getting Astro SEO right on a new build
Is Astro good for SEO?
Yes, because its default output is the kind of HTML search engines and AI crawlers handle best. Astro’s islands architecture documentation states that Astro components “render to static HTML with no client-side runtime.” Interactivity is added only as isolated “islands,” hydrated separately from the rest of the page. A content page with no interactive widgets ships no framework JavaScript at all. On this site, each post loads one external script, a 7 KB canvas background, and no framework runtime. Compare that with the web at large: the 2024 Web Almanac puts the median mobile page at 558 kilobytes of JavaScript, with 44% of the bytes delivered going unused during page load.
That matters because rendering is where many modern sites lose content. Google can render JavaScript, but its JavaScript SEO basics describe pages waiting in a render queue, and still recommend server-side or pre-rendering “because it makes your website faster for users and crawlers, and not all bots can run JavaScript.” Many AI crawlers fall into that “not all bots” group, as we cover in JavaScript rendering and AI crawlers.
What Astro does not do is SEO itself. There is no built-in title tag, canonical, sitemap or schema. You add those in layouts and integrations, which is the rest of this page.
Static HTML for crawlers and AI bots
Every page on this site is built to a finished HTML file before deployment. The build flow looks like this:
Scroll the diagram sideways to see all of it.
Two details from our build are worth copying:
- Frontmatter is validated. The blog collection is defined with a Zod schema in
src/content.config.ts: title, description, dates, author, tags and FAQ entries are typed. A post with a missing description fails the build instead of shipping without one. - The robots file invites AI crawlers. Our
robots.txtexplicitly allows GPTBot, OAI-SearchBot, PerplexityBot and Google-Extended, and points to the sitemap index. Static HTML is wasted if the crawler is blocked. Our robots.txt guide for AI crawlers explains each user agent.
One lesson came from a bug, not a feature. Our base layout fades sections in on scroll using an IntersectionObserver. It originally used a visibility ratio threshold, and because a ratio is measured against an element’s own height, very long articles could never reach it and stayed invisible. The HTML was intact for crawlers, but readers saw blank space. The fix was a zero threshold, a scroll-based fallback, and a <noscript> rule that forces everything visible. The lesson: static HTML protects crawlers, not humans. Test with real long pages.
Sitemaps with @astrojs/sitemap
Use the official integration and set site. The @astrojs/sitemap documentation says it “needs to know your site’s deployed URL” and writes sitemap-index.xml plus numbered files such as sitemap-0.xml at build time, starting a new file every 45,000 entries by default. It covers static routes, including dynamic routes like [...slug] that are prerendered, but it “cannot generate sitemap entries for dynamic routes in SSR mode.”
Our sitemap config adds just two options, and the latest build’s sitemap lists 189 pages: every built page except the 404 page and /thank-you/. The two options:
- A filter.
/thank-you/is excluded because it is the one page carrying anoindexrobots tag. A sitemap should never list a URL the page itself says not to index. Our/privacy/page was once excluded too, then restored, because it is indexable and linked from the footer, and the sitemap disagreed with the page. - A serializer that adds
lastmod. This is where we have to be honest. Our serializer stamps every URL with the build time. That makes every page look freshly modified on each deploy, which is not whatlastmodis for. Bing’s webmaster guidelines ask for “accurate lastmod values.” The better pattern, and the change we would recommend to anyone copying this, is to pass each post’supdatedDateorpubDateinto the serializer. The difference looks like this:
Scroll the diagram sideways to see all of it.
The layout also emits a <link rel="sitemap"> tag pointing at the sitemap index, and after each deploy we notify search engines of the sitemap URLs through IndexNow. For what belongs in the file generally, see our XML sitemap guide.
Titles, canonicals and Open Graph in one layout
Put every head tag in one base layout and pass values in as props. Ours, BaseLayout.astro, takes a title, description, optional schema nodes, an Open Graph type, a WebPage subtype, an image path and a noindex flag. From those it writes:
<title>as the page title plus ” | Ambeltek”, and the same string forog:titleandtwitter:title.- A meta description, falling back to the site description if a page forgets one.
- A canonical built from the current path and the configured site URL, so every page self-canonicalizes to the https, non-www address.
- Open Graph and Twitter card tags, with a 1200 by 630 image per post generated before the build by a script using the
sharplibrary. <meta name="robots" content="noindex, follow">only when the page passesnoindex.
The blog template passes the post’s title as the page title, which also becomes the H1. Keeping the H1 and <title> identical is a deliberate choice; our canonical tag guide explains why self-referencing canonicals are the safe default.
JSON-LD: one graph per page
We output one JSON-LD script per page containing a connected @graph, rather than several unrelated blocks. The layout always includes three nodes with stable @id values:
Scroll the diagram sideways to see all of it.
A few implementation choices are worth explaining:
- The full Person node travels with every post. A comment in our template explains why: search engines and AI systems parse each URL independently, so a post that only points at
#person“leaves the author unqualified wherever the author page is not also fetched.” Our guide to Person schema and author entities covers the pattern. - Visible dates match schema dates. Dates on the page are formatted in UTC, so the date a reader sees never disagrees with
datePublishedin the JSON-LD. - FAQPage comes from the same data as the visible FAQ. Frontmatter
faqentries render as a visible FAQ and as FAQPage markup, so the two cannot drift apart.
Page-specific nodes are passed to the layout as a schema prop and merged into the graph. For which types matter most, see structured data for AI search.
Images, view transitions and other caveats
Some Astro features help SEO, some are neutral, and one needs care. The table summarizes what we use and what we chose not to.
| Feature | SEO effect | Caveat | Used on this site? |
|---|---|---|---|
| Static output (default) | Complete HTML for every crawler | None for content sites | Yes |
Islands (client:*) | JS only where needed | Content inside an island may need JS to appear | No islands on content pages |
@astrojs/sitemap | Automatic sitemap index | Needs site; no SSR dynamic routes | Yes |
<Image /> | Sets width and height, requires alt | Only for local or authorized remote images | No; images are few |
<ClientRouter /> | Smooth navigation | Scripts may need reinitializing | No |
| SSR (on-demand) | Fresh data per request | Sitemap will not list dynamic SSR routes | No |
On images, Astro’s image guide says the <Image /> component makes alt mandatory and sets width and height automatically to avoid Cumulative Layout Shift. That is a good default; our Core Web Vitals guide explains why CLS matters.
On view transitions, Astro’s view transitions guide notes that by default “every page will use regular, full-page, browser navigation.” Adding <ClientRouter /> turns navigation into a single-page app, with “drawbacks, such as needing to manually reinitialize scripts or state after navigation.” Crawlers still request each URL directly, so indexing is not the risk. Analytics that only fire on full page loads, and scripts like our reveal animation, are.
Is Astro better than WordPress for SEO?
Not by default; it depends on who maintains the site. Both can produce fast, crawlable pages with correct metadata. The honest comparison:
- Astro gives developers total control of the HTML, ships little JavaScript unless you add it, and makes schema and templates code you can test. The cost is that editors need Markdown, a headless CMS, or a developer.
- WordPress gives editors a familiar interface and a plugin for everything. It is also the default choice for much of the web: the 2024 Web Almanac found it on 5.7 million of the more than 16 million mobile sites it crawled, 36% of the total. The cost is that themes and plugins add markup, scripts and URLs you did not ask for, which our WordPress SEO guide covers in detail.
For a content site maintained by a technical team, Astro removes a whole class of problems. For a team of non-technical editors publishing daily, WordPress set up carefully may be the better tool. The design decisions that affect SEO on either platform are in web design for SEO.
Getting Astro SEO right on a new build
A short checklist, in the order we would do it again:
- Set
siteinastro.config.mjsbefore anything else; the sitemap and canonicals depend on it. - Build one base layout that owns every head tag, and make title and description required props.
- Define content collections with a schema so missing metadata fails the build.
- Add
@astrojs/sitemap, filter out anynoindexURLs, and pass real per-page dates intolastmod. - Emit a single JSON-LD
@graphwith stable@idvalues, and include the full author node on articles. - Keep content pages free of islands unless interaction is genuinely needed.
- Test the built HTML with JavaScript disabled, on your longest page.
Astro makes the technical half of SEO easier to get right and harder to break by accident. If you would like a site built this way from the start, that is what our web development service delivers.
Frequently asked questions
Is Astro good for SEO?
Yes, mainly because it outputs complete HTML by default. Astro components render to static HTML with no client-side runtime, so crawlers and AI bots that do not run JavaScript still get the full content. Astro does not write your titles, schema or content for you; you still build those into your layouts.
Is Astro better than WordPress for SEO?
Not automatically. Both can produce fast, crawlable pages with correct metadata. Astro gives you complete control of the HTML and ships little JavaScript by default, while WordPress gives non-developers an editor and a large plugin ecosystem. The better choice depends on who edits the site and how much you want to hand-build.
Is Astro better than Next.js for SEO?
Neither ranks better by itself. What matters is whether the HTML a crawler receives contains the content, links and metadata. Astro defaults to static HTML with JavaScript only where you add it; other frameworks can reach the same result with server or static rendering. Test the delivered HTML, not the framework name.
Does Astro generate a sitemap automatically?
Only with the official @astrojs/sitemap integration, which writes sitemap-index.xml and sitemap-0.xml at build time. You must set the site option in astro.config.mjs to your deployed URL. The integration cannot generate entries for dynamic routes rendered on demand in SSR mode.
Do Astro view transitions hurt SEO?
They are opt-in, and by default Astro uses normal full-page navigation. Enabling the ClientRouter turns the site into a single-page app for navigation, which Astro notes can mean manually reinitializing scripts after navigation. Crawlers still request each URL directly, so the main risk is broken scripts or analytics, not indexing.
How do I add JSON-LD structured data in Astro?
Build the schema object in the layout's frontmatter and output it in a script tag of type application/ld+json. Using one @graph with stable @id values lets every page reference the same Organization and Person entities, and pages can pass extra nodes, such as BlogPosting or FAQPage, as props.