From 149ba03359a11981b266901d293ba9c0d722edf9 Mon Sep 17 00:00:00 2001 From: Ruben Date: Sat, 1 Nov 2025 22:50:02 +0100 Subject: [PATCH] Add modular architecture to router Extract configuration, helpers, content processing, and rendering logic into separate files Refactor router to use modular components --- app/config.php | 51 +++++ app/content.php | 225 ++++++++++++++++++++++ app/helpers.php | 57 ++++++ app/rendering.php | 126 +++++++++++++ app/router.php | 461 +--------------------------------------------- 5 files changed, 464 insertions(+), 456 deletions(-) create mode 100644 app/config.php create mode 100644 app/content.php create mode 100644 app/helpers.php create mode 100644 app/rendering.php diff --git a/app/config.php b/app/config.php new file mode 100644 index 0000000..cb0071b --- /dev/null +++ b/app/config.php @@ -0,0 +1,51 @@ + 0; +} + +$contentDir = $hasUserContent ? realpath($userContentDir) : realpath($demoContentDir); + +// Extract request information +$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/'; +$hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/'; +$requestPath = trim($requestUri, '/'); + +// Extract language from URL +$currentLang = $defaultLang; +$pathParts = explode('/', $requestPath); +if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) { + $currentLang = $pathParts[0]; + array_shift($pathParts); + $requestPath = implode('/', $pathParts); +} + +// Use custom templates with fallback to defaults +$customBaseTemplate = dirname(__DIR__) . '/custom/templates/base.php'; +$defaultBaseTemplate = __DIR__ . '/default/templates/base.php'; +$baseTemplate = file_exists($customBaseTemplate) ? $customBaseTemplate : $defaultBaseTemplate; + +$customPageTemplate = dirname(__DIR__) . '/custom/templates/page.php'; +$defaultPageTemplate = __DIR__ . '/default/templates/page.php'; +$pageTemplate = file_exists($customPageTemplate) ? $customPageTemplate : $defaultPageTemplate; + +$customListTemplate = dirname(__DIR__) . '/custom/templates/list.php'; +$defaultListTemplate = __DIR__ . '/default/templates/list.php'; +$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate; diff --git a/app/content.php b/app/content.php new file mode 100644 index 0000000..2ebf357 --- /dev/null +++ b/app/content.php @@ -0,0 +1,225 @@ += 3) { + // Pattern: name.lang.ext + $fileLang = $parts[count($parts) - 2]; + if (in_array($fileLang, ['no', 'en'])) { + // Only include if it matches current language + if ($fileLang === $lang) { + $contentFiles[] = [ + 'path' => $filePath, + 'name' => $file, + 'sort_key' => $parts[0] // Use base name for sorting + ]; + } + continue; + } + } + + // Default files (no language suffix) - include if current lang is default + // or if no language-specific version exists + $baseName = $parts[0]; + $hasLangVersion = false; + + if ($lang !== $defaultLang) { + // Check if language-specific version exists + foreach ($extensions as $checkExt) { + if (file_exists("$dir/$baseName.$lang.$checkExt")) { + $hasLangVersion = true; + break; + } + } + } + + if (!$hasLangVersion) { + $contentFiles[] = [ + 'path' => $filePath, + 'name' => $file, + 'sort_key' => $baseName + ]; + } + } + + // Sort by filename (alphanumerical) + usort($contentFiles, fn($a, $b) => strnatcmp($a['sort_key'], $b['sort_key'])); + + return array_column($contentFiles, 'path'); +} + +function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string { + // If default language, no translation needed + if ($lang === $defaultLang) { + return $requestPath; + } + + $parts = explode('/', trim($requestPath, '/')); + $resolvedParts = []; + $currentPath = $contentDir; + + foreach ($parts as $segment) { + if (empty($segment)) continue; + + // Check all subdirectories for slug matches + $found = false; + if (is_dir($currentPath)) { + $subdirs = array_filter( + scandir($currentPath) ?: [], + fn($item) => !in_array($item, ['.', '..']) && is_dir("$currentPath/$item") + ); + + foreach ($subdirs as $dir) { + $metadata = loadMetadata("$currentPath/$dir", $lang, $defaultLang); + if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) { + $resolvedParts[] = $dir; + $currentPath .= "/$dir"; + $found = true; + break; + } + } + } + + // If no slug match, use segment as-is + if (!$found) { + $resolvedParts[] = $segment; + $currentPath .= "/$segment"; + } + } + + return implode('/', $resolvedParts); +} + +function parseRequestPath(string $requestPath, string $contentDir, bool $hasTrailingSlash, string $lang, string $defaultLang): array { + // Resolve translated slugs to actual directory names + $resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang); + $contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/'); + + if (is_file($contentPath)) { + return ['type' => 'file', 'path' => realpath($contentPath)]; + } + + if (is_dir($contentPath)) { + // Check if directory has subdirectories + $hasSubdirs = !empty(array_filter( + scandir($contentPath) ?: [], + fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentPath/$item") + )); + + // If directory has subdirectories, it's an article-type folder (list view) + if ($hasSubdirs) { + return ['type' => 'directory', 'path' => realpath($contentPath)]; + } + + // No subdirectories - it's a page-type folder + // Find all content files in this directory + $contentFiles = findAllContentFiles($contentPath, $lang, $defaultLang); + + if (!empty($contentFiles)) { + return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$hasTrailingSlash]; + } + + // No content files found + return ['type' => 'directory', 'path' => realpath($contentPath)]; + } + + return ['type' => 'not_found', 'path' => $contentPath]; +} + +function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array { + $metadataFile = "$dirPath/metadata.ini"; + if (!file_exists($metadataFile)) return null; + + $metadata = parse_ini_file($metadataFile, true); + if (!$metadata) return null; + + // Extract base metadata (non-section values) + $baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY); + + // If current language is not default, merge language-specific overrides + if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) { + $baseMetadata = array_merge($baseMetadata, $metadata[$lang]); + } + + return $baseMetadata ?: null; +} + +function loadTranslations(string $lang): array { + $translationFile = dirname(__DIR__) . "/custom/languages/$lang.ini"; + if (file_exists($translationFile)) { + return parse_ini_file($translationFile) ?: []; + } + return []; +} + +function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array { + $navItems = []; + + // Scan top-level directories in content + $items = array_filter( + scandir($contentDir) ?: [], + fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentDir/$item") + ); + + foreach ($items as $item) { + $itemPath = "$contentDir/$item"; + $metadata = loadMetadata($itemPath, $currentLang, $defaultLang); + + // Check if this item should be in menu + if (!$metadata || empty($metadata['menu'])) { + continue; + } + + // Check if content exists for current language + if ($currentLang !== $defaultLang) { + $contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang); + + // If no content files, check if metadata has title for this language + $hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title'])); + + if (!$hasContent) continue; + } + + // Extract title and build URL + $title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? ucfirst($item); + $langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : ''; + + // Use translated slug if available + $urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) + ? $metadata['slug'] + : $item; + + $navItems[] = [ + 'title' => $title, + 'url' => $langPrefix . '/' . urlencode($urlSlug) . '/', + 'order' => (int)($metadata['menu_order'] ?? 999) + ]; + } + + // Sort by menu_order + usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']); + + return $navItems; +} diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..3bd70ed --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,57 @@ +]*>(.*?)<\/h1>/i', $content, $matches)) { + return strip_tags($matches[1]); + } + return null; +} + +function formatNorwegianDate(string $dateString): string { + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) { + $months = ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']; + $day = (int)$matches[3]; + $month = $months[(int)$matches[2] - 1]; + $year = $matches[1]; + return "$day. $month $year"; + } + return $dateString; +} + +function extractDateFromFolder(string $folderName): ?string { + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) { + return formatNorwegianDate($matches[1] . '-' . $matches[2] . '-' . $matches[3]); + } + return null; +} + +function findCoverImage(string $dirPath): ?string { + $extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif']; + foreach ($extensions as $ext) { + if (file_exists("$dirPath/cover.$ext")) { + return "cover.$ext"; + } + } + return null; +} + +function findPdfFile(string $dirPath): ?string { + $files = scandir($dirPath) ?: []; + foreach ($files as $file) { + if (pathinfo($file, PATHINFO_EXTENSION) === 'pdf') { + return $file; + } + } + return null; +} diff --git a/app/rendering.php b/app/rendering.php new file mode 100644 index 0000000..94c7c8b --- /dev/null +++ b/app/rendering.php @@ -0,0 +1,126 @@ +

