Sitearc

Astro SEO: the three config decisions and the layout that emits canonicals

What Astro's site, trailingSlash and output format actually control, plus the layout code that emits a canonical tag.

Answers
Set up an Astro build so its URLs, canonicals and sitemap agree with the host.
Included code
A copyable config, a layout canonical block and a six-step build check
Written for
Solo developers and agencies shipping Astro sites

What Astro does, and what it does not

Astro writes your pages and, with the sitemap integration, a URL set. It does not add a canonical tag to a page for you.

Three settings decide the URLs. Your layout decides the canonical. Both have to agree with the host, or search engines meet two versions of one page.

The three settings

- site            the production origin, used to build absolute urls
- trailingSlash   'never' strips the trailing slash from generated urls
- build.format    'file' writes dist/pricing.html, not dist/pricing/index.html

Here is the config this site builds with, in full.

import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://sitearc.ai',
  trailingSlash: 'never',
  build: { format: 'file' },
  integrations: [
    sitemap({ filter: (page) => page !== 'https://sitearc.ai/brand' }),
  ],
});

The filter is how a page stays out of the URL set. This site keeps one noindex page out of the sitemap that way.

The layout has to emit the canonical

site gives you the origin. The tag comes from your layout, once per page.

---
const path = Astro.url.pathname
  .replace(/\/index\.html$/, '/')
  .replace(/\.html$/, '')
  .replace(/(.)\/$/, '$1') || '/';
const canonical = new URL(path, 'https://example.com').href;
---
<link rel="canonical" href={canonical} />

Two details matter. Strip the file extension, because the build writes pricing.html while the host serves /pricing. And strip one trailing slash, because the host serves the version without it.

What a trailing-slash mismatch costs

If the host redirects /pricing/ to /pricing while your build emits the slash, every canonical points at a redirect.

The pages still load. The problem appears in reports: the URL you asked to be indexed is not the URL that answered.

The same is true in reverse. A host that keeps the slash and a build that strips it produces the same split.

Acceptance: the build is done when

  • dist/<route>.html exists for every route you intend to publish
  • one generated page carries exactly one rel="canonical", on the live origin
  • the canonical equals the URL the host serves, slash for slash
  • every URL in dist/sitemap-0.xml returns 200, with no redirect
  • no noindex route appears in that file
  • a page you excluded by filter is genuinely absent from it

Troubleshooting

Symptom Likely cause Fix
Canonical points at a preview host The layout read Astro.site unset, or a preview env leaked in Set site and rebuild; check one generated file
Sitemap lists a noindex page The filter does not cover it Add the URL to the filter, and rebuild
Sitemap lists /pricing/ but the host serves /pricing trailingSlash disagrees with the host Change the setting, not the tag
Two canonicals on one page The layout emits one and a component emits another Grep the built HTML for rel="canonical"
Every URL in the sitemap redirects build.format or the host’s clean-URL rule Read one URL with a HEAD request

Limits

Nothing here creates content, and none of it changes a ranking.

A canonical describes a preference. It does not merge two pages, and it does not undo a redirect that is already in place.

This page was written from the files named above. The Astro documentation was not reachable from the environment this bundle ran in. Nothing version-specific is asserted beyond what this repository’s own build produced.

Where this came from