🗺️Template Routing
Overview
Template routing lets requests find their controllers by convention — most themes need no template PHP files at all. No more three-line stub files whose only job is naming a controller and a Twig template: the filename already told us everything.
Think of it as the ship's watch rota: every request already knows its station.
Template routing is opt-in. Enable it by adding the service provider to your child theme's config/service-providers.php:
return [
// PressGang defaults — always keep these
\PressGang\ServiceProviders\TimberServiceProvider::class,
\PressGang\ServiceProviders\SeoServiceProvider::class,
// Opt-in: convention-based template routing
\PressGang\ServiceProviders\TemplateRoutingServiceProvider::class,
];Themes built on explicit template stubs are completely untouched by framework upgrades — nothing changes until you list the provider. And even once enabled, a physical template file in your child theme always wins, so you can adopt convention routing incrementally: delete stubs one at a time.
How It Works
When WordPress resolves a request, PressGang records the template hierarchy candidates (most specific first). If the request falls through to a parent-theme template — meaning your child theme had no stub for it — the dispatcher resolves a controller from those candidates and renders it.
Mechanically: template_include must return a PHP file for WordPress to load, so the dispatcher hands it the parent theme's dispatch.php — a one-line landing file that calls ControllerFactory::dispatch(). The routing decision happens in the filter; dispatch.php is just where the request touches down. (Custom route handlers reuse the same landing file — see below.)
For each candidate, resolution tries:
Your config map — an explicit entry in
config/controllers.phpNaming convention — a matching controller in your child theme's
Controllersnamespace
The matching {candidate}.twig in your views/ directory renders automatically; if it doesn't exist, the controller's own template inference applies.
The Naming Conventions
Beyond the literal StudlyCase name, the dispatcher understands WordPress template hierarchy semantics — matching the PressGang convention of plural controllers for archives, singular for single views:
search
SearchController
StudlyCase
front-page
FrontPageController
StudlyCase
archive-event
EventsController
archive-{type} → pluralised type
single-event
EventController
single-{type} → the subject
taxonomy-event-type
EventTypeController
taxonomy-{tax} → the subject
Parent framework controllers are never matched by convention — the parent theme's own templates already route to them — so dispatch only activates for controllers you define.
Hyphenated Template Names
WordPress builds hierarchy candidates from your registered keys, so a taxonomy named event_type traditionally demands taxonomy-event_type.php — an underscore in a world of kebab-case filenames. With routing enabled, every underscored candidate gets a hyphenated twin, so taxonomy-event-type.php (and taxonomy-event-type.twig) work too. Underscored names keep working.
The Config Map
Most themes need no entries at all — conventions cover the common cases. Use config/controllers.php only when a controller's name defies convention:
An explicit map entry always beats convention for its candidate.
File-less Page Templates
Page templates traditionally require physical files for WordPress to discover their Template Name: headers. With routing enabled, register them declaratively instead — no page-templates/ directory:
Each registered template resolves to its {Slug}Controller by convention (sidebar-page → SidebarPageController), falling back to the framework PageController, and renders {slug}.twig.
Migrating an existing theme? Use the legacy file-shaped ids shown above — they match the _wp_page_template values already stored on your pages, so assignments and the admin dropdown carry over with no data migration. New themes can use bare slugs like 'contact-page'.
When to Keep a Template File
A stub is still the right tool when there's genuine logic in template selection — the file always wins over dispatch, so nothing fights you:
Custom Route Handlers
config/routes.php maps custom URL patterns (via the Upstatement Routes library) to a template filename — or, for routes that need logic before rendering, to a class implementing RouteHandlerInterface:
A handler receives the matched route parameters and is responsible for loading a response — typically by building query args (Quartermaster's toArgs() is made for this) and handing off to the dispatcher:
TemplateHierarchy::prepend() seeds the candidate your route means, so controller resolution stays deterministic even when WordPress's conditionals disagree (an empty page 2, for example).
Precedence, In One Breath
Template file → config map → naming convention. Explicit always beats implicit, and your child theme always beats the framework.
Last updated