Add Atom feed support
Add feed URL to base template Refactor list item building into separate function Improve date extraction logic Add feed XML generation handler Update template variables handling
This commit is contained in:
parent
b03511f99b
commit
1cbfb67a4c
4 changed files with 166 additions and 60 deletions
|
|
@ -14,6 +14,9 @@
|
|||
<?php if (!empty($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($feedUrl)): ?>
|
||||
<link rel="alternate" type="application/atom+xml" title="<?= htmlspecialchars($pageTitle ?? 'Feed') ?>" href="<?= htmlspecialchars($feedUrl) ?>">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
146
app/router.php
146
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 '<?xml version="1.0" encoding="utf-8"?>' . "\n";
|
||||
echo '<feed xmlns="http://www.w3.org/2005/Atom">' . "\n";
|
||||
echo ' <title>' . htmlspecialchars($feedTitle) . '</title>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($listUrl) . '" rel="alternate"/>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($feedUrl) . '" rel="self"/>' . "\n";
|
||||
echo ' <id>' . htmlspecialchars($listUrl) . '</id>' . "\n";
|
||||
echo ' <updated>' . $updated . 'T00:00:00Z</updated>' . "\n";
|
||||
|
||||
foreach ($items as $item) {
|
||||
$absoluteUrl = $baseUrl . $item['url'];
|
||||
$itemDate = ($item['rawDate'] ?? date('Y-m-d')) . 'T00:00:00Z';
|
||||
|
||||
echo ' <entry>' . "\n";
|
||||
echo ' <title>' . htmlspecialchars($item['title']) . '</title>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($absoluteUrl) . '" rel="alternate"/>' . "\n";
|
||||
echo ' <id>' . htmlspecialchars($absoluteUrl) . '</id>' . "\n";
|
||||
echo ' <updated>' . $itemDate . '</updated>' . "\n";
|
||||
if ($item['summary']) {
|
||||
echo ' <summary>' . htmlspecialchars($item['summary']) . '</summary>' . "\n";
|
||||
}
|
||||
if ($item['content']) {
|
||||
$safeContent = str_replace(']]>', ']]]]><![CDATA[>', $item['content']);
|
||||
echo ' <content type="html"><![CDATA[' . $safeContent . ']]></content>' . "\n";
|
||||
}
|
||||
echo ' </entry>' . "\n";
|
||||
}
|
||||
|
||||
echo '</feed>' . "\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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue