folderweb/app/router.php
Ruben f8a352afce Add JavaScript support to page templates
Add support for page-specific JavaScript files with cache busting
via MD5 hash. The script is loaded at the end of the body with
defer attribute. The JavaScript file must be named script.js and
located in the same directory as the page content.
2026-02-06 18:47:17 +01:00

261 lines
9.6 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',
'js' => 'application/javascript',
];
$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 and JS
$pageCss = findPageCss($dir, $ctx->contentDir);
$pageCssUrl = $pageCss['url'] ?? null;
$pageCssHash = $pageCss['hash'] ?? null;
$pageJs = findPageJs($dir, $ctx->contentDir);
$pageJsUrl = $pageJs['url'] ?? null;
$pageJsHash = $pageJs['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('pageJsUrl', $pageJsUrl);
$ctx->set('pageJsHash', $pageJsHash);
$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,
'pageJsUrl' => $pageJsUrl,
'pageJsHash' => $pageJsHash,
'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;
}