403 Forbidden

Access denied.

", 403); + } + + $ext = pathinfo($realPath, PATHINFO_EXTENSION); + + if (in_array($ext, ['php', 'html', 'md'])) { + ob_start(); + if ($ext === 'md') { + if (!class_exists('Parsedown')) { + require_once __DIR__ . '/vendor/Parsedown.php'; + } + echo '
' . (new Parsedown())->text(file_get_contents($realPath)) . '
'; + } else { + include $realPath; + } + $content = ob_get_clean(); + + // Build navigation for templates + $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); + + // Load metadata for current page/directory + $pageDir = dirname($realPath); + $pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); + $pageTitle = $pageMetadata['title'] ?? null; + + // Load frontpage metadata for home button label + $frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang); + $homeLabel = $frontpageMetadata['slug'] ?? 'Home'; + + // Load translations + $translations = loadTranslations($currentLang); + + // Wrap content with page template + ob_start(); + include $pageTemplate; + $content = ob_get_clean(); + + // Wrap with base template + include $baseTemplate; + exit; + } + + // Serve other file types directly + header('Content-Type: ' . (mime_content_type($realPath) ?: 'application/octet-stream')); + readfile($realPath); + exit; +} + +function renderMultipleFiles(array $filePaths, string $pageDir): void { + global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang; + + // Validate all files are safe + foreach ($filePaths as $filePath) { + $realPath = realpath($filePath); + if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) { + renderTemplate("

