# Astro SEO: fast by default, and the layout work that remains

You picked Astro for speed and control. Good choice. Here is what Astro gives you for SEO, what it does not, and the layout pattern that closes the gaps.

Updated 2026-09-14 · Source: https://porteur.ai/guides/astro-seo

## What Astro gives you for SEO

Astro renders static HTML by default. Crawlers see your content without running JavaScript. That helps indexing.

It ships zero JavaScript by default. You opt in per island. That keeps pages light and fast when you avoid heavy client code.

It has built-in image optimisation. You can output responsive images with width, quality and format set at build time. That helps LCP and CLS.

It has content collections. You can type front matter and enforce fields across posts or docs. That keeps metadata consistent.

There is an official sitemap integration: @astrojs/sitemap. You add it to your config. It builds your XML sitemap at build time.

## What Astro does not ship

Astro has no built-in SEO layer. There is no automatic title or description system. You write them in your layout head.

There is no automatic canonical tag. You add a canonical link in your layout. It should point to the preferred URL for each page.

There is no sitemap unless you add @astrojs/sitemap. Install it and set your site URL. Without it, Search Console has to find pages by crawl alone.

There is no default robots.txt. You place a robots.txt file in public if you need one. Check the platform's current settings if in doubt.

> Rule: do not fetch and render primary content only on the client. Keep main content in the HTML Astro sends.

## The base layout for titles and descriptions

Put your SEO logic in one layout. Every page uses it. You pass a title and a description in front matter or props.

```js
---
// src/layouts/BaseLayout.astro
export interface Props {
  title?: string
  description?: string
  canonicalPath?: string // e.g. '/pricing'
  noindex?: boolean
  ogImage?: string
}

const {
  title: pageTitle,
  description: pageDescription,
  canonicalPath,
  noindex = false,
  ogImage
} = Astro.props as Props

const site = Astro.site?.toString().replace(/\/$/, '') || '' // from astro.config.mjs

// Compose the full title: 'Page Title | Your Product'
// Keep titles around 60 characters
const siteName = 'Your Product'
const title = pageTitle ? `${pageTitle} | ${siteName}` : siteName

// Meta description around 120 to 155 characters
const description = pageDescription || 'Short description of your product or page.'

const canonical = canonicalPath ? `${site}${canonicalPath}` : `${site}${Astro.url.pathname}`
---
<html lang="en">
  <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} />

    {noindex && <meta name="robots" content="noindex, nofollow" />}

    {/* Open Graph and Twitter for social cards */}
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonical} />
    {ogImage && <meta property="og:image" content={ogImage} />}
    <meta name="twitter:card" content="summary_large_image" />
  </head>
  <body>
    <slot />
  </body>
</html>

```

Set Astro.site in astro.config.mjs. The sitemap integration needs it. Your canonical tag uses it too.

```js
// astro.config.mjs
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'

export default defineConfig({
  site: 'https://yourproduct.com',
  integrations: [sitemap()],
})

```

Use the layout in pages. You pass page-specific fields. Keep the pattern the same across the site.

```js
---
// src/pages/pricing.astro
import BaseLayout from '../layouts/BaseLayout.astro'
---
<BaseLayout
  title="Pricing"
  description="Pick a plan. Start in minutes."
  canonicalPath="/pricing"
  ogImage="https://yourproduct.com/og/pricing.png"
>
  <main>
    <h1>Pricing</h1>
    <!-- Content -->
  </main>
</BaseLayout>

```

## A repeatable pattern for pages and posts

You want the same fields for every page type. Titles, descriptions, canonical, publish date, modified date, author for posts. One place to render them.

```js
---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro'

export interface Frontmatter {
  title: string
  description: string
  pubDate: string // ISO
  updatedDate?: string // ISO
  author?: string
  tags?: string[]
  ogImage?: string
}

const { frontmatter } = Astro.props as { frontmatter: Frontmatter }
---
<BaseLayout
  title={frontmatter.title}
  description={frontmatter.description}
  canonicalPath={Astro.url.pathname}
  ogImage={frontmatter.ogImage}
>
  <article>
    <header>
      <h1>{frontmatter.title}</h1>
      <p>
        <time dateTime={frontmatter.pubDate}>{new Date(frontmatter.pubDate).toLocaleDateString()}</time>
        {frontmatter.updatedDate && (
          <span> · Updated <time dateTime={frontmatter.updatedDate}>{new Date(frontmatter.updatedDate).toLocaleDateString()}</time></span>
        )}
      </p>
    </header>
    <slot />
  </article>
</BaseLayout>

```

A fixed page like /about can use BaseLayout direct. A blog post uses PostLayout. The head stays consistent, the content differs by slot content.

