This project is under very early, active development and may contain bugs or security issues. It is likely not ready for production websites.

You are responsible for reviewing, testing, and securing any deployment. Ava CMS is provided as free, open-source software without warranty (GNU General Public License), see LICENSE.

AI Reference

This is a condensed technical reference for AI assistants working with Ava CMS. It contains the essential framework details, conventions, and patterns needed to help users build themes, plugins, and content.

For raw Markdown (ideal for AI context): View on GitHub

Overview

Full docs: https://ava.addy.zone/docs

Ava CMS is a flat-file PHP CMS (PHP 8.3+) requiring no database. Content is Markdown with YAML frontmatter. Configuration is PHP arrays. No build step—edit, refresh, done.

This project is under very early, active development and may contain bugs or security issues. It is likely not ready for production websites.

You are responsible for reviewing, testing, and securing any deployment. Ava CMS is provided as free, open-source software without warranty (GNU General Public License), see LICENSE.

Installation: Always download from GitHub Releases—never clone the repository directly. The main branch may contain incomplete or untested work.

Core Philosophy:

  • Files are the source of truth (content, config, themes)
  • No WYSIWYG—users write Markdown in their preferred editor
  • No database required (SQLite optional for 10k+ items)
  • Immediate publishing—no build/deploy step
  • Bespoke by design—any content type without plugins

Project Structure:

mysite/
├── app/                  # Your code
│   ├── config/           # Configuration (ava.php, content_types.php, taxonomies.php)
│   ├── plugins/          # Plugin folders
│   ├── snippets/         # PHP snippets for  shortcode
│   └── themes/{name}/    # Theme templates, assets, partials
├── content/              # Markdown content files
│   ├── pages/            # Hierarchical pages
│   ├── posts/            # Blog posts
│   └── _taxonomies/      # Term registries
├── public/               # Web root (index.php, media/, assets/)
├── storage/cache/        # Index and page cache
└── ava                   # CLI tool

Requirements: PHP 8.3+, extensions: mbstring, json, ctype. Optional: pdo_sqlite, igbinary, opcache, imagick/gd (media uploads).

Configuration

Full docs: https://ava.addy.zone/docs/configuration

All settings in app/config/ as PHP arrays. Three main files:

  • ava.php — Main settings (site, paths, cache, themes, plugins, security, debug)
  • content_types.php — Content type definitions
  • taxonomies.php — Taxonomy definitions

Main Settings (ava.php)

Site Identity:

'site' => [
    'name'        => 'My Site',
    'base_url'    => 'https://example.com',  // No trailing slash
    'timezone'    => 'Europe/London',
    'locale'      => 'en_GB',
    'date_format' => 'F j, Y',
],

Paths:

'paths' => [
    'content'  => 'content',
    'themes'   => 'app/themes',
    'plugins'  => 'app/plugins',
    'snippets' => 'app/snippets',
    'storage'  => 'storage',
    'aliases'  => [
        '/media/' => '/media/',
    ],
],

Content Index (performance-critical):

'content_index' => [
    'mode'         => 'auto',   // 'auto' (dev), 'never' (prod), 'always' (debug)
    'backend'      => 'array',  // 'array' or 'sqlite' (for 10k+ items)
    'use_igbinary' => true,
    'prerender_html' => false,  // Optional: pre-render Markdown → HTML during rebuild
],

Webpage Cache:

'webpage_cache' => [
    'enabled' => true,
    'ttl'     => null,           // null = until rebuild
    'exclude' => ['/api/*'],
],

Content Parsing:

'content' => [
    'markdown' => [
        'allow_html'      => true,
        'heading_ids'     => true,
        'disallowed_tags' => ['script', 'noscript'],
    ],
    'id' => ['type' => 'ulid'],  // 'ulid' or 'uuid7'
],

Security:

'security' => [
    'shortcodes' => ['allow_php_snippets' => true],
    'preview_token' => 'secure-random-token',
],

Plugins: 'plugins' => ['sitemap', 'feed', 'redirects'],

Debug:

