folderweb/app/router.php
Ruben 1cbfb67a4c 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
2026-02-06 18:24:31 +01:00

252 lines
9.3 KiB
PHP

<?php
// Load modular components
require_once __DIR__ . '/constants.php';
require_once __DIR__ . '/hooks.php';
require_once __DIR__ . '/context.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/plugins.php';
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/content.php';
require_once __DIR__ . '/rendering.php';
// Create context
$ctx = createContext();
// Store globally for easy access
$GLOBALS['ctx'] = $ctx;
// Check for assets in /custom/assets/ served at root level
$assetPath = dirname(__DIR__) . '/custom/assets/' . $ctx->requestPath;
if (file_exists($assetPath) && is_file($assetPath)) {
header('Content-Type: ' . (mime_content_type($assetPath) ?: 'application/octet-stream'));
readfile($assetPath);
exit;
}
// Check for assets in content directory (CSS, images, etc.)
$contentAssetPath = $ctx->contentDir . '/' . $ctx->requestPath;
if (file_exists($contentAssetPath) && is_file($contentAssetPath)) {
$ext = pathinfo($contentAssetPath, PATHINFO_EXTENSION);
// Define MIME types for asset files
$mimeTypes = [
'css' => 'text/css',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'otf' => 'font/otf',
];
$extLower = strtolower($ext);
if (isset($mimeTypes[$extLower])) {
header('Content-Type: ' . $mimeTypes[$extLower]);
readfile($contentAssetPath);
exit;
}
}
// 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);
if (!empty($contentFiles)) {
renderMultipleFiles($ctx, $contentFiles, $ctx->contentDir);
}
}
// Parse and handle request
$parsedPath = parseRequestPath($ctx);
switch ($parsedPath['type']) {
case 'page':
$dir = $parsedPath['path'];
// Redirect to add trailing slash if needed
if (!$ctx->hasTrailingSlash) {
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
exit;
}
$contentFiles = findAllContentFiles($dir);
if (!empty($contentFiles)) {
renderMultipleFiles($ctx, $contentFiles, $dir);
}
break;
case 'list':
$dir = $parsedPath['path'];
// Redirect to add trailing slash if needed
if (!$ctx->hasTrailingSlash) {
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
exit;
}
// Check for page content files in this directory
$pageContent = null;
$contentFiles = findAllContentFiles($dir);
if (!empty($contentFiles)) {
$pageContent = implode('', array_map('renderContentFile', $contentFiles));
}
// Load metadata for this directory
$metadata = loadMetadata($dir);
// Check if hide_list is enabled - if so, treat as page
if (isset($metadata['hide_list']) && $metadata['hide_list']) {
if (!empty($contentFiles)) {
renderMultipleFiles($ctx, $contentFiles, $dir);
}
break;
}
// Select list template based on metadata page_template
$listTemplate = $ctx->templates->list;
if (isset($metadata['page_template']) && !empty($metadata['page_template'])) {
$templateName = $metadata['page_template'];
$customTemplate = dirname(__DIR__) . "/custom/templates/$templateName.php";
$defaultTemplate = __DIR__ . "/default/templates/$templateName.php";
if (file_exists($customTemplate)) {
$listTemplate = $customTemplate;
} elseif (file_exists($defaultTemplate)) {
$listTemplate = $defaultTemplate;
}
}
// Build list items
$items = buildListItems($dir, $ctx, $metadata);
// Prepare all variables for base template
$navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
$pageTitle = $metadata['title'] ?? null;
$metaDescription = extractMetaDescription($dir, $metadata);
// Check for page-specific CSS
$pageCss = findPageCss($dir, $ctx->contentDir);
$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,
'homeLabel' => $homeLabel,
'pageTitle' => $pageTitle,
'metaDescription' => $metaDescription,
'pageCssUrl' => $pageCssUrl,
'pageCssHash' => $pageCssHash,
'items' => $items,
'pageContent' => $pageContent,
'feedUrl' => $feedUrl
], $ctx);
extract($templateVars);
ob_start();
require $listTemplate;
$content = ob_get_clean();
renderTemplate($ctx, $content);
break;
case 'not_found':
http_response_code(404);
renderTemplate($ctx, "<h1>404 - Page Not Found</h1><p>The requested page could not be found.</p>", 404);
break;
}