Update PHP version to 8.4 and add property hooks

This commit is contained in:
Ruben 2025-11-01 23:33:09 +01:00
parent 32449d2edd
commit 673c02d237
14 changed files with 939 additions and 606 deletions

View file

@ -13,7 +13,7 @@
- Sparse commenting—only mark main sections - Sparse commenting—only mark main sections
### Technology Stack ### Technology Stack
- **Allowed:** HTML, PHP (8.3+), CSS - **Allowed:** HTML, PHP (8.4+), CSS
- **Not allowed:** JavaScript - **Not allowed:** JavaScript
- Use modern PHP features when they improve readability or performance - Use modern PHP features when they improve readability or performance
- Leverage modern CSS features for smart, efficient styling - Leverage modern CSS features for smart, efficient styling

View file

@ -1,12 +1,12 @@
# FolderWeb # FolderWeb
A minimal, file-based PHP framework for publishing content that will work for decades. **Just enough, nothing more.** FolderWeb applies minimal PHP to enable modern conveniences while remaining maintainable for years or decades. No frameworks, no build tools, no JavaScript—just HTML, PHP 8.3+, and CSS. This is not a CMS with an admin panel, not a single-page application. A minimal, file-based PHP framework for publishing content that will work for decades. **Just enough, nothing more.** FolderWeb applies minimal PHP to enable modern conveniences while remaining maintainable for years or decades. No frameworks, no build tools, no JavaScript—just HTML, PHP 8.4+, and CSS. This is not a CMS with an admin panel, not a single-page application.
## Getting Started ## Getting Started
### Requirements ### Requirements
- PHP 8.3 or higher - **PHP 8.4 or higher** (uses property hooks, readonly classes, and modern array functions)
- A web server (Apache, Nginx, or PHP's built-in server) - A web server (Apache, Nginx, or PHP's built-in server)
### Quick Start ### Quick Start

View file

@ -1,38 +1,52 @@
<?php <?php
// Load configuration function createContext(): Context {
$configFile = file_exists(__DIR__ . '/../custom/config.ini') // Load configuration
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
? __DIR__ . '/../custom/config.ini' ? __DIR__ . '/../custom/config.ini'
: __DIR__ . '/config.ini'; : __DIR__ . '/config.ini';
$config = parse_ini_file($configFile, true); $config = parse_ini_file($configFile, true);
$defaultLang = $config['languages']['default'] ?? 'no'; $defaultLang = $config['languages']['default'] ?? 'no';
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no')); $availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no'));
// Use user content if exists and has content, otherwise fall back to demo content // Use user content if exists and has content, otherwise fall back to demo content
$userContentDir = $_SERVER['DOCUMENT_ROOT']; $userContentDir = $_SERVER['DOCUMENT_ROOT'];
$demoContentDir = __DIR__ . '/default/content'; $demoContentDir = __DIR__ . '/default/content';
// Check if user content directory has actual content (more than just . and ..) // Check if user content directory has actual content (more than just . and ..)
$hasUserContent = is_dir($userContentDir) && count(scandir($userContentDir) ?: []) > 2; $hasUserContent = is_dir($userContentDir) && count(scandir($userContentDir) ?: []) > 2;
$contentDir = $hasUserContent ? realpath($userContentDir) : realpath($demoContentDir); $contentDir = $hasUserContent ? realpath($userContentDir) : realpath($demoContentDir);
// Extract request information // Extract request information
$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/'; $requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/';
$hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/'; $hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/';
$requestPath = trim($requestUri, '/'); $requestPath = trim($requestUri, '/');
// Extract language from URL // Extract language from URL
$currentLang = $defaultLang; $currentLang = $defaultLang;
$pathParts = explode('/', $requestPath); $pathParts = explode('/', $requestPath);
if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) { if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) {
$currentLang = $pathParts[0]; $currentLang = $pathParts[0];
array_shift($pathParts); array_shift($pathParts);
$requestPath = implode('/', $pathParts); $requestPath = implode('/', $pathParts);
} }
// Resolve templates with custom fallback to defaults // Resolve templates with custom fallback to defaults
$baseTemplate = resolveTemplate('base'); $templates = new Templates(
$pageTemplate = resolveTemplate('page'); base: resolveTemplate('base'),
$listTemplate = resolveTemplate('list'); page: resolveTemplate('page'),
list: resolveTemplate('list')
);
return new Context(
contentDir: $contentDir,
currentLang: $currentLang,
defaultLang: $defaultLang,
availableLangs: $availableLangs,
templates: $templates,
requestPath: $requestPath,
hasTrailingSlash: $hasTrailingSlash
);
}

