Add modular architecture to router
Extract configuration, helpers, content processing, and rendering logic into separate files Refactor router to use modular components
This commit is contained in:
parent
eda800d048
commit
149ba03359
5 changed files with 464 additions and 456 deletions
51
app/config.php
Normal file
51
app/config.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
// Load configuration
|
||||
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
|
||||
? __DIR__ . '/../custom/config.ini'
|
||||
: __DIR__ . '/config.ini';
|
||||
$config = parse_ini_file($configFile, true);
|
||||
|
||||
$defaultLang = $config['languages']['default'] ?? 'no';
|
||||
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no'));
|
||||
|
||||
// Use user content if exists and has content, otherwise fall back to demo content
|
||||
$userContentDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
$demoContentDir = __DIR__ . '/default/content';
|
||||
|
||||
// Check if user content directory has actual content (more than just . and ..)
|
||||
$hasUserContent = false;
|
||||
if (is_dir($userContentDir)) {
|
||||
$userFiles = scandir($userContentDir) ?: [];
|
||||
$userFiles = array_diff($userFiles, ['.', '..']);
|
||||
$hasUserContent = count($userFiles) > 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;
|
||||
225
app/content.php
Normal file
225
app/content.php
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<?php
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
57
app/helpers.php
Normal file
57
app/helpers.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
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[^>]*>(.*?)<\/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;
|
||||
}
|
||||
126
app/rendering.php
Normal file
126
app/rendering.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
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("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 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 '<article>' . (new Parsedown())->text(file_get_contents($realPath)) . '</article>';
|
||||
} 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("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 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 '<article>' . (new Parsedown())->text(file_get_contents($filePath)) . '</article>';
|
||||
} 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;
|
||||
}
|
||||
461
app/router.php
461
app/router.php
|
|
@ -1,461 +1,10 @@
|
|||
<?php
|
||||
//test
|
||||
// Load configuration
|
||||
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
|
||||
? __DIR__ . '/../custom/config.ini'
|
||||
: __DIR__ . '/config.ini';
|
||||
$config = parse_ini_file($configFile, true);
|
||||
|
||||
$defaultLang = $config['languages']['default'] ?? 'no';
|
||||
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no'));
|
||||
|
||||
// Use user content if exists and has content, otherwise fall back to demo content
|
||||
$userContentDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
$demoContentDir = __DIR__ . '/default/content';
|
||||
|
||||
// Check if user content directory has actual content (more than just . and ..)
|
||||
$hasUserContent = false;
|
||||
if (is_dir($userContentDir)) {
|
||||
$userFiles = scandir($userContentDir) ?: [];
|
||||
$userFiles = array_diff($userFiles, ['.', '..']);
|
||||
$hasUserContent = count($userFiles) > 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[^>]*>(.*?)<\/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("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 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 '<article>' . (new Parsedown())->text(file_get_contents($realPath)) . '</article>';
|
||||
} 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("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 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 '<article>' . (new Parsedown())->text(file_get_contents($filePath)) . '</article>';
|
||||
} 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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue