folderweb/docs/04-development/01-architecture.md

138 lines
6.9 KiB
Markdown
Raw Normal View History

# 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.