# 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: 1. Exact folder name match (`$slug === $item`) 2. Metadata slug match (`metadata.ini` `slug` field) 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, `` 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 ```ini [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 ```ini 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.