đź‘€ Get a glimpse of Kirby 5 Learn more
Skip to content

Routes

Screencast

Use the built-in router to create custom routes for redirects, stable URLs, or even virtual pages.

This video is loaded from YouTube servers with your consent. Read more…

When you create a new page in Kirby's content folder, this folder creates a new URL, for example, the page /content/projects/project-a will be available at https://yourdomain.com/projects/project-a. So when the user types that URL into the browser's address bar, the project page will be rendered.

Sometimes however, you may want to provide content under URLs that do not exist as pages in your content folder, for example:

  • to create virtual utility pages like a sitemap or an RSS feed,
  • to redirect to other parts of your installation or to remove parts of a page URL to make pages available at a shorter URL (for example, leave out the blog folder when accessing posts that would otherwise be available at https://yourdomain.com/blog/my-super-post),
  • to create custom API endpoints,
  • to return a response object,
  • to filter by URL rather than using parameters,
  • ...

That is where routes come into play. Routes react on an URL pattern. They can either return something to users when they visit an URL with that pattern, or they can have functional purposes, e.g. some function is executed when a script calls a particular URL.

Defining your own routes

Kirby has a built-in router, which can be extended with your own routes. Routes can be setup with the routes option in your config or in plugins. Routes are simple associative arrays with two required fields: pattern and action.

In your config

/site/config/config.php
return [
  'routes' => [
    [
      'pattern' => 'my/awesome/url',
      'action'  => function () {
        // do something here
        // when the URL matches the pattern above
      }
    ],
    [
      'pattern' => 'my/second/url',
      'action'  => function () {
        // ...
      }
    ]
  ]
];

In a plugin

Kirby::plugin('your/plugin', [
  'routes' => [
    [
      'pattern' => 'my/awesome/url',
      'action'  => function () {
        // do something here
        // when the URL matches the pattern above
      }
    ]
  ]
]);

You can also define your route definition as a callback, which gives you more control, e.g. to access information from inside Kirby in your patterns (in this example to use a Kirby option as part of the pattern):

Kirby::plugin('your/plugin', [
  'routes' => function ($kirby) {
      return [
          [
              'pattern' => 'something/' . $kirby->option('something'),
              'action' => function () {
                // ...
              }
          ]
      ];
  }
]);

Patterns

In your route patterns, you can use static, relative URL strings, e.g. some/static/url, but also dynamic placeholders:

Placeholders can also contain expressions, e.g. ([a-z]+), and will be passed as arguments to the callback function in the order they appear:

[
  'pattern' => 'my/awesome/url/(:any)/(:num)/(:all)',
  'action'  => function($any, $num, $all) {
    // ...
  }
]

If you want to use the same action for multiple patterns, you can either use regex expressions or pass an array of patterns:

[
  'pattern' => ['blog/(:any)', 'notes/(:any)'],
  'action'  => function() {
    // ...
  }
]

When using placeholders like (:any) and (:all) or regex patterns, make sure that they are not too greedy and you accidentally catch routes you want left intact, resulting in things not working as expected anymore.

Example: A pattern like (:any) will not only catch routes with a URL part like notes or photography, but also their JSON content representations notes.json or photography.json. In this case, using (:alphanum) can be more suitable.

Response types

Depending on your use case, routes can return various response types, which will be handled accordingly by Kirby:

Page object

[
  'pattern' => 'portfolio',
  'action'  => function () {
    return page('projects');
  }
]

File object

[
  'pattern' => 'load/(:any)',
  'action'  => function ($name) {
    return page('downloads')->file($name);
  }
]

String: HTML

[
  'pattern' => 'boring.html',
  'action'  => function () {
    return '<html><body>Boring!</body></html>';
  }
]

Not found: false, null or ''

[
  'pattern' => 'not/found',
  'action'  => function () {
    return false;
  }
]

JSON from array

[
  'pattern' => 'fancy.json',
  'action'  => function () {
    return [
      'status' => 'ok',
      'data'   => ['foo' => 'bar']
    ];
  }
]

Response object

[
  'pattern' => 'custom/response',
  'action'  => function () {
    return new Response('<foo>bar</foo>', 'text/html');
  }
]

Exception

[
  'pattern' => 'custom/response',
  'action'  => function () {
    throw new Exception('Something went horribly wrong');
  }
]

Methods

By default routes are only available for GET requests. You can define additional request methods for the route:

  • CONNECT
  • DELETE
  • GET
  • HEAD
  • OPTIONS
  • PATCH
  • POST
  • PUT
  • TRACE
[
  'pattern' => 'my/awesome/url',
  'action'  => function () {
    // ..
  },
  'method' => 'GET|POST|DELETE'
]

Multi-language setup

In case of multi-language sites you must call the $site->visit() method in order to activate the selected page and set a language.

[
  'pattern' => 'custom/response',
  'action'  => function () {
      return site()->visit('some/page', 'en');
  }
]

Language scope

In multi-lang installations, you can set a language scope for your routes:

return [
    'routes' => [
        [
            'pattern' => 'test',
            'language' => 'en',
            'action' => function ($language, $slug) {

                if (page($slug)) {
                    return $this->next();
                }

                if ($page = page('notes/' . $slug)) {
                    return $page;
                }

                return false;

            }
        ],
    ]
];

The above route will respond to a pattern like example.com/test (if en is the default language) or example.com/en/test. If en is not the default language, the route will only respond to example.com/en/test.

If you call the above language-scoped route pattern using another language like example.com/de/test, the above route will not respond and show the error page instead.

In a multi-language context, routes are supposed to be called with the language code in the URL (unless it is the default language). Otherwise, $language will always refer to the default language.

If you don't set the language scope in a multi-language context, the above route will only respond to example.com/test, not to URLs that contain a language code.

To make a route respond to all languages, you can set the language scope property to *.

return [
    'routes' => [
        [
            'pattern' => 'test',
            'language' => '*',
            'action' => function ($language, $slug) {

                if (page($slug)) {
                    return $this->next();
                }

                if ($page = page('notes/' . $slug)) {
                    return $page;
                }

                return false;

            }
        ],
    ]
];

This route pattern will respond to example.com/en/test, example.com/de/test or whatever languages you have defined.

Page scope

To make sure that translated slugs are automatically handled by Kirby, you can set a page scope:

return [
    'routes' => [
        [
            'pattern' => 'tag/(:any)',
            'language' => '*',
            'page' => 'notes',
            'action' => function ($language, $page, $filter) {
              return $page->render([
                'filter' => $filter
              ]);
            }
        ],
    ]
];

In this example, we return the notes page with the filter data. In your controller, you can now use this filter to return only the children with the given tag.

Since we have set the language scope to *, this route will listen to all languages.

next()

In some scenarios, you want to perform actions on all requests matching a route pattern, but then let the router find the next matching route. For this scenario you can use the $this->next() call within your route.

/site/config/config.php
return [
    'routes' => [
        [
            'pattern' => '(:any)',
            'action'  => function ($slug) {
                if ($page = page('photography')->find($slug)) {
                    return page($page);
                }

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

This is perfect for the example above when you want to intercept URLs like https://yourdomain.com/photography-project-name and create flat URLs. But at the same time you don’t want to break all the other pages. In this case you can search for the photography project in the route and if it doesn’t exist you can jump to the regular route for all the other pages with $this->next().

Pass data to controller

You can send additional data to the controller using the $page->render() method:

/site/config/config.php
<?php
return [
    'routes' => [
        [
            'pattern' => 'blog/tag/(:any)',
            'action' => function ($value) {
                $data = [
                  'tag' => $value,
                ];
                return page('blog')->render($data);
            }
        ]
    ]
];

You can then fetch this data in the controller.

Before and after hooks

You can register hooks for the event when a route has been found, but has not been executed yet, and when the route has just been executed. Those hooks are very useful to intercept certain routes based on your own rules, or to manipulate the result of a particular route.

route:before

/site/config/config.php
return [
  'hooks' => [
    'route:before' => function ($route, $path, $method) {
      if ($path === 'super/secret' && !kirby()->user()) {
        die('Nope');
      }
    }
  ]
];

route:after

/site/config/config.php
return [
  'hooks' => [
    'route:after' => function ($route, $path, $method, $result) {
      return myHtmlCompressor($result);
    }
  ]
];

Virtual Pages

A route can return a virtual page that doesn't really exist in the file system. This is very useful to mock pages with custom data from other data sources.

/site/config/config.php
return [
  'routes' => [
    [
      'pattern' => 'virtual-reality',
      'action'  => function () {
        return new Page([
          'slug' => 'virtual-reality',
          'template' => 'virtual-page',
          'content' => [
            'title' => 'This is not a real page',
            'text'  => 'Believe it or not, this page is not in the file system'
          ]
        ]);
      }
    ]
  ]
];

Removing the parent page slug from the URL

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);
            }
        ]
    ]
];

Replace blog with the slug of the parent page you want to remove from the URL.

Additionally, you can overwrite the $page->url() in a page model to return the desired target URL when you use this method.

Accessing parameters and query strings

Query strings and parameters cannot be part of a route pattern. However, you can get passed parameters or query strings with Kirby's param() and get() helpers inside a route:

/site/config/config.php
<?php

return [
    'routes' => [
        [
            'pattern' => 'products/(:any)',
            'action'  => function($uid) {
                // parameter, e.g. `https://example.com/products/color:black`
                if ($param = param('color')) {
                  // do something
                }
                // query string, e.g. `https://example.com/products?color=black`
                if ($query = get('color')) {
                  // do something
                }
                // ...
            }
        ],
    ]
];

More information

Filtering via routes