'debug' => [
    'enabled'        => false,
    'display_errors' => false,  // Never true in production
    'log_errors'     => true,
    'level'          => 'errors',  // 'all', 'errors', 'none'
],

Content Types (content_types.php)

return [
    'post' => [
        'label'       => 'Posts',
        'content_dir' => 'posts',
        'url' => [
            'type'    => 'pattern',
            'pattern' => '/blog/{slug}',
            'archive' => '/blog',
        ],
        'templates' => [
            'single'  => 'post.php',
            'archive' => 'archive.php',
        ],
        'taxonomies'   => ['category', 'tag'],
        'sorting'      => 'date_desc',
        'cache_fields' => ['author', 'featured_image'],
        'search' => [
            'enabled' => true,
            'fields'  => ['title', 'excerpt', 'body'],
            'weights' => [
                'title_phrase'   => 80,
                'title_token'    => 10,
                'excerpt_phrase' => 30,
                'body_phrase'    => 20,
                'body_token'     => 2,
                'featured'       => 15,
            ],
        ],
    ],
];

Content Type Options:

Option Required Description
label Yes Human-readable display name
content_dir Yes Folder inside content/ for this type
url Yes URL generation settings (see URL Types)
templates Yes Template file mappings (single, archive)
taxonomies No Which taxonomies apply. Default: []
sorting No Default sort: date_desc, date_asc, title, manual
cache_fields No Extra frontmatter fields for archive cache
search No Search config (enabled, fields, weights)

URL Types:

  • hierarchical — URLs mirror file paths. content/pages/about/team.md/about/team
  • pattern — Template-based. Placeholders: {slug}, {id}, {yyyy}, {mm}, {dd}

Taxonomies (taxonomies.php)

return [
    'category' => [
        'label'        => 'Categories',
        'hierarchical' => true,
        'public'       => true,
        'rewrite'      => ['base' => '/category'],
        'behaviour'    => ['allow_unknown_terms' => true, 'hierarchy_rollup' => true],
        'ui'           => ['show_counts' => true, 'sort_terms' => 'name_asc'],
    ],
];
Option Default Description
label Required Human-readable name
hierarchical false Support parent/child relationships
public true Create public archive pages
rewrite.base '/{taxonomy}' URL prefix for archives
behaviour.allow_unknown_terms true Auto-create terms when used
ui.sort_terms 'name_asc' Sort: name_asc, name_desc, count_asc, count_desc

Term registries: content/_taxonomies/{taxonomy}.yml — Pre-define terms with metadata.

Environment-Specific Config

$config = [...];
if (getenv('APP_ENV') === 'development') {
    $config['content_index']['mode'] = 'auto';
    $config['debug']['enabled'] = true;
}
return $config;

Content

Full docs: https://ava.addy.zone/docs/content

Content files are Markdown (.md) or HTML (.html) with YAML frontmatter. Located in content/ folder, organized by content type. File extension determines format: .md → Markdown parsing, .html → raw HTML (no Markdown parsing). Shortcodes and path aliases work in both formats.

File Structure

content/
├── pages/           # Hierarchical pages
│   ├── index.md     # Homepage (/)
│   ├── about.md     # /about
│   ├── custom.html  # /custom (HTML format, no Markdown parsing)
│   └── docs/
│       └── index.md # /docs
├── posts/           # Pattern-based posts
│   └── hello.md     # /blog/hello (if pattern is /blog/{slug})
└── _taxonomies/     # Term registries
    └── category.yml

Frontmatter

---
id: 01JGMK0000POST0000000001
title: My Post
slug: my-post
status: published
date: 2024-12-28
updated: 2024-12-30
excerpt: Short summary for listings
template: custom.php
order: 10
category:
  - tutorials
tag:
  - php
meta_title: SEO Title
meta_description: SEO description
canonical: https://example.com/post
og_image: "/media/social.jpg"
noindex: false
cache: true
redirect_from:
  - /old-url
assets:
  css:
    - "/media/css/custom.css"
  js:
    - "/media/js/script.js"
