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
126 lines
5.5 KiB
Markdown
126 lines
5.5 KiB
Markdown
# 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, `<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.
|