folderweb/docs/04-development/07-rendering.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

5.6 KiB

Rendering & Caching

Content Rendering

renderContentFile(string $filePath, ?Context $ctx = null): string

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.

Page Rendering

renderMultipleFiles(Context $ctx, array $files, string $pageDir): void

Used for both frontpage and page views:

  1. Load metadata for $pageDir
  2. Load page plugins (from metadata plugins field)
  3. Render all content files, concatenate HTML
  4. Compute: $pageTitle, $metaDescription, $pageCssUrl/$pageCssHash, $socialImageUrl
  5. Fire Hook::TEMPLATE_VARS with all variables
  6. extract() variables → render page.php → capture output as $content
  7. Render base.php with $content + base variables
  8. exit

List Rendering

Handled directly in router.php (not a separate function):

  1. Render directory's own content files as $pageContent
  2. Load metadata, check hide_list
  3. Select list template from page_template metadata
  4. Call buildListItems() from helpers.php (builds + sorts items)
  5. Store pageTitle, metaDescription, pageCssUrl, pageCssHash, feedUrl on context
  6. Fire Hook::TEMPLATE_VARS
  7. Render list template → capture as $content
  8. Pass to renderTemplate() which renders base.php

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.

Markdown Caching

Defined in app/cache.php. File-based cache in /tmp/folderweb_cache/.

Cache key: md5($filePath . $mtime . $langPrefix)

  • 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.
getCachedMarkdown(string $filePath, string $langPrefix = ''): ?string
setCachedMarkdown(string $filePath, string $html, string $langPrefix = ''): void

Static File Serving

Content Assets (router.php)

Before content routing, the router serves static files from content directory with an explicit MIME type allowlist:

css, jpg, jpeg, png, gif, webp, svg, pdf, woff, woff2, ttf, otf

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):

URL Path Filesystem Path
/app/styles/* custom/styles/*
/app/fonts/* custom/fonts/*
/app/assets/* custom/assets/*
/app/default-styles/* app/default/styles/*

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