The {% exit 404 %} doesn't work right on routed templates.

It's fine on pages that aren't affected by routes. Mysite.com/asdfasdf shows me the proper 404 page.

I've got some routes configured like this:

URL: projects/type/*
Template: _projects/category.twig

URL: projects/*
Template: _projects/entry.twig

In the top of entry.twig, I've got code that says:

{% set entrySlug = craft.app.request.getSegment(2) %}
{% set entry = craft.entries.slug(entrySlug).one() %}

{% if entry is not defined or entry is null %}
    {% exit 404 %}
{% endif %}

{# regular page template below here #}

mysite.com/projects/WorkingProject - this works right. mysite.com/projects/BrokenProjectLink - gives me a 500 error.

When I turn on dev mode, the Yii debugger gives me a 404 ( HTTP 404 – Not Found – yii\web\NotFoundHttpException ), which is the right thing. But turning devmode off, I don't get the 404 page, I get my 500 Internal Server Error page instead.

Again, 404s for pages that don't have any routing work fine. It's just things that are getting routed, and using the {% exit 404 %} code.

This all worked in v3.3.20 and earlier, but it doesn't seem to work the same in Craft

  • Answered below, but I'm compelled to ask why the native Entry/Category routing doesn't suffice? If you've set up your Sections and Category Groups to use the native Element URI/routing system, you wouldn't need to perform manual lookups at all! Jan 30, 2020 at 20:34
  • Yeah, it's because the categories and individual entries live in the same parent segment. I want everything that's /projects/type/ to use category, and /projects/<project name> to go to the actual project pages. (and I can't change that) With native routing, the /projects/type/non-existent-category, it tries to load the entry template, and gives the wrong sort of 404.
    – SamC
    Jan 30, 2020 at 21:00
  • This sounds like unusual behavior… Element templates should only be loaded if their complete URI matches the request (see the elements_sites table). Craft won't make any inference about the path down to your Entry when a specific one isn't found—it'll just continue on and look up a user-defined route (i.e. via the CP or routes.php). Jan 30, 2020 at 21:26

1 Answer 1


When a route is accessed, entry isn't automatically defined.

The actual value for * is available in the any variable in your template—see this example:

Route: projects/type/(*)
Template: _test
{# templates/_test.twig #}
Substituted route param: {{ any }}

Do note that entry is not automatically set by Craft for any custom routes! It's your responsibility to take the incoming value (in your case, any) and look up the Entry, within the template, or bail if nothing is found:

{# We can totally skip the test for `any`, because this template is only loaded when there's a non-zero-length value in the route: #}
{% set entry = craft.entries({ title: any }).one() %}

{% if not entry %}
  {% exit 404 %}
{% endif %}

{# At this point, you've set `entry`, and can use it however you like! #}
{{ entry.myFieldHandle }}

I'd recommend reviewing the requirements for your route, and consider using a more specific token in your route (i.e. (slug)), and matching based on a more stable value—titles can change, but IDs should stick around, and slugs are less prone to editing!

  • I'm sure you're right about the route. I'll take 'em all out, see how it works, and re-do them. . Added some more code to my original Q. I actually do have a chunk of code that gets an entry from the URL. For client reasons, I've got some subpages on projects that do some template magic based on the url, e.g. /project/this-old-house and /projects/this-old-house/thumbnails.
    – SamC
    Jan 30, 2020 at 21:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.