folderweb/docs/04-development/01-architecture.md
Ruben 069ce389ea Add Atom feed support to list pages
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
2026-02-06 18:24:39 +01:00

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.