Specify a canonical per page / article


I can’t figure out how to add canonical links that actually work with the Kirby multi language setup. Maybe someone can help?


  1. Each article / page has a urlKey
  2. Multi language is enabled
  3. Config with languages is properly setup
// Set languages
c::set('languages', array(
    'code'    => 'en',
    'name'    => 'English',
    'default' => true,
    'locale'  => 'en_US',
    'url'     => '/',
    'code'    => 'de',
    'name'    => 'Deutsch',
    'locale'  => 'de_DE',
    'url'     => '/de',

I have a site with English and the German content:
English: site.com/
German: site.com/de/

The site contains one blog post on the English website and no blog posts on the German post:

Because of Kirby’s auto language fallback, the content from post is also used for post. This causes an issue with SEO ranking. A canonical link could fix it.

site.com/blog/de/post/ should have the canonical

<link rel="canonical" link="site.com/blog/post/".

I have tried to set it up as follows just for quick testing purposes:

 <link rel="canonical" link="<?php echo $site->language()->url() ?>/<?php echo $page->urlKey() ?>"/>

Where urlKey get’s the CORRECT url because the post has it correctly set up as urlKey. There is no germanpost so it automatically grabs the urlKEY from post. Which is correct. BUT, the urlKey does not contain the language code. Therefore each canonical ends up without the original language code of the page request (not the content):


<link rel="canonical" link="site.com/blog/post/".


<link rel="canonical" link="site.com/de/blog/post/".

Where de should not be inserted.

Same error the other way around.

Anyone knows how it fix this?


So essentially you want the Canonical URL in this form regardless of language?

<link rel="canonical" link="site.com/blog/post/"/>

Forgive me if i have missed something but why don’t you just do this:

<link rel="canonical" link="site.com/blog/<?= $page->uid() ?>"/>

You could wrap it in a if template to check for the blog template so you get your dynamic canonicals with language codes on all other pages.


Just to clarify: Do we have to check if a translation exists or do blog posts in general have no counterpart in the non-default language? Do you need this check in general, i.e. also for non-post pages?


For a more general solution:

<link rel="canonical" link="<?= $page->isTranslated()? $page->url(): $page->url($site->defaultLanguage()->code())?>"/>

With this method in a plugin file (for example /site/plugins/page-methods.php)


page::$methods['isTranslated'] = function($page) {
    return $page->content(site()->language()->code())->exists();

This checks if there is a content file in the given language. Note that this will always return true for the default language.

Depending on your use case, @flokosiol’s Translations plugin may be useful.


Thanks for replying!

I think you are right, but I forgot to add something in my question: The /blog/ part is not consistent. I also have /updates/ and other sections.



This part is unfortunately false in my situation. A blog post may be only in German and not in English.

Aside from the Canonical solution, I could also just disable the fallback of language content?


I tested the general solution. It works out of the box for most scenario’s.

It does not work for the situation where the default language is not translated as per your comment.

I think I can fix the situation by using an else if depending on current language.

There is one thing though, that would be nice: Setting the canonical per page manually. The URL-Key can be set manually, do you know if I can change the Canonical manually per page/ blog post? This way I could handle the edge cases.


You can create as many fields as you want, so you might as well create a canonical field. Then use this as basis for your canonical checks.


Awesome. but how can I insert it in the head? I know how to do it on a page, but can I use the same structure in the header?

<?php echo $page->custom_canonical() ?>

When custom canonical is defined… I must overwrite the default canonical from the general solution.

Thanks for your help :smiley:


What do you plan to store in that field? And when? Only for German pages with no English translation?


Let me clarify:

I have

<link rel="canonical" link="<?= $page->isTranslated()? $page->url(): $page->url($site->defaultLanguage()->code())?>"/>


<link rel="canonical" link="<?php echo $page->custom_canonical()"/> 

The first one, is the general solution which works in 99% of the cases.
The custom_canonical now contains my defined canonical: https://site.com/blog/post/ for edge cases.

If I can overwrite the general solution when the custom_canonical is set; then I will have a 100% solution.

<?php if($page->custom_canonical()->isNotEmpty()): ?>
  <link rel="canonical" link="<?php echo $page->custom_canonical() ?>"/>
<?php else: ?>
  <link rel="canonical" link="<?= $page->isTranslated()? $page->url(): $page->url($site->defaultLanguage()->code())?>"/>
<?php endif ?>


Thank you Sonja! Works perfectly.