Skip to content
Astro as seo 4 min read

SEO in Astro

Search engines reward pages that are fast, crawlable, and semantically clear — and Astro hits all three almost by accident. Because Astro renders to static HTML by default and ships zero JavaScript unless you opt in, crawlers receive fully-formed content on the first request with no client-side rendering to wait on. That foundation makes the rest of SEO — meta tags, canonical URLs, sitemaps, and structured data — straightforward to layer on top.

Why static-first output wins for SEO

The single biggest SEO advantage in Astro is that the markup a crawler sees is the markup you authored at build time. There is no hydration step required to reveal headings, copy, or links. Googlebot does execute JavaScript, but it does so on a deferred, best-effort basis — content gated behind client-side rendering can be indexed late or inconsistently. Astro’s islands architecture flips this: the page body is plain HTML, and only the interactive bits ship JS.

Fast HTML also improves Core Web Vitals (LCP, CLS, INP), which are confirmed ranking signals. A page that paints meaningful content immediately and stays stable while loading scores well without special effort.

The fastest, most indexable page is one that is already complete in the HTML response. Keep your titles, descriptions, headings, and primary links in static markup — never behind a client:* directive.

Meta tags in the head

Every page needs a unique <title> and <meta name="description">. In Astro these live in the component template’s <head>, and because .astro files support expressions, you can drive them from props or content collection data.

---
const { title, description, canonical } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{title}</title>
  <meta name="description" content={description} />
  <link rel="canonical" href={canonical ?? canonicalURL.href} />

  <!-- Open Graph for social sharing -->
  <meta property="og:title" content={title} />
  <meta property="og:description" content={description} />
  <meta property="og:type" content="website" />
  <meta property="og:url" content={canonicalURL.href} />
</head>

Astro.site comes from the site field in astro.config.mjs, so canonical and Open Graph URLs resolve to absolute paths. Set it once and every page benefits.

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://example.com",
});

Sitemaps with the official integration

A sitemap tells crawlers exactly which URLs exist and how recently they changed. Astro’s first-party integration generates one automatically from your built routes.

npx astro add sitemap
// astro.config.mjs
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";

export default defineConfig({
  site: "https://example.com",
  integrations: [sitemap()],
});

After a build, the integration writes sitemap-index.xml and sitemap-0.xml into dist/. Reference them from robots.txt so search engines discover the index.

User-agent: *
Allow: /
Sitemap: https://example.com/sitemap-index.xml

Structured data with JSON-LD

Structured data (schema.org) helps search engines understand the meaning of a page and can unlock rich results — article bylines, breadcrumbs, FAQ accordions. The recommended format is JSON-LD embedded in a <script type="application/ld+json">. In Astro, serialize an object and inject it with set:html.

---
const { post } = Astro.props;
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: post.data.title,
  datePublished: post.data.pubDate.toISOString(),
  author: { "@type": "Person", name: post.data.author },
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />

Because this runs at build time, the JSON-LD is present in the static HTML — exactly where crawlers look for it, with no client JS involved.

Comparing the building blocks

TechniqueWhat it doesWhere it livesTooling
Meta tagsTitle, description, canonical, Open Graph<head> in a layoutNative .astro
SitemapLists all indexable URLsdist/sitemap-*.xml@astrojs/sitemap
Structured dataMachine-readable page meaningJSON-LD <script>Native, schema.org
PerformanceCore Web Vitals ranking signalsBuild output / islandsAstro defaults

Centralize SEO in a layout

Repeating head tags on every page invites drift. Put them in a shared layout that accepts SEO props, and pass page-specific values in. Content collections pair perfectly with this — schema-validated frontmatter feeds the layout’s title and description.

---
import BaseHead from "../components/BaseHead.astro";
const { frontmatter } = Astro.props;
---
<html lang="en">
  <head>
    <BaseHead title={frontmatter.title} description={frontmatter.description} />
  </head>
  <body>
    <slot />
  </body>
</html>

Best Practices

  • Set site in astro.config.mjs so canonical and Open Graph URLs are absolute and correct.
  • Give every page a unique, descriptive <title> and <meta name="description"> — never reuse boilerplate.
  • Keep titles, headings, copy, and primary navigation in static HTML; never hide SEO-critical content behind client:* hydration.
  • Add @astrojs/sitemap and link the generated index from robots.txt.
  • Emit a single canonical URL per page to avoid duplicate-content penalties.
  • Add JSON-LD structured data at build time with set:html for articles, products, and breadcrumbs.
  • Validate output with Google’s Rich Results Test and run Lighthouse to confirm Core Web Vitals stay green.
Last updated June 14, 2026
Was this helpful?