[], 'single' => []]; foreach ($extensions as $ext) { // Language-specific files first (if not default language) if ($lang !== $defaultLang) { $patterns['page'][] = "page.$lang.$ext"; $patterns['single'][] = "single.$lang.$ext"; $patterns['single'][] = "post.$lang.$ext"; $patterns['single'][] = "article.$lang.$ext"; } // Default files $patterns['page'][] = "page.$ext"; $patterns['single'][] = "single.$ext"; $patterns['single'][] = "post.$ext"; $patterns['single'][] = "article.$ext"; } return $patterns; } $pageFilePatterns = buildFilePatterns($currentLang, $defaultLang); function findMatchingFile(string $dir, array $patterns): ?string { foreach ($patterns as $pattern) { if (file_exists($file = "$dir/$pattern")) return $file; } return null; } 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, array $patterns, 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, treat as directory (for list views) // Otherwise, if it has a content file, treat as file if (!$hasSubdirs && ($file = findMatchingFile($contentPath, $patterns['single']) ?: findMatchingFile($contentPath, $patterns['page']))) { return ['type' => 'file', 'path' => $file, 'needsSlash' => !$hasTrailingSlash]; } 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, array $patterns): ?string { $file = findMatchingFile($filePath, $patterns['single']) ?: findMatchingFile($filePath, $patterns['page']); if (!$file) return null; $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 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 $pageFilePatterns): 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) { $extensions = ['php', 'html', 'md']; $hasContent = false; // Check for language-specific content files foreach ($extensions as $ext) { if (file_exists("$itemPath/single.$currentLang.$ext") || file_exists("$itemPath/post.$currentLang.$ext") || file_exists("$itemPath/article.$currentLang.$ext") || file_exists("$itemPath/page.$currentLang.$ext")) { $hasContent = true; break; } } // If no language-specific files, check if metadata has title for this language if (!$hasContent && $metadata && isset($metadata['title'])) { $hasContent = true; } if (!$hasContent) continue; } // Extract title and build URL $title = $metadata['title'] ?? extractTitle($itemPath, $pageFilePatterns) ?? 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, $pageFilePatterns; // Build navigation for templates $navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns); // 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, $pageFilePatterns; $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, $pageFilePatterns); // 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; } // Check for assets in /custom/assets/ served at root level $assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath; if (file_exists($assetPath) && is_file($assetPath)) { header('Content-Type: ' . (mime_content_type($assetPath) ?: 'application/octet-stream')); readfile($assetPath); exit; } // Check for static files in content directory $contentFilePath = rtrim($contentDir, '/') . '/' . ltrim($requestPath, '/'); if (file_exists($contentFilePath) && is_file($contentFilePath)) { $ext = pathinfo($contentFilePath, PATHINFO_EXTENSION); // Only serve non-PHP/non-HTML/non-MD files as static assets if (!in_array($ext, ['php', 'html', 'md'])) { header('Content-Type: ' . (mime_content_type($contentFilePath) ?: 'application/octet-stream')); readfile($contentFilePath); exit; } } // Handle frontpage if (empty($requestPath)) { // Try language-specific frontpage first, then default $frontPage = null; if ($currentLang !== $defaultLang && file_exists("$contentDir/frontpage.$currentLang.php")) { $frontPage = "$contentDir/frontpage.$currentLang.php"; } elseif (file_exists("$contentDir/frontpage.php")) { $frontPage = "$contentDir/frontpage.php"; } if ($frontPage) { renderFile($frontPage); } } // Parse and handle request $parsedPath = parseRequestPath($requestPath, $contentDir, $pageFilePatterns, $hasTrailingSlash, $currentLang, $defaultLang); switch ($parsedPath['type']) { case 'file': // Redirect to add trailing slash if this is a directory-based page if (!empty($parsedPath['needsSlash'])) { header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301); exit; } renderFile($parsedPath['path']); case 'directory': $dir = $parsedPath['path']; if (file_exists("$dir/index.php")) { renderFile("$dir/index.php"); } // Check for page content file in this directory $pageContent = null; if ($pageFile = findMatchingFile($dir, $pageFilePatterns['page'])) { $ext = pathinfo($pageFile, PATHINFO_EXTENSION); ob_start(); if ($ext === 'md') { if (!class_exists('Parsedown')) { require_once __DIR__ . '/vendor/Parsedown.php'; } echo (new Parsedown())->text(file_get_contents($pageFile)); } else { include $pageFile; } $pageContent = ob_get_clean(); } // Load metadata for this directory $metadata = loadMetadata($dir, $currentLang, $defaultLang); // Check for custom list template (prefer list-grid.php over list.php) $customListGridTemplate = dirname(__DIR__) . '/custom/templates/list-grid.php'; if (file_exists($customListGridTemplate)) { $listTemplate = $customListGridTemplate; } // Default directory listing $subdirs = array_filter( scandir($dir) ?: [], fn($item) => !in_array($item, ['.', '..']) && is_dir("$dir/$item") ); $items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang, $pageFilePatterns) { $itemPath = "$dir/$item"; // Check if content exists for current language if ($currentLang !== $defaultLang) { // For non-default languages, only check language-specific files $extensions = ['php', 'html', 'md']; $hasContent = false; foreach ($extensions as $ext) { if (file_exists("$itemPath/single.$currentLang.$ext") || file_exists("$itemPath/post.$currentLang.$ext") || file_exists("$itemPath/article.$currentLang.$ext") || file_exists("$itemPath/page.$currentLang.$ext")) { $hasContent = true; break; } } if (!$hasContent) return null; } // Build patterns for rendering (includes fallbacks) $patterns = buildFilePatterns($currentLang, $defaultLang); $metadata = loadMetadata($itemPath, $currentLang, $defaultLang); $coverImage = findCoverImage($itemPath); $title = $metadata['title'] ?? extractTitle($itemPath, $patterns) ?? $item; $date = null; if (isset($metadata['date'])) { $date = formatNorwegianDate($metadata['date']); } else { $date = extractDateFromFolder($item) ?: date("F d, Y", filemtime($itemPath)); } $langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : ''; // Use translated slug if available, otherwise use folder name $urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item; return [ 'title' => $title, 'date' => $date, 'url' => $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug), 'cover' => $coverImage ? $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug) . '/' . $coverImage : null, 'summary' => $metadata['summary'] ?? null ]; }, $subdirs)); ob_start(); include $listTemplate; $content = ob_get_clean(); // Build navigation for base template $navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns); include $baseTemplate; exit; case 'not_found': renderTemplate("

404 Not Found

The requested resource was not found.

", 404); }