folderweb/docs/04-development/02-content-system.md
Ruben f1447049e4 Add page-specific JavaScript support
Update documentation for new JS variables and rendering pipeline

Add script.js file support to content directories

Implement cache-busting for JavaScript files

Update static file serving to include JavaScript files

Document page-specific script loading in base.php
2026-02-06 18:47:23 +01:00

6.9 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)
feed bool false Enable Atom feed on list pages (feed.xml)
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

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.

Page-Specific JavaScript

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.

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.