---

Core Fields:

  • title — Display title (defaults to slugified filename)
  • slug — URL identifier. For hierarchical types, URL comes from file path, not slug
  • statusdraft, published, unlisted
  • id — Optional ULID/UUID7 for stable references
  • date, updated — Timestamps
  • excerpt — Summary for listings/search
  • template — Override default template

Taxonomy Fields:

category: tutorials           # Single term
category:                     # Multiple terms
  - tutorials
  - php
tax:                          # Alternative grouped format
  category: [tutorials, php]
  tag: beginner

SEO Fields: meta_title, meta_description, canonical, noindex, og_image

Behavior Fields:

  • cache: false — Disable caching for this page
  • redirect_from: [/old-url] — 301 redirects from old URLs
  • order — Manual sort order (use with sorting: 'manual')

Content Format: Determined by file extension (.md = Markdown, .html = raw HTML). If both about.md and about.html exist, .html wins.

Custom Fields: Any field is accessible via $item->get('field_name').

Content Status

Status Behavior
draft Not publicly routed. Viewable via preview token.
published Public. In listings, archives, taxonomy indexes.
unlisted Public via direct URL. Excluded from listings/archives/taxonomies.

Path Aliases

Defined in ava.php, expanded at render time:

![Image](/media/photo.jpg)  →  /media/photo.jpg

Creating Content

CLI: ./ava make post "Title" — Creates file with ULID, slug, date, draft status

Validation: ./ava lint — Checks YAML, required fields, duplicate slugs/IDs

CLI

Full docs: https://ava.addy.zone/docs/cli

Run from project root: ./ava <command> [options]

Command Description
status Site overview and health check
rebuild [--keep-webcache] Rebuild content index
lint Validate all content files
make <type> "Title" Create new content with scaffolding
cache:stats Webpage cache statistics
cache:clear [pattern] Clear cached pages
logs:stats Log file statistics
logs:tail [name] [-n N] Show last N lines of log
update:check [--force] Check for updates (cached 1 hour)
update:apply [--yes] [--dev] Download and apply update
update:stale [--dev] [--clean] [--yes] Detect/remove stale files from older releases

Plugin Commands: sitemap:stats, feed:stats, redirects:list, redirects:add <from> <to> [code], redirects:remove <from>

Theming

Full docs: https://ava.addy.zone/docs/theming

Themes are HTML + PHP templates. No build step, no custom templating language.

Structure

app/themes/mytheme/
├── templates/        # Page layouts
│   ├── index.php     # Default fallback
│   ├── page.php      # Pages
│   ├── post.php      # Posts
│   ├── archive.php   # Listings
│   ├── taxonomy.php  # Term archives
│   └── 404.php       # Not found
├── partials/         # Reusable fragments
├── assets/           # CSS, JS, images
└── theme.php         # Bootstrap (optional)

Template Variables

Variable Type Description
$content Item Current content item (single pages)
$query Query Query for archives/listings
$tax array Taxonomy context (taxonomy pages)
$site array Site config: name, url, timezone
$request Request Current HTTP request
$ava TemplateHelpers Helper methods

Content Item ($content) Methods

$content->id()              // ULID
$content->title()           // Title
$content->slug()            // URL slug
$content->type()            // 'page', 'post', etc.
$content->status()          // 'draft', 'published', 'unlisted'
$content->isPublished()     // Status check helpers
$content->date()            // DateTimeImmutable|null
$content->updated()         // DateTimeImmutable|null (falls back to date)
$content->excerpt()         // Excerpt string
$content->rawContent()      // Raw body content
$content->format()          // 'markdown' or 'html' (from file extension)
$content->isHtml()          // true if .html file
$content->terms('category') // Taxonomy terms array
$content->get('field')      // Custom frontmatter field
$content->get('field', 'default') // With default
$content->has('field')      // Check field exists
$content->metaTitle()       // SEO title
$content->metaDescription() // SEO description
$content->noindex()         // Should search engines skip?
$content->frontmatter()     // All frontmatter as array

