To add a canonical URL in Astro, set site in astro.config.mjs and render <link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} /> in the <head> of your base layout. That produces an absolute URL for every page, with query strings and fragments removed, so tracking parameters and duplicate paths all point search engines at one version.
Key Takeaways
- Build the canonical from
Astro.url.pathnameandAstro.site, not fromAstro.urldirectly.sitemust be your production origin, includingwwwif you use it.- Let pages override it with a prop, for syndicated or duplicated content.
- Canonicals should match your
trailingSlashsetting and your internal links.
The basic tag
---
// src/components/SEO.astro
interface Props { canonical?: string }
const canonical = Astro.props.canonical ?? new URL(Astro.url.pathname, Astro.site).href;
---
<link rel="canonical" href={canonical} />
<meta property="og:url" content={canonical} />
Include the component in your base layout’s <head>.
Why not just Astro.url?
Astro.url includes the query string. A visitor arriving from /pricing/?utm_source=newsletter would get a canonical with the tracking parameter, which defeats the purpose. Using only pathname gives /pricing/ every time.
It also matters on preview deploys. Astro.url on a branch preview reflects the preview host; Astro.site is always your configured production origin, so previews still point canonicals at the live site.
Set site correctly
// astro.config.mjs
export default defineConfig({
site: 'https://www.example.com',
});
If you serve the site from both example.com and www.example.com, pick one, redirect the other to it at the host or DNS level, and use the chosen one here. The custom domain tip covers the redirect.
Trailing slashes
With the default build.format: 'directory', pages are served at /about/. Astro.url.pathname reflects the configured trailingSlash and format, so the canonical follows your setup. The rule: your canonical, your internal links and your sitemap should all use the same form. If they disagree, search engines get conflicting signals. The trailing slash tip explains the settings.
Overriding per page
Some pages should point elsewhere:
---
// A post first published on another site
import BaseLayout from '../../layouts/BaseLayout.astro';
---
<BaseLayout
title="Why we moved to Astro"
canonical="https://engineering.example.org/why-we-moved-to-astro/"
>
...
</BaseLayout>
For content collections, add an optional field to the schema and pass it through:
canonical: z.url().optional(),
<BaseLayout title={post.data.title} canonical={post.data.canonical}>
Paginated archives
Each page of a paginated list is its own page and should be self-canonical. /blog/2/ canonicals to /blog/2/, not to /blog/. Pointing every page at page one tells search engines to ignore the posts linked only from later pages. The basic tag above already does the right thing.
Pages you don’t want indexed
A canonical doesn’t stop indexing. For thank-you pages, internal search results or staging, use:
<meta name="robots" content="noindex, follow" />
and leave out the canonical, or keep it self-referencing. Don’t canonical a noindex page to another URL; the signals conflict.
Check it
View source on a few page types (home, a post, a paginated list, a page with query parameters) and confirm the canonical is absolute, uses your production domain, and has no query string. Google Search Console’s URL Inspection shows which canonical Google selected and whether it agrees with yours.
The canonical tag is one item in the Astro SEO checklist, which also has a complete head component. For the Astro.url and Astro.site globals, see the Astro API reference.
FAQ
Does Astro add canonical URLs automatically?
No. Add a <link rel="canonical"> tag yourself, usually in a shared head component in your base layout.
Why is my Astro canonical URL showing localhost?
site isn’t set in astro.config.mjs, or you’re building the canonical from Astro.url during development. Use Astro.site and set it to your production URL.