diff --git a/docs/03-reference/02-metadata.md b/docs/03-reference/02-metadata.md
index 7f8d806..4dd742c 100644
--- a/docs/03-reference/02-metadata.md
+++ b/docs/03-reference/02-metadata.md
@@ -194,6 +194,20 @@ hide_list = true
**Type:** Boolean
**Use case:** Section landing pages that should show content instead of list
+### `feed`
+
+Enable an Atom feed for this list page, served at `/{list-path}/feed.xml`.
+
+```ini
+feed = true
+```
+
+**Default:** `false` (no feed generated)
+**Values:** `true` or `false`
+**Type:** Boolean
+**Applies to:** List pages only (directories with subdirectories)
+**Effect:** Generates an Atom XML feed containing the full rendered content of each list item. Also adds an autodiscovery ` ` tag in the HTML `
`.
+
## Language-Specific Overrides
Add language-specific sections to override fields:
@@ -327,20 +341,19 @@ Folder: content/blog/2024-12-15-my-post/
## Metadata in List Items
-When rendering list views, each item receives these metadata fields:
+When rendering list views, each item in the `$items` array has these keys:
```php
$item = [
- 'url' => '/blog/my-post/',
- 'path' => '/content/blog/2024-12-15-my-post',
- 'title' => 'My Post',
- 'summary' => 'Short description',
- 'date' => '2024-12-15',
- 'formatted_date' => '15. desember 2024', // Language-specific
- 'cover_image' => '/blog/my-post/cover.jpg', // If exists
- // All custom metadata fields...
- 'author' => 'Jane Doe',
- 'tags' => 'web,design',
+ 'title' => 'My Post', // From metadata, heading, or folder name
+ 'url' => '/blog/my-post/', // Full URL with trailing slash + lang prefix
+ 'date' => '15. desember 2024', // Formatted for display (plugin-processed)
+ 'rawDate' => '2024-12-15', // ISO YYYY-MM-DD (for feeds, elements)
+ 'summary' => 'Short description', // From metadata (nullable)
+ 'cover' => '/blog/2024-12-15-my-post/cover.jpg', // Cover image URL (nullable)
+ 'pdf' => '/blog/2024-12-15-my-post/doc.pdf', // First PDF URL (nullable)
+ 'redirect' => null, // External redirect URL (nullable)
+ 'dirPath' => '/path/to/content/blog/2024-12-15-my-post', // Filesystem path (internal)
];
```
@@ -349,13 +362,17 @@ Access in list templates:
```php
- = htmlspecialchars($item['title']) ?>
+
-
- By = htmlspecialchars($item['author']) ?>
+
+ = $item['date'] ?>
-
+
= htmlspecialchars($item['summary']) ?>
diff --git a/docs/03-reference/03-template-variables.md b/docs/03-reference/03-template-variables.md
index cd715bc..9bef00b 100644
--- a/docs/03-reference/03-template-variables.md
+++ b/docs/03-reference/03-template-variables.md
@@ -211,6 +211,21 @@ MD5 hash of page-specific CSS for cache busting.
**Optional:** Only set if `$pageCssUrl` exists
**Example:** See `$pageCssUrl` above
+### `$feedUrl`
+
+URL to the Atom feed for the current list page.
+
+**Type:** String (URL path)
+**Optional:** Only set on list pages with `feed = true` in metadata
+**Example:**
+```php
+
+
+
+```
+
+**Source:** Set when `feed = true` in the list directory's `metadata.ini`
+
## Page Template Variables
Available in `page.php`:
@@ -238,7 +253,6 @@ All metadata for the current page.
'title' => 'Page Title',
'summary' => 'Short description',
'date' => '2024-12-15',
- 'formatted_date' => '15. desember 2024',
'show_date' => true,
'author' => 'Jane Doe', // Custom fields
'tags' => 'web,design',
@@ -254,7 +268,7 @@ All metadata for the current page.
- = $metadata['formatted_date'] ?? $metadata['date'] ?>
+ = $metadata['date'] ?>
```
@@ -289,16 +303,15 @@ Array of items to display in the list.
```php
[
[
- 'url' => '/blog/my-post/',
- 'path' => '/content/blog/2024-12-15-my-post',
'title' => 'My Post',
+ 'url' => '/blog/my-post/',
+ 'date' => '15. desember 2024', // Formatted for display
+ 'rawDate' => '2024-12-15', // ISO YYYY-MM-DD
'summary' => 'Short description',
- 'date' => '2024-12-15',
- 'formatted_date' => '15. desember 2024',
- 'cover_image' => '/blog/my-post/cover.jpg',
- // All custom metadata fields...
- 'author' => 'Jane Doe',
- 'category' => 'Tutorial',
+ 'cover' => '/blog/2024-12-15-my-post/cover.jpg',
+ 'pdf' => null,
+ 'redirect' => null,
+ 'dirPath' => '/path/to/content/blog/2024-12-15-my-post',
],
// ... more items
]
@@ -308,8 +321,8 @@ Array of items to display in the list.
```php
-
-
+
@@ -319,13 +332,13 @@ Array of items to display in the list.
-
-
- = $item['formatted_date'] ?? $item['date'] ?>
+
+
+ = $item['date'] ?>
-
+
= htmlspecialchars($item['summary']) ?>
@@ -351,14 +364,15 @@ Each item in `$items` has these properties:
| Property | Type | Description | Optional |
|----------|------|-------------|----------|
-| `url` | String | Full URL to the item | No |
-| `path` | String | Filesystem path to item | No |
-| `title` | String | Item title | No |
-| `summary` | String | Short description | Yes |
-| `date` | String | ISO date (YYYY-MM-DD) | Yes |
-| `formatted_date` | String | Localized date string | Yes |
-| `cover_image` | String | URL to cover image | Yes |
-| Custom fields | Mixed | Any metadata fields | Yes |
+| `title` | String | Item title (from metadata, heading, or folder name) | No |
+| `url` | String | Full URL to the item (with trailing slash + lang prefix) | No |
+| `date` | String | Formatted date string (plugin-processed for display) | Yes |
+| `rawDate` | String | ISO date (YYYY-MM-DD) for feeds and `` elements | Yes |
+| `summary` | String | Short description from metadata | Yes |
+| `cover` | String | URL to cover image | Yes |
+| `pdf` | String | URL to first PDF file | Yes |
+| `redirect` | String | External redirect URL | Yes |
+| `dirPath` | String | Filesystem path to item directory (internal use) | No |
## Adding Custom Variables
@@ -401,6 +415,7 @@ Then use in templates:
| `$translations` | ✓ | — | — |
| `$pageCssUrl` | ✓ | — | — |
| `$pageCssHash` | ✓ | — | — |
+| `$feedUrl` | ✓ | — | — |
| `$metadata` | — | ✓ | ✓ |
| `$pageContent` | — | — | ✓ |
| `$items` | — | — | ✓ |
diff --git a/docs/04-development/01-architecture.md b/docs/04-development/01-architecture.md
index e7f6d27..a29edd2 100644
--- a/docs/04-development/01-architecture.md
+++ b/docs/04-development/01-architecture.md
@@ -76,22 +76,29 @@ router.php
├─ 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
+ ├─ 5. Path ends with feed.xml? → Atom feed generation
+ │ ├─ Strip feed.xml, resolve parent as list directory
+ │ ├─ Check feed = true in metadata, otherwise 404
+ │ ├─ buildListItems() + renderContentFile() for full content
+ │ └─ Output Atom XML + exit
│
- └─ 6. parseRequestPath() → {type, path}
+ ├─ 6. Empty path? → frontpage: findAllContentFiles + renderMultipleFiles
+ │
+ └─ 7. 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
+ ├─ "list": trailing slash redirect → buildListItems() from helpers.php
│ ├─ Check hide_list metadata → treat as page if true
│ ├─ Select list template from metadata page_template
- │ ├─ For each subdir: loadMetadata, extractTitle, extractDateFromFolder, findCoverImage
+ │ ├─ buildListItems(): metadata, titles, dates, covers for each subdir
│ ├─ Sort items by date (metadata `order` = ascending|descending)
+ │ ├─ Store pageTitle, metaDescription, feedUrl etc. on context
│ ├─ Fire Hook::TEMPLATE_VARS
- │ └─ Template chain: items → list-*.php → base.php
+ │ └─ Template chain: items → list-*.php → base.php (via renderTemplate)
│
└─ "not_found": 404 response
```
diff --git a/docs/04-development/02-content-system.md b/docs/04-development/02-content-system.md
index 1163dc6..c0d6970 100644
--- a/docs/04-development/02-content-system.md
+++ b/docs/04-development/02-content-system.md
@@ -53,6 +53,7 @@ Returns flat key-value array with a special `_raw` key containing the full parse
| `menu_order` | int | 999 | Navigation sort order (ascending) |
| `order` | string | `"descending"` | List sort direction (`ascending`\|`descending`) |
| `redirect` | string | — | External URL (list items can redirect) |
+| `feed` | bool | `false` | Enable Atom feed on list pages (`feed.xml`) |
| `plugins` | string | — | Comma-separated page-level plugin names |
### Settings Section
@@ -83,7 +84,9 @@ Any key not listed above is passed through to templates/plugins unchanged. Add w
## Date Extraction
-**`extractDateFromFolder(string $folderName): ?string`** — Extracts date from `YYYY-MM-DD-*` prefix.
+**`extractRawDateFromFolder(string $folderName): ?string`** — Extracts raw `YYYY-MM-DD` string from folder name prefix. Returns null if no date prefix. No hook processing.
+
+**`extractDateFromFolder(string $folderName): ?string`** — Calls `extractRawDateFromFolder()` then passes the result through `Hook::PROCESS_CONTENT($date, 'date_format')` for plugin formatting (e.g., `"1. January 2025"`).
If no date prefix exists and no `date` metadata is set, falls back to file modification time (`filemtime`). All dates pass through `Hook::PROCESS_CONTENT($date, 'date_format')` for plugin formatting.
@@ -91,6 +94,16 @@ If no date prefix exists and no `date` metadata is set, falls back to file modif
**Sorting with null dates:** Items without any date are sorted as empty strings via `strcmp`. Their relative order among other dateless items is undefined.
+## List Item Building
+
+**`buildListItems(string $dir, Context $ctx, ?array $parentMetadata): array`** — Builds and sorts the items array for list views. Defined in `helpers.php`.
+
+For each subdirectory: loads metadata, extracts title/date/cover/PDF, builds URL with lang prefix and slug. Returns sorted array — direction controlled by `order` metadata on parent (`descending` default).
+
+Each item contains both a formatted `date` (hook-processed for display) and a `rawDate` (ISO `YYYY-MM-DD` for Atom feeds and `` elements). Also includes `dirPath` (filesystem path) used by the feed generator to render full content.
+
+Used by both the list case in `router.php` and the Atom feed generator.
+
## Navigation
**`buildNavigation(Context $ctx): array`** — Scans top-level content directories.
diff --git a/docs/04-development/04-context-api.md b/docs/04-development/04-context-api.md
index 932113e..54cf1fd 100644
--- a/docs/04-development/04-context-api.md
+++ b/docs/04-development/04-context-api.md
@@ -44,6 +44,18 @@ Also supports magic property access: `$ctx->foo = 'bar'` / `$val = $ctx->foo`.
| `langPrefix` | string | languages.php | URL prefix: `""` for default, `"/no"` for others |
| `translations` | array | languages.php | Merged translation strings for current language |
+### Built-in Context Keys (set by router.php for list pages)
+
+These are set in the list case so that `renderTemplate()` can pass them to `base.php`:
+
+| Key | Type | Description |
+|---|---|---|
+| `pageTitle` | ?string | Page title from metadata (for `` tag) |
+| `metaDescription` | ?string | SEO description |
+| `pageCssUrl` | ?string | Page-specific CSS URL |
+| `pageCssHash` | ?string | CSS cache-bust hash |
+| `feedUrl` | ?string | Atom feed URL (set when `feed = true` in metadata) |
+
## Templates Class
Defined in `app/context.php`. Readonly value object.
diff --git a/docs/04-development/06-templates.md b/docs/04-development/06-templates.md
index 4ffde03..5c2a512 100644
--- a/docs/04-development/06-templates.md
+++ b/docs/04-development/06-templates.md
@@ -59,6 +59,7 @@ Variables are injected via `extract()` — each array key becomes a local variab
| `$homeLabel` | string | yes | Home link text |
| `$pageCssUrl` | ?string | no | Page-specific CSS URL |
| `$pageCssHash` | ?string | no | CSS cache-bust hash |
+| `$feedUrl` | ?string | no | Atom feed URL (only on lists with `feed = true`) |
| `$currentLang` | string | plugin | Language code (from languages plugin) |
| `$langPrefix` | string | plugin | URL language prefix |
| `$languageUrls` | array | plugin | `[lang => url]` for language switcher |
@@ -92,10 +93,12 @@ Each entry in `$items`:
| `title` | string | yes | From metadata, first heading, or folder name |
| `url` | string | yes | Full URL path with trailing slash and lang prefix |
| `date` | ?string | no | Formatted date string (plugin-processed) |
+| `rawDate` | ?string | no | ISO `YYYY-MM-DD` date (for feeds, `` elements) |
| `summary` | ?string | no | From metadata |
| `cover` | ?string | no | URL to cover image |
| `pdf` | ?string | no | URL to first PDF file |
| `redirect` | ?string | no | External redirect URL |
+| `dirPath` | string | yes | Filesystem path to item directory (internal use) |
Items sorted by date — direction controlled by `order` metadata on parent (`descending` default, `ascending` available).
diff --git a/docs/04-development/07-rendering.md b/docs/04-development/07-rendering.md
index 2fe68c4..721f676 100644
--- a/docs/04-development/07-rendering.md
+++ b/docs/04-development/07-rendering.md
@@ -36,13 +36,26 @@ 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. Build `$items` array from subdirectories (metadata + extraction)
-5. Sort items by date
+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.
+**`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 `` elements. Content is wrapped in `` with `]]>` safely escaped.
## Markdown Caching