requestPath); if ($realAsset && strncmp($realAsset, $assetsDir . '/', strlen($assetsDir) + 1) === 0 && is_file($realAsset)) { $assetMimeTypes = [ 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json', 'geojson' => 'application/json', 'svg' => 'image/svg+xml', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', 'otf' => 'font/otf', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', ]; $assetExt = strtolower(pathinfo($realAsset, PATHINFO_EXTENSION)); $mime = $assetMimeTypes[$assetExt] ?? 'application/octet-stream'; $cacheSeconds = in_array($assetExt, ['json', 'geojson']) ? 60 : 31536000; header('Content-Type: ' . $mime); header('Cache-Control: public, max-age=' . $cacheSeconds); readfile($realAsset); 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 '' . "\n"; echo '' . "\n"; echo ' ' . htmlspecialchars($feedTitle) . '' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; echo ' ' . htmlspecialchars($listUrl) . '' . "\n"; echo ' ' . $updated . 'T00:00:00Z' . "\n"; $feedAuthor = $metadata['author'] ?? $feedTitle; echo ' ' . htmlspecialchars($feedAuthor) . '' . "\n"; $dateCounts = []; foreach ($items as $item) { $absoluteUrl = $baseUrl . $item['url']; $rawDate = $item['rawDate'] ?? date('Y-m-d'); $dateIndex = $dateCounts[$rawDate] ?? 0; $dateCounts[$rawDate] = $dateIndex + 1; $itemDate = $rawDate . 'T00:00:' . sprintf('%02d', $dateIndex) . 'Z'; 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); 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, "

404 - Page Not Found

The requested page could not be found.

", 404); break; }