How to automatically add page field in URL?

Good afternoon,

as discussed in another thread, I am building a multi-langual FAQ using the page structure.

This is how a page.en.txt looks like:

Title: How expensive are they?
----
Answer: They are much too cheap
----
Category: Products
----
Uuid: NL0BH3zzl0vKngfY

You can see that I am using the title field as the question (which is useful for URL generation, in the blueprints etc) and I have added two other fields: Category (where I defined a limited set of options in the Blueprint) and the Answer field.

Now for this example, the URL slug is how-expensive-are-they with the total URL being blabla.com/en/faq/how-expensive-are-they. What I would like to achieve now is that the final URL also includes the category, i.e. blabla.com/en/faq/products/how-expensive-are-they. Of course I checked the Routes documentation but I did not find something which sprang to my eye as the immediate solution (i.e. how to access field data to create custom route), I am new to Kirby tough and learning every day.

Can someone point me into a direction please?

Thanks
Andreas

For cases like this you need a combination of a page model and a route. The route would look something like this:

'routes' => [
  [
  'pattern' => 'faq/(:any)/(:any)',
  'action'  => function($category, $slug) {
     if ($page = page('faq/' . $slug)) {
       return site()->visit($page);
     }
     $this->next();
  }
  ],
],

Then in the page model, you would overwrite the url() method, so that this method returns your target url instead of the standard url.

Additionally, you could create another route that redirects from the standard url to the target url.

Thank you @texnixe ! I just read through the Page Models - it amazes me how extremely flexible and thought through Kirby is. :smiling_face_with_three_hearts:

Just to make sure: when I do not have a live site with existing URLs, I do not need to create the second route which redirects from old to new one, right?

Thanks
Andreas

Well, without that additional route, the original URL will still be accessible by anyone who knows/guesses the URL. So if you want to be on the safe side, add the redirect as well.

@texnixe I need your help please with the page model part since I am not fully confident I understand the code correctly.

When I check out the source code of the $page->url() method and if I understand it correctly, for my multiligual environment, I actually need to change the urlForLanguage() method. But when I add the route as per your description and add the following code to a newly created site / models / question.php:

<?php

/**
 * When we want to put the category in the URL of each questions' page, 
*  we need to define a custom route for that and add a page model to 
*  to change the default URL method for this specific page to output
*  the URL with the category too. See Kirby Forum
*  https://forum.getkirby.com/t/how-to-automatically-add-page-field-in-url/29085
*  and the corresponding original URL method of the page model
*  https://github.com/getkirby/kirby/blob/3.9.5/src/Cms/Page.php#L1418
*/

class Question extends Page {
	
    /**
	 * Returns the Url
	 *
	 * @param array|string|null $options
	 * @return string
	 */
    public function url($options = null): string
	{
		if ($this->kirby()->multilang() === true) {
			if (is_string($options) === true) {
				return $this->urlForLanguage($options);
			}

			return $this->urlForLanguage(null, $options);
		}

		if ($options !== null) {
			return Url::to($this->url(), $options);
		}

		if (is_string($this->url) === true) {
			return $this->url;
		}

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

		if ($parent = $this->parent()) {
			if ($parent->isHomePage() === true) {
				return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid();
			}

			return $this->url = $this->parent()->url() . '/' . $this->uid();
		}

		return $this->url = $this->kirby()->url('base') . '/' . $this->uid();
	}

    /**
	 * Builds the Url for a specific language
	 *
	 * @internal
	 * @param string|null $language
	 * @param array|null $options
	 * @return string
	 */
	public function urlForLanguage($language = null, array $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->category($language) . '/' . $this->slug($language);
			}

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

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

It has no effect. Neither when I create new questions nor when I try to manually navigate to the URL, there the error page is shown.

My site / templates / question.php looks like this:

The Q: <?= $page->title() ?><br>
The A: <?= $page->answer() ?>

What did I misunderstand here?

Thanks
Andreas

Must be

class QuestionPage extends Page

Thanks @texnixe - the URL rewrite works now (stupid mistake again) but I get an 404.

If I remove the route and the page model and call the URL without the category in it, it works fine.

Do you have any hints for me please? I am lost.

Thanks
Andreas

Could you push the project with the code you tried to GitHub or upload somewhere for download so that I can take a look.

Ok, here we go:

The model:

<?php

use Kirby\Cms\Url;

class QuestionPage extends Page {
	
    /**
	 * Returns the Url
	 *
	 * @param array|string|null $options
	 * @return string
	 */
    public function url($options = null): string
	{
		if ($this->kirby()->multilang() === true) {
			if (is_string($options) === true) {
				return $this->urlForLanguage($options);
			}

			return $this->urlForLanguage(null, $options);
		}

		if ($options !== null) {
			return Url::to($this->url(), $options);
		}

		return $this->url = $this->kirby()->url('base') . '/' . $this->parent()->uid() . '/' . $this->category()->slug() . '/'. $this->uid();
	}


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

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

The two routes:

