Skip to content

Canonical uses relative URL

A canonical like <link rel="canonical" href="/about"> is technically invalid. The spec requires canonical hrefs to be absolute URLs. Google may process relative canonicals in some cases, but it can also silently ignore them, which means you get none of the URL consolidation benefit. This is a simple fix with a real impact.

Replace the relative path with the full absolute URL.

The alternates.canonical field accepts a string. Always use the full URL:

app/about/page.tsx
export const metadata = {
alternates: {
canonical: 'https://yourdomain.com/about',
},
}
import Head from 'next/head'
export default function AboutPage() {
return (
<>
<Head>
{/* Wrong */}
{/* <link rel="canonical" href="/about" /> */}
{/* Correct */}
<link rel="canonical" href="https://yourdomain.com/about" />
</Head>
</>
)
}

For static pages, use the full URL directly:

<svelte:head>
<link rel="canonical" href="https://yourdomain.com/about" />
</svelte:head>

For dynamic pages, $page.url.href is always absolute:

<script>
import { page } from '$app/stores'
</script>
<svelte:head>
<link rel="canonical" href={$page.url.href} />
</svelte:head>
pages/about.vue
useHead({
link: [
{ rel: 'canonical', href: 'https://yourdomain.com/about' },
],
})

For dynamic pages, useRequestURL() returns the full absolute URL:

const url = useRequestURL()
useHead({
link: [
{ rel: 'canonical', href: url.href },
],
})

Astro.url is always an absolute URL object. Use it as the fallback when no explicit canonical is passed:

src/layouts/Layout.astro
---
const canonical = Astro.props.canonical ?? Astro.url.href
---
<head>
<link rel="canonical" href={canonical} />
</head>

Check the canonical value in the response HTML:

Terminal window
curl -s https://yourdomain.com/ | grep canonical

The href attribute must start with https:// or http://. Re-run orino audit to confirm the check passes.