folderweb/docs/04-development/02-content-system.md
Ruben b03511f99b Update AGENT.md and add architecture documentation
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
2026-02-05 23:30:44 +01:00

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:

  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

[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_descriptionsummary → 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.