Renders a single content file to HTML string based on extension:
| Extension | Rendering |
|---|---|
| `md` | Parsedown + ParsedownExtra → HTML. Cached. Language prefix injected into internal links. |
| `html` | Included directly (output buffered) |
| `php` | Included with `Hook::TEMPLATE_VARS` variables extracted into scope |
PHP content files receive variables from `Hook::TEMPLATE_VARS` (starting with an empty array). This includes plugin-provided variables like `$translations`, `$currentLang`, etc. However, page-level context like `$metadata` and `$pageTitle` is **not** included — those are only available in the wrapping template (page.php/list.php), not in content files.
**`renderTemplate(Context $ctx, string $content, int $statusCode = 200): void`** — Wraps content in base template. Used for list views and error pages. Reads `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, `pageJsUrl`, `pageJsHash`, and `feedUrl` from the context object (set by the list case in `router.php`). For error pages, these context keys are unset, so base.php receives nulls.
Handled in `router.php` before `parseRequestPath()`. When a request path ends with `feed.xml`:
1. Strip `feed.xml` suffix, resolve parent as list directory via `parseRequestPath()`
2. Check `feed = true` in metadata — 404 if missing or if parent is not a list
3. Call `buildListItems()` to get items
4. For each item: call `findAllContentFiles()` + `renderContentFile()` to get full HTML content
5. Build Atom XML with absolute URLs (`$_SERVER['HTTP_HOST']` + scheme detection)
6. Output `Content-Type: application/atom+xml` and exit
Feed piggybacks on the existing Markdown cache — no separate feed cache needed. The `rawDate` field on items provides ISO dates for Atom `<updated>` elements. Content is wrapped in `<![CDATA[...]]>` with `]]>` safely escaped.
- **Does not track plugin state** — if a plugin modifies Markdown output (e.g., via PROCESS_CONTENT on files), changing plugin code won't bust the cache. Clear `/tmp/folderweb_cache/` manually after plugin changes that affect rendered Markdown.
Files in `custom/assets/` are served at the document root URL. Example: `custom/assets/favicon.ico` → `/favicon.ico`. Uses `mime_content_type()` for MIME detection.
### Framework Assets (static.php)
`/app/*` requests are handled by `static.php` with directory traversal protection (`../` stripped):
Page-specific CSS and JS get an MD5 hash appended: `?v={hash}`. Computed by `findPageCss()` and `findPageJs()` respectively. The default theme's CSS is linked directly without hash (uses browser caching).
Markdown rendering uses `Parsedown` + `ParsedownExtra` from `app/vendor/`. These are the only third-party dependencies. Loaded lazily on first Markdown render.
**Internal link rewriting:** After Markdown→HTML conversion, `href="/..."` links are prefixed with the current language prefix (e.g., `/no`). This ensures Markdown links work correctly in multilingual sites.
## Error Responses
- **403:** Invalid path (outside content directory or unreadable)
- **404:** Slug resolution failed or unknown route type
- Both render via `renderTemplate()` with appropriate status code