403 Forbidden

Access denied.

", 403); + } + } + + // Render all content files in order + $content = ''; + foreach ($filePaths as $filePath) { + $ext = pathinfo($filePath, PATHINFO_EXTENSION); + + ob_start(); + if ($ext === 'md') { + if (!class_exists('Parsedown')) { + require_once __DIR__ . '/vendor/Parsedown.php'; + } + echo '
' . (new Parsedown())->text(file_get_contents($filePath)) . '
'; + } elseif ($ext === 'html') { + include $filePath; + } elseif ($ext === 'php') { + include $filePath; + } + $content .= ob_get_clean(); + } + + // Build navigation for templates + $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); + + // Load metadata for current page/directory + $pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); + $pageTitle = $pageMetadata['title'] ?? null; + + // Load frontpage metadata for home button label + $frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang); + $homeLabel = $frontpageMetadata['slug'] ?? 'Home'; + + // Load translations + $translations = loadTranslations($currentLang); + + // Wrap content with page template + ob_start(); + include $pageTemplate; + $content = ob_get_clean(); + + // Wrap with base template + include $baseTemplate; + exit; +} diff --git a/app/router.php b/app/router.php index ef2f2ed..5ec37b0 100644 --- a/app/router.php +++ b/app/router.php @@ -1,461 +1,10 @@ 0; -} - -$contentDir = $hasUserContent ? realpath($userContentDir) : realpath($demoContentDir); - -$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/'; -$hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/'; -$requestPath = trim($requestUri, '/'); - -// Extract language from URL -$currentLang = $defaultLang; -$pathParts = explode('/', $requestPath); -if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) { - $currentLang = $pathParts[0]; - array_shift($pathParts); - $requestPath = implode('/', $pathParts); -} - -// Use custom templates with fallback to defaults -$customBaseTemplate = dirname(__DIR__) . '/custom/templates/base.php'; -$defaultBaseTemplate = __DIR__ . '/default/templates/base.php'; -$baseTemplate = file_exists($customBaseTemplate) ? $customBaseTemplate : $defaultBaseTemplate; - -$customPageTemplate = dirname(__DIR__) . '/custom/templates/page.php'; -$defaultPageTemplate = __DIR__ . '/default/templates/page.php'; -$pageTemplate = file_exists($customPageTemplate) ? $customPageTemplate : $defaultPageTemplate; - -$customListTemplate = dirname(__DIR__) . '/custom/templates/list.php'; -$defaultListTemplate = __DIR__ . '/default/templates/list.php'; -$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate; - -// Find all content files in a directory (supporting language variants) -function findAllContentFiles(string $dir, string $lang, string $defaultLang): array { - if (!is_dir($dir)) return []; - - $files = scandir($dir) ?: []; - $contentFiles = []; - $extensions = ['md', 'html', 'php']; - - foreach ($files as $file) { - if ($file === '.' || $file === '..') continue; - - // Exclude system files from content - if ($file === 'index.php') continue; - - $ext = pathinfo($file, PATHINFO_EXTENSION); - if (!in_array($ext, $extensions)) continue; - - $filePath = "$dir/$file"; - if (!is_file($filePath)) continue; - - // Parse filename to check for language variant - $parts = explode('.', $file); - - // Check if this is a language-specific file - if (count($parts) >= 3) { - // Pattern: name.lang.ext - $fileLang = $parts[count($parts) - 2]; - if (in_array($fileLang, ['no', 'en'])) { - // Only include if it matches current language - if ($fileLang === $lang) { - $contentFiles[] = [ - 'path' => $filePath, - 'name' => $file, - 'sort_key' => $parts[0] // Use base name for sorting - ]; - } - continue; - } - } - - // Default files (no language suffix) - include if current lang is default - // or if no language-specific version exists - $baseName = $parts[0]; - $hasLangVersion = false; - - if ($lang !== $defaultLang) { - // Check if language-specific version exists - foreach ($extensions as $checkExt) { - if (file_exists("$dir/$baseName.$lang.$checkExt")) { - $hasLangVersion = true; - break; - } - } - } - - if (!$hasLangVersion) { - $contentFiles[] = [ - 'path' => $filePath, - 'name' => $file, - 'sort_key' => $baseName - ]; - } - } - - // Sort by filename (alphanumerical) - usort($contentFiles, fn($a, $b) => strnatcmp($a['sort_key'], $b['sort_key'])); - - return array_column($contentFiles, 'path'); -} - - - -function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string { - // If default language, no translation needed - if ($lang === $defaultLang) { - return $requestPath; - } - - $parts = explode('/', trim($requestPath, '/')); - $resolvedParts = []; - $currentPath = $contentDir; - - foreach ($parts as $segment) { - if (empty($segment)) continue; - - // Check all subdirectories for slug matches - $found = false; - if (is_dir($currentPath)) { - $subdirs = array_filter( - scandir($currentPath) ?: [], - fn($item) => !in_array($item, ['.', '..']) && is_dir("$currentPath/$item") - ); - - foreach ($subdirs as $dir) { - $metadata = loadMetadata("$currentPath/$dir", $lang, $defaultLang); - if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) { - $resolvedParts[] = $dir; - $currentPath .= "/$dir"; - $found = true; - break; - } - } - } - - // If no slug match, use segment as-is - if (!$found) { - $resolvedParts[] = $segment; - $currentPath .= "/$segment"; - } - } - - return implode('/', $resolvedParts); -} - -function parseRequestPath(string $requestPath, string $contentDir, bool $hasTrailingSlash, string $lang, string $defaultLang): array { - // Resolve translated slugs to actual directory names - $resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang); - $contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/'); - - if (is_file($contentPath)) { - return ['type' => 'file', 'path' => realpath($contentPath)]; - } - - if (is_dir($contentPath)) { - // Check if directory has subdirectories - $hasSubdirs = !empty(array_filter( - scandir($contentPath) ?: [], - fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentPath/$item") - )); - - // If directory has subdirectories, it's an article-type folder (list view) - if ($hasSubdirs) { - return ['type' => 'directory', 'path' => realpath($contentPath)]; - } - - // No subdirectories - it's a page-type folder - // Find all content files in this directory - $contentFiles = findAllContentFiles($contentPath, $lang, $defaultLang); - - if (!empty($contentFiles)) { - return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$hasTrailingSlash]; - } - - // No content files found - return ['type' => 'directory', 'path' => realpath($contentPath)]; - } - - return ['type' => 'not_found', 'path' => $contentPath]; -} - -function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array { - $metadataFile = "$dirPath/metadata.ini"; - if (!file_exists($metadataFile)) return null; - - $metadata = parse_ini_file($metadataFile, true); - if (!$metadata) return null; - - // Extract base metadata (non-section values) - $baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY); - - // If current language is not default, merge language-specific overrides - if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) { - $baseMetadata = array_merge($baseMetadata, $metadata[$lang]); - } - - return $baseMetadata ?: null; -} - -function extractTitle(string $filePath, string $lang, string $defaultLang): ?string { - $files = findAllContentFiles($filePath, $lang, $defaultLang); - if (empty($files)) return null; - - // Check the first content file for a title - $file = $files[0]; - $ext = pathinfo($file, PATHINFO_EXTENSION); - $content = file_get_contents($file); - - if ($ext === 'md' && preg_match('/^#\s+(.+)$/m', $content, $matches)) { - return trim($matches[1]); - } - if (in_array($ext, ['html', 'php']) && preg_match('/]*>(.*?)<\/h1>/i', $content, $matches)) { - return strip_tags($matches[1]); - } - return null; -} - -function formatNorwegianDate(string $dateString): string { - if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) { - $months = ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']; - $day = (int)$matches[3]; - $month = $months[(int)$matches[2] - 1]; - $year = $matches[1]; - return "$day. $month $year"; - } - return $dateString; -} - -function extractDateFromFolder(string $folderName): ?string { - if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) { - return formatNorwegianDate($matches[1] . '-' . $matches[2] . '-' . $matches[3]); - } - return null; -} - -function findCoverImage(string $dirPath): ?string { - $extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif']; - foreach ($extensions as $ext) { - if (file_exists("$dirPath/cover.$ext")) { - return "cover.$ext"; - } - } - return null; -} - - -function findPdfFile(string $dirPath): ?string { - $files = scandir($dirPath) ?: []; - foreach ($files as $file) { - if (pathinfo($file, PATHINFO_EXTENSION) === 'pdf') { - return $file; - } - } - return null; -} - -function loadTranslations(string $lang): array { - $translationFile = dirname(__DIR__) . "/custom/languages/$lang.ini"; - if (file_exists($translationFile)) { - return parse_ini_file($translationFile) ?: []; - } - return []; -} - -function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array { - $navItems = []; - - // Scan top-level directories in content - $items = array_filter( - scandir($contentDir) ?: [], - fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentDir/$item") - ); - - foreach ($items as $item) { - $itemPath = "$contentDir/$item"; - $metadata = loadMetadata($itemPath, $currentLang, $defaultLang); - - // Check if this item should be in menu - if (!$metadata || empty($metadata['menu'])) { - continue; - } - - // Check if content exists for current language - if ($currentLang !== $defaultLang) { - $contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang); - - // If no content files, check if metadata has title for this language - $hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title'])); - - if (!$hasContent) continue; - } - - // Extract title and build URL - $title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? ucfirst($item); - $langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : ''; - - // Use translated slug if available - $urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) - ? $metadata['slug'] - : $item; - - $navItems[] = [ - 'title' => $title, - 'url' => $langPrefix . '/' . urlencode($urlSlug) . '/', - 'order' => (int)($metadata['menu_order'] ?? 999) - ]; - } - - // Sort by menu_order - usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']); - - return $navItems; -} - -function renderTemplate(string $content, int $statusCode = 200): void { - global $baseTemplate, $contentDir, $currentLang, $defaultLang; - - // Build navigation for templates - $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); - - // Load frontpage metadata for home button label - $frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang); - $homeLabel = $frontpageMetadata['slug'] ?? 'Home'; - - // Load translations - $translations = loadTranslations($currentLang); - - http_response_code($statusCode); - include $baseTemplate; - exit; -} - -function renderFile(string $filePath): void { - global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang; - - $realPath = realpath($filePath); - if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) { - renderTemplate("

403 Forbidden

Access denied.

", 403); - } - - $ext = pathinfo($realPath, PATHINFO_EXTENSION); - - if (in_array($ext, ['php', 'html', 'md'])) { - ob_start(); - if ($ext === 'md') { - if (!class_exists('Parsedown')) { - require_once __DIR__ . '/vendor/Parsedown.php'; - } - echo '
' . (new Parsedown())->text(file_get_contents($realPath)) . '
'; - } else { - include $realPath; - } - $content = ob_get_clean(); - - // Build navigation for templates - $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); - - // Load metadata for current page/directory - $pageDir = dirname($realPath); - $pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); - $pageTitle = $pageMetadata['title'] ?? null; - - // Load frontpage metadata for home button label - $frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang); - $homeLabel = $frontpageMetadata['slug'] ?? 'Home'; - - // Load translations - $translations = loadTranslations($currentLang); - - // Wrap content with page template - ob_start(); - include $pageTemplate; - $content = ob_get_clean(); - - // Wrap with base template - include $baseTemplate; - exit; - } - - // Serve other file types directly - header('Content-Type: ' . (mime_content_type($realPath) ?: 'application/octet-stream')); - readfile($realPath); - exit; -} - -function renderMultipleFiles(array $filePaths, string $pageDir): void { - global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang; - - // Validate all files are safe - foreach ($filePaths as $filePath) { - $realPath = realpath($filePath); - if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) { - renderTemplate("

403 Forbidden

Access denied.

", 403); - } - } - - // Render all content files in order - $content = ''; - foreach ($filePaths as $filePath) { - $ext = pathinfo($filePath, PATHINFO_EXTENSION); - - ob_start(); - if ($ext === 'md') { - if (!class_exists('Parsedown')) { - require_once __DIR__ . '/vendor/Parsedown.php'; - } - echo '
' . (new Parsedown())->text(file_get_contents($filePath)) . '
'; - } elseif ($ext === 'html') { - include $filePath; - } elseif ($ext === 'php') { - include $filePath; - } - $content .= ob_get_clean(); - } - - // Build navigation for templates - $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); - - // Load metadata for current page/directory - $pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); - $pageTitle = $pageMetadata['title'] ?? null; - - // Load frontpage metadata for home button label - $frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang); - $homeLabel = $frontpageMetadata['slug'] ?? 'Home'; - - // Load translations - $translations = loadTranslations($currentLang); - - // Wrap content with page template - ob_start(); - include $pageTemplate; - $content = ob_get_clean(); - - // Wrap with base template - include $baseTemplate; - exit; -} +// Load modular components +require_once __DIR__ . '/config.php'; +require_once __DIR__ . '/helpers.php'; +require_once __DIR__ . '/content.php'; +require_once __DIR__ . '/rendering.php'; // Check for assets in /custom/assets/ served at root level $assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath;