Routing
Sloth comes with two complementary routing mechanisms: the WordPress Template Hierarchy for standard content pages, and a custom router for anything that goes beyond what WordPress offers.
WordPress Template Hierarchy
For most pages — posts, archives, taxonomies, custom post types — you don't need to define routes at all. Place a Twig file with the right name in theme/View/Layout/ and Sloth picks it up automatically via the WordPress template hierarchy.
theme/View/Layout/
├── index.twig # Fallback for everything
├── single.twig # Single posts
├── single-project.twig # Single post of type "project"
├── archive.twig # Archives
├── archive-project.twig # Archive for post type "project"
├── page.twig # Pages
├── page-about.twig # Page with slug "about"
├── taxonomy.twig # Taxonomy archives
├── search.twig # Search results
└── 404.twig # Not found
See the WordPress Template Hierarchy for the full list of template names.
Custom Routes
When you need custom URLs or parameters that don't map to WordPress content, use Sloth's router.
Define routes in app/routes/web.php or theme/routes/web.php:
<?php
use Sloth\Facades\Response;
use Sloth\Facades\Route;
Route::get('/feed/custom', function () {
return Response::make(renderFeed(), 200)
->header('Content-Type', 'application/xml');
});
Both files are loaded if present — app/routes/web.php first, then theme/routes/web.php.
HTTP methods
Route::get('/projects', fn() => Response::view('projects/index'));
Route::post('/contact', fn() => handleForm());
Route::put('/projects/{id}', fn(string $id) => update($id));
Route::delete('/projects/{id}', fn(string $id) => delete($id));
URL parameters
Parameters in curly braces are passed to the callback in order:
Route::get('/projects/{slug}', function (string $slug) {
$project = Project::where('post_name', $slug)->firstOrFail();
return Response::view('projects/show', ['project' => $project]);
});
Named routes
Assign a name by chaining ->name():
Route::get('/projects', fn() => Response::view('projects/index'))
->name('projects.index');
Route::get('/projects/{slug}', fn(string $slug) => showProject($slug))
->name('projects.show');
Dispatch
Routes are matched and dispatched on the template_redirect hook at priority 1 — before WordPress loads its own templates. If a route matches, WordPress template loading is bypassed entirely.
Responses
Route callbacks return a Response instance. Sloth's Response class extends Illuminate's with static factory methods:
use Sloth\Facades\Response;
// Render a Twig template
Route::get('/projects', function () {
return Response::view('Layout/projects', [
'projects' => Project::all(),
]);
});
// With status code and headers
Route::get('/feed', function () {
return Response::make(renderFeed(), 200)
->header('Content-Type', 'application/xml');
});
// JSON
Route::get('/api/projects', function () {
return Response::json(Project::all()->toArray());
});
Route::get('/api/projects/{id}', function (string $id) {
$project = Project::find($id);
if (!$project) {
return Response::json(['error' => 'Not found'], 404);
}
return Response::json($project->toArray());
});
// Redirect
Route::get('/old-url', function () {
return Response::redirect('/new-url'); // 302
return Response::redirect('/new-url', 301); // permanent
});
// File download
Route::get('/download', function () {
return Response::download('/path/to/file.pdf', 'report.pdf');
});
// Inline file (display in browser)
Route::get('/preview', function () {
return Response::file('/path/to/file.pdf');
});
// 204 No Content
Route::delete('/projects/{id}', function (string $id) {
Project::find($id)?->delete();
return Response::noContent();
});
URL Generation
The URL facade and url() helper generate URLs for named routes and WordPress locations:
use Sloth\Facades\URL;
// Named routes
URL::route('projects.index'); // /projects
URL::route('projects.show', ['slug' => 'my-project']); // /projects/my-project
// WordPress URLs
URL::home(); // https://example.com
URL::to('/about'); // https://example.com/about
URL::theme(); // https://example.com/wp-content/themes/my-theme
URL::theme('css/app.css'); // https://example.com/.../my-theme/css/app.css
URL::asset('css/app.css'); // https://example.com/.../my-theme/public/css/app.css
URL::content(); // https://example.com/wp-content
URL::uploads(); // https://example.com/wp-content/uploads
URL::current(); // /current/path (no host)
URL::full(); // https://example.com/current/path
In Twig:
{{ url().route('projects.index') }}
{{ url().theme('css/app.css') }}
{{ url().asset('js/app.js') }}
Or via the global url() helper:
url('/about'); // https://example.com/about
url()->route('projects.show', [...]); // named route