Introduce `feed` metadata option to enable Atom feeds Update list item structure with standardized fields Add `$feedUrl` template variable for autodiscovery Improve date handling with raw/processed date separation Document feed generation in architecture and rendering docs Update template examples to use new item structure
7.3 KiB
Architecture
Directory Layout
app/ # Framework core — stable API surface
router.php # Entry point: all requests route here
constants.php # CONTENT_EXTENSIONS, COVER_IMAGE_EXTENSIONS
hooks.php # Hook enum + Hooks class
context.php # Context + Templates classes
config.php # createContext(): config merge + bootstrap
helpers.php # Utility functions (template resolution, extraction)
content.php # Content discovery, slug resolution, navigation
rendering.php # Markdown/HTML/PHP rendering, template wrapping
cache.php # Markdown render cache (/tmp/folderweb_cache)
plugins.php # PluginManager class
static.php # /app/* asset serving with traversal protection
vendor/ # Parsedown + ParsedownExtra (Markdown→HTML)
plugins/global/ # Built-in plugins (languages.php)
default/ # Demo/fallback theme (NOT for production use)
config.ini # Default configuration
templates/ # base.php, page.php, list.php, list-compact.php, list-grid.php
styles/ # Default stylesheet
languages/ # en.ini, no.ini
content/ # Demo content shown when custom/ has no content
custom/ # User layer — all site-specific work goes here
config.ini # Config overrides (merged over app/default/config.ini)
templates/ # Override any template by matching filename
styles/ # Stylesheets served via /app/styles/*
fonts/ # Fonts served via /app/fonts/*
assets/ # Static files served at document root (favicon, robots.txt)
languages/ # Translation overrides/additions (*.ini)
plugins/global/ # Custom global plugins
plugins/page/ # Custom page plugins (reserved, not yet active)
content/ # Website content (= document root in production)
devel/ # Dev environment (Containerfile, compose, Apache, perf tools)
docs/ # Documentation (01-03 human-facing, 04 machine-facing)
Stable Contracts
app/ is the framework. When developing custom/ sites, never modify app/. When developing the framework itself, preserve these contracts:
| Contract | Guaranteed Behavior |
|---|---|
| Override chain | custom/* always takes priority over app/default/* for templates, styles, languages, config |
| Template names | base.php, page.php, list.php are the three core template names |
| Hook enum | Hook::CONTEXT_READY, Hook::PROCESS_CONTENT, Hook::TEMPLATE_VARS — signatures documented in 05-hooks-plugins.md |
| Context API | $ctx->set(), $ctx->get(), $ctx->has() — stable key/value store |
| Content extensions | md, html, php — defined in CONTENT_EXTENSIONS |
| Config format | INI with sections, merged via array_replace_recursive |
| Metadata format | INI file named metadata.ini in content directories |
| URL structure | Folder path = URL path, with slug overrides via metadata |
| Plugin locations | app/plugins/{scope}/ and custom/plugins/{scope}/ |
| Asset routes | /app/styles/* → custom/styles/, /app/fonts/* → custom/fonts/, /app/default-styles/* → app/default/styles/ |
| Trailing slash | Pages and lists enforce trailing slash via 301 redirect |
Request Flow
Browser request
│
├─ Apache rewrite: all non-/app/ requests → app/router.php
│
▼
router.php
│
├─ 1. Load modules (constants, hooks, context, helpers, plugins, config, content, rendering)
├─ 2. createContext()
│ ├─ Parse + merge config (default ← custom)
│ ├─ loadGlobalPlugins() → fires Hook::CONTEXT_READY
│ ├─ Determine contentDir (custom content or demo fallback)
│ ├─ Parse REQUEST_URI → requestPath
│ └─ Resolve template paths (custom fallback to default)
│
├─ 3. Check custom/assets/{path} → serve static file + exit
├─ 4. Check content/{path} for static asset (css/img/pdf/font) → serve + exit
│
├─ 5. Path ends with feed.xml? → Atom feed generation
│ ├─ Strip feed.xml, resolve parent as list directory
│ ├─ Check feed = true in metadata, otherwise 404
│ ├─ buildListItems() + renderContentFile() for full content
│ └─ Output Atom XML + exit
│
├─ 6. Empty path? → frontpage: findAllContentFiles + renderMultipleFiles
│
└─ 7. parseRequestPath() → {type, path}
│
├─ "page": trailing slash redirect → findAllContentFiles → renderMultipleFiles
│ (fires Hook::PROCESS_CONTENT for file filtering)
│ (fires Hook::TEMPLATE_VARS before template render)
│ Template chain: content → page.php → base.php
│
├─ "list": trailing slash redirect → buildListItems() from helpers.php
│ ├─ Check hide_list metadata → treat as page if true
│ ├─ Select list template from metadata page_template
│ ├─ buildListItems(): metadata, titles, dates, covers for each subdir
│ ├─ Sort items by date (metadata `order` = ascending|descending)
│ ├─ Store pageTitle, metaDescription, feedUrl etc. on context
│ ├─ Fire Hook::TEMPLATE_VARS
│ └─ Template chain: items → list-*.php → base.php (via renderTemplate)
│
└─ "not_found": 404 response
Module Dependency Order
Loaded sequentially in router.php:
constants.php → hooks.php → context.php → helpers.php → plugins.php → config.php → content.php → rendering.php
config.php calls createContext() which triggers plugin loading, so hooks.php and plugins.php must be loaded before it.
Page vs List Detection
A resolved directory becomes a list if it contains subdirectories, otherwise a page. Override with hide_list = true in metadata to force page rendering on directories with children.
Deployment Models
Framework development (this repo)
Both app/ and custom/ live in the same repository. custom/ holds demo/test overrides.
Site development (separate repo)
The site is its own git repository containing custom/, content, and deployment config. app/ is included as a symlink, git submodule, or copied directory pointing to a specific framework version. The site repo never modifies app/.
Typical site repo layout:
my-site/ # Site git repo
app/ → ../folderweb/app # Symlink to framework (or submodule, or copy)
custom/ # Site-specific templates, styles, plugins, config
content/ # Website content (often the document root)
devel/ # Site's own dev environment config (optional)
Either direction works: app/ symlinked into a site repo, or custom/ symlinked into the framework repo during development.
Demo Fallback
When content/ (document root) has no files, app/default/content/ is used automatically. This is demo mode only — production sites always provide their own content via the document root.