diff --git a/README.md b/README.md
index 288c2ce..cc7a7f3 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ A minimal, file-based PHP framework for publishing content that will work for de
### Creating Content
1. Create a directory for your content in the document root
-2. Add a content file (.md, .php or .html)
+2. Add one or more content files (`.md`, `.php`, or `.html`)
3. Optionally add `metadata.ini` for title, date, and summary
4. Optionally add `cover.jpg|webp` for list view thumbnails
@@ -29,8 +29,12 @@ Content is immediately accessible at the URL matching the directory path.
```
/content/
+ 00-welcome.php
+ 01-introduction.md
about/
- page.md
+ 00-overview.md
+ 01-philosophy.html
+ 02-technology.php
blog/
2025-11-01-hello-world/
article.md
@@ -38,6 +42,37 @@ Content is immediately accessible at the URL matching the directory path.
metadata.ini
```
+### Multi-File Pages
+
+Any folder without subdirectories is a **page-type folder**. All `.md`, `.html`, and `.php` files in that folder render together as a single page in **alphanumerical order**.
+
+This allows you to:
+
+- Break long content into manageable sections
+- Mix file formats freely (Markdown, HTML, PHP)
+- Reorder sections by renaming files
+- Include dynamic PHP content alongside static content
+
+**Example:**
+
+```
+/content/
+ docs/
+ 00-introduction.md
+ 01-setup.md
+ 02-advanced.html
+ 03-examples.php
+```
+
+All four files render as one page at `/docs/`, in that order.
+
+### Folder Types
+
+FolderWeb automatically determines how to render folders:
+
+- **Page-type folder** (no subdirectories) → Renders all content files as a single page
+- **Article-type folder** (has subdirectories) → Shows list view with links to subdirectories
+
### Customization
All customization lives in `custom/`:
@@ -53,10 +88,11 @@ Never modify files in `/app/default/`—always override them in `custom/`.
The folder hierarchy dictates URL structure:
-- `/about/page.md` → `yoursite.com/about`
-- `/blog/2025-11-01-post/article.md` → `yoursite.com/blog/2025-11-01-post`
+- Root content files → `yoursite.com/`
+- `/about/` (with content files) → `yoursite.com/about/`
+- `/blog/2025-11-01-post/` (with content files) → `yoursite.com/blog/2025-11-01-post/`
- Dates in folder names are automatically extracted and formatted
-- Directories with subdirectories automatically show list views
+- Content file names can be anything (not limited to `page.md` or `article.md`)
### Metadata
diff --git a/app/default/content/00-welcome.php b/app/default/content/00-welcome.php
new file mode 100644
index 0000000..c59e257
--- /dev/null
+++ b/app/default/content/00-welcome.php
@@ -0,0 +1,6 @@
+
+
Welcome to FolderWeb
+
+ A minimalist PHP framework that turns folders into websites. No JavaScript, no build tools, just simple files.
+
+
diff --git a/app/default/content/00a-getting-started.md b/app/default/content/00a-getting-started.md
new file mode 100644
index 0000000..8d11e2a
--- /dev/null
+++ b/app/default/content/00a-getting-started.md
@@ -0,0 +1,7 @@
+## Getting Started
+
+This is demo content to help you understand how FolderWeb works. To replace it with your own content:
+
+1. Create a `/content` folder in your project root
+2. Add your content files (`.md`, `.html`, or `.php`)
+3. This demo will automatically disappear
diff --git a/app/default/content/01-core-concepts.md b/app/default/content/01-core-concepts.md
new file mode 100644
index 0000000..de0044f
--- /dev/null
+++ b/app/default/content/01-core-concepts.md
@@ -0,0 +1,15 @@
+## Core Concepts
+
+### File-Based Routing
+
+Drop a file in a folder and it's instantly accessible at a URL matching that path. Your folder structure becomes your URL structure.
+
+### Multiple Content Types
+
+- **Markdown** - Write in `.md` files, automatically converted to HTML
+- **HTML** - Pure HTML files for complete control
+- **PHP** - Dynamic content when you need it
+
+### Multi-File Pages
+
+Any folder without subfolders renders all content files (`.md`, `.html`, `.php`) in alphanumerical order. Mix formats freely!
diff --git a/app/default/content/02-features.html b/app/default/content/02-features.html
new file mode 100644
index 0000000..b766430
--- /dev/null
+++ b/app/default/content/02-features.html
@@ -0,0 +1,13 @@
+
+
Smart Features
+
+
+
Metadata - Use metadata.ini files for titles, dates, summaries
+
Date extraction - Folder names like 2025-11-01-title automatically show dates
+
Cover images - Add cover.jpg for list view thumbnails
+
Templates - Custom templates in /custom/templates/ override defaults
+
+
+
Explore the Demo
+
Check out the Articles and About pages to see different content types in action.
+
diff --git a/app/default/content/03-this-page-demo.md b/app/default/content/03-this-page-demo.md
new file mode 100644
index 0000000..e5d7cb5
--- /dev/null
+++ b/app/default/content/03-this-page-demo.md
@@ -0,0 +1,11 @@
+## About This Frontpage
+
+**This frontpage demonstrates the multi-file approach!** It's composed of:
+
+1. `00-welcome.php` - Hero header (PHP/HTML)
+2. `00a-getting-started.md` - Getting started guide (Markdown)
+3. `01-core-concepts.md` - Core concepts (Markdown)
+4. `02-features.html` - Features and links (HTML)
+5. `03-this-page-demo.md` - This explanation (Markdown)
+
+All files in the root `/content` folder are rendered together, just like pages in subfolders. Name your files anything you want—they'll render in alphanumerical order.
diff --git a/app/default/content/about/00-introduction.md b/app/default/content/about/00-introduction.md
new file mode 100644
index 0000000..470d79f
--- /dev/null
+++ b/app/default/content/about/00-introduction.md
@@ -0,0 +1,15 @@
+# About FolderWeb
+
+FolderWeb is a minimalist PHP framework designed for simplicity, longevity, and maintainability. It's built on a simple philosophy: **just enough, nothing more**.
+
+## Philosophy
+
+Modern web development has become unnecessarily complex. Build tools, package managers, JavaScript frameworks that change every few months—it's exhausting and unsustainable.
+
+FolderWeb is different. It's built to:
+
+- **Work for decades** without requiring constant updates
+- **Be understandable** by reading a few hundred lines of code
+- **Stay maintainable** without specialized knowledge
+- **Load fast** with no JavaScript overhead
+- **Just work** without configuration or setup
diff --git a/app/default/content/about/01-design-principles.html b/app/default/content/about/01-design-principles.html
new file mode 100644
index 0000000..690303b
--- /dev/null
+++ b/app/default/content/about/01-design-principles.html
@@ -0,0 +1,15 @@
+
+
Design Principles
+
+
Minimalism
+
Use only what is strictly necessary. No frameworks, no build tools, no package managers for frontend code. Every line of code must justify its existence.
+
+
File-Based Everything
+
Your folder structure is your URL structure. Drop a file in a folder and it's instantly accessible. No routes to configure, no databases to set up.
+
+
Override, Never Modify
+
Custom templates and styles go in /custom/ and automatically override defaults. The core files in /app/default/ remain untouched and updateable.
+
+
Modern Standards
+
Use modern PHP 8.3+ features and modern CSS capabilities. Avoid JavaScript entirely—it's not needed for content-focused sites.
PHP 8.3+ - Modern PHP with type hints, arrow functions, match expressions
+
Apache - With mod_rewrite for clean URLs
+
Parsedown - Simple, reliable Markdown parser
+
+
+
Frontend
+
+
HTML5 - Semantic markup following best practices
+
CSS3 - Modern features like Grid, clamp(), OKLCH colors, CSS nesting
+
No JavaScript - By design, for faster loads and simpler maintenance
+
+
+
+
diff --git a/app/default/content/about/03-what-it-is-not.md b/app/default/content/about/03-what-it-is-not.md
new file mode 100644
index 0000000..89ca489
--- /dev/null
+++ b/app/default/content/about/03-what-it-is-not.md
@@ -0,0 +1,19 @@
+## What It's Not
+
+FolderWeb is **not**:
+
+- A CMS with an admin panel
+- A single-page application framework
+- A solution for complex web applications
+- Trying to scale to millions of users
+- Following current trends and fads
+
+## What It Is
+
+FolderWeb **is**:
+
+- A simple way to publish content
+- A foundation that will work for decades
+- A teaching tool for web fundamentals
+- A protest against unnecessary complexity
+- Perfect for documentation, blogs, portfolios, small business sites
diff --git a/app/default/content/about/04-get-started.md b/app/default/content/about/04-get-started.md
new file mode 100644
index 0000000..80c52c8
--- /dev/null
+++ b/app/default/content/about/04-get-started.md
@@ -0,0 +1,21 @@
+## Get Started
+
+Ready to build something simple and lasting?
+
+1. Create a `/content` folder
+2. Add your first `.md` file
+3. That's it—you're publishing
+
+No build step. No npm install. No configuration files. Just content.
+
+---
+
+**This page demonstrates FolderWeb's multi-file approach.** Notice how this page is built from multiple files that render in alphanumerical order:
+
+- `00-introduction.md` - Markdown content
+- `01-design-principles.html` - Static HTML
+- `02-technology-stack.php` - Dynamic PHP (shows current date)
+- `03-what-it-is-not.md` - More Markdown
+- `04-get-started.md` - This section
+
+Mix file types freely. They all render together seamlessly!
diff --git a/app/default/content/about/page.md b/app/default/content/about/page.md
deleted file mode 100644
index e400e3e..0000000
--- a/app/default/content/about/page.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# About FolderWeb
-
-FolderWeb is a minimalist PHP framework designed for simplicity, longevity, and maintainability. It's built on a simple philosophy: **just enough, nothing more**.
-
-## Philosophy
-
-Modern web development has become unnecessarily complex. Build tools, package managers, JavaScript frameworks that change every few months—it's exhausting and unsustainable.
-
-FolderWeb is different. It's built to:
-
-- **Work for decades** without requiring constant updates
-- **Be understandable** by reading a few hundred lines of code
-- **Stay maintainable** without specialized knowledge
-- **Load fast** with no JavaScript overhead
-- **Just work** without configuration or setup
-
-## Design Principles
-
-### Minimalism
-Use only what is strictly necessary. No frameworks, no build tools, no package managers for frontend code. Every line of code must justify its existence.
-
-### File-Based Everything
-Your folder structure is your URL structure. Drop a file in a folder and it's instantly accessible. No routes to configure, no databases to set up.
-
-### Override, Never Modify
-Custom templates and styles go in `/custom/` and automatically override defaults. The core files in `/app/default/` remain untouched and updateable.
-
-### Modern Standards
-Use modern PHP 8.3+ features and modern CSS capabilities. Avoid JavaScript entirely—it's not needed for content-focused sites.
-
-## Technology Stack
-
-### Backend
-- **PHP 8.3+** - Modern PHP with type hints, arrow functions, match expressions
-- **Apache** - With mod_rewrite for clean URLs
-- **Parsedown** - Simple, reliable Markdown parser
-
-### Frontend
-- **HTML5** - Semantic markup following best practices
-- **CSS3** - Modern features like Grid, clamp(), OKLCH colors, CSS nesting
-- **No JavaScript** - By design, for faster loads and simpler maintenance
-
-## What It's Not
-
-FolderWeb is **not**:
-
-- A CMS with an admin panel
-- A single-page application framework
-- A solution for complex web applications
-- Trying to scale to millions of users
-- Following current trends and fads
-
-## What It Is
-
-FolderWeb **is**:
-
-- A simple way to publish content
-- A foundation that will work for decades
-- A teaching tool for web fundamentals
-- A protest against unnecessary complexity
-- Perfect for documentation, blogs, portfolios, small business sites
-
-## Use Cases
-
-FolderWeb excels at:
-
-- **Documentation sites** - Clear structure, easy to navigate
-- **Personal blogs** - Simple publishing workflow
-- **Portfolio sites** - Showcase your work without bloat
-- **Small business sites** - Professional presence without complexity
-- **Project pages** - Quick site for your open source project
-
-## Who Created This?
-
-FolderWeb emerged from frustration with modern web development complexity. It's built for developers who appreciate simplicity and maintainability over features and frameworks.
-
-## License
-
-FolderWeb is open source. Check the repository for license details.
-
-## Get Started
-
-Ready to build something simple and lasting?
-
-1. Create a `/content` folder
-2. Add your first `.md` file
-3. That's it—you're publishing
-
-No build step. No npm install. No configuration files. Just content.
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/00-introduction.md b/app/default/content/articles/2025-11-02-multi-file-content/00-introduction.md
new file mode 100644
index 0000000..bf15bdf
--- /dev/null
+++ b/app/default/content/articles/2025-11-02-multi-file-content/00-introduction.md
@@ -0,0 +1,14 @@
+# Multi-File Content Pages
+
+One of FolderWeb's most powerful features is the ability to compose a single page from multiple content files. This gives you flexibility in how you organize and author your content.
+
+## How It Works
+
+When a folder contains **no subdirectories**, FolderWeb treats it as a **page-type folder**. All `.md`, `.html`, and `.php` files in that folder are rendered in **alphanumerical order**.
+
+This means you can:
+
+- Break long content into manageable sections
+- Mix file formats freely (Markdown, HTML, PHP)
+- Reorder sections by renaming files
+- Include dynamic PHP content alongside static content
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/01-examples.md b/app/default/content/articles/2025-11-02-multi-file-content/01-examples.md
new file mode 100644
index 0000000..5633aa1
--- /dev/null
+++ b/app/default/content/articles/2025-11-02-multi-file-content/01-examples.md
@@ -0,0 +1,24 @@
+## File Naming Examples
+
+Here are some example naming patterns:
+
+```
+/my-page/
+ 00-introduction.md
+ 01-getting-started.md
+ 02-advanced-topics.html
+ 03-conclusion.php
+```
+
+Files render in this order:
+1. `00-introduction.md`
+2. `01-getting-started.md`
+3. `02-advanced-topics.html`
+4. `03-conclusion.php`
+
+## Folder Types
+
+FolderWeb automatically determines folder type:
+
+- **Page-type folder**: No subdirectories → Renders all content files as a single page
+- **Article-type folder**: Has subdirectories → Shows list view with links to subdirectories
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/02-use-cases.html b/app/default/content/articles/2025-11-02-multi-file-content/02-use-cases.html
new file mode 100644
index 0000000..547c1ab
--- /dev/null
+++ b/app/default/content/articles/2025-11-02-multi-file-content/02-use-cases.html
@@ -0,0 +1,15 @@
+
+
Use Cases
+
+
Long Documentation
+
Break lengthy documentation into logical sections. Each section gets its own file, making editing and maintenance easier.
+
+
Mixed Content Types
+
Use Markdown for simple text, HTML for complex layouts, and PHP for dynamic content—all on the same page.
+
+
Collaborative Editing
+
Multiple authors can work on different sections simultaneously without merge conflicts.
+
+
Progressive Enhancement
+
Start with simple Markdown files. Later, enhance specific sections with HTML or PHP without restructuring.
This section is a PHP file that generates dynamic content. Here are some examples:
+
+
+
Server Information
+
+
Current Time: = date('H:i:s') ?>
+
Today's Date: = date('l, F j, Y') ?>
+
PHP Version: = PHP_VERSION ?>
+
+
+
+
PHP files can access all the same variables and functions available throughout FolderWeb, making it easy to create dynamic, data-driven content.
+
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/04-best-practices.md b/app/default/content/articles/2025-11-02-multi-file-content/04-best-practices.md
new file mode 100644
index 0000000..ded8a89
--- /dev/null
+++ b/app/default/content/articles/2025-11-02-multi-file-content/04-best-practices.md
@@ -0,0 +1,23 @@
+## Best Practices
+
+### Use Descriptive Prefixes
+
+Number your files with two-digit prefixes (`00-`, `01-`, `02-`) to maintain clear ordering:
+
+- Allows up to 100 sections before needing three digits
+- Keeps files sorted in file managers
+- Makes reordering easy (just rename)
+
+### Choose the Right Format
+
+- **Markdown (`.md`)** - For most content. Simple, clean, readable.
+- **HTML (`.html`)** - For complex layouts or embedded media.
+- **PHP (`.php`)** - For dynamic content, calculations, or data display.
+
+### Keep It Simple
+
+Don't overcomplicate. If your page works well as a single file, keep it that way. Use multiple files when they genuinely make maintenance easier.
+
+---
+
+**This article itself demonstrates the multi-file approach.** View the source folder to see how it's structured!
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/metadata.ini b/app/default/content/articles/2025-11-02-multi-file-content/metadata.ini
new file mode 100644
index 0000000..ac44f1d
--- /dev/null
+++ b/app/default/content/articles/2025-11-02-multi-file-content/metadata.ini
@@ -0,0 +1,3 @@
+title = "Multi-File Content Pages"
+date = "2025-11-02"
+summary = "Learn how to create pages from multiple content files in any format"
diff --git a/app/default/content/frontpage.php b/app/default/content/frontpage.php
deleted file mode 100644
index a275eb3..0000000
--- a/app/default/content/frontpage.php
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
Welcome to FolderWeb
-
- A minimalist PHP framework that turns folders into websites. No JavaScript, no build tools, just simple files.
-
-
-
-
-
Getting Started
-
- This is demo content to help you understand how FolderWeb works. To replace it with your own content:
-
-
-
Create a /content folder in your project root
-
Add your content files (.md, .html, or .php)
-
This demo will automatically disappear
-
-
-
Core Concepts
-
-
File-Based Routing
-
- Drop a file in a folder and it's instantly accessible at a URL matching that path.
- Your folder structure becomes your URL structure.
-
-
-
Multiple Content Types
-
-
Markdown - Write in .md files, automatically converted to HTML
-
HTML - Pure HTML files for complete control
-
PHP - Dynamic content when you need it
-
-
-
Smart Features
-
-
Metadata - Use metadata.ini files for titles, dates, summaries
-
Date extraction - Folder names like 2025-11-01-title automatically show dates
-
Cover images - Add cover.jpg for list view thumbnails
-
Templates - Custom templates in /custom/templates/ override defaults
-
-
-
Explore the Demo
-
- Check out the Articles and About pages to see different content types in action.
-
-
-
diff --git a/app/router.php b/app/router.php
index a97b978..92b0148 100644
--- a/app/router.php
+++ b/app/router.php
@@ -49,38 +49,74 @@ $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";
+// 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;
+
+ $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
+ ];
}
-
- // Default files
- $patterns['page'][] = "page.$ext";
- $patterns['single'][] = "single.$ext";
- $patterns['single'][] = "post.$ext";
- $patterns['single'][] = "article.$ext";
}
-
- return $patterns;
+
+ // Sort by filename (alphanumerical)
+ usort($contentFiles, fn($a, $b) => strnatcmp($a['sort_key'], $b['sort_key']));
+
+ return array_column($contentFiles, 'path');
}
-$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
@@ -124,7 +160,7 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
return implode('/', $resolvedParts);
}
-function parseRequestPath(string $requestPath, string $contentDir, array $patterns, bool $hasTrailingSlash, string $lang, string $defaultLang): array {
+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, '/');
@@ -140,11 +176,20 @@ function parseRequestPath(string $requestPath, string $contentDir, array $patter
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];
+ // 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)];
}
@@ -169,10 +214,12 @@ function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?arra
return $baseMetadata ?: null;
}
-function extractTitle(string $filePath, array $patterns): ?string {
- $file = findMatchingFile($filePath, $patterns['single']) ?: findMatchingFile($filePath, $patterns['page']);
- if (!$file) return 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);
@@ -232,7 +279,7 @@ function loadTranslations(string $lang): array {
return [];
}
-function buildNavigation(string $contentDir, string $currentLang, string $defaultLang, array $pageFilePatterns): array {
+function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array {
$navItems = [];
// Scan top-level directories in content
@@ -252,30 +299,16 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul
// 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;
- }
+ $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, $pageFilePatterns) ?? ucfirst($item);
+ $title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? ucfirst($item);
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
// Use translated slug if available
@@ -297,10 +330,10 @@ function buildNavigation(string $contentDir, string $currentLang, string $defaul
}
function renderTemplate(string $content, int $statusCode = 200): void {
- global $baseTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
+ global $baseTemplate, $contentDir, $currentLang, $defaultLang;
// Build navigation for templates
- $navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
+ $navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
// Load frontpage metadata for home button label
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
@@ -315,7 +348,7 @@ function renderTemplate(string $content, int $statusCode = 200): void {
}
function renderFile(string $filePath): void {
- global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
+ global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang;
$realPath = realpath($filePath);
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) {
@@ -337,7 +370,7 @@ function renderFile(string $filePath): void {
$content = ob_get_clean();
// Build navigation for templates
- $navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
+ $navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
// Load metadata for current page/directory
$pageDir = dirname($realPath);
@@ -367,6 +400,60 @@ function renderFile(string $filePath): void {
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("
403 Forbidden
Access denied.
", 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 '' . (new Parsedown())->text(file_get_contents($filePath)) . '';
+ } 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;
+}
+
// Check for assets in /custom/assets/ served at root level
$assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath;
if (file_exists($assetPath) && is_file($assetPath)) {
@@ -377,24 +464,29 @@ if (file_exists($assetPath) && is_file($assetPath)) {
// 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);
+ // Find all content files in the root content directory
+ $contentFiles = findAllContentFiles($contentDir, $currentLang, $defaultLang);
+
+ if (!empty($contentFiles)) {
+ renderMultipleFiles($contentFiles, $contentDir);
}
}
// Parse and handle request
-$parsedPath = parseRequestPath($requestPath, $contentDir, $pageFilePatterns, $hasTrailingSlash, $currentLang, $defaultLang);
+$parsedPath = parseRequestPath($requestPath, $contentDir, $hasTrailingSlash, $currentLang, $defaultLang);
switch ($parsedPath['type']) {
+ case 'page':
+ // Page-type folder with content files (no subdirectories)
+ // Redirect to add trailing slash if needed
+ if (!empty($parsedPath['needsSlash'])) {
+ header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
+ exit;
+ }
+ renderMultipleFiles($parsedPath['files'], $parsedPath['path']);
+
case 'file':
+ // Direct file access or legacy single 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);
@@ -408,18 +500,21 @@ switch ($parsedPath['type']) {
renderFile("$dir/index.php");
}
- // Check for page content file in this directory
+ // Check for page content files in this directory
$pageContent = null;
- if ($pageFile = findMatchingFile($dir, $pageFilePatterns['page'])) {
- $ext = pathinfo($pageFile, PATHINFO_EXTENSION);
+ $contentFiles = findAllContentFiles($dir, $currentLang, $defaultLang);
+ if (!empty($contentFiles)) {
ob_start();
- if ($ext === 'md') {
- if (!class_exists('Parsedown')) {
- require_once __DIR__ . '/vendor/Parsedown.php';
+ foreach ($contentFiles as $file) {
+ $ext = pathinfo($file, PATHINFO_EXTENSION);
+ if ($ext === 'md') {
+ if (!class_exists('Parsedown')) {
+ require_once __DIR__ . '/vendor/Parsedown.php';
+ }
+ echo (new Parsedown())->text(file_get_contents($file));
+ } else {
+ include $file;
}
- echo (new Parsedown())->text(file_get_contents($pageFile));
- } else {
- include $pageFile;
}
$pageContent = ob_get_clean();
}
@@ -451,34 +546,20 @@ switch ($parsedPath['type']) {
fn($item) => !in_array($item, ['.', '..']) && is_dir("$dir/$item")
);
- $items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang, $pageFilePatterns) {
+ $items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang) {
$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;
+ $contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang);
+ if (empty($contentFiles)) 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;
+ $title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? $item;
$date = null;
if (isset($metadata['date'])) {
$date = formatNorwegianDate($metadata['date']);
@@ -509,7 +590,7 @@ switch ($parsedPath['type']) {
$content = ob_get_clean();
// Build navigation for base template
- $navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
+ $navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
include $baseTemplate;
exit;