Add about page with project philosophy and technical details Add articles about Markdown, templates, and getting started Implement demo content system that shows when no user content exists Update logo to show FolderWeb branding Improve Apache configuration for development environment
519 lines
19 KiB
PHP
519 lines
19 KiB
PHP
<?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;
|
|
|
|
// 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 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 $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);
|
|
|
|
// 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, $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);
|
|
$pdfFile = findPdfFile($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,
|
|
'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, $pageFilePatterns);
|
|
|
|
include $baseTemplate;
|
|
exit;
|
|
|
|
case 'not_found':
|
|
renderTemplate("<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404);
|
|
}
|