# 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. Empty path? → frontpage: findAllContentFiles + renderMultipleFiles │ └─ 6. 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 → build items array from subdirectories │ ├─ Check hide_list metadata → treat as page if true │ ├─ Select list template from metadata page_template │ ├─ For each subdir: loadMetadata, extractTitle, extractDateFromFolder, findCoverImage │ ├─ Sort items by date (metadata `order` = ascending|descending) │ ├─ Fire Hook::TEMPLATE_VARS │ └─ Template chain: items → list-*.php → base.php │ └─ "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.