diff --git a/app/default/templates/base.php b/app/default/templates/base.php
index ccf18d6..ce25bfd 100644
--- a/app/default/templates/base.php
+++ b/app/default/templates/base.php
@@ -14,6 +14,9 @@
+
+
+
diff --git a/app/helpers.php b/app/helpers.php
index a84c786..489202e 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -32,11 +32,73 @@ function extractTitle(string $filePath): ?string {
return null;
}
-function extractDateFromFolder(string $folderName): ?string {
+// Build sorted list items for a directory
+function buildListItems(string $dir, Context $ctx, ?array $parentMetadata): array {
+ $subdirs = getSubdirectories($dir);
+
+ $items = array_filter(array_map(function($item) use ($dir, $ctx) {
+ $itemPath = "$dir/$item";
+ $metadata = loadMetadata($itemPath);
+ $coverImage = findCoverImage($itemPath);
+ $pdfFile = findPdfFile($itemPath);
+
+ $title = $metadata['title'] ?? extractTitle($itemPath) ?? $item;
+
+ $rawDate = null;
+ $date = null;
+ if (isset($metadata['date'])) {
+ $rawDate = $metadata['date'];
+ $date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
+ } else {
+ $rawDate = extractRawDateFromFolder($item);
+ if ($rawDate) {
+ $date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
+ } else {
+ $rawDate = date("Y-m-d", filemtime($itemPath));
+ $date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
+ }
+ }
+
+ $urlSlug = ($metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item;
+
+ $langPrefix = $ctx->get('langPrefix', '');
+ $baseUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($urlSlug);
+ $assetUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($item);
+
+ return [
+ 'title' => $title,
+ 'url' => $baseUrl . '/',
+ 'date' => $date,
+ 'rawDate' => $rawDate,
+ 'summary' => $metadata['summary'] ?? null,
+ 'cover' => $coverImage ? "$assetUrl/$coverImage" : null,
+ 'pdf' => $pdfFile ? "$assetUrl/$pdfFile" : null,
+ 'redirect' => $metadata['redirect'] ?? null,
+ 'dirPath' => $itemPath
+ ];
+ }, $subdirs));
+
+ $sortOrder = strtolower($parentMetadata['order'] ?? 'descending');
+ if ($sortOrder === 'ascending') {
+ usort($items, fn($a, $b) => strcmp($a['date'] ?? '', $b['date'] ?? ''));
+ } else {
+ usort($items, fn($a, $b) => strcmp($b['date'] ?? '', $a['date'] ?? ''));
+ }
+
+ return $items;
+}
+
+function extractRawDateFromFolder(string $folderName): ?string {
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) {
- $dateString = $matches[1] . '-' . $matches[2] . '-' . $matches[3];
- // Let plugins format the date
- return Hooks::apply(Hook::PROCESS_CONTENT, $dateString, 'date_format');
+ return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
+ }
+ return null;
+}
+
+function extractDateFromFolder(string $folderName): ?string {
+ $raw = extractRawDateFromFolder($folderName);
+ if ($raw) {
+ return Hooks::apply(Hook::PROCESS_CONTENT, $raw, 'date_format');
}
return null;
}
diff --git a/app/rendering.php b/app/rendering.php
index 7d61f89..ef4e57a 100644
--- a/app/rendering.php
+++ b/app/rendering.php
@@ -48,13 +48,16 @@ function renderTemplate(Context $ctx, string $content, int $statusCode = 200): v
$navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
- $pageTitle = null;
$templateVars = Hooks::apply(Hook::TEMPLATE_VARS, [
'content' => $content,
'navigation' => $navigation,
'homeLabel' => $homeLabel,
- 'pageTitle' => $pageTitle
+ 'pageTitle' => $ctx->get('pageTitle'),
+ 'metaDescription' => $ctx->get('metaDescription'),
+ 'pageCssUrl' => $ctx->get('pageCssUrl'),
+ 'pageCssHash' => $ctx->get('pageCssHash'),
+ 'feedUrl' => $ctx->get('feedUrl')
], $ctx);
extract($templateVars);
diff --git a/app/router.php b/app/router.php
index 64e706a..d9710e0 100644
--- a/app/router.php
+++ b/app/router.php
@@ -52,6 +52,82 @@ if (file_exists($contentAssetPath) && is_file($contentAssetPath)) {
}
}
+// Handle Atom feed requests
+if (str_ends_with($ctx->requestPath, 'feed.xml')) {
+ $feedPath = preg_replace('#/?feed\.xml$#', '', $ctx->requestPath);
+
+ // Temporarily set requestPath to the parent directory for resolution
+ $reflection = new ReflectionProperty($ctx, 'requestPath');
+ $originalPath = $ctx->requestPath;
+ $reflection->setValue($ctx, $feedPath);
+
+ $parsedFeed = parseRequestPath($ctx);
+
+ if ($parsedFeed['type'] !== 'list') {
+ $reflection->setValue($ctx, $originalPath);
+ } else {
+ $dir = $parsedFeed['path'];
+ $metadata = loadMetadata($dir);
+
+ if (!isset($metadata['feed']) || !$metadata['feed']) {
+ $reflection->setValue($ctx, $originalPath);
+ } else {
+ $items = buildListItems($dir, $ctx, $metadata);
+
+ // Render full content for each item
+ foreach ($items as &$item) {
+ $item['content'] = '';
+ $contentFiles = findAllContentFiles($item['dirPath']);
+ foreach ($contentFiles as $file) {
+ $item['content'] .= renderContentFile($file, $ctx);
+ }
+ }
+ unset($item);
+
+ // Build Atom XML
+ $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
+ $baseUrl = "$scheme://$host";
+ $langPrefix = $ctx->get('langPrefix', '');
+ $listUrl = $baseUrl . $langPrefix . '/' . trim($feedPath, '/') . '/';
+ $feedUrl = $baseUrl . $langPrefix . '/' . trim($feedPath, '/') . '/feed.xml';
+ $feedTitle = $metadata['title'] ?? 'Feed';
+ $updated = !empty($items) ? ($items[0]['rawDate'] ?? date('Y-m-d')) : date('Y-m-d');
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+ echo '' . "\n";
+ echo '' . "\n";
+ echo ' ' . htmlspecialchars($feedTitle) . '' . "\n";
+ echo ' ' . "\n";
+ echo ' ' . "\n";
+ echo ' ' . htmlspecialchars($listUrl) . '' . "\n";
+ echo ' ' . $updated . 'T00:00:00Z' . "\n";
+
+ foreach ($items as $item) {
+ $absoluteUrl = $baseUrl . $item['url'];
+ $itemDate = ($item['rawDate'] ?? date('Y-m-d')) . 'T00:00:00Z';
+
+ echo ' ' . "\n";
+ echo ' ' . htmlspecialchars($item['title']) . '' . "\n";
+ echo ' ' . "\n";
+ echo ' ' . htmlspecialchars($absoluteUrl) . '' . "\n";
+ echo ' ' . $itemDate . '' . "\n";
+ if ($item['summary']) {
+ echo ' ' . htmlspecialchars($item['summary']) . '' . "\n";
+ }
+ if ($item['content']) {
+ $safeContent = str_replace(']]>', ']]]]>', $item['content']);
+ echo ' ' . "\n";
+ }
+ echo ' ' . "\n";
+ }
+
+ echo '' . "\n";
+ exit;
+ }
+ }
+}
+
// Handle frontpage
if (empty($ctx->requestPath)) {
$contentFiles = findAllContentFiles($ctx->contentDir);
@@ -121,59 +197,7 @@ switch ($parsedPath['type']) {
}
// Build list items
- $subdirs = getSubdirectories($dir);
-
- $items = array_filter(array_map(function($item) use ($dir, $ctx) {
- $itemPath = "$dir/$item";
- $metadata = loadMetadata($itemPath);
- $coverImage = findCoverImage($itemPath);
- $pdfFile = findPdfFile($itemPath);
-
- $title = $metadata['title'] ?? extractTitle($itemPath) ?? $item;
- $date = null;
- if (isset($metadata['date'])) {
- $date = $metadata['date'];
- // Let plugins format date
- $date = Hooks::apply(Hook::PROCESS_CONTENT, $date, 'date_format');
- } else {
- $extractedDate = extractDateFromFolder($item);
- if ($extractedDate) {
- $date = $extractedDate;
- } else {
- // Convert timestamp to ISO format and let plugins format it
- $isoDate = date("Y-m-d", filemtime($itemPath));
- $date = Hooks::apply(Hook::PROCESS_CONTENT, $isoDate, 'date_format');
- }
- }
-
- // Use slug if available
- $urlSlug = ($metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item;
-
- $langPrefix = $ctx->get('langPrefix', '');
- $baseUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($urlSlug);
-
- // Assets (cover, PDF) must use actual folder name, not translated slug
- $assetUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($item);
-
- return [
- 'title' => $title,
- 'url' => $baseUrl . '/',
- 'date' => $date,
- 'summary' => $metadata['summary'] ?? null,
- 'cover' => $coverImage ? "$assetUrl/$coverImage" : null,
- 'pdf' => $pdfFile ? "$assetUrl/$pdfFile" : null,
- 'redirect' => $metadata['redirect'] ?? null
- ];
- }, $subdirs));
-
- // Sort by date - check metadata for order preference
- $sortOrder = strtolower($metadata['order'] ?? 'descending');
- if ($sortOrder === 'ascending') {
- usort($items, fn($a, $b) => strcmp($a['date'] ?? '', $b['date'] ?? ''));
- } else {
- // Default: descending (newest first)
- usort($items, fn($a, $b) => strcmp($b['date'] ?? '', $a['date'] ?? ''));
- }
+ $items = buildListItems($dir, $ctx, $metadata);
// Prepare all variables for base template
$navigation = $ctx->navigation;
@@ -186,6 +210,19 @@ switch ($parsedPath['type']) {
$pageCssUrl = $pageCss['url'] ?? null;
$pageCssHash = $pageCss['hash'] ?? null;
+ // Build feed URL if feed is enabled
+ $langPrefix = $ctx->get('langPrefix', '');
+ $feedUrl = (isset($metadata['feed']) && $metadata['feed'])
+ ? $langPrefix . '/' . trim($ctx->requestPath, '/') . '/feed.xml'
+ : null;
+
+ // Store for base template (renderTemplate reads these from context)
+ $ctx->set('pageTitle', $pageTitle);
+ $ctx->set('metaDescription', $metaDescription);
+ $ctx->set('pageCssUrl', $pageCssUrl);
+ $ctx->set('pageCssHash', $pageCssHash);
+ $ctx->set('feedUrl', $feedUrl);
+
// Let plugins add template variables
$templateVars = Hooks::apply(Hook::TEMPLATE_VARS, [
'navigation' => $navigation,
@@ -195,7 +232,8 @@ switch ($parsedPath['type']) {
'pageCssUrl' => $pageCssUrl,
'pageCssHash' => $pageCssHash,
'items' => $items,
- 'pageContent' => $pageContent
+ 'pageContent' => $pageContent,
+ 'feedUrl' => $feedUrl
], $ctx);
extract($templateVars);