onAdminSave() for setting route default override

Follow-up of


Do you have a sample to force the default route override with a combined pattern of date and slug?

Current frontmatter:

slug: hello-world
date: 2018-08-06

New frontmatter:

slug: hello-world
date: 2018-08-06
  default: '/2018/08/06/hello-world'

I need a generic function, that also works when slug is not explicitly specified in the frontmatter, but determined from the folder name.

And if possible it should retain any existing canonical or aliases.


And the same for date. If it is not explicitly then it should use the modified date, just like Grav does.

This would be a workaround for simulating Wordpress permalinks.

@metbril I’m afraid it is not exactly clear to me what you would like to achieve?

Would you like to:

  • Redirect Wordpress like request to internal folder structure.
    Are you converting a Wordpress site and want to redirect Wordpress urls like mydomain/2018/08/06/foo to urls based on Grav’s folder structure and let the browser/user (and Google analytics) know the page has moved permanently to /mydomain/path/foo?

    In that case I think regex based site redirection in site.yaml might be able solve the problem.

      /[0-9]{4}/[0-9]{2}/[0-9]{2}/(.*): '/path/$1[301]'
  • Or map from internal folder structure to Wordpress urls?
    For this, a default route should be set in a page’s frontmatter, like:

      default: '/2018/08/07/mypage'

    This default route will also be used for mapping requests in the form of mydomain/2018/08/07/mypage to the internal path.

    To automate this when a page is created/updated in Admin I guess a plugin should come to the rescue. Which could look like:

    public function onPluginsInitialized()
      // Don't proceed if we are not in the admin plugin
      if (!$this->isAdmin()) {
      // Only proceed if page is being saved
      $paths = $this->grav['uri']->paths();
      if (count($paths) < 3 || $paths[1] !== 'pages') {
      // Enable the main event we are interested in
          'onAdminSave' => ['onAdminSave', 0]
    public function onAdminSave(Event $e)
      $page = $e['object'];
      $header = $page->header();
      if (strpos($page->template(), 'modular/') === 0) {
      // Set date to today if not set in Admin
      $date = isset($header->date) ? $header->date : date('Y-m-d');
      $date = substr(preg_replace('/-/', '/', $date), 0, 10);
      // Set default route for page to /yyyy-mm-dd/slug
      // Note: For nested pages, more logic is required.
      $header->routes['default'] = '/'.$date.'/'.$page->slug();

    Note, in system.yaml a default dateformat has been set:

      default: 'Y-m-d'


I’m sorry if I did not make myself clear. It is the latter is was looking for. I would like to retain my permalink structure. At least, I’m investigating how much effort it would take.

The disadvantage of my way and your script is that it needs to be in the frontmatter of every item. Manually or through a plugin that acts in the admin panel.

The other way would be to maintain a physical nested folder structure like



This has the disadvantage that, next from the actual post, I need to create the extra blog.md files in every folder to create a browseable dated archive (and really think-through the needed collections like self.descendants).

In both cases it requires a lot of maintenance, which i’m not looking for.

What I’m REALLY looking/hoping/waiting for is, that I can set the default route at the site or system level with some regex pattern based on the date and slug front matter. Or a plugin that does this for me (without the need to do extra work).

Would you mind elaborate on why you would like to keep the Wordpress style urls? Is it pageranking, or …

It’s just a personal preference. I would like to be able to browse it like some kind of a calendar/journal.

An example (not Grav, but Kirby) of what I like is this:


If you click a date header, you see all posts from that day.
If you click the month, you get to see all posts from that month
If I enter a url like http://mydomain.com/2018/08 I immediately see the monthly archive.

Are there any alternatives for implementing this in Grav?

I think this will be possible with redirects, but I like the visual clue in the url / address bar.

I was thinking that adding the date to the folder name like


would make it possible to create aliases https://learn.getgrav.org/content/routing#regex-based-aliases but a generated collection would always use the default route. Or perhaps I would then create templates that point to the alias. And modify the feeds, etc…

@pamtbaau I found the Events plugin

with calendar functionality. That would be another way to mimic my required functionality.

Instead of physically adding the default route override, I could also achieve this by dynamically adding it the the FrontMatter in the onPagesInitialized() event. I don’t mind hacking some of this. But do you know of (links to) any examples how to manipulate the FrontMatter? Pointers to plugins should do.

I think I figured it out…

I can now just place all blog items under the main blog page and create one big collection. And still use the nested structure. :smiley:

This is what I did. I will do some additional testing. And the plugin needs some configurability. I will submit a repo and will eventually submit it to the plugin library once its stable enough.

Created a plugin that rewrites the default route:

    public function onPagesInitialized(Event $event)
        $pages = $event['pages'];
        $collection = $pages->all(); // todo: get from plugin settings
        $items = $collection->ofType('item');
        foreach ($items as $item) {
            $header = $item->header();
            $date = isset($header->date) ? date('Y-m-d', strtotime($header->date)) : date('Y-m-d');
            $date = substr(preg_replace('/-/', '/', $date), 0, 10);
            $route = '/'.$date.'/'.$item->slug();
            $header->routes['default'] = $route;

And additionally create an alias in site.yaml:

  '/[0-9]{4}/[0-9]{2}/[0-9]{2}/(.*)': /blog/$1

A few thoughts come to mind:

  • What is the performance overhead of looping through all blog items on every page request?
  • Could you jump out when type of current page is not ‘blog’ and/or ‘item’?
  • Why not…
    • Run through all blog pages ‘on demand’ from Admin plugin config page, and save page with updated header.
      Adding $item->save(); to the current loop will save the new header in the item.md file.
    • And add/update routes:default of a single page when saved by Admin?
  • How about links in the content of a page referring another blog item? Like “in my previous blog I talked about preserving Wordpress url styles

Bumping an old thread. I decided to do things differently. Now using the onPageProcessed event. This has no impact on performance and I don’t need to manually create aliases. There is one disadvantage (if at all): the default routes will only be computed when the cache needs to be refreshed. I can live with that. :open_mouth:

This is the code I’m currently using (I’m wrapping up a nice plugin that I will soon release):

public function onPageProcessed(Event $e)
        $page = $e['page'];

        // only parse pages with 'item' template (blog entries)
        $template = $page->template();
        if (!($template == 'item')) {

        $header = $page->header();
        $date = isset($header->date) ? date('Y-m-d', strtotime($header->date)) : date('Y-m-d');
        $date = substr(preg_replace('/-/', '/', $date), 0, 10);
        $slug = $page->slug();
        $parent = $page->parent();
        $parent_route = $parent->route();
        $route = $parent_route.'/'.$date.'/'.$slug;
        $header->routes['default'] = $route;