Template Helpers ($ava) Methods

Rendering:

$ava->body($content)              // Render content body (uses pre-render cache when enabled)
$ava->markdown($string)           // Render Markdown string
$ava->partial('header', $data)    // Include partial with data

URLs:

$ava->url('post', 'my-slug')      // Content URL
$ava->termUrl('category', 'php')  // Term archive URL
$ava->asset('style.css')          // Theme asset with cache-bust
$ava->baseUrl()                   // Site base URL

Queries:

$ava->query()                     // New query builder
$ava->recent('post', 5)           // Recent items shortcut
$ava->get('page', 'about')        // Get specific item
$ava->terms('category')           // All terms for taxonomy

Utilities:

$ava->date($date, 'F j, Y')       // Format date
$ava->ago($date)                  // "2 days ago"
$ava->e($value)                   // HTML escape
$ava->metaTags($content)          // Output SEO meta tags
$ava->itemAssets($content)        // Output per-item CSS/JS
$ava->pagination($query, $path)   // Render pagination
$ava->config('key.subkey')        // Get config value

Query Builder

$posts = $ava->query()
    ->type('post')
    ->published()
    ->whereTax('category', 'tutorials')
    ->where('featured', true)
    ->orderBy('date', 'desc')
    ->perPage(10)
    ->page(1)
    ->search('query')
    ->get();

// Result methods
$query->count()        // Total items
$query->totalPages()   // Page count
$query->hasMore()      // More pages?
$query->pagination()   // Full pagination info

Where operators: =, !=, >, >=, <, <=, in, not_in, like

Template Resolution

  1. Frontmatter template: landingtemplates/landing.php
  2. Content type's configured template
  3. single.php fallback
  4. index.php fallback

Theme Bootstrap (theme.php)

<?php
return function (\Ava\Application $app): void {
    // Register shortcodes
    $app->shortcodes()->register('mycode', fn() => 'output');
    
    // Add to template context
    Hooks::addFilter('render.context', function ($ctx) {
        $ctx['custom'] = 'value';
        return $ctx;
    });
    
    // Custom routes
    $app->router()->addRoute('/search', function ($request) use ($app) {
        // ...
    });
};

Routing

Full docs: https://ava.addy.zone/docs/routing

URLs are generated automatically from content structure and configuration. Routes are cached in binary files for instant lookups.

URL Types

Hierarchical — URLs mirror file paths:

'page' => [
    'url' => ['type' => 'hierarchical', 'base' => '/'],
]
// content/pages/about/team.md → /about/team
// content/pages/index.md → /

Pattern — Template-based URLs:

'post' => [
    'url' => [
        'type'    => 'pattern',
        'pattern' => '/blog/{slug}',
        'archive' => '/blog',
    ],
]

Placeholders: {slug}, {id}, {yyyy}, {mm}, {dd}

Route Matching Order

  1. Hook interception (router.before_match)
  2. Trailing slash redirect (301)
  3. Redirects from redirect_from frontmatter
  4. Custom routes ($router->addRoute())
  5. Content routes (from cache)
  6. Preview mode (drafts with token)
  7. Prefix routes ($router->addPrefixRoute())
  8. Taxonomy routes
  9. 404

Custom Routes

// In theme.php or plugin.php
$router->addRoute('/api/search', function ($request) use ($app) {
    return \Ava\Http\Response::json(['results' => []]);
});

// With parameters
$router->addRoute('/api/posts/{id}', function ($request, $params) {
    $id = $params['id'];
    // ...
});

// Prefix routes (matches /api/*)
$router->addPrefixRoute('/api/', function ($request) {
    // ...
});

Taxonomy Routes

When public: true in taxonomy config:

  • /categorytaxonomy-index.php
  • /category/tutorialstaxonomy.php

Preview Mode

Access drafts: ?preview=1&token=YOUR_TOKEN

Configure: 'security' => ['preview_token' => 'random-token']

Generating URLs

