Canonical URLs & trailing slashes – why no global URL generation setting?

Howdy :waving_hand:

I believe this topic has been touched multiple times before, but I don’t think it has ever been resolved clearly or extensively, so I’d like to raise it again from a slightly broader perspective.

1. URL generation vs URL enforcement

At the moment, $page->url() always produces URLs without a trailing slash:

/blog/my-post

There is no configuration option to globally control whether Kirby should generate URLs with or without trailing slashes.

This is where things start to feel awkward:

  • Kirby generates URLs without a slash
  • users who prefer trailing slashes are expected to enforce them at the server level (e.g. .htaccess)
  • this results in internal links that always redirect (200 → 301 → 200)

In my opinion, generating URLs in one format and enforcing another format via redirects is a poor practice, especially when this happens on every internal link.

I am fully aware of server-level enforcement and the official article here:
https://getkirby.com/docs/quicktips/trailing-slash
This is not about how to enforce — it’s about URL generation being configurable in the first place.

2. CMS-level enforcement would also be welcome

Closely related: it would be very nice if Kirby could optionally enforce the chosen URL format (slash / no slash) at the CMS level as well.

I understand the argument that redirects belong to the web server — but when Kirby already defines the canonical shape of URLs internally, having an optional CMS-level enforcement would be a big DX improvement.

3. Canonical links as partial mitigation

Another common workaround is adding a canonical link tag, which does help with SEO duplication to some extent.

However, this leads to another question:

4. Canonical URL generation with current Kirby setup

What is the recommended elegant way to generate canonical URLs in Kirby?

Example:

http://localhost:8000/blog/category:hello-world/?foo=bar

Calling $page->url() in this context returns:

http://localhost:8000/blog

Which is technically correct for the page — but often not what you want as a canonical when params or filters are involved.

Is there a recommended way to generate canonicals that:

  • respect the desired trailing-slash convention
  • handle params / query strings predictably
  • don’t require custom helper functions sprinkled across templates?

5. Kirby’s own website behavior

I also noticed that https://getkirby.com/ itself does not enforce trailing slash rules via redirects, but it does use canonical tags.

Would you be willing to share:

  • how canonical URLs are generated internally on the Kirby website?
  • whether this is considered the “preferred” approach by the core team?

6. Core concern (the main point)

The biggest missing piece, in my view, is simply this:

There is no way to configure how Kirby generates URLs globally (with or without trailing slashes).

Given that $page->url() is a core primitive used everywhere, this feels like something that should be configurable — especially since overriding it cleanly is currently not possible.


Thanks for reading, and thanks for Kirby in general — this isn’t meant as a complaint, but as a genuine DX and architectural question. I’d love to hear the team’s perspective on whether this is something that could (or should) be addressed at the core level.

:folded_hands:

This has been a frustration for us too, honestly. We’ve built a handful of sites for clients using Kirby and the trailing slash question always comes up.

What I’ve been doing for canonicals is a small helper in the site snippet:

function canonicalUrl() {
    return rtrim(page()->url(), '/');
}

Then in the head template: <link rel="canonical" href="<?= canonicalUrl() ?>">. That at least ensures our canonical tags are consistent regardless of what the server does with trailing slashes. It doesn’t solve the redirect chain issue you describe, but it does fix the SEO duplication problem.

For the server-side piece on Apache we add a rewrite rule to strip trailing slashes (or add them, depending on what the client wants), and then $page->url() without a trailing slash matches what the server enforces. Not elegant, agreed – but it works reliably.

Your point about URL generation being configurable is fair. If you’re using nginx, you can often avoid the redirect chain entirely by having the server accept both formats to the same resource without a redirect, and let the canonical tag handle deduplication for search engines. Less clean semantically but very practical.

The route parameter / query string situation you mention in point 4 is the one I don’t have a clean answer for – we just strip params before outputting the canonical in those cases, which may not always be right for paginated content or filters.

Thanks for sharing this — that pretty much matches our experience as well.

I guess the honest conclusion is that, for now, we have to settle for solutions that are good enough on a per-project basis: a mix of server rules, canonicals, and small helpers, depending on what hurts least in that particular setup. It works, but it’s definitely not elegant.

The params / filters case is exactly where this starts to feel brittle, and where a first-class way to control URL generation (with or without slashes) would really help.

Anyway, appreciate you taking the time to write this up — it’s good to know we’re not the only ones running into this.

:man_shrugging: