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`, 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.
## Atom Feed Rendering
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.
- Invalidates when file is modified (mtime changes)
- Invalidates per-language (different link rewriting)
- No explicit TTL — entries persist until temp directory cleanup
- **Does not track plugin state** — if a plugin modifies Markdown output (e.g., via PROCESS_CONTENT on files), changing plugin config won't bust the cache. Clear `/tmp/folderweb_cache/` manually after plugin changes that affect rendered Markdown.
Files not in this list are not served as static assets. Notably, `.js` files are excluded — JavaScript must be placed in `custom/assets/` to be served (at the document root URL), or linked from an external source.
### Custom Assets (router.php)
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):
MIME types resolved from extension map, falling back to `mime_content_type()`.
## CSS Cache Busting
Page-specific CSS gets an MD5 hash appended: `?v={hash}`. Computed by `findPageCss()`. The default theme's CSS is linked directly without hash (uses browser caching).
## Parsedown
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