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;