Skip to content

fix(seo): use Bolt's canonical URL as the og:url fallback - #63

Merged
Fredxd merged 1 commit into
AppoloDev:mainfrom
Vondry:fix/canonical-respect-bolt-settings
Oct 23, 2026
Merged

Fredxd merged 1 commit into
AppoloDev:mainfrom
Vondry:fix/canonical-respect-bolt-settings

Conversation

@Vondry

@Vondry Vondry commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #32

Summary

When nothing sets a canonical explicitly, Seo::canonical() fell back to the raw request URL ($request->getUri()). That value feeds og:url and twitter:url, so those tags:

  • ignored localization.remove_default_locale_on_canonical (this is what remove_default_locale_on_canonical option is ignored #32 reports),
  • ignored the configured general/canonical host,
  • kept the query string, e.g. /page/foo?page=2&utm_source=newsletter,
  • disagreed with the <link rel="canonical"> that Bolt itself renders on the same page.

Change

The final fallback now uses Bolt's public canonical() Twig function (Bolt\Twig\HtmlExtension::canonical()). This is the same Bolt\Canonical service that renders Bolt's <link rel="canonical">. Seo already holds the Twig environment, so it calls the function through that. The Bolt\Canonical service itself is private, and in a real install it isn't available from the container when the extension initializes. The request URL is kept as a last resort for requests without a matched route.

The resolution order is otherwise unchanged:

  1. override_default.<route>.canonical
  2. The record's SEO field
  3. default.canonical
  4. Bolt's canonical URL ← new
  5. Request URL

Backwards compatibility

  • No constructor or config changes.
  • Explicitly configured canonicals (override, SEO field, default.canonical) are unaffected. I verified this on a real site.
  • The only output change is that og:url and twitter:url now match <link rel="canonical"> when no canonical was set explicitly. Anything Bolt puts in its canonical URL now appears in og:url as well, for example ?order=…&status=… on listing pages, which bolt/core#3735 removes.

Manual verification (Bolt 6.1.9)

I tested this on a fresh bolt/project install with the demo fixtures, entries made localized (en, nl), and products on a custom /shop/{slugOrId} route. Bolt caches canonical links for 60 seconds in cache.app, which cache:clear doesn't empty, so I ran cache:pool:clear cache.app before each check. The table compares og:url with Bolt's <link rel="canonical"> on the same page.

Requested URL og:url before og:url after (= canonical)
/page/this-is-a-page?utm_source=newsletter&page=2 …?page=2&utm_source=newsletter ❌ /page/this-is-a-page ✅
/en/entry/… (default locale) /en/entry/… ❌ /entry/… ✅
/nl/entry/… /nl/entry/… ✅ /nl/entry/… ✅
/pages (listing) /pages ❌ /pages?order=id&status=published ✅
/page/does-not-exist (404) /page/does-not-exist ❌ /block/404-not-found ✅
/page/…, /, /shop/… ✅ ✅

With the change, og:url matches <link rel="canonical"> on all 9 URL types I checked, with remove_default_locale_on_canonical both true and false, and with general/canonical set to another host. Before the change, 4 of 9 differed with the setting on and 6 of 9 with it off. A canonical set in a record's SEO field still takes precedence for og:url.

Tests

  • testCanonicalFallsBackToBoltCanonical: Bolt's canonical URL is used when nothing else sets one. It fails without the fix.
  • testCanonicalPrefersSeoFieldOverBoltCanonical: an explicit SEO field value still wins.
  • testCanonicalFallsBackToRequestUriWhenBoltCanonicalHasNoRoute: the request URL is used when Bolt has no canonical.

The suite passes (115 tests), and PHPStan is clean.

When no override, SEO field or `default.canonical` sets a canonical,
Seo::canonical() returned the raw request URI. og:url and twitter:url
therefore ignored `remove_default_locale_on_canonical` and
`general/canonical`, kept the query string, and disagreed with the
<link rel="canonical"> that Bolt renders.

Fall back to Bolt's public `canonical()` Twig function instead, which is
what Bolt's own canonical link uses. Bolt's Canonical service is private,
so it can't be fetched from the container at extension initialization.
The request URI is kept as the last resort for requests without a matched
route.

Fixes AppoloDev#32
@Vondry
Vondry force-pushed the fix/canonical-respect-bolt-settings branch from 3da6257 to e6225a9 Compare October 23, 2026 12:49
@Fredxd
Fredxd merged commit 4d3f718 into AppoloDev:main Oct 23, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remove_default_locale_on_canonical option is ignored

2 participants