Routing
Routing is the process by which Craft directs incoming requests to specific content or functionality.
Understanding Craft’s high-level approach to routing can help you troubleshoot template loading, plugin action URLs, dynamic routes, and unexpected 404 errors. While this list is an abstraction, it represents the order of Craft’s internal checks:
-
Should Craft handle this request in the first place?
It’s important to keep in mind that Craft doesn’t get involved for every request that touches your server—just those that go through your
index.php
.The
.htaccess
file that comes with Craft (opens new window) will silently send all requests that don’t match a directory or file on your web server viaindex.php
. If you point your browser directly at a file that does exist (such as an image, CSS, or JavaScript file), your web server will serve that file directly, without initializing Craft or PHP. -
Is it an action request?
Action requests either have a path that begins with
/actions/
(or whatever your actionTrigger config setting is set to), or anaction
parameter in the POST body or query string. Every request Craft handles is ultimately routed to a controller action, but explicit action requests take precedence and guarantee that essential features are always accessible. If you look at your browser’s Network tab while using the control panel, you’ll see a lot of action requests, like/index.php?action=users/session-info
. -
Is it an element request?
If the URI matches an element ’s URI, Craft lets the element decide how to route the request. For example, if an entry ’s URI is requested, Craft will render the template specified in its section’s settings, automatically injecting an
entry
variable .Whenever an element is saved, its URI is rendered and stored in the
elements_sites
database table.Modules and plugins can re-map an element’s route to a different controller using the EVENT_SET_ROUTE (opens new window) event.
-
Does the URI match a route or URI rule?
If the URI matches any dynamic routes or URI rules , the template or controller action specified by it will get loaded.
-
Does the URI match a template?
Craft will check if the URI is a valid template path . If it is, Craft will render the matched template.
If any of the URI segments begin with an underscore (e.g.
blog/_archive/index
), Craft will skip this step. -
404
If none of the above criteria are met, Craft will throw a NotFoundHttpException (opens new window) .
If an exception is thrown at any point during a request, Craft will display an error page instead of the expected content.
If Dev Mode is enabled, an error report for the exception will be shown. Otherwise, an error will be returned using either your custom error template or Craft’s own default.
# Dynamic Routes
In some cases, you may want a URL to load a template, but its location in your
templates/
folder doesn’t agree with the URI (therefore bypassing step #4), or the URI itself is dynamic.
A good example of this is a yearly archive page, where you want a year to be one of the segments in the URL (e.g.
blog/archive/2018
). Creating a static route or template for every year would be impractical—instead, you can define a single route with placeholders for dynamic values:
# Creating Routes
To create a new Route, go to Settings → Routes and choose New Route . A modal window will appear where you can define the route settings:
- What should the URI look like?
- Which template should get loaded?
The first setting can contain “tokens”, which represent a range of possible matches, rather than a specific string. (The
year
token, for example, represents four consecutive digits.) When you click on a token, Craft inserts it into the URI setting wherever the cursor is.
If you want to match URIs that look like
blog/archive/2018
, type
blog/archive/
into the URI field and choose the
year
token.
Route URIs should
not
begin with a slash (
/
).
After defining your URI pattern and entering a template path, press Save . The modal will close, revealing your new route on the page.
When you point your browser to
https://my-project.tld/blog/archive/2018
, it will match your new route, and Craft will load the specified template with value of the
year
token automatically available in a variable called
year
:
{# Fetch posts in the specified `year`: #}
{% set posts = craft.entries()
.section('posts')
.postDate([
'and',
">= #{year}",
"< #{year + 1}",
.all() %}
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
{{ post.description | md }}
<a href="{{ post.url }}">{{ 'Read More' | t }}</a>
</article>
{% endfor %}
Routes automatically support
pagination
, so this one route covers other URIs like
/blog/archive/2018/page/2
(assuming your
pageTrigger
was
page/
). If you wanted to break the archive into smaller logical chunks, you could use additional
tokens
to collect results by month—or even by day!
# Available Tokens
The following tokens are available to the URI setting:
-
*
– Any string of characters, except for a forward slash (/
) -
day
– Day of a month (1
-31
or01
-31
) -
month
– Numeric representation of a month (1-12 or 01-12) -
number
– Any positive integer -
page
– Any positive integer -
uid
– A v4 compatible UUID (universally unique ID) -
slug
– Any string of characters, except for a forward slash (/
) -
tag
– Any string of characters, except for a forward slash (/
) -
year
– Four consecutive digits
If you define a route using a wildcard token (
*
) in the control panel, it will automatically be available as a named parameter called
any
.
The template for
my-project.tld/foo/some-slug
could then use
{{ any }}
:
It seems you’re looking for `{{ any }}`.
{# output: It seems you’re looking for `some-slug`. #}
# Advanced Routing with URL Rules
In addition to routes defined via the control panel, you can define
URL rules
(opens new window)
in
config/routes.php
.
return [
// Route blog/archive/YYYY to a controller action
'blog/archive/<year:\d{4}>' => 'controller/action/path',
// Route blog/archive/YYYY to a template
'blog/archive/<year:\d{4}>' => ['template' => 'blog/_archive'],
If your Craft installation has multiple sites, you can create site-specific URL rules by placing them in a sub-array, and set the key to the site’s handle. Craft will take care of determining the site’s base URL via this handle, so you don’t need to declare it as part of the route.
return [
'siteHandle' => [
'blog/archive/<year:\d{4}>' => 'controller/action/path',
A subset of the tokens above can be used within the regular expression portion of your named parameters (opens new window) :
-
{handle}
– matches a field handle, volume handle, etc. -
{slug}
– matches an entry slug, category slug, etc. -
{uid}
– matches a v4 UUID.
return [
// Be aware that URIs matching an existing element route will be picked up by step #2, above!
'blog/<entrySlug:{slug}>' => 'controller/action/path',
# Accessing Named Parameters in your Templates
URL rules that route to a template (
['template' => 'my/template/path']
) will pass any named parameters to the template as variables—just like CP-defined routes. For example, this rule…
'blog/archive/<year:\d{4}>' => ['template' => 'blog/_archive'],
…will load
blog/_archive.twig
with a
year
variable set to
2022
when requesting
https://my-project.tld/blog/archive/2022
.
<h1>Blog Entries from {{ year }}</h1>
# Accessing Named Parameters in your Controllers
Named route parameters are automatically passed to matching controller action arguments.
For example, this URL rule…
'comment/<postId:\d+>' => 'my-module/blog/comment',
…would match the numeric ID in the route to the
$id
argument of this
custom controller
action:
namespace modules\controllers;
use craft\elements\Entry;
use craft\web\Controller;
class BlogController extends Controller
* Create a comment for the specified blog post ID.
* @param int $postId Blog Post ID defined by route parameters.
public function actionComment(int $postId)
$this->requirePostRequest();
// Use the ID to look up the entry...
$entry = Entry::find()
->section('posts')
->id($postId)
->one();
// ...and grab the comment content from the request:
$comment = Craft::$app->getRequest()->getBodyParam('comment');
// ...
This rule only serves as an alias to the controller action, which will always be directly accessible via an
action request
—in this case by using the
actionInput()
function:
<form method="post">
{{ csrfInput() }}
{{ actionInput('my-module/blog/comment') }}
{{ hiddenInput('postId', entry.id) }}
<textarea name="comment"></textarea>
<button>Post Comment</button>
</form>
# Error Templates
You can provide your own error templates for Craft to use when returning errors on the front end.
When an error is encountered, Craft will look for a template in your
templates/
directory, in the following order:
-
A template matching the error’s status code, like
404.twig
. -
For a 503 error, a template named
offline.twig
. -
A template named
error.twig
.
You can tell Craft to look for the error template in a nested template directory, using the errorTemplatePrefix config setting.
If Craft finds a matching error template, it will render it with the following variables:
-
message
– error message -
code
– exception code -
file
– file that threw the exception -
line
– line in which the exception occurred -
statusCode
– error’s HTTP status code
Custom error templates are only used when Dev Mode is disabled . When it’s enabled, an exception view will be rendered instead.