From d937ca616656bbc27f7f220f07fcd8bebc993af7 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 Oct 2025 20:16:52 +0200 Subject: [PATCH] Add page template with metadata display and copyright translation Add support for language translations in footer Add page template to template resolution logic --- app/default/templates/base.php | 2 +- app/default/templates/page.php | 25 ++++++ app/router.php | 159 ++++++++++++++++++++------------- 3 files changed, 122 insertions(+), 64 deletions(-) create mode 100644 app/default/templates/page.php diff --git a/app/default/templates/base.php b/app/default/templates/base.php index 9d6010d..3782607 100644 --- a/app/default/templates/base.php +++ b/app/default/templates/base.php @@ -23,7 +23,7 @@ diff --git a/app/default/templates/page.php b/app/default/templates/page.php new file mode 100644 index 0000000..6ed654c --- /dev/null +++ b/app/default/templates/page.php @@ -0,0 +1,25 @@ + + + + + diff --git a/app/router.php b/app/router.php index aba6f92..b79ce48 100644 --- a/app/router.php +++ b/app/router.php @@ -1,7 +1,7 @@ [], 'single' => []]; - + foreach ($extensions as $ext) { // Language-specific files first (if not default language) if ($lang !== $defaultLang) { @@ -52,14 +57,14 @@ function buildFilePatterns(string $lang, string $defaultLang): array { $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; } @@ -77,14 +82,14 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $ 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)) { @@ -92,7 +97,7 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $ 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) { @@ -103,14 +108,14 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $ } } } - + // If no slug match, use segment as-is if (!$found) { $resolvedParts[] = $segment; $currentPath .= "/$segment"; } } - + return implode('/', $resolvedParts); } @@ -118,46 +123,46 @@ function parseRequestPath(string $requestPath, string $contentDir, array $patter // 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)) { if ($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]); } @@ -195,28 +200,38 @@ function findCoverImage(string $dirPath): ?string { 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") || @@ -226,56 +241,65 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul 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) ?? $item; + $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'] + $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, $contentDir, $currentLang, $defaultLang, $pageFilePatterns; - + 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); + renderTemplate("

403 Forbidden

Access denied.

", 403); } - + $ext = pathinfo($realPath, PATHINFO_EXTENSION); - + if (in_array($ext, ['php', 'html', 'md'])) { ob_start(); if ($ext === 'md') { @@ -287,23 +311,32 @@ function renderFile(string $filePath): void { 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); @@ -319,7 +352,7 @@ if (empty($requestPath)) { } elseif (file_exists("$contentDir/frontpage.php")) { $frontPage = "$contentDir/frontpage.php"; } - + if ($frontPage) { renderFile($frontPage); } @@ -336,22 +369,22 @@ switch ($parsedPath['type']) { exit; } renderFile($parsedPath['path']); - + case 'directory': $dir = $parsedPath['path']; if (file_exists("$dir/index.php")) { renderFile("$dir/index.php"); } - + // 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 @@ -368,13 +401,13 @@ switch ($parsedPath['type']) { } 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'])) { @@ -382,14 +415,14 @@ switch ($parsedPath['type']) { } 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'] + $urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) + ? $metadata['slug'] : $item; - + return [ 'title' => $title, 'date' => $date, @@ -398,17 +431,17 @@ switch ($parsedPath['type']) { '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); }