Removing parent page slug from the URL (Multilanguage and grandchildren)

Hello,

I am referring to the following page in the Kirby Docs:

Sometimes we want to get rid of part of an URL for cleaner URLs, for example to replace the following URL

http://yourdomain.com/blog/my-awesome-article

…with…

http://yourdomain.com/my-awesome-article

This can be achieved with the following two routes in your config.php (or in a plugin):

/site/config/config.php

<?php

return [
    'routes' => [
        [
            'pattern' => '(:any)',
            'action'  => function($uid) {
                $page = page($uid);
                if (!$page) $page = page('blog/' . $uid);
                if (!$page) $page = site()->errorPage();
                return site()->visit($page);
            }
        ],
        [
            'pattern' => 'blog/(:any)',
            'action'  => function($uid) {
                go($uid);
            }
        ]
    ]
];

In my case:

I am working on a multilingual website with a folder called “products”, which contains subpages. The URLs would be website.com/products/product-1, website.com/products/product-2, etc.

I would like the URL to be website.com/product-1 instead, without /products as part of it. This works with the solution provided in the Kirby Docs, but with two limitations:

  1. French version website.de/fr/produits/produit-1 still contains the parent slug. (means that it’s not working with the multilanguage setting)

  2. I would also like it to work with grandchildren, e.g.: website.de/product-1/productfeature

Is there a good solution for this? Ideally one that takes all languages into account, in case others are added besides /fr?

If anyone can help me, I would be very happy. :slight_smile:

Please check out the routing docs regarding multilanguage routing. This should also help with creating routes for the grandchildren.

Please check out the routing docs for multilanguage routing Routes | Kirby CMS

This will also help for the grandchildren routing

Sorry, but I don’t get it… :unamused_face:

At the moment, my solution looks like this. It does work, but I have no idea how to streamline and generalize it and include the children’s children…

[     
  'pattern'  => '/fr',
  'language' => '*',
   'action'   => function($lang) {
     return site()->visit(site()->language($lang->code())->homePage());
  }
],
[
  'pattern'  => 'produkte/(:any)',
  'language' => 'de',
   'action'   => function($lang, $slug) {
       go($slug);
   }
],
[
  'pattern'  => 'produits/(:any)',
  'language' => 'fr',
  'action'   => function($lang, $slug) {
    go($lang->code() . '/' . $slug);
  }
],
[
  'pattern'  => '(:any)',
  'language' => '*',
  'action'   => function($lang, $slug) {
    $page = page($slug);
    if (!$page) {
      $produkteParent = page($lang->code() . '/produkte') ?? page('produkte');
      if ($produkteParent) {
        $page = $produkteParent->children()->find($slug);
      }
    }
    if (!$page) $page = site()->errorPage();
      return site()->visit($page, $lang->code());
    }
],

Unfortunately, you code is not copy/pasteble, so here is a version based on the starterkit:

    'routes' => [
	    [
		    'pattern' => '(:any)',
		    'language' => '*',
		    'action'  => function($lang, $uid) {
			    $page = page($uid);
			
			    if (!$page) {
				    $page = page('notes/' . $uid);
			    }
			
			    if (!$page) {
				    $page = site()->errorPage();
			    }
			
			    return site()->visit($page, $lang->code());
		    }
	    ],
	    [
		    'pattern' => '(:any)/(:any)',
		    'language' => '*',
		    'action'  => function($lang, $uid, $uidChild) {
			    $page = page($uid . '/' . $uidChild);
			
			    if (!$page) {
				    $page = page('notes/' . $uid . '/' .
					    $uidChild);
			    }
			
			    if (!$page) {
				    $page = site()->errorPage();
			    }
			
			    return site()->visit($page, $lang->code());
		    }
	    ],
	    [
		    'pattern' => 'notes/(:all)',
		    'language' => '*',
		    'action'  => function($lang, $uid) {
			    go($lang->code() . '/' . $uid);
		    }
	    ],
    ]

Next time you paste a whole block of code, please use three backticks before and after the code block to get a readable code block, thanks. Here’s what it should look like

three-backticks-76x76

Oh, I’m sorry, I wasn’t aware of that. It’s fixed now!

Thank you so much for the code! Unfortunately it’s still not working. :frowning:

