Ava CMS themes are HTML-first templates with PHP available when you need it. Start with normal HTML, then sprinkle in <?= ?> to output data or call helpers. There's no custom templating language, no build step, and no new syntax to learn.
<!-- Output a title -->
<h1><?= $content->title() ?></h1>
<!-- Render the Markdown content as HTML -->
<div class="content">
<?= $ava->body($content) ?>
</div>
<!-- Link to a stylesheet in your theme's assets folder -->
<link rel="stylesheet" href="<?= $ava->asset('style.css') ?>">
<!-- Loop through recent posts -->
<?php foreach ($ava->recent('post', 5) as $entry): ?>
<article>
<h2><a href="<?= $ava->url('post', $entry->slug()) ?>"><?= $entry->title() ?></a></h2>
<time><?= $ava->date($entry->date()) ?></time>
</article>
<?php endforeach; ?>
You decide how much custom PHP to use: none for simple pages, or more for dynamic layouts. The helpers are there when you want them, but HTML remains the core.
Why HTML + PHP (and not a custom templating language)?
What you gain
- Familiar building blocks — If you know HTML, you can start immediately. Output is just
<?= $variable ?>and the$avahelper. - No build pipeline — Save the file, refresh the browser. No extra compilers, watchers or dependencies needed.
- Full power available — Need a loop, conditional, or a custom helper? Use plain PHP. No special template language or custom syntax.
- Easy to debug — Standard PHP errors, standard stack traces. Nothing is hidden behind a template engine.
Who this suits
- Designers comfortable with HTML/CSS who want minimal new concepts
- Developers who want flexibility without adopting a custom template language or dealing with additional tooling
- Beginners who want to learn web fundamentals instead of framework-specific magic
- Teams that prefer transparency and portability
What is `<?= ?>`?
What is <?= ?>?
<?= ?> is a short way to output a value in PHP. It’s exactly the same as writing <?php echo ?>, just shorter and easier to read.
It’s called a short echo tag and is always enabled in modern PHP.
⚠️ It does not escape output automatically. Use htmlspecialchars() or $ava->e() when outputting user-provided data to prevent XSS attacks.
Don’t confuse it with <? ?>:
That older shorthand (without the =) is discouraged and disabled by default. As long as you include the =, you’re using the correct syntax.
Theme Structure
A theme is just a folder in app/themes/. Here's a typical layout:
app/themes/
└── default/
├── templates/ # Your page layouts
│ ├── index.php # The default layout
│ ├── page.php # For standard pages
│ ├── post.php # For blog posts
│ └── 404.php # "Page not found" error
├── partials/ # Reusable template fragments
│ ├── header.php
│ └── footer.php
├── assets/ # CSS, JS, images
│ ├── style.css
│ └── script.js
└── theme.php # Optional setup code
Using Assets
Ava CMS makes it easy to include your CSS and JS files. It even handles cache-busting automatically, so your visitors always see the latest version based on the files modified time.
<!-- Just ask $ava for the asset URL -->
<link rel="stylesheet" href="<?= $ava->asset('style.css') ?>">
<script src="<?= $ava->asset('script.js') ?>"></script>
/theme/style.css?v=123456, ensuring instant updates when you change the file without worrying about browser or CDN caching.
Treat your theme's `assets/` folder as a public directory: never place sensitive files, configuration, or executable code there. For private theme files like PHP templates, use the `templates/` or `partials/` folders which are not web-accessible.
Template Basics
In your template files (like page.php), you have access to your content and helper variables.
<!-- templates/post.php -->
<?= $ava->partial('header', ['title' => $content->title()]) ?>
<article>
<h1><?= $content->title() ?></h1>
<div class="content">
<?= $ava->body($content) ?>
</div>
<?php if ($content->date()): ?>
<time><?= $ava->date($content->date()) ?></time>
<?php endif; ?>
</article>
<?= $ava->partial('footer') ?>
It's just HTML with simple tags to show your data.
Quick Reference
This section gives you a complete overview of what's available in templates.
Template Variables
These variables are available in your templates:
| Variable | Type | Description |
|---|---|---|
$content |
Item |
The current content item (single post/page templates only) |
$query |
Query |
Query object for archive/listing templates |
$tax |
array |
Taxonomy context (taxonomy templates only). See details below. |
$site |
array |
Site config: name, url, timezone |
$theme |
array |
Theme info: name, path, url |
$request |
Request |
Current HTTP request (path, query params, etc.) |
$route |
RouteMatch |
Matched route information (type, params, template) |
$ava |
TemplateHelpers |
Helper methods for rendering, URLs, queries, and more |
The $tax Variable
The $tax array is available in taxonomy templates and has different contents depending on the route type:
In taxonomy.php (term archive):
| Key | Type | Description |
|---|---|---|
$tax['name'] |
string |
Taxonomy name (e.g., 'category') |
$tax['term'] |
array |
Term data: slug, name, count, items, plus any custom fields from registry |
In taxonomy-index.php (all terms listing):
| Key | Type | Description |
|---|---|---|
$tax['name'] |
string |
Taxonomy name (e.g., 'category') |
$tax['terms'] |
array |
All terms with their data (keyed by slug) |
$content only exists on single content pages, while $query is for archives.
The $content Object — All Properties
When displaying a single piece of content (a page, post, etc.), use $content to access its data:
| Method | Returns | Description |
|---|---|---|
| Identity | ||
id() |
string|null |
Unique identifier (ULID) |
title() |
string |
Title from frontmatter |
slug() |
string |
URL-friendly identifier |
type() |
string |
Content type (page, post, etc.) |
status() |
string |
draft, published, or unlisted |
| Status Checks | ||
isPublished() |
bool |
Is status "published"? |
isDraft() |
bool |
Is status "draft"? |
isUnlisted() |
bool |
Is status "unlisted"? |
| Dates | ||
date() |
DateTimeImmutable|null |
Publication date (and time if specified) |
updated() |
DateTimeImmutable|null |
Last updated (falls back to date()) |
| Content | ||
rawContent() |
string |
Raw Markdown body (before rendering) |
excerpt() |
string|null |
Excerpt from frontmatter |
rawHtml() |
bool |
Whether to render body as raw HTML (skips Markdown) |
| Taxonomies | ||
terms() |
array |
All taxonomy terms |
terms('category') |
array |
Terms for a specific taxonomy |
| SEO | ||
metaTitle() |
string|null |
Custom meta title |
metaDescription() |
string|null |
Meta description |
noindex() |
bool |
Should search engines skip this? |
canonical() |
string|null |
Canonical URL |
ogImage() |
string|null |
Open Graph image URL |
| Custom Fields | ||
get('field') |
mixed |
Get any frontmatter field |
get('field', 'default') |
mixed |
Get field with default value |
has('field') |
bool |
Check if field exists |
| Assets & Structure | ||
css() |
array |
Per-item CSS files |
js() |
array |
Per-item JS files |
template() |
string|null |
Custom template name |
parent() |
string|null |
Parent page slug |
order() |
int |
Manual sort order |
redirectFrom() |
array |
Old URLs that redirect here |
filePath() |
string |
Path to the Markdown file |
html() |
string|null |
Pre-rendered HTML (if available) |
frontmatter() |
array |
All frontmatter fields as an array |
The $ava Helper — All Methods
The $ava object provides helper methods for common tasks:
| Method | Description |
|---|---|
| Rendering | |
body($content) |
Render content's Markdown body to HTML |
markdown($string) |
Render a Markdown string to HTML |
partial($name, $data) |
Render a partial template |
expand($path) |
Expand path aliases (e.g., @media:) |
| URLs | |
url($type, $slug) |
URL for a content item |
termUrl($taxonomy, $term) |
URL for a taxonomy term page |
baseUrl() |
Get the site base URL (from config) |
asset($path) |
Theme asset URL with cache-busting |
fullUrl($path) |
Full absolute URL from a path |
| Queries | |
query() |
Start a new content query |
recent($type, $count) |
Get recent items (shortcut) |
get($type, $slug) |
Get a specific item by slug |
terms($taxonomy) |
Get all terms for a taxonomy |
termName($taxonomy, $slug) |
Get display name for a term |
| Dates | |
date($date, $format) |
Format a date (uses site timezone) |
ago($date) |
Relative time ("2 days ago") |
| HTML | |
metaTags($content) |
Output SEO meta tags |
itemAssets($content) |
Output per-content CSS/JS |
pagination($query, $path) |
Render pagination links |
| Utilities | |
e($value) |
Escape HTML (for user input) |
excerpt($text, $words) |
Truncate text to word count |
config($key) |
Get a config value |
Detailed Guide
Rendering Content
Display a page or post's main content with $ava->body($content):
<div class="content">
<?= $ava->body($content) ?>
</div>
Why `$ava->body($content)` instead of `$content->body()`?
The $content object holds your raw Markdown text, but $ava->body() does the processing:
- Converts Markdown → HTML
- Processes shortcodes like
[button] - Expands path aliases like
@media: - Applies plugin filters
- Uses pre-rendered cache when available for better performance
Think of $content as the ingredients, and $ava as the kitchen that prepares the final dish.
Working with Dates
Format dates using $ava->date(), which automatically converts to your site's timezone:
<!-- Uses site's default format from config -->
<?= $ava->date($content->date()) ?>
<!-- Or specify a custom format -->
<?= $ava->date($content->date(), 'F j, Y') ?> // December 31, 2025
<?= $ava->date($content->date(), 'M j, g:ia') ?> // Dec 31, 2:30pm
Dates in frontmatter can include times:
date: 2025-12-31 # Date only
date: 2025-12-31 14:30 # Date with time
date: 2025-12-31T14:30:00 # ISO 8601 format
For relative times, use $ava->ago():
<?= $ava->ago($content->date()) ?> // "2 hours ago", "3 days ago"
Y (year), m (month), d (day), F (full month name), M (short month), j (day without zero), g (12-hour), H (24-hour), i (minutes), a (am/pm).
Escaping HTML
Use $ava->e() to escape HTML characters in user-submitted data:
<!-- User input from URL - escape it! -->
<p>Search results for: <?= $ava->e($request->query('q')) ?></p>
When do you need `$ava->e()`?
You only need it for user-submitted data like search queries or form input. Your own content from Markdown files is safe to output directly—you control it, so there's no security risk.
Custom Fields
Access any frontmatter field via $content->get():
<p>Role: <?= $content->get('role', 'Unknown') ?></p>
<?php if ($content->get('featured')): ?>
<span class="badge">Featured</span>
<?php endif; ?>
Taxonomies
Display categories, tags, or any taxonomy terms:
<!-- Terms for the current content item -->
<?php foreach ($content->terms('category') as $term): ?>
<a href="<?= $ava->termUrl('category', $term) ?>"><?= $term ?></a>
<?php endforeach; ?>
<!-- All terms across your site -->
<?php foreach ($ava->terms('category') as $slug => $info): ?>
<a href="<?= $ava->termUrl('category', $slug) ?>"><?= $info['name'] ?></a>
<span>(<?= $info['count'] ?>)</span>
<?php endforeach; ?>
Querying Content
The $ava->query() method returns a fluent query builder:
// Get the 5 most recent published posts
$posts = $ava->query()
->type('post')
->published()
->orderBy('date', 'desc')
->perPage(5)
->get();
foreach ($posts as $entry) {
echo $entry->title();
}
Building Queries
| Method | Description | Example |
|---|---|---|
type($type) |
Filter by content type | ->type('post') |
status($status) |
Filter by status | ->status('published') |
published() |
Shortcut for published status | ->published() |
whereTax($tax, $term) |
Filter by taxonomy term | ->whereTax('category', 'tutorials') |
where($field, $value, $op) |
Filter by field value (default $op is =) |
->where('featured', true) |
orderBy($field, $dir) |
Sort results | ->orderBy('date', 'desc') |
perPage($count) |
Items per page (max 100) | ->perPage(10) |
page($num) |
Current page number | ->page(2) |
search($query) |
Full-text search | ->search('php tutorial') |
searchWeights($weights) |
Customize search scoring | ->searchWeights(['title_phrase' => 100]) |
fromParams($params) |
Build query from array | ->fromParams($request->query()) |
Comparison operators for where():
The third parameter accepts: =, !=, >, >=, <, <=, in, not_in, like
// Comparison operators
->where('price', 100, '>') // Greater than
->where('status', ['a', 'b'], 'in') // Value in array
->where('title', 'php', 'like') // Contains (case-insensitive)
Result Methods
| Method | Returns | Description |
|---|---|---|
get() |
Item[] |
Execute and get items |
first() |
Item|null |
Get first match |
count() |
int |
Total count (before pagination) |
totalPages() |
int |
Number of pages |
currentPage() |
int |
Current page number |
hasMore() |
bool |
Are there more pages? |
hasPrevious() |
bool |
Are there previous pages? |
pagination() |
array |
Full pagination info |
isEmpty() |
bool |
No results? |
The pagination() method returns:
$info = $query->pagination();
// [
// 'current_page' => 1,
// 'per_page' => 10,
// 'total' => 42,
// 'total_pages' => 5,
// 'has_more' => true,
// 'has_previous' => false,
// ]
Shortcuts
// Recent items
$posts = $ava->recent('post', 5);
// Get specific item
$about = $ava->get('page', 'about');
Using Partials
Partials are reusable template fragments in app/themes/{theme}/partials/:
<!-- Render a partial -->
<?= $ava->partial('header') ?>
<!-- Pass data to it -->
<?= $ava->partial('header', ['title' => $content->title()]) ?>
Inside partials, passed data becomes variables:
<!-- partials/header.php -->
<header>
<h1><?= $title ?? $site['name'] ?></h1>
</header>
Partials automatically inherit $site, $theme, $request, and $ava.
URLs
<?= $ava->url('post', 'hello-world') ?> // /blog/hello-world
<?= $ava->termUrl('category', 'tutorials') ?> // /category/tutorials
<?= $ava->asset('style.css') ?> // /theme/style.css?v=123456
<?= $ava->fullUrl('/about') ?> // https://example.com/about
SEO and Meta Tags
Output all SEO meta tags for a content item:
<head>
<?= $ava->metaTags($content) ?>
</head>
This outputs meta description, Open Graph tags, canonical URL, and noindex if set.
Per-content CSS/JS Assets
If your content frontmatter defines per-item assets (CSS/JS), output them in your page <head>:
<?= $ava->itemAssets($content) ?>
This helper outputs the appropriate <link> and <script> tags for that item.
Template Resolution
When a content item is requested, Ava CMS looks for a template in this order:
- Frontmatter
templatefield — If the item specifiestemplate: landing, usetemplates/landing.php - Content type's template — From
content_types.php, e.g., posts usepost.php single.phpfallback — A generic single-item templateindex.phpfallback — The ultimate default
For archives and taxonomy pages:
| Route Type | Primary Template | Fallback |
|---|---|---|
| Content type archive | archive.php |
index.php |
| Taxonomy term archive | taxonomy.php |
index.php |
| Taxonomy index (all terms) | taxonomy-index.php |
index.php |
For error pages:
| Error | Template | Built-in Fallback |
|---|---|---|
| 404 Not Found | 404.php |
Ava CMS's built-in 404 page (if theme doesn't provide one) |
| 500 Server Error | 500.php |
Ava CMS's built-in error page |
Taxonomy Templates
Taxonomy pages require specific templates to display term archives and term listings.
Term Archive Template (taxonomy.php)
Displays content tagged with a specific term:
<!-- templates/taxonomy.php -->
<?= $ava->partial('header', ['title' => $tax['term']['name']]) ?>
<h1><?= $ava->e($tax['term']['name']) ?></h1>
<?php if (!empty($tax['term']['description'])): ?>
<p class="term-description"><?= $ava->e($tax['term']['description']) ?></p>
<?php endif; ?>
<p><?= $tax['term']['count'] ?> items in this <?= $tax['name'] ?></p>
<?php foreach ($query->get() as $item): ?>
<article>
<h2>
<a href="<?= $ava->url($item->type(), $item->slug()) ?>">
<?= $item->title() ?>
</a>
</h2>
<?php if ($item->excerpt()): ?>
<p><?= $item->excerpt() ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?= $ava->pagination($query, $request->path()) ?>
<?= $ava->partial('footer') ?>
Taxonomy Index Template (taxonomy-index.php)
Displays all terms in a taxonomy:
<!-- templates/taxonomy-index.php -->
<?= $ava->partial('header', ['title' => ucfirst($tax['name'])]) ?>
<h1>All <?= ucfirst($tax['name']) ?>s</h1>
<ul class="term-list">
<?php foreach ($tax['terms'] as $slug => $term): ?>
<li>
<a href="<?= $ava->termUrl($tax['name'], $slug) ?>">
<?= $ava->e($term['name']) ?>
</a>
<span class="count">(<?= $term['count'] ?>)</span>
<?php if (!empty($term['description'])): ?>
<p><?= $ava->e($term['description']) ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?= $ava->partial('footer') ?>
Complete Examples
Header Partial
<!-- partials/header.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?? $site['name'] ?></title>
<?php if (isset($content)): ?>
<?= $ava->metaTags($content) ?>
<?= $ava->itemAssets($content) ?>
<?php endif; ?>
<link rel="stylesheet" href="<?= $ava->asset('style.css') ?>">
</head>
<body>
<header class="site-header">
<a href="/" class="logo"><?= $site['name'] ?></a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>
Footer Partial
<!-- partials/footer.php -->
</main>
<footer class="site-footer">
<p>© <?= date('Y') ?> <?= $site['name'] ?></p>
</footer>
<script src="<?= $ava->asset('script.js') ?>"></script>
</body>
</html>
Page Template
<!-- templates/page.php -->
<?= $ava->partial('header', ['title' => $content->title(), 'content' => $content]) ?>
<article class="page">
<h1><?= $content->title() ?></h1>
<div class="content">
<?= $ava->body($content) ?>
</div>
</article>
<?= $ava->partial('footer') ?>
Post Template
<!-- templates/post.php -->
<?= $ava->partial('header', ['title' => $content->title(), 'content' => $content]) ?>
<article class="post">
<header class="post-header">
<h1><?= $content->title() ?></h1>
<?php if ($content->date()): ?>
<time datetime="<?= $content->date()->format('c') ?>">
<?= $ava->date($content->date()) ?>
</time>
<?php endif; ?>
<?php if ($categories = $content->terms('category')): ?>
<div class="categories">
<?php foreach ($categories as $term): ?>
<a href="<?= $ava->termUrl('category', $term) ?>"><?= $term ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</header>
<div class="content">
<?= $ava->body($content) ?>
</div>
</article>
<?= $ava->partial('footer') ?>
Archive Template
<!-- templates/archive.php -->
<?= $ava->partial('header', ['title' => 'Blog']) ?>
<h1>Blog</h1>
<?php foreach ($query->get() as $entry): ?>
<article class="post-summary">
<h2><a href="<?= $ava->url('post', $entry->slug()) ?>"><?= $entry->title() ?></a></h2>
<time><?= $ava->date($entry->date()) ?></time>
<?php if ($entry->excerpt()): ?>
<p><?= $entry->excerpt() ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?= $ava->pagination($query, $request->path()) ?>
<?= $ava->partial('footer') ?>
Search
Ava CMS includes full-text search. Add ->search() to any query to search titles, excerpts, and content:
$results = $ava->query()
->published()
->search('your query')
->perPage(10)
->get();
Results are automatically scored and ranked by relevance.
Tuning Search Relevance
By default, exact phrase matches in titles score highest. Customize weights:
- Globally — configure
search.weightsin content type config - Per query — override with
->searchWeights():
$results = $ava->query()
->published()
->searchWeights(['title_phrase' => 120, 'body_phrase' => 5])
->search($q)
->get();
Adding Search to Your Theme
Register a route in theme.php to handle search requests:
$app->router()->addRoute('/search', function ($request) use ($app) {
$q = trim($request->query('q', ''));
$results = $q ? $app->query()->published()->search($q)->perPage(10)->get() : [];
return $app->render('search', [
'query' => $q,
'results' => $results,
]);
});
Working examples
- Default theme (server-rendered search page): theme.php and search.php template
- Docs theme (AJAX popup): theme.php — registers
/searchand/search.jsonroutes; the front-end JS renders results in a modal
Theme Bootstrap
theme.php runs when your theme loads. It should return a function that receives the application instance. Use it for hooks, shortcodes, and custom routes:
<?php
// app/themes/yourtheme/theme.php
use Ava\Application;
use Ava\Plugins\Hooks;
return function (Application $app): void {
// Register shortcodes
$app->shortcodes()->register('theme_version', fn() => '1.0.0');
// Add data to all templates
Hooks::addFilter('render.context', function (array $context) {
$context['social_links'] = [
'twitter' => 'https://twitter.com/yoursite',
'github' => 'https://github.com/yoursite',
];
return $context;
});
// Custom route
$app->router()->addRoute('/search', function ($request) use ($app) {
// Handle search...
});
};
Organizing Larger Themes
Split a large theme.php into multiple files:
// app/themes/yourtheme/theme.php
return function (\Ava\Application $app): void {
(require __DIR__ . '/inc/shortcodes.php')($app);
(require __DIR__ . '/inc/routes.php')($app);
};
Each file returns a function receiving $app—everything stays portable with your theme folder.
Community Themes
Looking for ready-made themes? Check out the Community Themes page for themes shared by other Ava CMS users.
Built a theme you'd like to share? Submit it to the community gallery!