Redirect to first child page and show the child page url

Hi,

I think I could use some help with Kirbys routing.

Some parent pages in my project do not hold individual content and I need them to get redirected to their first child page.

The following router config basically works but does not change to the child page url. It always remains the url of the parent page. Also, I’d like to have this behaviour for all internal links (page(‘parent’)->url() should output the first child url) to a parent page.

  [
    'pattern' => '(:any)',
    'language' => '*',
    'action'  => function($language, $id) {
      $page = page($id);

      if ($page->hasListedChildren()) {
        if ($firstChild = $page->children()->listed()->first()) {
          return site()->visit($firstChild, $language);
        }
      }

      $this->next();
    }
  ]

How would you solve it? Is there a more elegant way to do it?

Thanks for your help
René

You should override the URL method of the parent page in a page model.

Thanks for your quick reply.

That’s what I‘m trying now:

<?php

class DefaultPage extends Page {
  public function url($options = null): string {
    if ($this->hasListedChildren()) {
      if ($firstChild = $this->children()->listed()->first()) {
        return $firstChild->url();
      }
    }

    return parent::url();
  }
}

Unfortunately this does not affect any of the links. The name of my template being used is default, so that should not be the issue. Also, I put the model in site/models/default.php.

Looks like a default model doesn’t work.

With a different model, however, your code gives me a maximum nesting error on Kirby 3.4, which could be fixed using

 return site()->url() . '/' . $firstChild->id();

instead.

Thank you, my model is working now. I had to match the content file names to that of my model (default.de.md). Before that they all had different names with none matching a template or model. Didn’t know that this makes a difference in kirbys behaviour.

Now to my new issues with the router :slight_smile:

  [
    'pattern' => '(:any)',
    'language' => '*',
    'action'  => function($id) {
      if ($page = page($id) && $page->hasListedChildren()) {
        if ($firstChild = $page->children()->listed()->first()) {
          return $firstChild;
        }
      }

      $this->next();
    }
  ]

For most of my pages with listed children it works but for some not. And I have no idea what to look for because the structure is all the same. The redirect just does not happen and the parent page is shown. What could be the reason for this?

Are all pages that should listen to the route on the first level?

Yes, only first level pages should be affected by this route.

You need a pair of parentheses around if ($page = page($id) ..)

if (($page = page($id)) && $page->hasListedChildren()) {

Also, your route only returns the first child instead of redirecting to it, is that intended behavior? If you want to redirect, it should be:

 [
            'pattern' => '(:any)',
            'language' => '*',
            'action'  => function($id) {
              if ( ( $page = page($id) ) && $page->hasListedChildren()) {
                if ($firstChild = $page->children()->listed()->first()) {
                  return $firstChild->go();
                }
              }
        
              $this->next();
            }
          ]

Thanks for your hints. I always thought only allowed response types are possible?

Unfortunately, even with your changes the routes behave exactly the same – so no redirecting on some of the first level pages. :frowning:

By the way, the same code snippet slightly changed and put in the template gives correct results even on the first level pages that are not redirected:

  <?php
    if ($page->hasListedChildren()) {
      if ($firstChild = $page->children()->listed()->first()) {
        echo $firstChild->url();
      }
    }
  ?>

What’s the difference between the pages where it works and where it doesn’t. Are you sure all pages have listed children? What about other routes that might interfere?

I’m onto something.

The first level pages that don’t work get a wrong $id value.

  [
    'pattern' => '(:any)',
    'language' => '*',
    'action'  => function($id) {
      dump($id);
     }
  ]

This route for example outputs the following on not working first level pages:

<pre>Kirby\Cms\Language Object
(
    [code] => de
    [default] => 1
    [direction] => ltr
    [locale] => Array
        (
            [0] => de_DE.utf8
            [1] => de_DE.utf8
            [3] => de_DE.utf8
            [4] => de_DE.utf8
            [5] => de_DE.utf8
            [6] => de_DE.utf8
            [2] => de_DE.utf8
        )

    [name] => Deutsch
    [rules] => Array
        (
            [Ä] => AE
            [Ö] => OE
            [Ü] => UE
            [ß] => ss
            [ä] => ae
            [ö] => oe
            [ü] => ue
        )

    [url] => http://kunststoffbauten.test
)
</pre>

Working pages do not have any output. They just redirect to the first subpage even with no routing logic but with the page model in place.

Do you have any clue why this happens?

Hm, shouldn’t the language be the first parameter in multi-language routes? Or does this only affect the default language and you don’t use the language code in the url?

That’s my language configuration for the default language:

<?php

return [
  'code' => 'de',
  'default' => true,
  'direction' => 'ltr',
  'name' => 'Deutsch',
  'url' => '/',
  'smartypants' => [
    'singlequote.open' => '&#8218;',
    'singlequote.close' => '&#8216;',
    'doublequote.open' => '&#8222;',
    'doublequote.close' => '&#8220;',
  ],
  'locale' => [
    LC_ALL => 'de_DE.utf8',
    LC_COLLATE => 'de_DE.utf8',
    LC_MONETARY => 'de_DE.utf8',
    LC_NUMERIC => 'de_DE.utf8',
    LC_TIME => 'de_DE.utf8',
    LC_MESSAGES => 'de_DE.utf8',
    LC_CTYPE => 'de_DE.utf8'
  ],
  'translations' => Yaml::decode(F::read(kirby()->root('languages').'/vars/de.yml'))
];

Only other languages shall have the slug in the url. Or do you think that this is not possible in my case?

Not sure where the error is coming from? Does it affect German pages only? Then it might be a problem with the route and you should probably have different routes per language.

Ok, it gets more and more complicated.

One of the affected first level pages works when using the english language version. The other doesn’t and I get instead this output from the dump:

Kirby\Cms\Language Object
(
    [code] => en
    [default] => 
    [direction] => ltr
    [locale] => Array
        (
            [0] => en_US.utf8
            [1] => en_US.utf8
            [3] => en_US.utf8
            [4] => en_US.utf8
            [5] => en_US.utf8
            [6] => en_US.utf8
            [2] => en_US.utf8
        )

    [name] => English
    [rules] => Array
        (
        )

    [url] => http://kunststoffbauten.test/en
)

I found the reason for all my issues :scream_cat:

The route function parameters were missing a variable for the language (e.g. $language). Now everything is working fine. Thank you so much for your help.

Route:

  [
    'pattern' => '(:any)',
    'language' => '*',
    'action'  => function($language, $id) {
      $page = page($id);

      if ($page && $page->hasListedChildren()) {
        if ($firstChild = $page->children()->listed()->first()) {
          return $firstChild->go();
        }
      }

      $this->next();
    }
  ]

Page model:

<?php

class DefaultPage extends Page {
  public function url($options = null): string {
    if ($this->hasListedChildren()) {
      if ($firstChild = $this->children()->listed()->first()) {
        return $firstChild->url();
      }
    }

    return parent::url();
  }
}