Guide 03 · Developer docs

The theme repo and the asset pipeline

Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.

L1–L2
Unit 05 · Around the main content

Error templates

Where a theme repo's 404, 403, 400 and 500 pages live, which ones a site overrides, what each receives, and why the 500 page depends on nothing at all.

Who this is for. Developers building or launching a site who want its error pages to match the site and keep working when something has gone wrong. After this page you can override each error page a site can override, choose what each one may depend on, and test them on the right environments.

Where error templates live

When a request fails, the platform looks for a template with a fixed file name in the theme repo's templates/errors/ folder. When the file exists, it is rendered through the site's template engine, Smarty or Twig, with the common variables. When it does not, the platform sends its own copy of that file as it is. Overriding an error page is therefore a matter of adding a file with the right name.

File in templates/errors/StatusShown for
not-found.html404No record at the URL, an unpublished record, or a template that calls $_response->returnNotFound()
fatal-error.html500An exception while handling the request, on any environment not running in development mode
not-authorized.html403A failed API-key or token check
bad-request.html400A malformed request
oauth.html400An OAuth sign-in error. A file named for the engine, oauth-smarty.html or oauth-twig.html, is used first when it exists

Restricted records

A visitor who may not see a record gets a 404 by default, so the existence of restricted content is not given away. Two site settings choose what happens instead: Globals.security.accessDeniedUnAuthenticated for a visitor who is not signed in, and Globals.security.accessDeniedAuthenticated for a signed-in member without access. Set the unauthenticated setting to show unauthorized.html (401), or to send the visitor to the sign-in page instead, carrying the address to return to and a queued message for the page that follows (Messages after a redirect). Set the authenticated setting to show forbidden.html (403) for a signed-in member without access. Either way, the page that goes with a 401 or 403 is the platform's own.

What an error template receives

An error template gets the common variables every frontend template gets, including _currentSite, _currentUser, _request, _response, _messages, _siteData, _menu, _globalComponents, _query and _inlineScriptNonce. It gets nothing about a record: there is no $page and no $record, so a 404 page cannot know which record was missing. It can read the requested path from _request and suggest where to go next:

Template code templates/errors/not-found.html
{extends file='includes/frontend/base-template.html'}
{block name="content"}
    <main id="main">
        <h1>Page not found</h1>
        <p>Nothing is published at <code>{$_request->getPath()|escape}</code>.</p>
        <p><a href="/">Go to the home page</a> or <a href="/support">talk to a human</a>.</p>
    </main>
{/block}
{% extends 'includes/frontend/base-template.html' %}
{% block content %}
    <main id="main">
        <h1>Page not found</h1>
        <p>Nothing is published at <code>{{ _request.getPath() }}</code>.</p>
        <p><a href="/">Go to the home page</a> or <a href="/support">talk to a human</a>.</p>
    </main>
{% endblock %}

The path is something a visitor typed, so the Smarty version escapes it; Twig escapes output by default. The rules are on Escaping output.

Warm and cold error pages

Two patterns cover every error page. Which one fits depends on whether the rest of the site is working when the page is shown.

  • Warm: the page extends the site's base template, with its navigation, footer and search, as the 404 above does. A 404 is shown by a working site, and the common variables supply what a base template usually needs: _menu, _currentSite, _messages and _inlineScriptNonce. Keep the page, and everything it extends, to those variables.
  • Cold: the page is one file with no extends, no include, no platform variable and every style rule inline. Use it for fatal-error.html. A 500 is shown when something has already gone wrong, and that something could be a shared include, a menu or a data file the page would otherwise pull in. A page that depends on nothing is shown whatever the cause.

Many sites make bad-request.html cold too, so the two pages that say something went wrong look alike and neither looks like a working page.

HTML templates/errors/fatal-error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex, nofollow">
    <title>Something went wrong</title>
    <style>
        body { margin: 0; padding: 4rem 1rem; font-family: system-ui, sans-serif; background: #fff; color: #1a1a1a; }
        main { max-width: 36rem; margin: 0 auto; }
        a { color: #0b57d0; }
        @media (prefers-color-scheme: dark) {
            body { background: #121212; color: #ededed; }
            a { color: #8ab4f8; }
        }
    </style>
</head>
<body>
    <main>
        <h1>Something went wrong</h1>
        <p>This page could not be shown. Please try again in a few minutes.</p>
        <p><a href="/">Go to the home page</a></p>
    </main>
</body>
</html>

The page asks search engines not to index it, and follows prefers-color-scheme instead of offering a theme switch, because a switch needs JavaScript the page does not load. It contains no template syntax, so the same file serves a Smarty or a Twig site.

JSON requests and caching

  • A JSON request gets a JSON error. A request through the API, or one that sends Accept: application/json, receives a JSON error body from every error handler instead of a template, so an error template only ever renders HTML for browsers.
  • Error responses are not page-cached. Any response other than a 200 is rendered afresh on every request. Keep a 404 light: a busy site can serve many of them, and each one runs the whole template.
Error pages have no $page

Anything an error page extends or includes runs without $page. A shared head or navigation partial that calls $page->getName() or reads one of its layout areas works on every normal page and fails on the 404. Guard such calls with {if isset($page)} in Smarty or {% if page is defined %} in Twig, or keep them out of the base template.

Testing error pages

How much a 500 shows depends on the environment, not on who is signed in. On an environment running in development mode, the platform shows the full exception and stack trace to every visitor, signed in or not, so a developer can see the cause. On every other environment, the visitor gets your fatal-error.html. Nothing in the error handling looks at _currentUser, and the environment's configuration is not readable from a template: _config returns *** for it.

So test each error page on two environments, rather than as two user roles:

  1. On a development environment, trigger the error you are handling and read the trace.
  2. On a production-like environment, signed out, request an address that does not exist. Check that the status is 404 and that the page shows the site's navigation and footer.
  3. Open fatal-error.html straight from the repository in a browser. A cold page has no template syntax and depends on nothing, so what you see is what a visitor sees.
  4. Request a missing address with Accept: application/json and check that the body is JSON.
Go deeper

Was this page helpful?

Your answer goes to the docs team.