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>.htmlexists 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.xmlreturns 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.