
Move asset handling to after language extraction The asset handling code was previously located in the language extraction section, which could interfere with proper language detection. This commit moves the asset handling code to after the language extraction logic, ensuring that language detection works correctly before any asset handling occurs.
488 lines
18 KiB
PHP
488 lines
18 KiB
PHP
<?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'));
|
|
|
|
$contentDir = realpath($_SERVER['DOCUMENT_ROOT']);
|
|
$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);
|
|
}
|
|
|
|
// Determine if this is a docs request
|
|
$isDocsRequest = str_starts_with($requestPath, 'docs');
|
|
|
|
// Use docs templates for /docs, otherwise use custom/default templates
|
|
if ($isDocsRequest) {
|
|
$baseTemplate = __DIR__ . '/default/docs/templates/base.php';
|
|
$pageTemplate = __DIR__ . '/default/docs/templates/base.php';
|
|
$listTemplate = __DIR__ . '/default/docs/templates/list.php';
|
|
} else {
|
|
$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;
|
|
}
|
|
|
|
// Build file patterns with language variants
|
|
function buildFilePatterns(string $lang, string $defaultLang): array {
|
|
$extensions = ['php', 'html', 'md'];
|
|
$patterns = ['page' => [], '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[^>]*>(.*?)<\/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("<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, $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;
|
|
}
|
|
|
|
// 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("<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404);
|
|
}
|