Web DevelopmentTechnical

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?

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:

How an Astro page on ambeltek.com goes from Markdown to crawlable HTML At build time: a Markdown post with frontmatter in the content collection is validated by a Zod schema, rendered by the blog template inside the base layout, and written out as static HTML with title, canonical, Open Graph tags and a JSON-LD graph. The sitemap integration writes sitemap-index.xml, and a prebuild script generates Open Graph images. At request time: Googlebot, Bingbot and AI crawlers receive the finished HTML, with no JavaScript required to see the content, links or schema. From Markdown to crawlable HTML Everything happens at build time; crawlers get a finished file Build time Markdown post frontmatter: title, description, faq Zod schema content collection rejects bad fields Template + layout title, canonical, OG, JSON-LD @graph Static HTML file + sitemap-index.xml + generated OG image Request time Googlebot, Bingbot, GPTBot, OAI-SearchBot, PerplexityBot receive the finished HTML No JavaScript needed to see content, internal links or structured data

Scroll the diagram sideways to see all of it.

Source: Ambeltek diagram of the ambeltek.com build (astro.config.mjs, src/content.config.ts, BaseLayout.astro, blog template).

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.txt explicitly 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:

  1. A filter. /thank-you/ is excluded because it is the one page carrying a noindex robots 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.
  2. 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 what lastmod is 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’s updatedDate or pubDate into the serializer. The difference looks like this:
Sitemap lastmod stamped with build time versus real content dates Two sitemap excerpts for the same three URLs. Left, build-time lastmod, our current setup: the homepage, a post published September 1 and a post updated September 18 all show the same lastmod, the date of the latest deploy, so nothing tells a crawler which page actually changed. Right, content-date lastmod, the recommended fix: the homepage shows the deploy date, the September 1 post shows September 1, and the updated post shows September 18, so the crawler can prioritize the page that really changed. What lastmod tells a crawler Same three URLs, two ways of setting lastmod Build time (our current setup) / deploy date /blog/post-a/ (pub. 1 Sep) deploy date /blog/post-b/ (upd. 18 Sep) deploy date Every page claims it just changed Content date (recommended) / deploy date /blog/post-a/ (pub. 1 Sep) 2026-09-01 /blog/post-b/ (upd. 18 Sep) 2026-09-18 Only the changed page looks fresh

Scroll the diagram sideways to see all of it.

Source: Ambeltek diagram of the ambeltek.com sitemap serializer (astro.config.mjs) and the per-page alternative; illustrative URLs.

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 for og:title and twitter: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 sharp library.
  • <meta name="robots" content="noindex, follow"> only when the page passes noindex.

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:

The JSON-LD graph emitted on every blog post of ambeltek.com Seven connected nodes. From the base layout on every page: ProfessionalService organization with id slash hash organization; WebSite with id hash website, published by the organization; WebPage with id url hash webpage, part of the WebSite. Added by the blog template: Person for the author, working for the organization; BlogPosting, main entity of the WebPage, authored by the Person and published by the organization; BreadcrumbList for Home, Blog and the post; FAQPage built from the frontmatter FAQ. One @graph, linked by @id Layout nodes on every page; template nodes on blog posts From BaseLayout (every page) ProfessionalService @id /#organization WebSite publisher: organization WebPage isPartOf: WebSite From the blog template (posts only) Person worksFor: organization BlogPosting author, publisher, dates BreadcrumbList Home, Blog, post FAQPage from frontmatter faq Dashed lines: BlogPosting is mainEntityOfPage of the WebPage; Person worksFor the organization. Every node references others by @id, so one organization entity is shared site-wide.

Scroll the diagram sideways to see all of it.

Source: Ambeltek diagram of the JSON-LD emitted by src/layouts/BaseLayout.astro and src/pages/blog/[...slug].astro.

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 datePublished in the JSON-LD.
  • FAQPage comes from the same data as the visible FAQ. Frontmatter faq entries 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.

FeatureSEO effectCaveatUsed on this site?
Static output (default)Complete HTML for every crawlerNone for content sitesYes
Islands (client:*)JS only where neededContent inside an island may need JS to appearNo islands on content pages
@astrojs/sitemapAutomatic sitemap indexNeeds site; no SSR dynamic routesYes
<Image />Sets width and height, requires altOnly for local or authorized remote imagesNo; images are few
<ClientRouter />Smooth navigationScripts may need reinitializingNo
SSR (on-demand)Fresh data per requestSitemap will not list dynamic SSR routesNo

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:

  1. Set site in astro.config.mjs before anything else; the sitemap and canonicals depend on it.
  2. Build one base layout that owns every head tag, and make title and description required props.
  3. Define content collections with a schema so missing metadata fails the build.
  4. Add @astrojs/sitemap, filter out any noindex URLs, and pass real per-page dates into lastmod.
  5. Emit a single JSON-LD @graph with stable @id values, and include the full author node on articles.
  6. Keep content pages free of islands unless interaction is genuinely needed.
  7. 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.

OK

Olga Kunger

Founder & Lead Strategist, Ambeltek

Olga leads Ambeltek's web development, AI SEO, and GEO work — helping brands rank on Google and get cited by AI engines. More about Olga →

Need a site built to rank and get cited?

Fast, structured websites. Launch tier from $5k, live in about two weeks.