- 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.
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, `<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`) |
**`extractRawDateFromFolder(string $folderName): ?string`** — Extracts raw `YYYY-MM-DD` string from folder name prefix. Returns null if no date prefix. No hook processing.
**`extractDateFromFolder(string $folderName): ?string`** — Calls `extractRawDateFromFolder()` then passes the result through `Hook::PROCESS_CONTENT($date, 'date_format')` for plugin formatting (e.g., `"1. January 2025"`).
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.
**`buildListItems(string $dir, Context $ctx, ?array $parentMetadata): array`** — Builds and sorts the items array for list views. Defined in `helpers.php`.
For each subdirectory: loads metadata, extracts title/date/cover/PDF, builds URL with lang prefix and slug. Returns sorted array — direction controlled by `order` metadata on parent (`descending` default).
Each item contains both a formatted `date` (hook-processed for display) and a `rawDate` (ISO `YYYY-MM-DD` for Atom feeds and `<time>` elements). Also includes `dirPath` (filesystem path) used by the feed generator to render full content.
Used by both the list case in `router.php` and the Atom feed generator.
Returns items with `menu = 1` metadata, sorted by `menu_order`. Each item: `['title' => string, 'url' => string, 'order' => int]`. URLs include language prefix if applicable.
**`findPageJs(string $dirPath, string $contentDir): ?array`** — Checks for `script.js` in content directory.
Returns `['url' => string, 'hash' => string]` or null. Hash is MD5 of file content for cache busting. The script is loaded with the `defer` attribute in `base.php`, placed before `</body>` for non-blocking progressive enhancement.