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
This commit is contained in:
parent
f2c18659dc
commit
b507a0c676
20 changed files with 458 additions and 240 deletions
46
README.md
46
README.md
|
|
@ -19,7 +19,7 @@ A minimal, file-based PHP framework for publishing content that will work for de
|
||||||
### Creating Content
|
### Creating Content
|
||||||
|
|
||||||
1. Create a directory for your content in the document root
|
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
|
3. Optionally add `metadata.ini` for title, date, and summary
|
||||||
4. Optionally add `cover.jpg|webp` for list view thumbnails
|
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/
|
/content/
|
||||||
|
00-welcome.php
|
||||||
|
01-introduction.md
|
||||||
about/
|
about/
|
||||||
page.md
|
00-overview.md
|
||||||
|
01-philosophy.html
|
||||||
|
02-technology.php
|
||||||
blog/
|
blog/
|
||||||
2025-11-01-hello-world/
|
2025-11-01-hello-world/
|
||||||
article.md
|
article.md
|
||||||
|
|
@ -38,6 +42,37 @@ Content is immediately accessible at the URL matching the directory path.
|
||||||
metadata.ini
|
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
|
### Customization
|
||||||
|
|
||||||
All customization lives in `custom/`:
|
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:
|
The folder hierarchy dictates URL structure:
|
||||||
|
|
||||||
- `/about/page.md` → `yoursite.com/about`
|
- Root content files → `yoursite.com/`
|
||||||
- `/blog/2025-11-01-post/article.md` → `yoursite.com/blog/2025-11-01-post`
|
- `/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
|
- 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
|
### Metadata
|
||||||
|
|
||||||
|
|
|
||||||
6
app/default/content/00-welcome.php
Normal file
6
app/default/content/00-welcome.php
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<header style="text-align: center; padding: 4rem 0 2rem;">
|
||||||
|
<h1 style="font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">Welcome to FolderWeb</h1>
|
||||||
|
<p style="font-size: clamp(1.125rem, 2vw, 1.5rem); color: oklch(0.5 0 0); max-width: 60ch; margin: 0 auto;">
|
||||||
|
A minimalist PHP framework that turns folders into websites. No JavaScript, no build tools, just simple files.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
7
app/default/content/00a-getting-started.md
Normal file
7
app/default/content/00a-getting-started.md
Normal file
|
|
@ -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
|
||||||
15
app/default/content/01-core-concepts.md
Normal file
15
app/default/content/01-core-concepts.md
Normal file
|
|
@ -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!
|
||||||
13
app/default/content/02-features.html
Normal file
13
app/default/content/02-features.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<article>
|
||||||
|
<h2>Smart Features</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Metadata</strong> - Use <code>metadata.ini</code> files for titles, dates, summaries</li>
|
||||||
|
<li><strong>Date extraction</strong> - Folder names like <code>2025-11-01-title</code> automatically show dates</li>
|
||||||
|
<li><strong>Cover images</strong> - Add <code>cover.jpg</code> for list view thumbnails</li>
|
||||||
|
<li><strong>Templates</strong> - Custom templates in <code>/custom/templates/</code> override defaults</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Explore the Demo</h2>
|
||||||
|
<p>Check out the <a href="/articles/">Articles</a> and <a href="/about/">About</a> pages to see different content types in action.</p>
|
||||||
|
</article>
|
||||||
11
app/default/content/03-this-page-demo.md
Normal file
11
app/default/content/03-this-page-demo.md
Normal file
|
|
@ -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.
|
||||||
15
app/default/content/about/00-introduction.md
Normal file
15
app/default/content/about/00-introduction.md
Normal file
|
|
@ -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
|
||||||
15
app/default/content/about/01-design-principles.html
Normal file
15
app/default/content/about/01-design-principles.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<article>
|
||||||
|
<h2>Design Principles</h2>
|
||||||
|
|
||||||
|
<h3>Minimalism</h3>
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h3>File-Based Everything</h3>
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<h3>Override, Never Modify</h3>
|
||||||
|
<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>
|
||||||
|
</article>
|
||||||
21
app/default/content/about/02-technology-stack.php
Normal file
21
app/default/content/about/02-technology-stack.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<article>
|
||||||
|
<h2>Technology Stack</h2>
|
||||||
|
|
||||||
|
<h3>Backend</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>PHP 8.3+</strong> - Modern PHP with type hints, arrow functions, match expressions</li>
|
||||||
|
<li><strong>Apache</strong> - With mod_rewrite for clean URLs</li>
|
||||||
|
<li><strong>Parsedown</strong> - Simple, reliable Markdown parser</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Frontend</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>HTML5</strong> - Semantic markup following best practices</li>
|
||||||
|
<li><strong>CSS3</strong> - Modern features like Grid, clamp(), OKLCH colors, CSS nesting</li>
|
||||||
|
<li><strong>No JavaScript</strong> - By design, for faster loads and simpler maintenance</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<aside style="background: #f5f5f5; padding: 1rem; border-left: 4px solid #333; margin: 2rem 0;">
|
||||||
|
<p><strong>Note:</strong> This section demonstrates using PHP for dynamic content. The current date is: <strong><?= date('F j, Y') ?></strong></p>
|
||||||
|
</aside>
|
||||||
|
</article>
|
||||||
19
app/default/content/about/03-what-it-is-not.md
Normal file
19
app/default/content/about/03-what-it-is-not.md
Normal file
|
|
@ -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
|
||||||
21
app/default/content/about/04-get-started.md
Normal file
21
app/default/content/about/04-get-started.md
Normal file
|
|
@ -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!
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<article>
|
||||||
|
<h2>Use Cases</h2>
|
||||||
|
|
||||||
|
<h3>Long Documentation</h3>
|
||||||
|
<p>Break lengthy documentation into logical sections. Each section gets its own file, making editing and maintenance easier.</p>
|
||||||
|
|
||||||
|
<h3>Mixed Content Types</h3>
|
||||||
|
<p>Use Markdown for simple text, HTML for complex layouts, and PHP for dynamic content—all on the same page.</p>
|
||||||
|
|
||||||
|
<h3>Collaborative Editing</h3>
|
||||||
|
<p>Multiple authors can work on different sections simultaneously without merge conflicts.</p>
|
||||||
|
|
||||||
|
<h3>Progressive Enhancement</h3>
|
||||||
|
<p>Start with simple Markdown files. Later, enhance specific sections with HTML or PHP without restructuring.</p>
|
||||||
|
</article>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<article>
|
||||||
|
<h2>Dynamic Content Example</h2>
|
||||||
|
|
||||||
|
<p>This section is a PHP file that generates dynamic content. Here are some examples:</p>
|
||||||
|
|
||||||
|
<div style="background: #f0f9ff; border: 1px solid #0284c7; padding: 1.5rem; border-radius: 8px; margin: 1.5rem 0;">
|
||||||
|
<h3>Server Information</h3>
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
<li><strong>Current Time:</strong> <?= date('H:i:s') ?></li>
|
||||||
|
<li><strong>Today's Date:</strong> <?= date('l, F j, Y') ?></li>
|
||||||
|
<li><strong>PHP Version:</strong> <?= PHP_VERSION ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>PHP files can access all the same variables and functions available throughout FolderWeb, making it easy to create dynamic, data-driven content.</p>
|
||||||
|
</article>
|
||||||
|
|
@ -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!
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<article>
|
|
||||||
<header style="text-align: center; padding: 4rem 0 2rem;">
|
|
||||||
<h1 style="font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">Welcome to FolderWeb</h1>
|
|
||||||
<p style="font-size: clamp(1.125rem, 2vw, 1.5rem); color: oklch(0.5 0 0); max-width: 60ch; margin: 0 auto;">
|
|
||||||
A minimalist PHP framework that turns folders into websites. No JavaScript, no build tools, just simple files.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section style="max-width: 70ch; margin: 3rem auto;">
|
|
||||||
<h2>Getting Started</h2>
|
|
||||||
<p>
|
|
||||||
This is demo content to help you understand how FolderWeb works. To replace it with your own content:
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>Create a <code>/content</code> folder in your project root</li>
|
|
||||||
<li>Add your content files (<code>.md</code>, <code>.html</code>, or <code>.php</code>)</li>
|
|
||||||
<li>This demo will automatically disappear</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>Core Concepts</h2>
|
|
||||||
|
|
||||||
<h3>File-Based Routing</h3>
|
|
||||||
<p>
|
|
||||||
Drop a file in a folder and it's instantly accessible at a URL matching that path.
|
|
||||||
Your folder structure becomes your URL structure.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Multiple Content Types</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Markdown</strong> - Write in <code>.md</code> files, automatically converted to HTML</li>
|
|
||||||
<li><strong>HTML</strong> - Pure HTML files for complete control</li>
|
|
||||||
<li><strong>PHP</strong> - Dynamic content when you need it</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Smart Features</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Metadata</strong> - Use <code>metadata.ini</code> files for titles, dates, summaries</li>
|
|
||||||
<li><strong>Date extraction</strong> - Folder names like <code>2025-11-01-title</code> automatically show dates</li>
|
|
||||||
<li><strong>Cover images</strong> - Add <code>cover.jpg</code> for list view thumbnails</li>
|
|
||||||
<li><strong>Templates</strong> - Custom templates in <code>/custom/templates/</code> override defaults</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Explore the Demo</h2>
|
|
||||||
<p>
|
|
||||||
Check out the <a href="/articles/">Articles</a> and <a href="/about/">About</a> pages to see different content types in action.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
277
app/router.php
277
app/router.php
|
|
@ -49,38 +49,74 @@ $customListTemplate = dirname(__DIR__) . '/custom/templates/list.php';
|
||||||
$defaultListTemplate = __DIR__ . '/default/templates/list.php';
|
$defaultListTemplate = __DIR__ . '/default/templates/list.php';
|
||||||
$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate;
|
$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate;
|
||||||
|
|
||||||
// Build file patterns with language variants
|
// Find all content files in a directory (supporting language variants)
|
||||||
function buildFilePatterns(string $lang, string $defaultLang): array {
|
function findAllContentFiles(string $dir, string $lang, string $defaultLang): array {
|
||||||
$extensions = ['php', 'html', 'md'];
|
if (!is_dir($dir)) return [];
|
||||||
$patterns = ['page' => [], 'single' => []];
|
|
||||||
|
$files = scandir($dir) ?: [];
|
||||||
foreach ($extensions as $ext) {
|
$contentFiles = [];
|
||||||
// Language-specific files first (if not default language)
|
$extensions = ['md', 'html', 'php'];
|
||||||
if ($lang !== $defaultLang) {
|
|
||||||
$patterns['page'][] = "page.$lang.$ext";
|
foreach ($files as $file) {
|
||||||
$patterns['single'][] = "single.$lang.$ext";
|
if ($file === '.' || $file === '..') continue;
|
||||||
$patterns['single'][] = "post.$lang.$ext";
|
|
||||||
$patterns['single'][] = "article.$lang.$ext";
|
$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 {
|
function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string {
|
||||||
// If default language, no translation needed
|
// If default language, no translation needed
|
||||||
|
|
@ -124,7 +160,7 @@ function resolveTranslatedPath(string $requestPath, string $contentDir, string $
|
||||||
return implode('/', $resolvedParts);
|
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
|
// Resolve translated slugs to actual directory names
|
||||||
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang);
|
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang);
|
||||||
$contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/');
|
$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")
|
fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentPath/$item")
|
||||||
));
|
));
|
||||||
|
|
||||||
// If directory has subdirectories, treat as directory (for list views)
|
// If directory has subdirectories, it's an article-type folder (list view)
|
||||||
// Otherwise, if it has a content file, treat as file
|
if ($hasSubdirs) {
|
||||||
if (!$hasSubdirs && ($file = findMatchingFile($contentPath, $patterns['single']) ?: findMatchingFile($contentPath, $patterns['page']))) {
|
return ['type' => 'directory', 'path' => realpath($contentPath)];
|
||||||
return ['type' => 'file', 'path' => $file, 'needsSlash' => !$hasTrailingSlash];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)];
|
return ['type' => 'directory', 'path' => realpath($contentPath)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,10 +214,12 @@ function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?arra
|
||||||
return $baseMetadata ?: null;
|
return $baseMetadata ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTitle(string $filePath, array $patterns): ?string {
|
function extractTitle(string $filePath, string $lang, string $defaultLang): ?string {
|
||||||
$file = findMatchingFile($filePath, $patterns['single']) ?: findMatchingFile($filePath, $patterns['page']);
|
$files = findAllContentFiles($filePath, $lang, $defaultLang);
|
||||||
if (!$file) return null;
|
if (empty($files)) return null;
|
||||||
|
|
||||||
|
// Check the first content file for a title
|
||||||
|
$file = $files[0];
|
||||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||||
$content = file_get_contents($file);
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
|
@ -232,7 +279,7 @@ function loadTranslations(string $lang): array {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang, array $pageFilePatterns): array {
|
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang): array {
|
||||||
$navItems = [];
|
$navItems = [];
|
||||||
|
|
||||||
// Scan top-level directories in content
|
// 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
|
// Check if content exists for current language
|
||||||
if ($currentLang !== $defaultLang) {
|
if ($currentLang !== $defaultLang) {
|
||||||
$extensions = ['php', 'html', 'md'];
|
$contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang);
|
||||||
$hasContent = false;
|
|
||||||
|
// If no content files, check if metadata has title for this language
|
||||||
// Check for language-specific content files
|
$hasContent = !empty($contentFiles) || ($metadata && isset($metadata['title']));
|
||||||
foreach ($extensions as $ext) {
|
|
||||||
if (file_exists("$itemPath/single.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/post.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/article.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/page.$currentLang.$ext")) {
|
|
||||||
$hasContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no language-specific files, check if metadata has title for this language
|
|
||||||
if (!$hasContent && $metadata && isset($metadata['title'])) {
|
|
||||||
$hasContent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$hasContent) continue;
|
if (!$hasContent) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract title and build URL
|
// 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" : '';
|
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
|
||||||
|
|
||||||
// Use translated slug if available
|
// 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 {
|
function renderTemplate(string $content, int $statusCode = 200): void {
|
||||||
global $baseTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
|
global $baseTemplate, $contentDir, $currentLang, $defaultLang;
|
||||||
|
|
||||||
// Build navigation for templates
|
// Build navigation for templates
|
||||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
|
||||||
|
|
||||||
// Load frontpage metadata for home button label
|
// Load frontpage metadata for home button label
|
||||||
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
|
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
|
||||||
|
|
@ -315,7 +348,7 @@ function renderTemplate(string $content, int $statusCode = 200): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFile(string $filePath): void {
|
function renderFile(string $filePath): void {
|
||||||
global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
|
global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang;
|
||||||
|
|
||||||
$realPath = realpath($filePath);
|
$realPath = realpath($filePath);
|
||||||
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) {
|
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) {
|
||||||
|
|
@ -337,7 +370,7 @@ function renderFile(string $filePath): void {
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
|
|
||||||
// Build navigation for templates
|
// Build navigation for templates
|
||||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
|
||||||
|
|
||||||
// Load metadata for current page/directory
|
// Load metadata for current page/directory
|
||||||
$pageDir = dirname($realPath);
|
$pageDir = dirname($realPath);
|
||||||
|
|
@ -367,6 +400,60 @@ function renderFile(string $filePath): void {
|
||||||
exit;
|
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("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 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 '<article>' . (new Parsedown())->text(file_get_contents($filePath)) . '</article>';
|
||||||
|
} 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
|
// Check for assets in /custom/assets/ served at root level
|
||||||
$assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath;
|
$assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath;
|
||||||
if (file_exists($assetPath) && is_file($assetPath)) {
|
if (file_exists($assetPath) && is_file($assetPath)) {
|
||||||
|
|
@ -377,24 +464,29 @@ if (file_exists($assetPath) && is_file($assetPath)) {
|
||||||
|
|
||||||
// Handle frontpage
|
// Handle frontpage
|
||||||
if (empty($requestPath)) {
|
if (empty($requestPath)) {
|
||||||
// Try language-specific frontpage first, then default
|
// Find all content files in the root content directory
|
||||||
$frontPage = null;
|
$contentFiles = findAllContentFiles($contentDir, $currentLang, $defaultLang);
|
||||||
if ($currentLang !== $defaultLang && file_exists("$contentDir/frontpage.$currentLang.php")) {
|
|
||||||
$frontPage = "$contentDir/frontpage.$currentLang.php";
|
if (!empty($contentFiles)) {
|
||||||
} elseif (file_exists("$contentDir/frontpage.php")) {
|
renderMultipleFiles($contentFiles, $contentDir);
|
||||||
$frontPage = "$contentDir/frontpage.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($frontPage) {
|
|
||||||
renderFile($frontPage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and handle request
|
// Parse and handle request
|
||||||
$parsedPath = parseRequestPath($requestPath, $contentDir, $pageFilePatterns, $hasTrailingSlash, $currentLang, $defaultLang);
|
$parsedPath = parseRequestPath($requestPath, $contentDir, $hasTrailingSlash, $currentLang, $defaultLang);
|
||||||
|
|
||||||
switch ($parsedPath['type']) {
|
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':
|
case 'file':
|
||||||
|
// Direct file access or legacy single file
|
||||||
// Redirect to add trailing slash if this is a directory-based page
|
// Redirect to add trailing slash if this is a directory-based page
|
||||||
if (!empty($parsedPath['needsSlash'])) {
|
if (!empty($parsedPath['needsSlash'])) {
|
||||||
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
|
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
|
||||||
|
|
@ -408,18 +500,21 @@ switch ($parsedPath['type']) {
|
||||||
renderFile("$dir/index.php");
|
renderFile("$dir/index.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for page content file in this directory
|
// Check for page content files in this directory
|
||||||
$pageContent = null;
|
$pageContent = null;
|
||||||
if ($pageFile = findMatchingFile($dir, $pageFilePatterns['page'])) {
|
$contentFiles = findAllContentFiles($dir, $currentLang, $defaultLang);
|
||||||
$ext = pathinfo($pageFile, PATHINFO_EXTENSION);
|
if (!empty($contentFiles)) {
|
||||||
ob_start();
|
ob_start();
|
||||||
if ($ext === 'md') {
|
foreach ($contentFiles as $file) {
|
||||||
if (!class_exists('Parsedown')) {
|
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||||
require_once __DIR__ . '/vendor/Parsedown.php';
|
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();
|
$pageContent = ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
@ -451,34 +546,20 @@ switch ($parsedPath['type']) {
|
||||||
fn($item) => !in_array($item, ['.', '..']) && is_dir("$dir/$item")
|
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";
|
$itemPath = "$dir/$item";
|
||||||
|
|
||||||
// Check if content exists for current language
|
// Check if content exists for current language
|
||||||
if ($currentLang !== $defaultLang) {
|
if ($currentLang !== $defaultLang) {
|
||||||
// For non-default languages, only check language-specific files
|
$contentFiles = findAllContentFiles($itemPath, $currentLang, $defaultLang);
|
||||||
$extensions = ['php', 'html', 'md'];
|
if (empty($contentFiles)) return null;
|
||||||
$hasContent = false;
|
|
||||||
foreach ($extensions as $ext) {
|
|
||||||
if (file_exists("$itemPath/single.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/post.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/article.$currentLang.$ext") ||
|
|
||||||
file_exists("$itemPath/page.$currentLang.$ext")) {
|
|
||||||
$hasContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$hasContent) return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build patterns for rendering (includes fallbacks)
|
|
||||||
$patterns = buildFilePatterns($currentLang, $defaultLang);
|
|
||||||
|
|
||||||
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
|
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
|
||||||
$coverImage = findCoverImage($itemPath);
|
$coverImage = findCoverImage($itemPath);
|
||||||
$pdfFile = findPdfFile($itemPath);
|
$pdfFile = findPdfFile($itemPath);
|
||||||
|
|
||||||
$title = $metadata['title'] ?? extractTitle($itemPath, $patterns) ?? $item;
|
$title = $metadata['title'] ?? extractTitle($itemPath, $currentLang, $defaultLang) ?? $item;
|
||||||
$date = null;
|
$date = null;
|
||||||
if (isset($metadata['date'])) {
|
if (isset($metadata['date'])) {
|
||||||
$date = formatNorwegianDate($metadata['date']);
|
$date = formatNorwegianDate($metadata['date']);
|
||||||
|
|
@ -509,7 +590,7 @@ switch ($parsedPath['type']) {
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
|
|
||||||
// Build navigation for base template
|
// Build navigation for base template
|
||||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang);
|
||||||
|
|
||||||
include $baseTemplate;
|
include $baseTemplate;
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue