From b507a0c676ce5d06348491e265a8aa240ff24acc Mon Sep 17 00:00:00 2001 From: Ruben Date: Sat, 1 Nov 2025 18:20:23 +0100 Subject: [PATCH] Add multi-file page support and improve documentation Update README to explain multi-file page functionality Add example content files demonstrating the feature Improve folder type detection logic Implement new routing for page-type folders Add support for mixed content types in single pages Update navigation and metadata handling for multi-file pages Remove legacy frontpage.php in favor of multi-file approach Improve file-based routing documentation Add examples of different content types working together Update router to handle multi-file content rendering Implement proper sorting of content files Add best practices for multi-file content organization --- README.md | 46 ++- app/default/content/00-welcome.php | 6 + app/default/content/00a-getting-started.md | 7 + app/default/content/01-core-concepts.md | 15 + app/default/content/02-features.html | 13 + app/default/content/03-this-page-demo.md | 11 + app/default/content/about/00-introduction.md | 15 + .../content/about/01-design-principles.html | 15 + .../content/about/02-technology-stack.php | 21 ++ .../content/about/03-what-it-is-not.md | 19 ++ app/default/content/about/04-get-started.md | 21 ++ app/default/content/about/page.md | 89 ------ .../00-introduction.md | 14 + .../01-examples.md | 24 ++ .../02-use-cases.html | 15 + .../03-dynamic-demo.php | 16 + .../04-best-practices.md | 23 ++ .../metadata.ini | 3 + app/default/content/frontpage.php | 48 --- app/router.php | 277 +++++++++++------- 20 files changed, 458 insertions(+), 240 deletions(-) create mode 100644 app/default/content/00-welcome.php create mode 100644 app/default/content/00a-getting-started.md create mode 100644 app/default/content/01-core-concepts.md create mode 100644 app/default/content/02-features.html create mode 100644 app/default/content/03-this-page-demo.md create mode 100644 app/default/content/about/00-introduction.md create mode 100644 app/default/content/about/01-design-principles.html create mode 100644 app/default/content/about/02-technology-stack.php create mode 100644 app/default/content/about/03-what-it-is-not.md create mode 100644 app/default/content/about/04-get-started.md delete mode 100644 app/default/content/about/page.md create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/00-introduction.md create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/01-examples.md create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/02-use-cases.html create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/03-dynamic-demo.php create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/04-best-practices.md create mode 100644 app/default/content/articles/2025-11-02-multi-file-content/metadata.ini delete mode 100644 app/default/content/frontpage.php 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

+ + + +

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.

+
diff --git a/app/default/content/about/02-technology-stack.php b/app/default/content/about/02-technology-stack.php new file mode 100644 index 0000000..71ab92b --- /dev/null +++ b/app/default/content/about/02-technology-stack.php @@ -0,0 +1,21 @@ +
+

Technology Stack

+ +

Backend

+ + +

Frontend

+ + + +
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.

+
diff --git a/app/default/content/articles/2025-11-02-multi-file-content/03-dynamic-demo.php b/app/default/content/articles/2025-11-02-multi-file-content/03-dynamic-demo.php new file mode 100644 index 0000000..5c49108 --- /dev/null +++ b/app/default/content/articles/2025-11-02-multi-file-content/03-dynamic-demo.php @@ -0,0 +1,16 @@ +
+

Dynamic Content Example

+ +

This section is a PHP file that generates dynamic content. Here are some examples:

+ +
+

Server Information

+
    +
  • Current Time:
  • +
  • Today's Date:
  • +
  • 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: -

-
    -
  1. Create a /content folder in your project root
  2. -
  3. Add your content files (.md, .html, or .php)
  4. -
  5. This demo will automatically disappear
  6. -
- -

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;