## Content collections keep metadata typed

Use content collections for posts, guides or changelogs. You define a schema. Astro validates front matter at build time. Bad entries fail the build, which is good for SEO hygiene.

```js
// src/content/config.ts
import { defineCollection, z } from 'astro:content'

const posts = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string().max(155),
    pubDate: z.string(),
    updatedDate: z.string().optional(),
    author: z.string().default('Team'),
    tags: z.array(z.string()).default([]),
    ogImage: z.string().url().optional(),
  }),
})

export const collections = { posts }

```

```
---
// src/content/posts/getting-started.md
// Front matter must match the schema
Title: Getting started with Your Product
Description: A simple guide to set up Your Product and ship your first task.
PubDate: 2026-07-12
UpdatedDate: 2026-09-13
Tags: ["setup", "guide"]
OgImage: https://yourproduct.com/og/guides/getting-started.png
---

## First steps

Content here.

```

Render posts with a route like /guides/[slug].astro. Pull entries from collections and pass their front matter to PostLayout. Every post gets a valid title and description by design.

## Sitemap and canonical URLs

Add @astrojs/sitemap. Set site in the config. The integration crawls your routes at build time and writes sitemap.xml. Submit that in Search Console.

1. **Install the integration** npm i @astrojs/sitemap
2. **Enable it in astro.config.mjs** import sitemap from '@astrojs/sitemap' and add sitemap() to integrations. Set site: 'https://yourproduct.com'.
3. **Exclude routes you do not want indexed** Pass options like exclude: ['/drafts/**'] if needed.
4. **Build and check the output** Run the build. Open dist/sitemap.xml. Check URLs, lastmod and priority if you set them.
5. **Submit in Search Console** In Search Console, open Sitemaps, add https://yourproduct.com/sitemap.xml, and watch the status.

Set one canonical per page. Point to the preferred URL, with or without trailing slash, and the right casing. Avoid query strings in canonicals unless the parameter defines content.

```js
// In BaseLayout.astro, already shown
<link rel="canonical" href={canonical} />

```

Example: your /guides/getting-started page must canonicalise to https://yourproduct.com/guides/getting-started. Avoid duplicates at /guides/getting-started/ or with ?ref parameters. Redirect stray variants at the host level too.

## Robots and indexing controls

Serve a robots.txt from public/robots.txt. Disallow drafts, private routes or staging hosts. Keep the production site crawlable. Add a reference to your sitemap URL.

```plaintext
# public/robots.txt
User-agent: *
Disallow: /drafts/
Allow: /
Sitemap: https://yourproduct.com/sitemap.xml

```

For specific pages, add a robots meta. Use noindex on thin or temporary pages. Example: /changelog/2021-migration-test should not index. Pass noindex to the layout on that page only.

## Images and speed on Astro

Use Astro’s image component for hero images. Optimise width, format and quality. Aim for Largest Contentful Paint under 2.5 seconds as of 2026.

```js
---
import { Image } from 'astro:assets'
import hero from '../assets/hero.png'
---
<Image src={hero} alt="Dashboard screenshot" widths={[512, 768, 1024, 1280]} sizes="(max-width: 768px) 100vw, 1280px" format="webp" quality={70} loading="eager" />

```

Prevent layout shift. Set explicit width and height where you can. Keep Cumulative Layout Shift under 0.1. Avoid CSS that moves content after load.

Keep client JavaScript small. Hydrate only what you need. Target Interaction to Next Paint under 200 ms. Heavy islands and third-party scripts will slow it down.

Test with PageSpeed Insights. Field data shows Core Web Vitals from the last 28 days when there is traffic. Lighthouse in the same tool is one lab run and can vary.

## Structured data: JSON-LD in your layouts

Add JSON-LD in the head for rich results. Start with Organization on the home page, then Article for posts. Keep it in the layout so it stays consistent.

```js
---
// In PostLayout.astro head section, after meta tags
const articleLd = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: frontmatter.title,
  description: frontmatter.description,
  datePublished: frontmatter.pubDate,
  dateModified: frontmatter.updatedDate || frontmatter.pubDate,
  author: frontmatter.author ? { '@type': 'Person', name: frontmatter.author } : undefined,
  mainEntityOfPage: canonical,
}
---
<script type="application/ld+json">{JSON.stringify(articleLd)}</script>

```

Validate with the Rich Results Test. Fix missing required fields. Example: an Article needs a headline and a datePublished to qualify for some features as of 2026. If you are not sure, keep the schema small and accurate.

## Navigation and internal links on a static site

Link your key pages from the navigation and the footer. Keep click depth low. Surface /pricing, /docs and your top guides from the home page content too.

