Routing is how your CMS decides which content to show when someone visits a URL on your site — like matching /about to your about page.
In Ava CMS, you don't write route files. URLs are generated automatically based on your content structure and configuration.
How It Works
- 💾 Save a file in your content directory
- 👀 Ava indexes it (automatically in
autoindex mode or via./ava rebuild) - ✨ The URL works — routing is handled for you
Routes are compiled into a binary cache for instant lookups.
URL Styles
Choose how URLs are generated per content type in app/config/content_types.php.
Hierarchical URLs
Best for most simple content types that aren't organised by date. URLs mirror your file structure.
'page' => [
'url' => [
'type' => 'hierarchical',
'base' => '/',
],
]
How it maps files to URLs:
| File Path | URL |
|---|---|
content/pages/about.md |
/about |
content/pages/about/team.md |
/about/team |
content/pages/services/web.md |
/services/web |
content/pages/index.md |
/ (homepage) |
content/pages/docs/index.md |
/docs |
Key points:
index.mdfiles represent the parent folder URLbaseadds a URL prefix (e.g.,'base' => '/docs'makesabout.md→/docs/about)- The file path determines the URL —
slug:in frontmatter is ignored - Use path-based keys for lookups:
$ava->get('page', 'about/team')
Pattern URLs
Best for blogs and news. URLs follow a specified pattern regardless of file organization.
'post' => [
'url' => [
'type' => 'pattern',
'pattern' => '/blog/{slug}',
'archive' => '/blog', // Optional: listing page URL
],
]
Available placeholders:
| Placeholder | Description | Example |
|---|---|---|
{slug} |
Item's slug from frontmatter or filename | my-post |
{id} |
Item's unique ID (ULID or UUID7) | 01HXYZ4567ABCD |
{yyyy} |
4-digit year from date field |
2024 |
{mm} |
2-digit month from date field |
03 |
{dd} |
2-digit day from date field |
15 |
Examples:
'/blog/{slug}' // → /blog/my-post
'/blog/{yyyy}/{slug}' // → /blog/2024/my-post
'/news/{yyyy}/{mm}/{dd}/{slug}' // → /news/2024/03/15/my-post
'/p/{id}' // → /p/01HXYZ4567ABCD (rename-proof)
Key points:
- Use the
slugfield for lookups:$ava->get('post', 'hello-world') - File organization doesn't affect URLs
archivecreates a listing page
Content Status and Routing
Content files must have a status in their frontmatter to determine routing behavior:
---
title: Sample Post
status: published # draft, published, unlisted
---
Depending on the status, routing behaves as follows:
| Status | Routing Behavior |
|---|---|
draft |
Not routed publicly. Only accessible via preview mode. |
published |
Fully accessible. Appears in listings and sitemaps. |
unlisted |
Accessible via direct URL but excluded from listings. |
Preview Mode
View draft content before publishing by adding ?preview=1&token=... to the URL:
https://example.com/blog/my-draft?preview=1&token=YOUR_SECRET_TOKEN
Configure in ava.php:
'security' => [
'preview_token' => 'your-secure-random-token',
]
Generate a secure token:
php -r "echo bin2hex(random_bytes(32));"
Both preview=1 and token parameters are required. Without a configured token, preview mode is disabled.
Redirects
Redirect old URLs when you move or rename content:
---
title: New Page Title
redirect_from:
- /old-page
- /legacy/old-url
---
Ava issues 301 (permanent) redirects from old URLs to the current URL. For external redirects or arbitrary mappings, use the Redirects plugin.
Trailing Slash
Enforce consistent URL format in ava.php:
'routing' => [
'trailing_slash' => false, // /about (recommended)
]
Ava issues 301 redirects to enforce your choice:
false:/about/→/abouttrue:/about→/about/
The root / is always valid.
Taxonomy Routes
Taxonomies with public: true automatically get routes:
'category' => [
'label' => 'Categories',
'public' => true, // Enable public routes
'rewrite' => [
'base' => '/category', // URL prefix
],
],
Routes created:
/category— Taxonomy index (all terms)/category/tutorials— Term archive (items with this term)
Template variables:
$tax['name']— Taxonomy name$tax['term']— Term data (term archives)$tax['terms']— All terms (index)$query— Pre-filtered Query object
Adding Custom Routes
Register custom routes in your theme.php for APIs or special pages.
Exact Routes
return function (\Ava\Application $app): void {
$router = $app->router();
$router->addRoute('/api/search', function ($request) use ($app) {
$results = $app->query()
->published()
->search($request->query('q', ''))
->perPage(10)
->get();
return \Ava\Http\Response::json([
'results' => array_map(fn($item) => [
'title' => $item->title(),
'url' => $app->router()->urlFor($item->type(), $item->slug()),
], $results),
]);
});
};
Routes with Parameters
$router->addRoute('/api/posts/{id}', function ($request, $params) use ($app) {
$item = $app->repository()->getById($params['id']);
if (!$item) {
return \Ava\Http\Response::json(['error' => 'Not found'], 404);
}
return \Ava\Http\Response::json(['title' => $item->title()]);
});
### Prefix Routes
```php
$router->addPrefixRoute('/api/', function ($request) use ($app) {
// Handles all /api/* requests
return \Ava\Http\Response::json(['path' => $request->path()]);
});
Prefix routes are checked after exact and content routes.
Using Hooks
Intercept routing with the router.before_match filter (checked first):
use Ava\Plugins\Hooks;
use Ava\Http\Response;
Hooks::addFilter('router.before_match', function ($match, $request) use ($app) {
if ($request->path() === '/custom-page') {
return Response::html($app->render('custom-template'));
}
return $match;
});
See Understanding Hooks for details.
Route Caching
Routes are compiled to binary cache files for instant lookups:
storage/cache/routes.bin— All route mappingsstorage/cache/slug_lookup.bin— Content key lookups
Rebuilding:
content_index.mode = 'auto'— Automatic when content changescontent_index.mode = 'never'— Run./ava rebuildmanually
See Performance for details.
Generating URLs in Templates
Use $ava helper methods:
<?= $ava->url('post', 'hello-world') ?> // Content: /blog/hello-world
<?= $ava->url('page', 'about/team') ?> // Hierarchical: /about/team
<?= $ava->termUrl('category', 'tutorials') ?> // Term: /category/tutorials
<?= $ava->fullUrl('/about') ?> // Full: https://example.com/about
<?= $ava->baseUrl() ?> // Base: https://example.com
Route Matching Order
When a request comes in, Ava CMS checks routes in this specific order:
- Hook interception —
router.before_matchfilter can intercept and return a response early - Trailing slash redirect — Enforces your canonical URL style (301 redirect)
- Redirects — 301 redirects from
redirect_fromfrontmatter - System routes — Custom routes registered via
$router->addRoute() - Exact routes — Content URLs from the routes cache
- Preview mode — Allows draft access with valid preview token
- Prefix routes — Custom routes registered via
$router->addPrefixRoute() - Taxonomy routes — Archives like
/category/tutorials - 404 — No match found
Understanding this order helps when debugging why a route isn't matching as expected.
Debugging Routes
Inspect route cache:
$routes = $app->repository()->routes();
var_dump($routes['exact']['/my-path'] ?? 'not found');
Common issues:
| Issue | Solution |
|---|---|
| 404 for new content | Run ./ava rebuild or set content_index.mode = 'auto' |
| Wrong hierarchical URL | Use path-based key: $ava->get('page', 'about/team') |
| Redirect loop | Check for circular redirect_from |
| Preview not working | Verify security.preview_token in config |