# canonical and og:url Must Agree

**MUST** · **ID:** `design-canonical-og-agreement` · **Category:** design
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-canonical-og-agreement

> MUST: `canonical` must point at the page's preferred URL and `og:url` must be that same string. When they disagree (`/blog/post` vs `/blog/post?utm_source=x`) the page identifies itself differently depending on who asks: every share links to the tracked variant, so the authority accrues to a URL the page itself disowns, and the crawler gets a duplicate to reconcile. Compute the canonical once and feed the same variable to both tags. Campaign params belong in the link you hand out, never in the tags a page declares about itself.

The URL a page declares as canonical and the URL it hands to social crawlers must be the same string, or one page becomes two

From the canonical/indexing and social cards rules in ibelick's fixing-metadata skill. These are two tags in different sections of the head, usually written by different people at different times, and they quietly answer the same question: which URL IS this page? When they disagree — canonical says `/blog/post`, og:url says `/blog/post?ref=twitter&utm_source=x` — you have shipped a page that identifies itself differently depending on who is asking. Everyone who shares it links to the tracked variant, so the authority those shares earn accrues to a URL that the page itself disowns, and the crawler is handed a duplicate to reconcile that it did not need. The failure is not loud: both URLs work, both render, and the split only shows up later as a page that never quite ranks. The fix is mechanical — compute the canonical once and feed the same variable to both tags, so drift becomes impossible rather than merely unlikely. Campaign parameters belong in the link you hand out, never in the tags a page declares about itself.

## Bad — do not do this

`design-canonical-og-agreement-bad`

```tsx
function ShareCard({ url, label }: { url: string; label: string }) {
  return (
    <div className="overflow-hidden rounded-lg border border-border">
      <div className="flex h-14 items-center justify-center bg-muted">
        <span className="text-xs text-muted-foreground">{label}</span>
      </div>
      <div className="bg-background p-2">
        <p className="truncate text-xs text-muted-foreground">{url}</p>
        <p className="text-xs font-medium text-foreground">Shipping fast — MyApp Blog</p>
      </div>
    </div>
  );
}

export function CanonicalOgAgreementBad() {
  return (
    <div className="w-full space-y-4">
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1">
          <p className="text-xs font-semibold text-foreground">What the crawler indexes</p>
          <ShareCard label="canonical" url="myapp.com/blog/post" />
        </div>
        <div className="space-y-1">
          <p className="text-xs font-semibold text-foreground">What the share links to</p>
          <ShareCard label="og:url" url="myapp.com/blog/post?ref=twitter&utm_source=x" />
        </div>
      </div>

      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs text-foreground"><code>{`<link rel="canonical" href="https://myapp.com/blog/post" />
<meta property="og:url"
      content="https://myapp.com/blog/post?ref=twitter&utm_source=x" />`}</code></pre>

      <p className="text-xs text-error">
        One page, two URLs. Every share points at the tracked variant, so the links that page earns
        accrue to a URL the canonical disowns — and the crawler now has a duplicate to reconcile.
      </p>
    </div>
  );
}
```

## Good — do this

`design-canonical-og-agreement-good`

```tsx
function ShareCard({ url, label }: { url: string; label: string }) {
  return (
    <div className="overflow-hidden rounded-lg border border-border">
      <div className="flex h-14 items-center justify-center bg-accent">
        <span className="text-xs text-accent-foreground">{label}</span>
      </div>
      <div className="bg-background p-2">
        <p className="truncate text-xs text-muted-foreground">{url}</p>
        <p className="text-xs font-medium text-foreground">Shipping fast — MyApp Blog</p>
      </div>
    </div>
  );
}

export function CanonicalOgAgreementGood() {
  const url = 'myapp.com/blog/post';

  return (
    <div className="w-full space-y-4">
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1">
          <p className="text-xs font-semibold text-foreground">What the crawler indexes</p>
          <ShareCard label="canonical" url={url} />
        </div>
        <div className="space-y-1">
          <p className="text-xs font-semibold text-foreground">What the share links to</p>
          <ShareCard label="og:url" url={url} />
        </div>
      </div>

      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs text-foreground"><code>{`const canonical = \`\${SITE_URL}/blog/post\`; // one value, two tags

<link rel="canonical" href={canonical} />
<meta property="og:url" content={canonical} />`}</code></pre>

      <p className="text-xs text-success">
        Both tags read the same variable, so they cannot drift. Keep campaign parameters in the link
        you hand out, never in the tags the page declares about itself.
      </p>
    </div>
  );
}
```

## References

- [fixing-metadata (skill source)](https://raw.githubusercontent.com/ibelick/ui-skills/main/skills/fixing-metadata/SKILL.md)
- [Google: Consolidate duplicate URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
- [The Open Graph protocol](https://ogp.me/)
