folderweb/docs/04-development/02-content-system.md

140 lines
6.5 KiB
Markdown
Raw Normal View History

# 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) |
| `feed` | bool | `false` | Enable Atom feed on list pages (`feed.xml`) |
| `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
**`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.
## List Item Building
**`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.
## 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.