$ava->url('post', 'my-slug')         // /blog/my-slug
$ava->url('page', 'about/team')      // /about/team (hierarchical path)
$ava->termUrl('category', 'php')     // /category/php
$ava->fullUrl('/about')              // https://example.com/about

Shortcodes

Full docs: https://ava.addy.zone/docs/shortcodes

Dynamic content in Markdown via [tag] syntax. Processed after Markdown conversion.

Built-in Shortcodes

Shortcode Output
2026 Current year
2026-04-11 Formatted current date
Ava CMS Site name from config
https://ava.addy.zone Site URL from config
you@example.com Obfuscated mailto link
Renders app/snippets/file.php

Creating Shortcodes

// In theme.php
$app->shortcodes()->register('greeting', function ($attrs, $content, $tag) {
    $name = $attrs['name'] ?? 'friend';
    return "Hello, " . htmlspecialchars($name) . "!";
});

Callback parameters:

  • $attrs — Array of attributes
  • $content — Content between tags (null if self-closing)
  • $tag — Shortcode name (lowercase)

Snippets

PHP files in app/snippets/ folder, invoked via .

Variables available:

  • $params — Attributes array
  • $content — Content between tags
  • $ava — Rendering engine
  • $app — Application instance
<?php // app/snippets/cta.php ?>
<?php $heading = $params['heading'] ?? 'Ready?'; ?>
<div class="cta">
    <h3><?= htmlspecialchars($heading) ?></h3>
    <?= $content ?>
</div>

Security: Snippet names can't contain .. or /. Disable with security.shortcodes.allow_php_snippets = false.

Limitations

  • No nested shortcodes
  • Paired content stops at next [ character

Plugins

Full docs: https://ava.addy.zone/docs/creating-plugins

Reusable extensions in app/plugins/{name}/plugin.php. Survive theme changes.

<?php
return [
    'name' => 'My Plugin',
    'version' => '1.0.0',
    'boot' => function($app) {
        // Routes, hooks, etc.
    },
    'commands' => [
        ['name' => 'myplugin:task', 'description' => 'Do something', 
         'handler' => function($args, $cli, $app) { return 0; }],
    ],
];

Enable in ava.php: 'plugins' => ['sitemap', 'feed', 'my-plugin']

Hooks

Filters — Modify and return data:

use Ava\Plugins\Hooks;
Hooks::addFilter('render.context', fn($ctx) => [...$ctx, 'custom' => 'value'], priority: 10);

Actions — React to events:

Hooks::addAction('indexer.rebuild', function($app) { /* sync content */ });
Hook Type Description
router.before_match Filter Intercept routing
content.loaded Filter Modify loaded content item
render.context Filter Add template variables
render.output Filter Modify final HTML
markdown.configure Action Configure CommonMark
indexer.rebuild Action After content index rebuild
cli.rebuild Action After CLI rebuild command

Bundled Plugins

Full docs: https://ava.addy.zone/docs/bundled-plugins

Sitemap

Generates /sitemap.xml for search engines.

'sitemap' => [
    'enabled' => true,
],

Exclude pages with noindex: true in frontmatter. CLI: ./ava sitemap:stats

RSS Feed

Generates /feed.xml for RSS readers.

'feed' => ['enabled' => true, 'items_per_feed' => 20, 'full_content' => false, 'types' => null],

Add to theme: <link rel="alternate" type="application/rss+xml" href="/feed.xml">

Redirects

Manage URL redirects via CLI. Stored in storage/redirects.json.

Status codes: 301, 302, 307, 308 (redirects), 410, 451, 503 (status-only)

Alternative: Use redirect_from: in content frontmatter for content-based redirects.

Performance

Full docs: https://ava.addy.zone/docs/performance

Two-layer system: Content Indexing + Webpage Caching.

Content Index

Pre-built binary index of content metadata. Avoids parsing Markdown on every request.

Cache files: storage/cache/recent_cache.bin, slug_lookup.bin, content_index.bin, tax_index.bin, routes.bin

Tier Cache Use Case Speed
1 Recent Homepage, RSS ~0.2ms
2 Slug Lookup Single item ~1-15ms
3 Full Index Search, pagination ~15-300ms

Backends: array (default, fastest) or sqlite (10k+ items, lower memory)

Mode Behavior
auto Rebuild on file changes (development)
never Only via ./ava rebuild (production)
always Every request (debugging only)

Webpage Cache

Stores fully-rendered HTML.

'webpage_cache' => ['enabled' => true, 'ttl' => null, 'exclude' => ['/api/*']],

Speed: Uncached ~5ms → Cached ~0.02ms (250× faster)

Per-page control: cache: false in frontmatter

Not cached: POST requests, query strings

Invalidation: ./ava rebuild clears both caches (use --keep-webcache to preserve webpage cache)

Recommendations: Development: mode: auto, cache disabled. Production: mode: never, cache enabled, ./ava rebuild after deploys.

Hosting

Full docs: https://ava.addy.zone/docs/hosting

Requirements: PHP 8.3+, Composer, SSH access recommended.

Structure: Only public/ should be web-accessible. Keep app/, content/, core/, storage/ above web root.

Local Development: php -S localhost:8000 -t public

Nginx:

server {
    root /path/to/public;
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; include fastcgi_params; }
}