Create related links in layouts. On a post page, link to the parent guide and two child pages. Use plain anchors, not client-side routers, unless you need them. Crawlers follow normal links just fine.

## The Astro SEO checklist

- Set site in astro.config.mjs to your HTTPS root
- Install and configure @astrojs/sitemap, then submit sitemap.xml in Search Console
- Create a BaseLayout that sets title, meta description, canonical and social cards
- Pass canonicalPath or compute it from Astro.url.pathname in layouts
- Add robots.txt in public with a link to the sitemap and no Disallow for live routes
- Add noindex on drafts and thin pages only
- Use content collections with a schema that caps description at 155 characters
- Render JSON-LD for Organization on / and Article on posts
- Use astro:assets to optimise hero and product images, set sizes and formats
- Avoid client-only rendering of primary content
- Hydrate only the components that need interactivity
- Keep LCP under 2.5 s, CLS under 0.1 and INP under 200 ms as of 2026
- Add internal links to key pages from the home page and from related content
- Test titles are within about 60 characters and read well on mobile
- Use trailing slashes or not, but be consistent, redirect the other form and set canonicals
- Check PageSpeed Insights for field data, then fix slow URLs first

A fixed page like /pricing should load fast, with a clear H1, a concise title, a description around 120 to 155 characters and a canonical to the clean URL. A blog post should show dates, an author, next steps and valid Article JSON-LD. Both should sit in your sitemap and be linked from somewhere useful.

## Questions

### Is Astro good for SEO?

Yes. Astro renders static HTML and ships no JavaScript by default. Crawlers can read your content fast. You still need to add metadata, canonicals, a sitemap and structured data in your layouts.

### Do I need an SEO integration or plugin for Astro?

There is no all-in-one SEO plugin. Use @astrojs/sitemap for the sitemap. Put titles, descriptions, canonical and robots meta in your base layout. Add JSON-LD by hand. This gives you full control.

### How do I add a sitemap in Astro?

Install @astrojs/sitemap, set site in astro.config.mjs and build. The integration writes sitemap.xml. Submit it in Search Console. You can exclude routes you do not want listed.

### Where do I set the meta title and description in Astro?

In your layout’s head. Pass title and description from pages or content collections. Keep the pattern in one BaseLayout so you do not repeat yourself across pages.

### How do I set canonical URLs in Astro?

Compute them from Astro.site and the page path. Set a link rel="canonical" in your layout. Keep the format consistent across the site and redirect stray variants on the server.

### What slows an Astro site down?

Large images, heavy client islands and third-party scripts. Optimise images with astro:assets. Hydrate only what you need. Watch Core Web Vitals. Test and trim any marketing scripts that block input.

## Read next

- [On-page SEO checklist: the twelve things on the page](https://porteur.ai/guides/on-page-seo-checklist): The twelve page elements that move clicks and rankings, with examples you can copy to your own product pages today.
- [Technical SEO checklist for a small site](https://porteur.ai/guides/technical-seo-checklist): Run these 20 technical SEO checks, in order. Each shows how to check it free and what fixed looks like for a site under 1,000 pages.
- [The Sitemaps report: what Success, Has errors and Couldn’t fetch mean](https://porteur.ai/guides/search-console-sitemaps-report): How to submit your sitemap URL, read Success, Has errors and Couldn’t fetch, and fix invalid XML, 404s, cross‑host URLs and missing child sitemaps.
- [Canonical tags: what they do and the mistakes that cost rankings](https://porteur.ai/guides/canonical-tag): What a canonical tag does, when to use one, the mistakes that cost rankings, and how to check and fix canonicals on a small site.
- [Core Web Vitals for a product site: the three numbers](https://porteur.ai/guides/core-web-vitals): The product founder’s guide to LCP, CLS and INP: what Google measures, why phones decide the pass, common causes, and how to test and fix.
- [Image optimisation for page speed: the hero image and everything below it](https://porteur.ai/guides/image-optimisation-for-page-speed): Fix the hero image first for LCP, then lazy-load the rest. Choose AVIF or WebP, set srcset and sizes, and add width and height to stop shift.
- [Docusaurus SEO: docs that rank and versions that do not fight](https://porteur.ai/guides/docusaurus-seo): Set up Docusaurus SEO the right way: front matter titles and descriptions, sitemap config, version canonicals, i18n, search, and llms.txt for docs.
- [Hugo SEO: what the templates must do, since nothing is automatic](https://porteur.ai/guides/hugo-seo): Hugo does nothing automatic for SEO. This guide shows the head partial, sitemap, robots, canonicals, taxonomies and images you must template.

Paste your home page URL to see where your Astro site stands: the free check reads your site, the searches around it and rivals on them in about thirty seconds and shows three findings whole. Free check: https://porteur.ai/