    'routes' => [
        [
	        'pattern' => 'faq/(:any)/(:any)',
	        'language' => '*',
	        'action'  => function($lang, $category, $slug) {
	           if ($page = page('faq/' . $slug)) {
	             return site()->visit($page, $lang );
	           }
	           $this->next();
	        }
        ],
        [
	        'pattern' => 'faq/(:any)',
	        'language' => '*',
	        'action'  => function($lang, $slug) {
		        if ($page = page('faq/' . $slug)) {
			   go(site()->urlForLanguage($lang) . '/faq/' . $page->category()->slug() . '/' . $page->slug());

		        }
		        $this->next();
	        }
        ],
    ],
1 Like

Lots of thanks @texnixe Sonja for fixing this for me, very much appreciated. Danke!!

Andreas

I know I am a pain here but this routing does not work with content representations. The documentation warns about this and suggests to use :alphanum instead of :any which leads to a 404 again.

I want to generate pngs for these pages dynamically.

Is there any way to get this done @texnixe ?

Thanks
Andreas

Do I understand correctly, that if you replace all (:any) in the above routes with (:alphanum) it doesn’t work?

Exactly - all three :any where replaced with :alphanum and it did not work

Doesn’t make any difference when I replace the existing routes with (:alphanum), still working. So what exactly doesn’t work? The routes themselves, or your content representations? But as I wrote in the other thread, default.png.php will not work for other page types.

The content representation - specifically build for this template.

So I have a question.php template which is accessible via this modified URL - and a question.png.php.

So for example. URL

mysite.com/en/faq/service123/what-license-requirements-does-the-service-have

successfully shows me the content as defined in the question.php template.

But

mysite.com/en/faq/service123/what-license-requirements-does-the-service-have.png

does not generate the image as defined in question.png.php but also shows the actual page using the question.php template.

Sorry for not being clearer on this earlier.

Thanks
Andreas

That somehow does not make sense. When you use alphanum instead of any, you should get the error page, while with (:any), you should get the standard page content.

I agree. Better to sleep over it and check again tomorrow, chances are I am just too tired to see the error in front of me :+1:

Sleep well :sleeping:

Right, so I slept a night over it - or a few more :wink: and my hopes of the problem vanishing by itself did not materialize :slight_smile:

So:

    'routes' => [
        [
	        'pattern' => 'faq/(:any)/(:any)',
	        'language' => '*',
	        'action'  => function($lang, $category, $slug) {
	           if ($page = page('faq/' . $slug)) {
	             return site()->visit($page, $lang );
	           }
	           $this->next();
	        }
        ],
        [
	        'pattern' => 'faq/(:any)',
	        'language' => '*',
	        'action'  => function($lang, $slug) {
		        if ($page = page('faq/' . $slug)) {
			   go(site()->urlForLanguage($lang) . '/faq/' . $page->category()->slug() . '/' . $page->slug());

		        }
		        $this->next();
	        }
        ],
    ],

Works as expected: I can access the page including the category. Adding .png to the page does not trigger the content representation template - nor does it throw a 404! It simply shows the page.

Now this:

    'routes' => [
        [
	        'pattern' => 'faq/(:any)/(:alphanum)',
	        'language' => '*',
	        'action'  => function($lang, $category, $slug) {
	           if ($page = page('faq/' . $slug)) {
	             return site()->visit($page, $lang );
	           }
	           $this->next();
	        }
        ],
        [
	        'pattern' => 'faq/(:any)',
	        'language' => '*',
	        'action'  => function($lang, $slug) {
		        if ($page = page('faq/' . $slug)) {
			   go(site()->urlForLanguage($lang) . '/faq/' . $page->category()->slug() . '/' . $page->slug());

		        }
		        $this->next();
	        }
        ],
    ],

redirects faq/pagename to faq/category/pagename but then spits out a 404. Content representation does not work either.

With this:

    // We are adding two custom routes to show the catgory in the URL of each FAQ entry
    'routes' => [
        [
	        'pattern' => 'faq/(:any)/(:alphanum)',
	        'language' => '*',
	        'action'  => function($lang, $category, $slug) {
	           if ($page = page('faq/' . $slug)) {
	             return site()->visit($page, $lang );
	           }
	           $this->next();
	        }
        ],
        [
	        'pattern' => 'faq/(:alphanum)',
	        'language' => '*',
	        'action'  => function($lang, $slug) {
		        if ($page = page('faq/' . $slug)) {
			   go(site()->urlForLanguage($lang) . '/faq/' . $page->category()->slug() . '/' . $page->slug());

		        }
		        $this->next();
	        }
        ],
    ],

faq/pagename as well as faq/pagename.png works.
faq/category/pagename throws a 404.

And finally

    // We are adding two custom routes to show the catgory in the URL of each FAQ entry
    'routes' => [
        [
	        'pattern' => 'faq/(:alphanum)/(:alphanum)',
	        'language' => '*',
	        'action'  => function($lang, $category, $slug) {
	           if ($page = page('faq/' . $slug)) {
	             return site()->visit($page, $lang );
	           }
	           $this->next();
	        }
        ],
        [
	        'pattern' => 'faq/(:alphanum)',
	        'language' => '*',
	        'action'  => function($lang, $slug) {
		        if ($page = page('faq/' . $slug)) {
			   go(site()->urlForLanguage($lang) . '/faq/' . $page->category()->slug() . '/' . $page->slug());

		        }
		        $this->next();
	        }
        ],
    ],

Same as above: no redirect from faq/pagename to faq/category/pagename but faq/pagename as well as faq/pagename.png works, faq/category/pagename throws a 404.

Thinking about it, I think I will remove these routes and live with the missing category in the URL. In this way, content representation also works, otherwise I would have to basteln even more to get it to work.