Deployment: git pull && composer install --no-dev && ./ava rebuild

Pre-Launch: PHP 8.3+, HTTPS enabled, ./ava rebuild run, debug disabled, cache enabled.

Updates

Full docs: https://ava.addy.zone/docs/updating

./ava update:check          # Check for updates (cached 1 hour)
./ava update:check --force  # Bypass cache
./ava update:apply          # Download and apply latest release
./ava update:apply --yes    # Skip confirmation
./ava update:apply --dev    # Update from main branch (dev/testing only)
./ava update:stale          # Scan for stale files
./ava update:stale --clean  # Remove stale files

Updated: core/, ava, bootstrap.php, composer.json, composer.lock, public/index.php, public/.htaccess, index.php, .htaccess, nginx.conf.example, bundled plugins (clean sync). Preserved: content/, app/config/, app/themes/, app/snippets/, vendor/, storage/, custom plugins.

Post-update (automatic): Finalize (remove stale dirs) → composer install --no-dev./ava rebuild

Custom paths block updates: If paths.themes, paths.plugins, or paths.snippets differ from defaults, auto-update is blocked.

Version format: CalVer YY.M.PATCH (e.g., 26.4.1). Dev mode: {current}-dev.{shortsha}.

API

Full docs: https://ava.addy.zone/docs/api

Building blocks for custom APIs (no predefined structure).

Request/Response

$request->method()                    // GET, POST, etc.
$request->query('key', $default)      // Query parameter
$request->header('X-Api-Key')         // Header
$request->body()                      // Request body

Response::json($data, 200)            // JSON response
Response::redirect($url, 302)         // Redirect
Response::json(['ok' => true])->withHeader('Cache-Control', 'no-store')

JSON API Example

$router->addRoute('/api/posts', function($request, $params) use ($app) {
    $posts = $app->query()->type('post')->published()
        ->orderBy('date', 'desc')->perPage(10)->get();
    return \Ava\Http\Response::json([
        'data' => array_map(fn($p) => ['title' => $p->title(), 'slug' => $p->slug()], $posts),
    ]);
});

Authentication Pattern

$apiKey = $request->header('X-API-Key') ?? $request->query('api_key');
$validKeys = $app->config('api.keys', []);
if (!in_array($apiKey, $validKeys, true)) {
    return Response::json(['error' => 'Unauthorized'], 401);
}

Store keys: 'api' => ['keys' => ['your-secret-key']] in ava.php


License

Ava CMS is released under the GNU General Public License v3.0 (GPL-3.0). This means you are free to use, modify, and distribute it, but any derivative works must also be released under the GPL. When using AI assistants to generate code for Ava CMS projects, please ensure the generated code respects the GPL license terms.


Ava CMS is provided as free, open-source software without warranty. You are responsible for reviewing, testing, and securing any deployment.