Update AGENT.md to reflect current project structure and philosophy Add comprehensive architecture documentation covering: - Directory layout - Stable contracts - Request flow - Module dependencies - Page vs list detection - Deployment models - Demo fallback Remove outdated plugin system documentation Add new content system documentation Add configuration documentation Add context API documentation Add hooks and plugins documentation Add templates documentation Add rendering documentation Add development environment documentation
5.5 KiB
Content System
Content Discovery
findAllContentFiles(string $dir): array — Returns sorted file paths from $dir.
- Scans for files with extensions in
CONTENT_EXTENSIONS(md,html,php) - Skips
index.php(reserved for server entry points) - Fires
Hook::PROCESS_CONTENT($files, $dir)— plugins filter files (e.g., language variants) - Sorts by filename via
strnatcmp(natural sort:00-hero,01-intro,02-body) - Returns flat array of absolute paths
File ordering convention: Prefix filenames with NN- for explicit ordering. Files without prefix sort after numbered files.
URL Routing
Trailing slash enforcement: All page and list URLs are canonicalized with a trailing slash. Requests without one receive a 301 redirect (e.g., /about → /about/). The frontpage (/) is exempt.
Frontpage handling: Empty request path is handled before parseRequestPath() — it renders the content root directly via renderMultipleFiles(). The "frontpage" type from parseRequestPath() is never reached in the router's switch statement.
parseRequestPath(Context $ctx): array — Returns ['type' => string, 'path' => string].
Types:
"frontpage"— empty request path (handled before this function is called)"page"— resolved directory with no subdirectories"list"— resolved directory with subdirectories"not_found"— slug resolution failed
resolveSlugToFolder(string $parentDir, string $slug): ?string — Matches URL slug to directory name.
Resolution order:
- Exact folder name match (
$slug === $item) - Metadata slug match (
metadata.inislugfield)
Each URL segment is resolved independently, walking the directory tree. This enables language-specific slugs (e.g., /no/om/ → content/about/ via [no] slug = "om").
Metadata
loadMetadata(string $dirPath): ?array — Parses metadata.ini if present.
Returns flat key-value array with a special _raw key containing the full parsed INI structure (including sections). The _raw key is an internal contract — the language plugin reads $data['_raw'][$lang] to merge language-specific overrides. Plugins processing metadata must preserve _raw if present. Fires Hook::PROCESS_CONTENT($metadata, $dirPath, 'metadata').
Core Fields
| Field | Type | Default | Purpose |
|---|---|---|---|
title |
string | First # heading or folder name |
Page title, list item title, <title> tag |
summary |
string | — | List item description, fallback meta description |
date |
string (YYYY-MM-DD) | Folder date prefix or file mtime | Sorting, display |
search_description |
string | Falls back to summary |
<meta name="description"> |
slug |
string | Folder name | URL override |
menu |
bool/int | 0 | Include in navigation |
menu_order |
int | 999 | Navigation sort order (ascending) |
order |
string | "descending" |
List sort direction (ascending|descending) |
redirect |
string | — | External URL (list items can redirect) |
plugins |
string | — | Comma-separated page-level plugin names |
Settings Section
[settings]
page_template = "list-grid" # List template override (without .php)
show_date = true # Show date in list items (default: true)
hide_list = false # Force page rendering even with subdirectories
Language Sections
title = "About"
slug = "about"
[no]
title = "Om oss"
slug = "om"
Supported language-overridable fields: title, summary, search_description, slug.
Custom Fields
Any key not listed above is passed through to templates/plugins unchanged. Add whatever fields your templates need.
Date Extraction
extractDateFromFolder(string $folderName): ?string — Extracts date from YYYY-MM-DD-* prefix.
If no date prefix exists and no date metadata is set, falls back to file modification time (filemtime). All dates pass through Hook::PROCESS_CONTENT($date, 'date_format') for plugin formatting.
Note: Date extraction uses regex only — 2025-13-45-slug would extract 2025-13-45 without validation. Invalid dates pass through to templates as-is.
Sorting with null dates: Items without any date are sorted as empty strings via strcmp. Their relative order among other dateless items is undefined.
Navigation
buildNavigation(Context $ctx): array — Scans top-level content directories.
Returns items with menu = 1 metadata, sorted by menu_order. Each item: ['title' => string, 'url' => string, 'order' => int]. URLs include language prefix if applicable.
Cover Images
findCoverImage(string $dirPath): ?string — Finds cover.* file.
Checks extensions in COVER_IMAGE_EXTENSIONS order: jpg, jpeg, png, webp, gif. Returns filename (not full path) or null.
PDF Discovery
findPdfFile(string $dirPath): ?string — Returns basename of first *.pdf found, or null.
Page-Specific CSS
findPageCss(string $dirPath, string $contentDir): ?array — Checks for styles.css in content directory.
Returns ['url' => string, 'hash' => string] or null. Hash is MD5 of file content for cache busting.
Meta Description Extraction
extractMetaDescription(string $dirPath, ?array $metadata): ?string
Priority: search_description → summary → first paragraph from content files (>20 chars).
Title Extraction
extractTitle(string $filePath): ?string — Reads first content file in directory.
Markdown: first # heading. HTML/PHP: first <h1> tag. Returns null if no heading found.