[
      'pattern' => '(:any)',
      'language' => '*',
      'action'  => function($lang, $uid) {
        $page = page($uid);
    
        if (!$page) {
          $page = page('produkte/' . $uid);
        }
    
        if (!$page) {
          $page = site()->errorPage();
        }
    
        return site()->visit($page, $lang->code());
      }
    ],
    [
      'pattern' => '(:any)/(:any)',
      'language' => '*',
      'action'  => function($lang, $uid, $uidChild) {
        $page = page($uid . '/' . $uidChild);
    
        if (!$page) {
          $page = page('produkte/' . $uid . '/' .
            $uidChild);
        }
    
        if (!$page) {
          $page = site()->errorPage();
        }
    
        return site()->visit($page, $lang->code());
      }
    ],
    [
      'pattern' => 'produkte/(:all)',
      'language' => '*',
      'action'  => function($lang, $uid) {
        go($lang->code() . '/' . $uid);
      }
],

Right now I’m getting the following issues:

  1. French not working at all. (example.de/fr, example.de/fr/produits, etc., all 404.)

  2. German works, but subpages of “produkte” now redirect to /de/produkt-1 (404), should be /produkt-1 without the language prefix, because German ist the main language. (Secondary language has a /fr prefix.)

  3. When I type in the URL manually, e.g., example.com/produkt-1, it works, but not the redirect from example.com/produkte/produkt-1 to example.com/produkt-1

  4. Same with subpages, e.g. example.com/produkt-1/sub-produkt, works too.

I’m sorry to bother you again.

I think the language routes don’t work as expected when the language code is missing from the default language. You might have to use different routes per language in that case, not 100% sure and cannot test now.

Hi, I use this and it works:

[
	'pattern' => '(:any)',
	'language' => '*',
	'action'  => function ($lang, $uid) {
		if (page($uid)) {
			return $this->next();
		}
		if ($page = page('produkte/' . $uid)) {
			return $page;
		}
		return false;
	}
],
[
	'pattern' => '(:any)/(:all)',
	'language' => '*',
	'action'  => function ($lang, $uid, $childUid) {
		if($uid === page('produkte')->slug()) {
			return false;
		}
		if (page($uid . '/' . $childUid)) {
			return $this->next();
		}
		if ($page = page('produkte/' . $uid . '/' . $childUid)) {
			return $page;
		}
		return false;
	}
]

I also added this in default model (default.php in models folder) so the $page→url() returns the new URL without parent:

class DefaultPage extends Page
{
	public function urlForLanguage($language = null, array|null $options = null): string
	{
		if ($options !== null) {
			return Url::to($this->urlForLanguage($language), $options);
		}

		if ($this->isHomePage() === true) {
			return $this->url = $this->site()->urlForLanguage($language);
		}

		if ($parent = $this->parent()) {
			if ($parent->isHomePage() === true) {
				return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language);
			}

			// Remove produkte page slug
			$slug = page('produkte')->slug($language) . '/';
			$this->url = str_replace($slug, '', $this->parent()->urlForLanguage($language) . '/' . $this->slug($language));
			return $this->url;
		}

		return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
	}

}

Whoop, whoop! I think I’ve solved it. Thank you so much @sly31 !

My final solution looks like this:

models/default.php

class DefaultPage extends Page
{
	public function urlForLanguage($language = null, array|null $options = null): string
	{
		if ($options !== null) {
			return Url::to($this->urlForLanguage($language), $options);
		}

		if ($this->isHomePage() === true) {
			return $this->url = $this->site()->urlForLanguage($language);
		}

		if ($parent = $this->parent()) {
			if ($parent->isHomePage() === true) {
				return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language);
			}

			// Remove produkte page slug
			$slug = page('produkte')->slug($language) . '/';
			$this->url = str_replace($slug, '', $this->parent()->urlForLanguage($language) . '/' . $this->slug($language));
			return $this->url;
		}

		return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
	}

}

Routes in site/config.php:

  [
    'pattern' => '/fr',
    'language' => '*',
    'action' => function($lang) {
        return site()->visit(site()->language($lang->code())->homePage());
      }
  ],
  [
    'pattern' => '(:any)',
    'language' => '*',
    'action'  => function ($lang, $uid) {
      if (page($uid)) {
        return $this->next();
      }
      if ($page = page('produkte/' . $uid)) {
        return $page;
      }
      return false;
    }
  ],
  [
    'pattern' => '(:any)/(:any)',
    'language' => '*',
    'action'  => function ($lang, $uid, $childUid) {
      if (page($uid . '/' . $childUid)) {
        return $this->next();
      }
      if ($uid === page('produkte')->slug($lang)) {
        return $this->next();
      }
      if ($page = page('produkte/' . $uid . '/' . $childUid)) {
        return $page;
      }
      return $this->next();
    }
  ],

I think that route is not needed:

[ 'pattern' => '/fr', 'language' => '*', 'action' => function($lang) { return site()->visit(site()->language($lang->code())->homePage()); } ]

Kirby will make it works by default if your language is correctly configured.

I tested it without it and unfortunately it only works with the additional route for the French home page. Everything else works fine. No idea why. :thinking: