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; } // 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; } // Handle frontpage if (empty($requestPath)) { // Find all content files in the root content directory $contentFiles = findAllContentFiles($contentDir, $currentLang, $defaultLang); if (!empty($contentFiles)) { renderMultipleFiles($contentFiles, $contentDir); } } // Parse and handle request $parsedPath = parseRequestPath($requestPath, $contentDir, $hasTrailingSlash, $currentLang, $defaultLang); switch ($parsedPath['type']) { case 'page': // Page-type folder with content files (no subdirectories) // Redirect to add trailing slash if needed if (!empty($parsedPath['needsSlash'])) { header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301); exit; } renderMultipleFiles($parsedPath['files'], $parsedPath['path']); case 'file': // Direct file access or legacy single 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 files in this directory $pageContent = null; $contentFiles = findAllContentFiles($dir, $currentLang, $defaultLang); if (!empty($contentFiles)) { ob_start(); foreach ($contentFiles as $file) { $ext = pathinfo($file, PATHINFO_EXTENSION); if ($ext === 'md') { if (!class_exists('Parsedown')) { require_once __DIR__ . '/vendor/Parsedown.php'; } echo (new Parsedown())->text(file_get_contents($file)); } else { include $file; } } $pageContent = ob_get_clean(); } // Load metadata for this directory $metadata = loadMetadata($dir, $currentLang, $defaultLang); // Select list template based on metadata page_template if (isset($metadata['page_template']) && !empty($metadata['page_template'])) { $templateName = $metadata['page_template']; // Add .php extension if not present if (!str_ends_with($templateName, '.php')) { $templateName .= '.php'; } $customTemplate = dirname(__DIR__) . '/custom/templates/' . $templateName; $defaultTemplate = __DIR__ . '/default/templates/' . $templateName; if (file_exists($customTemplate)) { $listTemplate = $customTemplate; } elseif (file_exists($defaultTemplate)) { $listTemplate = $defaultTemplate; } // If template doesn't exist, fall back to default $listTemplate } // 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) { $itemPath = "$dir/$item"; // Check if content exists for current language if ($currentLang !== $defaultLang) { $contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang); if (empty($contentFiles)) return null; } $metadata = loadMetadata($itemPath, $currentLang, $defaultLang); $coverImage = findCoverImage($itemPath); $pdfFile = findPdfFile($itemPath); $title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? $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, 'pdf' => $pdfFile ? $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug) . '/' . $pdfFile : null, 'redirect' => $metadata['redirect'] ?? null ]; }, $subdirs)); ob_start(); include $listTemplate; $content = ob_get_clean(); // Build navigation for base template $navigation = buildNavigation($contentDir, $currentLang, $defaultLang); include $baseTemplate; exit; case 'not_found': renderTemplate("

404 Not Found

The requested resource was not found.

", 404); }