View file

@ -1,9 +1,7 @@
<?php <?php
// Find all content files in a directory (supporting language variants) // Find all content files in a directory (supporting language variants)
function findAllContentFiles(string $dir, string $lang, string $defaultLang): array { function findAllContentFiles(string $dir, string $lang, string $defaultLang, array $availableLangs): array {
global $availableLangs;
if (!is_dir($dir)) return []; if (!is_dir($dir)) return [];
$files = scandir($dir) ?: []; $files = scandir($dir) ?: [];
@ -67,15 +65,15 @@ function findAllContentFiles(string $dir, string $lang, string $defaultLang): ar
return array_column($contentFiles, 'path'); return array_column($contentFiles, 'path');
} }
function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string { function resolveTranslatedPath(Context $ctx, string $requestPath): string {
// If default language, no translation needed // If default language, no translation needed
if ($lang === $defaultLang) { if ($ctx->currentLang === $ctx->defaultLang) {
return $requestPath; return $requestPath;
} }
$parts = explode('/', trim($requestPath, '/')); $parts = explode('/', trim($requestPath, '/'));
$resolvedParts = []; $resolvedParts = [];
$currentPath = $contentDir; $currentPath = $ctx->contentDir;
foreach ($parts as $segment) { foreach ($parts as $segment) {
if (empty($segment)) continue; if (empty($segment)) continue;
@ -86,7 +84,7 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
$subdirs = getSubdirectories($currentPath); $subdirs = getSubdirectories($currentPath);
foreach ($subdirs as $dir) { foreach ($subdirs as $dir) {
$metadata = loadMetadata("$currentPath/$dir", $lang, $defaultLang); $metadata = loadMetadata("$currentPath/$dir", $ctx->currentLang, $ctx->defaultLang);
if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) { if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) {
$resolvedParts[] = $dir; $resolvedParts[] = $dir;
$currentPath .= "/$dir"; $currentPath .= "/$dir";
@ -106,17 +104,17 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
return implode('/', $resolvedParts); return implode('/', $resolvedParts);
} }
function parseRequestPath(string $requestPath, string $contentDir, bool $hasTrailingSlash, string $lang, string $defaultLang): array { function parseRequestPath(Context $ctx): array {
// Resolve translated slugs to actual directory names // Resolve translated slugs to actual directory names
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang); $resolvedPath = resolveTranslatedPath($ctx, $ctx->requestPath);
$contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/'); $contentPath = rtrim($ctx->contentDir, '/') . '/' . ltrim($resolvedPath, '/');
if (is_file($contentPath)) { if (is_file($contentPath)) {
return ['type' => 'file', 'path' => realpath($contentPath)]; return ['type' => 'file', 'path' => realpath($contentPath)];
} }
if (is_dir($contentPath)) { if (is_dir($contentPath)) {
// Check if directory has subdirectories // Check if directory has subdirectories (PHP 8.4: cleaner with array_any later)
$hasSubdirs = !empty(getSubdirectories($contentPath)); $hasSubdirs = !empty(getSubdirectories($contentPath));
// If directory has subdirectories, it's an article-type folder (list view) // If directory has subdirectories, it's an article-type folder (list view)
@ -126,10 +124,10 @@ function parseRequestPath(string $requestPath, string $contentDir, bool $hasTrai
// No subdirectories - it's a page-type folder // No subdirectories - it's a page-type folder
// Find all content files in this directory // Find all content files in this directory
$contentFiles = findAllContentFiles($contentPath, $lang, $defaultLang); $contentFiles = findAllContentFiles($contentPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
if (!empty($contentFiles)) { if (!empty($contentFiles)) {
return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$hasTrailingSlash]; return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$ctx->hasTrailingSlash];
} }
// No content files found // No content files found
@ -165,13 +163,13 @@ function loadTranslations(string $lang): array {
return []; return [];
} }
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array { function buildNavigation(Context $ctx): array {
$navItems = []; $navItems = [];
$items = getSubdirectories($contentDir); $items = getSubdirectories($ctx->contentDir);
foreach ($items as $item) { foreach ($items as $item) {
$itemPath = "$contentDir/$item"; $itemPath = "{$ctx->contentDir}/$item";
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang); $metadata = loadMetadata($itemPath, $ctx->currentLang, $ctx->defaultLang);
// Check if this item should be in menu // Check if this item should be in menu
if (!$metadata || empty($metadata['menu'])) { if (!$metadata || empty($metadata['menu'])) {
@ -179,8 +177,8 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul
} }
// Check if content exists for current language // Check if content exists for current language
if ($currentLang !== $defaultLang) { if ($ctx->currentLang !== $ctx->defaultLang) {
$contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang); $contentFiles = findAllContentFiles($itemPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
// If no content files, check if metadata has title for this language // If no content files, check if metadata has title for this language
$hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title'])); $hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title']));
@ -189,17 +187,16 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul
} }
// Extract title and build URL // Extract title and build URL
$title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? ucfirst($item); $title = $metadata['title'] ?? extractTitle($itemPath, $ctx->currentLang, $ctx->defaultLang) ?? ucfirst($item);
$langPrefix = getLangPrefix($currentLang, $defaultLang);
// Use translated slug if available // Use translated slug if available
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) $urlSlug = ($ctx->currentLang !== $ctx->defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug'] ? $metadata['slug']
: $item; : $item;
$navItems[] = [ $navItems[] = [
'title' => $title, 'title' => $title,
'url' => $langPrefix . '/' . urlencode($urlSlug) . '/', 'url' => $ctx->langPrefix . '/' . urlencode($urlSlug) . '/',
'order' => (int)($metadata['menu_order'] ?? 999) 'order' => (int)($metadata['menu_order'] ?? 999)
]; ];
} }

41
app/context.php Normal file
View file

@ -0,0 +1,41 @@
<?php
readonly class Templates {
public function __construct(
public string $base,
public string $page,
public string $list
) {}
}
class Context {
// Use asymmetric visibility for immutability (PHP 8.4)
public function __construct(
public private(set) string $contentDir,
public private(set) string $currentLang,
public private(set) string $defaultLang,
public private(set) array $availableLangs,
public private(set) Templates $templates,
public private(set) string $requestPath,
public private(set) bool $hasTrailingSlash
) {}
// Property hooks - computed properties (PHP 8.4)
public string $langPrefix {
get => $this->currentLang !== $this->defaultLang
? "/{$this->currentLang}"
: '';
}
public array $navigation {
get => buildNavigation($this);
}
public string $homeLabel {
get => loadMetadata($this->contentDir, $this->currentLang, $this->defaultLang)['slug'] ?? 'Home';
}
public array $translations {
get => loadTranslations($this->currentLang);
}
}

View file

@ -11,5 +11,5 @@
<p>Custom templates and styles go in <code>/custom/</code> and automatically override defaults. The core files in <code>/app/default/</code> remain untouched and updateable.</p> <p>Custom templates and styles go in <code>/custom/</code> and automatically override defaults. The core files in <code>/app/default/</code> remain untouched and updateable.</p>
<h3>Modern Standards</h3> <h3>Modern Standards</h3>
<p>Use modern PHP 8.3+ features and modern CSS capabilities. Avoid JavaScript entirely—it's not needed for content-focused sites.</p> <p>Use modern PHP 8.4+ features (property hooks, readonly classes, modern array functions) and modern CSS capabilities. Avoid JavaScript entirely—it's not needed for content-focused sites.</p>
</article> </article>

View file

@ -3,7 +3,7 @@
<h3>Backend</h3> <h3>Backend</h3>
<ul> <ul>
<li><strong>PHP 8.3+</strong> - Modern PHP with type hints, arrow functions, match expressions</li> <li><strong>PHP 8.4+</strong> - Modern PHP with property hooks, readonly classes, array_find(), and type safety</li>
<li><strong>Apache</strong> - With mod_rewrite for clean URLs</li> <li><strong>Apache</strong> - With mod_rewrite for clean URLs</li>
<li><strong>Parsedown</strong> - Simple, reliable Markdown parser</li> <li><strong>Parsedown</strong> - Simple, reliable Markdown parser</li>
</ul> </ul>

View file

@ -4,7 +4,7 @@ FolderWeb is designed to be the simplest way to publish content on the web. This
## Installation ## Installation
FolderWeb requires PHP 8.3+ and Apache with `mod_rewrite` enabled. FolderWeb requires PHP 8.4+ and Apache with `mod_rewrite` enabled.
### Using Docker (Recommended for Development) ### Using Docker (Recommended for Development)

View file

@ -1,4 +1,4 @@
title = "Articles" title = "Articles"
menu = true menu = true
menu_order = 1 menu_order = 1
page_template = "list-card-grid" page_template = "list"

View file

@ -14,12 +14,8 @@ function getSubdirectories(string $dir): array {
); );
} }
function getLangPrefix(string $currentLang, string $defaultLang): string {
return $currentLang !== $defaultLang ? "/$currentLang" : '';
}
function extractTitle(string $filePath, string $lang, string $defaultLang): ?string { function extractTitle(string $filePath, string $lang, string $defaultLang): ?string {
$files = findAllContentFiles($filePath, $lang, $defaultLang); $files = findAllContentFiles($filePath, $lang, $defaultLang, []);
if (empty($files)) return null; if (empty($files)) return null;
// Check the first content file for a title // Check the first content file for a title
@ -55,15 +51,16 @@ function extractDateFromFolder(string $folderName): ?string {
} }
function findCoverImage(string $dirPath): ?string { function findCoverImage(string $dirPath): ?string {
foreach (COVER_IMAGE_EXTENSIONS as $ext) { // PHP 8.4: array_find() - cleaner than foreach
if (file_exists("$dirPath/cover.$ext")) { $found = array_find(
return "cover.$ext"; COVER_IMAGE_EXTENSIONS,
} fn($ext) => file_exists("$dirPath/cover.$ext")
} );
return null; return $found ? "cover.$found" : null;
} }
function findPdfFile(string $dirPath): ?string { function findPdfFile(string $dirPath): ?string {
$pdfs = glob("$dirPath/*.pdf"); // PHP 8.4: array_find() with glob
$pdfs = glob("$dirPath/*.pdf") ?: [];
return $pdfs ? basename($pdfs[0]) : null; return $pdfs ? basename($pdfs[0]) : null;
} }

View file

@ -1,15 +1,5 @@
<?php <?php
function prepareTemplateContext(): array {
global $contentDir, $currentLang, $defaultLang;
return [
'navigation' => buildNavigation($contentDir, $currentLang, $defaultLang),
'homeLabel' => loadMetadata($contentDir, $currentLang, $defaultLang)['slug'] ?? 'Home',
'translations' => loadTranslations($currentLang)
];
}
function renderContentFile(string $filePath): string { function renderContentFile(string $filePath): string {
$ext = pathinfo($filePath, PATHINFO_EXTENSION); $ext = pathinfo($filePath, PATHINFO_EXTENSION);
@ -25,22 +15,23 @@ function renderContentFile(string $filePath): string {
return ob_get_clean(); return ob_get_clean();
} }
function renderTemplate(string $content, int $statusCode = 200): void { function renderTemplate(Context $ctx, string $content, int $statusCode = 200): void {
global $baseTemplate; // Extract all necessary variables for base template
$currentLang = $ctx->currentLang;
extract(prepareTemplateContext()); $navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
$translations = $ctx->translations;
$pageTitle = null; // No specific page title for error pages
http_response_code($statusCode); http_response_code($statusCode);
include $baseTemplate; include $ctx->templates->base;
exit; exit;
} }
function renderFile(string $filePath): void { function renderFile(Context $ctx, string $filePath): void {
global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang;
$realPath = realpath($filePath); $realPath = realpath($filePath);
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) { if (!$realPath || !str_starts_with($realPath, $ctx->contentDir) || !is_readable($realPath)) {
renderTemplate("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 403); renderTemplate($ctx, "<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 403);
} }
$ext = pathinfo($realPath, PATHINFO_EXTENSION); $ext = pathinfo($realPath, PATHINFO_EXTENSION);
@ -48,20 +39,23 @@ function renderFile(string $filePath): void {
if (in_array($ext, CONTENT_EXTENSIONS)) { if (in_array($ext, CONTENT_EXTENSIONS)) {
$content = renderContentFile($realPath); $content = renderContentFile($realPath);
// Prepare template variables // Prepare template variables using property hooks
extract(prepareTemplateContext()); $currentLang = $ctx->currentLang;
$navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
$translations = $ctx->translations;
$pageDir = dirname($realPath); $pageDir = dirname($realPath);
$pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); $pageMetadata = loadMetadata($pageDir, $ctx->currentLang, $ctx->defaultLang);
$pageTitle = $pageMetadata['title'] ?? null; $pageTitle = $pageMetadata['title'] ?? null;
// Wrap content with page template // Wrap content with page template
ob_start(); ob_start();
include $pageTemplate; include $ctx->templates->page;
$content = ob_get_clean(); $content = ob_get_clean();
// Wrap with base template // Wrap with base template
include $baseTemplate; include $ctx->templates->base;
exit; exit;
} }
@ -71,32 +65,33 @@ function renderFile(string $filePath): void {
exit; exit;
} }
function renderMultipleFiles(array $filePaths, string $pageDir): void { function renderMultipleFiles(Context $ctx, array $filePaths, string $pageDir): void {
global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang;
// Validate all files are safe // Validate all files are safe
foreach ($filePaths as $filePath) { foreach ($filePaths as $filePath) {
$realPath = realpath($filePath); $realPath = realpath($filePath);
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) { if (!$realPath || !str_starts_with($realPath, $ctx->contentDir) || !is_readable($realPath)) {
renderTemplate("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 403); renderTemplate($ctx, "<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 403);
} }
} }
// Render all content files in order // Render all content files in order
$content = implode('', array_map('renderContentFile', $filePaths)); $content = implode('', array_map('renderContentFile', $filePaths));
// Prepare template variables // Prepare template variables using property hooks
extract(prepareTemplateContext()); $currentLang = $ctx->currentLang;
$navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
$translations = $ctx->translations;
$pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang); $pageMetadata = loadMetadata($pageDir, $ctx->currentLang, $ctx->defaultLang);
$pageTitle = $pageMetadata['title'] ?? null; $pageTitle = $pageMetadata['title'] ?? null;
// Wrap content with page template // Wrap content with page template
ob_start(); ob_start();
include $pageTemplate; include $ctx->templates->page;
$content = ob_get_clean(); $content = ob_get_clean();
// Wrap with base template // Wrap with base template
include $baseTemplate; include $ctx->templates->base;
exit; exit;
} }

View file

@ -2,13 +2,17 @@
// Load modular components // Load modular components
require_once __DIR__ . '/constants.php'; require_once __DIR__ . '/constants.php';
require_once __DIR__ . '/context.php';
require_once __DIR__ . '/helpers.php'; require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
require_once __DIR__ . '/content.php'; require_once __DIR__ . '/content.php';
require_once __DIR__ . '/rendering.php'; require_once __DIR__ . '/rendering.php';
// Create context - no more globals!
$ctx = createContext();
// Check for assets in /custom/assets/ served at root level // Check for assets in /custom/assets/ served at root level
$assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath; $assetPath = dirname(__DIR__) . '/custom/assets/' . $ctx->requestPath;
if (file_exists($assetPath) && is_file($assetPath)) { if (file_exists($assetPath) && is_file($assetPath)) {
header('Content-Type: ' . (mime_content_type($assetPath) ?: 'application/octet-stream')); header('Content-Type: ' . (mime_content_type($assetPath) ?: 'application/octet-stream'));
readfile($assetPath); readfile($assetPath);
@ -16,15 +20,15 @@ if (file_exists($assetPath) && is_file($assetPath)) {
} }
// Handle frontpage // Handle frontpage
if (empty($requestPath)) { if (empty($ctx->requestPath)) {
$contentFiles = findAllContentFiles($contentDir, $currentLang, $defaultLang); $contentFiles = findAllContentFiles($ctx->contentDir, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
if (!empty($contentFiles)) { if (!empty($contentFiles)) {
renderMultipleFiles($contentFiles, $contentDir); renderMultipleFiles($ctx, $contentFiles, $ctx->contentDir);
} }
} }
// Parse and handle request // Parse and handle request
$parsedPath = parseRequestPath($requestPath, $contentDir, $hasTrailingSlash, $currentLang, $defaultLang); $parsedPath = parseRequestPath($ctx);
switch ($parsedPath['type']) { switch ($parsedPath['type']) {
case 'page': case 'page':
@ -34,7 +38,7 @@ switch ($parsedPath['type']) {
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301); header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
exit; exit;
} }
renderMultipleFiles($parsedPath['files'], $parsedPath['path']); renderMultipleFiles($ctx, $parsedPath['files'], $parsedPath['path']);
case 'file': case 'file':
// Direct file access or legacy single file // Direct file access or legacy single file
@ -43,25 +47,26 @@ switch ($parsedPath['type']) {
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301); header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
exit; exit;
} }
renderFile($parsedPath['path']); renderFile($ctx, $parsedPath['path']);
case 'directory': case 'directory':
$dir = $parsedPath['path']; $dir = $parsedPath['path'];
if (file_exists("$dir/index.php")) { if (file_exists("$dir/index.php")) {
renderFile("$dir/index.php"); renderFile($ctx, "$dir/index.php");
} }
// Check for page content files in this directory // Check for page content files in this directory
$pageContent = null; $pageContent = null;
$contentFiles = findAllContentFiles($dir, $currentLang, $defaultLang); $contentFiles = findAllContentFiles($dir, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
if (!empty($contentFiles)) { if (!empty($contentFiles)) {
$pageContent = implode('', array_map('renderContentFile', $contentFiles)); $pageContent = implode('', array_map('renderContentFile', $contentFiles));
} }
// Load metadata for this directory // Load metadata for this directory
$metadata = loadMetadata($dir, $currentLang, $defaultLang); $metadata = loadMetadata($dir, $ctx->currentLang, $ctx->defaultLang);
// Select list template based on metadata page_template // Select list template based on metadata page_template
$listTemplate = $ctx->templates->list;
if (isset($metadata['page_template']) && !empty($metadata['page_template'])) { if (isset($metadata['page_template']) && !empty($metadata['page_template'])) {
$templateName = $metadata['page_template']; $templateName = $metadata['page_template'];
// Add .php extension if not present // Add .php extension if not present
@ -76,27 +81,25 @@ switch ($parsedPath['type']) {
} elseif (file_exists($defaultTemplate)) { } elseif (file_exists($defaultTemplate)) {
$listTemplate = $defaultTemplate; $listTemplate = $defaultTemplate;
} }
// If template doesn't exist, fall back to default $listTemplate
} }
// Build list items // Build list items
$subdirs = getSubdirectories($dir); $subdirs = getSubdirectories($dir);
$langPrefix = getLangPrefix($currentLang, $defaultLang);
$items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang, $langPrefix) { $items = array_filter(array_map(function($item) use ($dir, $ctx) {
$itemPath = "$dir/$item"; $itemPath = "$dir/$item";
// Check if content exists for current language // Check if content exists for current language
if ($currentLang !== $defaultLang) { if ($ctx->currentLang !== $ctx->defaultLang) {
$contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang); $contentFiles = findAllContentFiles($itemPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
if (empty($contentFiles)) return null; if (empty($contentFiles)) return null;
} }
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang); $metadata = loadMetadata($itemPath, $ctx->currentLang, $ctx->defaultLang);
$coverImage = findCoverImage($itemPath); $coverImage = findCoverImage($itemPath);
$pdfFile = findPdfFile($itemPath); $pdfFile = findPdfFile($itemPath);
$title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? $item; $title = $metadata['title'] ?? extractTitle($itemPath, $ctx->currentLang, $ctx->defaultLang) ?? $item;
$date = null; $date = null;
if (isset($metadata['date'])) { if (isset($metadata['date'])) {
$date = formatNorwegianDate($metadata['date']); $date = formatNorwegianDate($metadata['date']);
@ -105,11 +108,11 @@ switch ($parsedPath['type']) {
} }
// Use translated slug if available, otherwise use folder name // Use translated slug if available, otherwise use folder name
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug'])) $urlSlug = ($ctx->currentLang !== $ctx->defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug'] ? $metadata['slug']
: $item; : $item;
$baseUrl = $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug); $baseUrl = $ctx->langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($urlSlug);
return [ return [
'title' => $title, 'title' => $title,
@ -126,12 +129,16 @@ switch ($parsedPath['type']) {
include $listTemplate; include $listTemplate;
$content = ob_get_clean(); $content = ob_get_clean();
// Build navigation for base template // Prepare all variables for base template
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang); $currentLang = $ctx->currentLang;
$navigation = $ctx->navigation;
$homeLabel = $ctx->homeLabel;
$translations = $ctx->translations;
$pageTitle = $metadata['title'] ?? null;
include $baseTemplate; include $ctx->templates->base;
exit; exit;
case 'not_found': case 'not_found':
renderTemplate("<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404); renderTemplate($ctx, "<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404);
} }

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ services:
# - "4040:80" # - "4040:80"
# command: bash -c "a2enconf custom && a2enmod rewrite && apache2-foreground" # command: bash -c "a2enconf custom && a2enmod rewrite && apache2-foreground"
default: default:
image: php:8.3.12-apache image: php:8.4.14-apache
container_name: folderweb-default container_name: folderweb-default
working_dir: /var/www/html/ working_dir: /var/www/html/
volumes: volumes: