For the complete documentation index, see llms.txt. This page is also available as Markdown.

🗺️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:

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:

  1. Your config map — an explicit entry in config/controllers.php

  2. Naming convention — a matching controller in your child theme's Controllers namespace

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:

Candidate
Resolves to
Rule

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-pageSidebarPageController), 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