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
### Technology Stack
- **Allowed:** HTML, PHP (8.3+), CSS
- **Allowed:** HTML, PHP (8.4+), CSS
- **Not allowed:** JavaScript
- Use modern PHP features when they improve readability or performance
- Leverage modern CSS features for smart, efficient styling

View file

@ -1,12 +1,12 @@
# 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
### 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)
### Quick Start

View file

@ -1,38 +1,52 @@
<?php
// Load configuration
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
? __DIR__ . '/../custom/config.ini'
: __DIR__ . '/config.ini';
$config = parse_ini_file($configFile, true);
function createContext(): Context {
// 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'));
$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';
// 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 = is_dir($userContentDir) && count(scandir($userContentDir) ?: []) > 2;
// Check if user content directory has actual content (more than just . and ..)
$hasUserContent = is_dir($userContentDir) && count(scandir($userContentDir) ?: []) > 2;
$contentDir = $hasUserContent ? realpath($userContentDir) : realpath($demoContentDir);
$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 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);
// 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);
}
// Resolve templates with custom fallback to defaults
$templates = new Templates(
base: resolveTemplate('base'),
page: resolveTemplate('page'),
list: resolveTemplate('list')
);
return new Context(
contentDir: $contentDir,
currentLang: $currentLang,
defaultLang: $defaultLang,
availableLangs: $availableLangs,
templates: $templates,
requestPath: $requestPath,
hasTrailingSlash: $hasTrailingSlash
);
}
// Resolve templates with custom fallback to defaults
$baseTemplate = resolveTemplate('base');
$pageTemplate = resolveTemplate('page');
$listTemplate = resolveTemplate('list');

View file

@ -1,9 +1,7 @@
<?php
// Find all content files in a directory (supporting language variants)
function findAllContentFiles(string $dir, string $lang, string $defaultLang): array {
global $availableLangs;
function findAllContentFiles(string $dir, string $lang, string $defaultLang, array $availableLangs): array {
if (!is_dir($dir)) return [];
$files = scandir($dir) ?: [];
@ -67,15 +65,15 @@ function findAllContentFiles(string $dir, string $lang, string $defaultLang): ar
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 ($lang === $defaultLang) {
if ($ctx->currentLang === $ctx->defaultLang) {
return $requestPath;
}
$parts = explode('/', trim($requestPath, '/'));
$resolvedParts = [];
$currentPath = $contentDir;
$currentPath = $ctx->contentDir;
foreach ($parts as $segment) {
if (empty($segment)) continue;
@ -86,7 +84,7 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
$subdirs = getSubdirectories($currentPath);
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) {
$resolvedParts[] = $dir;
$currentPath .= "/$dir";
@ -106,17 +104,17 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
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
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang);
$contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/');
$resolvedPath = resolveTranslatedPath($ctx, $ctx->requestPath);
$contentPath = rtrim($ctx->contentDir, '/') . '/' . ltrim($resolvedPath, '/');
if (is_file($contentPath)) {
return ['type' => 'file', 'path' => realpath($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));
// 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
// Find all content files in this directory
$contentFiles = findAllContentFiles($contentPath, $lang, $defaultLang);
$contentFiles = findAllContentFiles($contentPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
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
@ -165,13 +163,13 @@ function loadTranslations(string $lang): array {
return [];
}
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array {
function buildNavigation(Context $ctx): array {
$navItems = [];
$items = getSubdirectories($contentDir);
$items = getSubdirectories($ctx->contentDir);
foreach ($items as $item) {
$itemPath = "$contentDir/$item";
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
$itemPath = "{$ctx->contentDir}/$item";
$metadata = loadMetadata($itemPath, $ctx->currentLang, $ctx->defaultLang);
// Check if this item should be in 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
if ($currentLang !== $defaultLang) {
$contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang);
if ($ctx->currentLang !== $ctx->defaultLang) {
$contentFiles = findAllContentFiles($itemPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
// If no content files, check if metadata has title for this language
$hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title']));
@ -189,17 +187,16 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul
}
// Extract title and build URL
$title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? ucfirst($item);
$langPrefix = getLangPrefix($currentLang, $defaultLang);
$title = $metadata['title'] ?? extractTitle($itemPath, $ctx->currentLang, $ctx->defaultLang) ?? ucfirst($item);
// Use translated slug if available
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug']))
$urlSlug = ($ctx->currentLang !== $ctx->defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug']
: $item;
$navItems[] = [
'title' => $title,
'url' => $langPrefix . '/' . urlencode($urlSlug) . '/',
'url' => $ctx->langPrefix . '/' . urlencode($urlSlug) . '/',
'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>
<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>

View file

@ -3,7 +3,7 @@
<h3>Backend</h3>
<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>Parsedown</strong> - Simple, reliable Markdown parser</li>
</ul>

View file

@ -4,7 +4,7 @@ FolderWeb is designed to be the simplest way to publish content on the web. This
## 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)

View file

@ -1,4 +1,4 @@
title = "Articles"
menu = true
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 {
$files = findAllContentFiles($filePath, $lang, $defaultLang);
$files = findAllContentFiles($filePath, $lang, $defaultLang, []);
if (empty($files)) return null;
// Check the first content file for a title
@ -55,15 +51,16 @@ function extractDateFromFolder(string $folderName): ?string {
}
function findCoverImage(string $dirPath): ?string {
foreach (COVER_IMAGE_EXTENSIONS as $ext) {
if (file_exists("$dirPath/cover.$ext")) {
return "cover.$ext";
}
}
return null;
// PHP 8.4: array_find() - cleaner than foreach
$found = array_find(
COVER_IMAGE_EXTENSIONS,
fn($ext) => file_exists("$dirPath/cover.$ext")
);
return $found ? "cover.$found" : null;
}
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;
}

View file

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

View file

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

1234
app/vendor/Parsedown.php vendored

File diff suppressed because it is too large Load diff

View file

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