Compare commits

...

3 commits

Author SHA1 Message Date
Ruben
0e19040473 Create a simple hero section with title, subtitle, and call-to-action
button Add a features section with icons and descriptions Include a
stats section with live data Add a responsive design with modern CSS
features
2025-11-27 21:31:47 +01:00
Ruben
f0d50ff8bf Add fallback config loading with custom overrides 2025-11-27 21:29:57 +01:00
Ruben
ca91a18cb7 Add default styles and enable language plugin
Add stylesheet and enable language plugin with Norwegian support
2025-11-27 21:29:35 +01:00
108 changed files with 1591 additions and 7544 deletions

View file

@ -1,3 +0,0 @@
[languages]
default = "no"
available = "no,en"

View file

@ -1,11 +1,17 @@
<?php <?php
function createContext(): Context { function createContext(): Context {
// Load configuration // Load configuration with fallback: custom -> default
$configFile = file_exists(__DIR__ . '/../custom/config.ini') $defaultConfig = __DIR__ . '/default/config.ini';
? __DIR__ . '/../custom/config.ini' $customConfig = __DIR__ . '/../custom/config.ini';
: __DIR__ . '/config.ini';
$config = parse_ini_file($configFile, true); // Start with default config
$config = file_exists($defaultConfig) ? parse_ini_file($defaultConfig, true) : [];
// Merge with custom config if it exists
if (file_exists($customConfig)) {
$config = array_replace_recursive($config, parse_ini_file($customConfig, true) ?: []);
}
// Load global plugins // Load global plugins
getPluginManager()->loadGlobalPlugins($config); getPluginManager()->loadGlobalPlugins($config);

View file

@ -1,3 +1,6 @@
[languages] [languages]
default = "en" default = "en"
available = "en" available = "no,en"
[plugins]
enabled = "languages"

View file

@ -0,0 +1,50 @@
<?php
// Dynamisk hero-seksjon som viser PHP-muligheter
$features = [
['icon' => '📁', 'title' => 'Filbasert', 'description' => 'Mapper blir URL-er automatisk', 'url' => '/examples/file-based-routing/'],
['icon' => '⚡', 'title' => 'Uten byggesteg', 'description' => 'Rediger og oppdater—det er alt', 'url' => '/examples/no-build-step/'],
['icon' => '🎨', 'title' => 'Bland formater', 'description' => 'Kombiner .md, .html og .php', 'url' => '/examples/mix-formats/'],
['icon' => '🌍', 'title' => 'Flerspråklig', 'description' => 'Innebygd i18n-støtte', 'url' => '/multilingual/'],
['icon' => '📝', 'title' => 'Markdown', 'description' => 'Skriv innhold i markdown', 'url' => '/examples/2024-12-15-markdown-demo/'],
['icon' => '🎭', 'title' => 'Maler', 'description' => 'Egendefinerte layout og stiler', 'url' => '/examples/templates-demo/'],
];
$stats = [
'files' => count(glob(__DIR__ . '/**/*', GLOB_BRACE)) ?: 0,
'generated' => number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) . 'ms',
'php_version' => PHP_VERSION,
];
?>
<section class="hero">
<h1 class="hero-title">FolderWeb</h1>
<p class="hero-subtitle">
Slipp filer i mapper. De rendres umiddelbart.
Ingen konfigurasjon, ingen byggesteg, ingen kompleksitet.
</p>
<div class="features">
<?php foreach ($features as $feature): ?>
<a href="<?= htmlspecialchars($feature['url']) ?>" class="feature-card">
<span class="feature-icon"><?= $feature['icon'] ?></span>
<h3 class="feature-title"><?= htmlspecialchars($feature['title']) ?></h3>
<p class="feature-description"><?= htmlspecialchars($feature['description']) ?></p>
</a>
<?php endforeach; ?>
</div>
<div class="stats">
<div class="stat">
<span class="stat-value"><?= $stats['files'] ?></span>
<span class="stat-label">Filer i innhold</span>
</div>
<div class="stat">
<span class="stat-value"><?= $stats['generated'] ?></span>
<span class="stat-label">Side generert</span>
</div>
<div class="stat">
<span class="stat-value">PHP <?= $stats['php_version'] ?></span>
<span class="stat-label">Runtime</span>
</div>
</div>
</section>

View file

@ -0,0 +1,50 @@
<?php
// Dynamic hero section showcasing PHP capabilities
$features = [
['icon' => '📁', 'title' => 'File-Based', 'description' => 'Folders become URLs automatically', 'url' => '/examples/file-based-routing/'],
['icon' => '⚡', 'title' => 'No Build Step', 'description' => 'Edit and refresh—that\'s it', 'url' => '/examples/no-build-step/'],
['icon' => '🎨', 'title' => 'Mix Formats', 'description' => 'Combine .md, .html, and .php', 'url' => '/examples/mix-formats/'],
['icon' => '🌍', 'title' => 'Multilingual', 'description' => 'Built-in i18n support', 'url' => '/multilingual/'],
['icon' => '📝', 'title' => 'Markdown', 'description' => 'Write content in markdown', 'url' => '/examples/2024-12-15-markdown-demo/'],
['icon' => '🎭', 'title' => 'Templates', 'description' => 'Custom layouts and styles', 'url' => '/examples/templates-demo/'],
];
$stats = [
'files' => count(glob(__DIR__ . '/**/*', GLOB_BRACE)) ?: 0,
'generated' => number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) . 'ms',
'php_version' => PHP_VERSION,
];
?>
<section class="hero">
<h1 class="hero-title">FolderWeb</h1>
<p class="hero-subtitle">
Drop files in folders. They render immediately.
No configuration, no build step, no complexity.
</p>
<div class="features">
<?php foreach ($features as $feature): ?>
<a href="<?= htmlspecialchars($feature['url']) ?>" class="feature-card">
<span class="feature-icon"><?= $feature['icon'] ?></span>
<h3 class="feature-title"><?= htmlspecialchars($feature['title']) ?></h3>
<p class="feature-description"><?= htmlspecialchars($feature['description']) ?></p>
</a>
<?php endforeach; ?>
</div>
<div class="stats">
<div class="stat">
<span class="stat-value"><?= $stats['files'] ?></span>
<span class="stat-label">Files in content</span>
</div>
<div class="stat">
<span class="stat-value"><?= $stats['generated'] ?></span>
<span class="stat-label">Page generated</span>
</div>
<div class="stat">
<span class="stat-value">PHP <?= $stats['php_version'] ?></span>
<span class="stat-label">Runtime</span>
</div>
</div>
</section>

View file

@ -1,6 +0,0 @@
<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>

View file

@ -1,7 +0,0 @@
## 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

View file

@ -1,15 +0,0 @@
## 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!

View file

@ -0,0 +1,45 @@
<!-- The hero.php file renders above this markdown content -->
<!-- Files are combined in alphabetical order: hero.php, then index.md -->
## Quick Start
1. **Empty the content folder** Delete everything in `/content/` to start fresh
2. **Copy the default folder** Copy `/app/default/` to `/custom/` (one level up from app/) to create your own theme
3. **Add your content** Drop `.md`, `.html`, or `.php` files in `/content/` and they're live
4. **Customize templates** Edit files in `/custom/templates/` to change the look
5. **Style it** Add CSS in `/custom/styles/styles.css` for global styles
## What Makes FolderWeb Different
**File-based routing** Your folder structure is your URL structure. No configuration needed.
**No JavaScript** Fast, simple, accessible websites that work everywhere.
**Modern PHP & CSS** Clean code using the latest language features and CSS capabilities.
**No build step** Edit a file, refresh the page. That's it.
**Decade-proof** Minimal dependencies mean this will work for years without breaking.
## How This Page Works
This frontpage demonstrates how you can **mix different file types** to create dynamic, custom layouts:
- **`hero.php`** Dynamic PHP component with live stats and styling
- **`index.md`** Static markdown content (this section!)
Files in the same directory are rendered alphabetically. Want a different order? Prefix filenames with numbers like `00-hero.php` and `01-content.md`.
## Features Demonstrated Here {#features}
Explore this demo site to see what FolderWeb can do:
- [Examples](/examples/) Markdown, metadata, dates, and cover images
- [Multilingual](/multilingual/) Build sites in multiple languages
- [Nested Content](/examples/nested/) Organize content as deep as you need
## Philosophy
**Just enough, nothing more.** This framework uses minimal PHP to enable modern conveniences while remaining maintainable for years or decades.
Read more in `CLAUDE.md` to understand the principles behind FolderWeb.

View file

@ -0,0 +1,45 @@
<!-- hero.no.php-filen rendres over dette markdown-innholdet -->
<!-- Filer kombineres i alfabetisk rekkefølge: hero.no.php, deretter index.no.md -->
## Hurtigstart
1. **Tøm innholdsmappen** Slett alt i `/content/` for å starte fra bunnen av
2. **Kopier standardmappen** Kopier `/app/default/` til `/custom/` (ett nivå opp fra app/) for å lage ditt eget tema
3. **Legg til innhold** Slipp `.md`, `.html`, eller `.php`-filer i `/content/` og de er live
4. **Tilpass maler** Rediger filer i `/custom/templates/` for å endre utseendet
5. **Stil det** Legg til CSS i `/custom/styles/styles.css` for globale stiler
## Hva gjør FolderWeb annerledes
**Filbasert routing** Mappestrukturen din er URL-strukturen din. Ingen konfigurasjon nødvendig.
**Ingen JavaScript** Raske, enkle, tilgjengelige nettsteder som fungerer overalt.
**Moderne PHP & CSS** Ren kode som bruker de nyeste språkfunksjonene og CSS-mulighetene.
**Uten byggesteg** Rediger en fil, oppdater siden. Det er det.
**Tiår-sikker** Minimale avhengigheter betyr at dette vil fungere i årevis uten å gå i stykker.
## Hvordan denne siden fungerer
Denne forsiden demonstrerer hvordan du kan **blande forskjellige filtyper** for å lage dynamiske, egendefinerte layout:
- **`hero.no.php`** Dynamisk PHP-komponent med live statistikk og styling
- **`index.no.md`** Statisk markdown-innhold (denne seksjonen!)
Filer i samme katalog rendres alfabetisk. Vil du ha en annen rekkefølge? Prefikser filnavn med tall som `00-hero.php` og `01-innhold.md`.
## Funksjoner demonstrert her {#funksjoner}
Utforsk denne demosiden for å se hva FolderWeb kan gjøre:
- [Eksempler](/examples/) Markdown, metadata, datoer og forsidebilder
- [Flerspråklig](/multilingual/) Bygg nettsteder på flere språk
- [Nestet innhold](/examples/nested/) Organiser innhold så dypt du trenger
## Filosofi
**Akkurat nok, ikke mer.** Dette rammeverket bruker minimal PHP for å muliggjøre moderne bekvemmeligheter samtidig som det forblir vedlikeholdbart i årevis eller tiår.
Les mer i `CLAUDE.md` for å forstå prinsippene bak FolderWeb.

View file

@ -1,13 +0,0 @@
<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>

View file

@ -1,11 +0,0 @@
## 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.

View file

@ -1,15 +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

View file

@ -1,15 +0,0 @@
<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.4+ features (property hooks, readonly classes, modern array functions) and modern CSS capabilities. Avoid JavaScript entirely—it's not needed for content-focused sites.</p>
</article>

View file

@ -1,21 +0,0 @@
<article>
<h2>Technology Stack</h2>
<h3>Backend</h3>
<ul>
<li><strong>PHP 8.4+</strong> - Modern PHP with property hooks, readonly classes, array_find(), and type safety</li>
<li><strong>Apache</strong> - With mod_rewrite for clean URLs</li>
<li><strong>Parsedown</strong> - Simple, reliable Markdown parser</li>
</ul>
<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>

View file

@ -1,19 +0,0 @@
## 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

View file

@ -1,21 +0,0 @@
## 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!

View file

@ -1,3 +0,0 @@
title = "About FolderWeb"
menu = true
menu_order = 2

View file

@ -1,19 +0,0 @@
/* Page-specific styles for About page */
article {
border-left: 4px solid oklch(0.65 0.15 250);
padding-left: 1.5rem;
}
article h2 {
color: oklch(0.50 0.12 250);
font-size: 1.8rem;
}
aside {
background-color: oklch(0.95 0.02 250);
padding: 1.5rem;
border-radius: 0.5rem;
margin-top: 2rem;
border: 2px solid oklch(0.85 0.05 250);
}

View file

@ -1,206 +0,0 @@
# Markdown Guide
Markdown is a lightweight markup language that's easy to write and read. FolderWeb uses [Parsedown](https://parsedown.org/) to convert your Markdown files into beautiful HTML.
## Headings
Use `#` symbols for headings:
```markdown
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```
## Emphasis
Make text **bold** or *italic*:
```markdown
*italic text* or _italic text_
**bold text** or __bold text__
***bold and italic*** or ___bold and italic___
```
## Lists
### Unordered Lists
```markdown
- Item one
- Item two
- Item three
- Nested item
- Another nested item
```
### Ordered Lists
```markdown
1. First item
2. Second item
3. Third item
1. Nested item
2. Another nested item
```
## Links
```markdown
[Link text](https://example.com)
[Link with title](https://example.com "Title text")
```
Example: [Visit FolderWeb](#)
## Images
```markdown
![Alt text](image.jpg)
![Alt text with title](image.jpg "Image title")
```
## Code
### Inline Code
Use backticks for `inline code`:
```markdown
Use the `$variable` in your code
```
### Code Blocks
Use triple backticks for code blocks:
````markdown
```php
<?php
echo "Hello, World!";
?>
```
````
Renders as:
```php
<?php
echo "Hello, World!";
?>
```
## Blockquotes
```markdown
> This is a blockquote.
> It can span multiple lines.
>
> And multiple paragraphs.
```
Result:
> This is a blockquote.
> It can span multiple lines.
## Horizontal Rules
Create a horizontal rule with three or more hyphens, asterisks, or underscores:
```markdown
---
***
___
```
---
## Tables
```markdown
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
```
Result:
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
## Best Practices
### Use Semantic Structure
Start with `# H1` for your page title, then use `## H2`, `### H3`, etc. for sections.
### Write Readable Markdown
```markdown
# Good Example
This paragraph is easy to read with proper spacing.
## Section Heading
- List items are clear
- Each on its own line
---
# Bad Example
No spacing makes it hard to read.
##SectionWithoutSpace
-ListItemsSmashed-Together
```
### Links in FolderWeb
Internal links work best with absolute paths:
```markdown
[About page](/about/)
[Articles](/articles/)
[Specific article](/articles/2025-10-15-markdown-guide/)
```
## Advanced Features
### HTML in Markdown
You can use HTML directly in Markdown when needed:
```markdown
<div class="custom-class">
Custom HTML content
</div>
```
### Escaping Characters
Use backslash to escape Markdown characters:
```markdown
\*This text is not italic\*
\[This is not a link\]
```
## Tips for FolderWeb
1. **Use descriptive filenames** - `article.md` is better than `content.md`
2. **Add metadata** - Use `metadata.ini` for titles, dates, and summaries
3. **Include images** - Place images in the same directory as your content
4. **Add cover images** - Use `cover.jpg` for list view thumbnails
## Further Reading
- [Markdown Guide](https://www.markdownguide.org/) - Comprehensive Markdown reference
- [Parsedown Documentation](https://parsedown.org/) - The parser FolderWeb uses
- [CommonMark Spec](https://commonmark.org/) - Markdown specification

View file

@ -1,3 +0,0 @@
title = "Markdown Guide"
date = "2025-10-15"
summary = "Master Markdown syntax for beautiful, semantic content in FolderWeb."

View file

@ -1,165 +0,0 @@
# Templates and Customization
FolderWeb is built on a simple principle: **never modify defaults, always override**. This guide shows you how to customize your site while keeping it maintainable.
## The Override System
FolderWeb checks for custom files before falling back to defaults:
1. Check `/custom/templates/` → Use custom template
2. Fall back to `/app/default/templates/` → Use default template
This means you can override any part of the system without touching the core files.
## Available Templates
### Base Template
The main HTML structure with header, navigation, and footer.
**Override**: `/custom/templates/base.php`
### Page Template
Wraps single pages and articles.
**Override**: `/custom/templates/page.php`
### List Templates
FolderWeb includes multiple list view variants:
- `list.php` - Simple list
- `list-grid.php` - Grid layout
- `list-card-grid.php` - Card grid with images
- `list-faq.php` - Expandable FAQ format
**Select via metadata**:
```ini
page_template = "list-card-grid"
```
## Customizing Styles
### Add Your Own CSS
Create `/custom/styles/base.css` and it automatically overrides default styles.
Example custom stylesheet:
```css
:root {
--color-primary: oklch(0.5 0.2 270);
--color-background: oklch(0.98 0 0);
--font-body: 'Georgia', serif;
}
article h1 {
color: var(--color-primary);
font-size: clamp(2rem, 5vw, 3rem);
}
```
### Modern CSS Features
FolderWeb's default styles use modern CSS:
- **CSS Nesting** - Scope styles naturally
- **OKLCH Colors** - Perceptually uniform colors
- **CSS Grid** - Flexible layouts
- **Clamp()** - Responsive sizing
- **Logical Properties** - Better internationalization
## Custom Fonts
1. Place font files in `/custom/fonts/`
2. Reference them in your custom CSS:
```css
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
}
body {
font-family: 'MyFont', sans-serif;
}
```
## Metadata Options
Control content behavior with `metadata.ini` files:
```ini
; Basic fields
title = "Page Title"
date = "2025-10-28"
summary = "Short description"
; Navigation
menu = true
menu_order = 1
; Templates
page_template = "list-card-grid"
; Redirects
redirect = "https://example.com"
; Attachments
pdf = "document.pdf"
```
## Template Variables
Templates have access to specific variables:
### Base Template
- `$content` - Page content
- `$currentLang` - Current language code
- `$navigation` - Navigation items array
- `$pageTitle` - Page title
### Page Template
- `$content` - Article content
- `$pageMetadata` - Metadata array
- `$translations` - Translation strings
### List Templates
- `$items` - Array of subitems
- `$metadata` - Directory metadata
- `$pageContent` - Intro text
- `$translations` - Translation strings
## Creating a Custom Template
Example custom page template:
```php
<?php
// /custom/templates/page.php
?>
<article class="custom-layout">
<?php if (isset($pageMetadata['date'])): ?>
<time><?= htmlspecialchars($pageMetadata['date']) ?></time>
<?php endif; ?>
<?= $content ?>
<footer>
<p>Custom footer content</p>
</footer>
</article>
```
## Best Practices
1. **Never modify `/app/default/`** - Always create overrides in `/custom/`
2. **Use metadata** - Keep configuration in `metadata.ini` files
3. **Leverage CSS variables** - Easy theming without rewriting styles
4. **Keep it simple** - The less custom code, the easier to maintain
## Next Steps
- Explore default templates in `/app/default/templates/`
- Study the default CSS in `/app/default/styles/base.css`
- Check out the [Markdown Guide](/articles/2025-10-15-markdown-guide/) for content formatting

View file

@ -1,3 +0,0 @@
title = "Templates and Customization"
date = "2025-10-28"
summary = "Customize your FolderWeb site with templates, styles, and metadata options."

View file

@ -1,79 +0,0 @@
# Getting Started with FolderWeb
FolderWeb is designed to be the simplest way to publish content on the web. This guide will walk you through the core concepts and get you publishing in minutes.
## Installation
FolderWeb requires PHP 8.4+ and Apache with `mod_rewrite` enabled.
### Using Docker (Recommended for Development)
```bash
cd development
docker compose up
```
Visit `http://localhost:8080` to see your site.
### Manual Installation
1. Point Apache's document root to the `/content` directory
2. Ensure the `/app` directory is accessible at the same level
3. Enable `mod_rewrite` in Apache
4. That's it!
## Creating Your First Page
The easiest way to understand FolderWeb is to create some content.
### Create a Simple Page
1. Create a new directory: `/content/hello/`
2. Add a file: `/content/hello/page.md`
3. Write some Markdown:
```markdown
# Hello World
This is my first page in FolderWeb!
```
Your page is now live at `/hello/`
### Create an Article with Metadata
For richer content, add metadata:
1. Create: `/content/articles/2025-11-01-my-article/`
2. Add metadata: `/content/articles/2025-11-01-my-article/metadata.ini`
```ini
title = "My First Article"
date = "2025-11-01"
summary = "A brief description of my article"
```
3. Add content: `/content/articles/2025-11-01-my-article/article.md`
The date in the folder name is automatically extracted and displayed.
## File Types
FolderWeb supports three content types:
- **Markdown (`.md`)** - Write in Markdown, rendered as HTML
- **HTML (`.html`)** - Pure HTML for complete control
- **PHP (`.php`)** - Dynamic content when needed
## File Naming Conventions
- **Page content**: `page.md`, `page.html`, `page.php`
- **Articles/posts**: `article.md`, `post.md`, `single.md` (and `.html`/`.php` variants)
- **Frontpage**: `/content/frontpage.php`
- **Index override**: `index.php` in any directory takes precedence
## Next Steps
- Read the [Templates Guide](/articles/2025-10-28-templates-and-customization/) to customize your site
- Learn [Markdown syntax](/articles/2025-10-15-markdown-guide/) for better content
- Explore the default templates in `/app/default/templates/`

View file

@ -1,3 +0,0 @@
title = "Getting Started with FolderWeb"
date = "2025-11-01"
summary = "Learn the basics of FolderWeb and create your first content in minutes."

View file

@ -1,14 +0,0 @@
# 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

View file

@ -1,24 +0,0 @@
## 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

View file

@ -1,15 +0,0 @@
<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>

View file

@ -1,16 +0,0 @@
<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>

View file

@ -1,23 +0,0 @@
## 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!

View file

@ -1,3 +0,0 @@
title = "Multi-File Content Pages"
date = "2025-11-02"
summary = "Learn how to create pages from multiple content files in any format"

View file

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

View file

@ -1,3 +0,0 @@
# Articles
A collection of guides and tutorials to help you get the most out of FolderWeb.

View file

@ -1,22 +0,0 @@
/* Page-specific styles for Articles list */
.list-item {
border: 2px solid oklch(0.85 0.05 250);
transition: border-color 0.3s ease;
}
.list-item:hover {
border-color: oklch(0.65 0.15 250);
}
.list-item h2 a {
color: oklch(0.50 0.12 250);
}
.list-item .date {
background-color: oklch(0.95 0.02 250);
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.85rem;
display: inline-block;
}

View file

@ -0,0 +1,5 @@
<svg width="800" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="400" fill="#f0f0f0"/>
<circle cx="400" cy="200" r="80" fill="#6366f1" opacity="0.8"/>
<text x="400" y="340" font-family="system-ui, sans-serif" font-size="24" fill="#333" text-anchor="middle">Cover Image Example</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -0,0 +1,39 @@
# Cover Images and Assets
FolderWeb automatically detects and uses cover images for social sharing and list views.
## How Cover Images Work
Place an image named `cover.jpg`, `cover.png`, or `cover.webp` in your content folder and it's automatically:
- Used as the Open Graph image for social media
- Displayed in list views (when using templates that support it)
- Available at the same URL as your content
## Supported Formats
- `cover.jpg` or `cover.jpeg`
- `cover.png`
- `cover.webp`
- `cover.gif`
The system checks for these in order and uses the first one found.
## Other Assets
Any file in your content folder is accessible at the same URL path:
- Images: `photo.jpg`, `diagram.png`
- Documents: `download.pdf`, `report.docx`
- Data: `data.json`, `spreadsheet.csv`
Just reference them in your markdown:
```markdown
![Diagram](diagram.png)
[Download PDF](report.pdf)
```
## This Folder
This folder includes a simple SVG as the cover image. Check the folder structure to see how it's organized.

View file

@ -0,0 +1,2 @@
title = "Cover Images and Assets"
summary = "Learn how cover images and static assets work in FolderWeb"

View file

@ -0,0 +1,53 @@
# Working with Metadata
Metadata provides structured information about your pages and controls how they display.
## What is metadata.ini?
Every folder can have a `metadata.ini` file that defines:
- Title (overrides automatic title extraction)
- Date (overrides folder-based date detection)
- Summary (appears in list views)
- Custom template selection
- Other custom fields
## Example metadata.ini
```ini
title = "My Custom Title"
date = "2024-11-20"
summary = "A brief description that appears in lists"
[settings]
page_template = "list-grid"
show_date = false
```
## Common Use Cases
**Override automatic titles** If you want a title different from the folder name.
**Set explicit dates** When the folder name doesn't include a date.
**Add summaries** Give context in list views without showing full content.
**Choose templates** Different sections can use different list templates.
**Hide metadata** Set `show_date = false` to hide dates in lists.
## Custom Fields
You can add any fields you want:
```ini
author = "Jane Doe"
category = "Tutorial"
tags = "metadata, configuration, tutorial"
```
Then access them in custom templates via `$metadata['author']`.
## This Page's Metadata
Check out `metadata.ini` in this folder to see how the title, date, and summary are defined.

View file

@ -0,0 +1,3 @@
title = "Working with Metadata"
date = "2024-11-20"
summary = "Learn how to use metadata.ini files to control page information and behavior"

View file

@ -0,0 +1,74 @@
# Markdown Demonstration
This page shows the full range of Markdown formatting available through Parsedown.
## Text Formatting
You can use **bold text**, *italic text*, and even ***bold italic***.
Use `inline code` for technical terms or file paths like `/app/default/templates/`.
## Lists
Unordered lists:
- First item
- Second item
- Nested item
- Another nested item
- Third item
Ordered lists:
1. Step one
2. Step two
3. Step three
## Links and Images
Here's a [link to the homepage](/).
Images work too (when you add them to the folder):
![Example image](placeholder.jpg)
## Code Blocks
```php
<?php
function greet(string $name): string {
return "Hello, {$name}!";
}
echo greet('World');
```
## Blockquotes
> This is a blockquote. Perfect for highlighting important information or quotes from other sources.
>
> It can span multiple paragraphs.
## Tables
| Feature | Status |
|---------|--------|
| Markdown | ✓ |
| HTML | ✓ |
| PHP | ✓ |
| JavaScript | ✗ |
## Horizontal Rules
---
That line above is a horizontal rule, useful for separating sections.
## What You Can Do
- Write content in simple Markdown
- Mix HTML when needed
- Include images stored alongside your content
- Use all standard Markdown features
The file for this page is just `index.md` in a dated folder. The date is automatically extracted and displayed.

View file

@ -0,0 +1,74 @@
# Markdown-demonstrasjon
Denne siden viser hele utvalget av Markdown-formatering tilgjengelig gjennom Parsedown.
## Tekstformatering
Du kan bruke **fet tekst**, *kursiv tekst*, og til og med ***fet kursiv***.
Bruk `inline kode` for tekniske termer eller filstier som `/app/default/templates/`.
## Lister
Uspesifiserte lister:
- Første element
- Andre element
- Nestet element
- Enda et nestet element
- Tredje element
Ordnede lister:
1. Steg én
2. Steg to
3. Steg tre
## Lenker og bilder
Her er en [lenke til forsiden](/).
Bilder fungerer også (når du legger dem til i mappen):
![Eksempelbilde](placeholder.jpg)
## Kodeblokker
```php
<?php
function hils(string $navn): string {
return "Hei, {$navn}!";
}
echo hils('Verden');
```
## Blokksitater
> Dette er et blokksitat. Perfekt for å fremheve viktig informasjon eller sitater fra andre kilder.
>
> Det kan spenne over flere avsnitt.
## Tabeller
| Funksjon | Status |
|---------|--------|
| Markdown | ✓ |
| HTML | ✓ |
| PHP | ✓ |
| JavaScript | ✗ |
## Horisontale linjer
---
Linjen over er en horisontal linje, nyttig for å skille seksjoner.
## Hva du kan gjøre
- Skriv innhold i enkel Markdown
- Bland inn HTML når det trengs
- Inkluder bilder lagret sammen med innholdet ditt
- Bruk alle standard Markdown-funksjoner
Filen for denne siden er bare `index.no.md` i en datert mappe. Datoen blir automatisk hentet ut og vist.

View file

@ -0,0 +1,2 @@
title = "Markdown Demonstration"
summary = "Shows all the Markdown formatting features available through Parsedown"

View file

@ -0,0 +1,52 @@
# File-Based Routing
FolderWeb's routing is beautifully simple: **your folder structure is your URL structure**. No configuration files, no route definitions, no magic strings.
## How It Works
When you visit a URL, FolderWeb looks for matching folders and files:
```
/content/
├── index.md → /
├── about/
│ └── index.md → /about/
└── blog/
├── 2024-11-01-post/
│ └── index.md → /blog/post/
└── index.md → /blog/
```
## Automatic Features
**Folder names become URLs** Create a folder called `projects` and it's instantly available at `/projects/`
**Date prefixes are stripped** `2024-11-01-my-post` becomes `/my-post/` in the URL
**Custom slugs via metadata** Override the default URL with `slug = "custom-url"` in `metadata.ini`
**Trailing slashes** Directories always redirect to include trailing slashes for consistency
## Example
This very page demonstrates file-based routing! The path is:
```
app/default/content/examples/file-based-routing/index.md
```
Which renders at:
```
/examples/file-based-routing/
```
No routes to define. No configuration to update. Just files and folders.
## Benefits
- **Intuitive** If you can navigate folders, you understand the routing
- **Refactor-friendly** Moving content means moving folders
- **No broken links** URLs match the filesystem
- **Fast** No route matching overhead, direct file lookup
- **Predictable** What you see is what you get

View file

@ -0,0 +1,52 @@
# Filbasert routing
FolderWebs routing er vakkert enkelt: **mappestrukturen din er URL-strukturen din**. Ingen konfigurasjonsfiler, ingen rutedefinisjon, ingen magiske strenger.
## Hvordan det fungerer
Når du besøker en URL, ser FolderWeb etter matchende mapper og filer:
```
/content/
├── index.md → /
├── om/
│ └── index.md → /om/
└── blogg/
├── 2024-11-01-innlegg/
│ └── index.md → /blogg/innlegg/
└── index.md → /blogg/
```
## Automatiske funksjoner
**Mappenavn blir URL-er** Lag en mappe kalt `prosjekter` og den er umiddelbart tilgjengelig på `/prosjekter/`
**Datoprefikser fjernes** `2024-11-01-mitt-innlegg` blir `/mitt-innlegg/` i URL-en
**Egendefinerte slugs via metadata** Overstyr standard URL med `slug = "egendefinert-url"` i `metadata.ini`
**Avsluttende skråstrek** Kataloger omdirigerer alltid til å inkludere avsluttende skråstrek for konsistens
## Eksempel
Denne siden demonstrerer filbasert routing! Stien er:
```
app/default/content/examples/file-based-routing/index.no.md
```
Som rendres på:
```
/examples/file-based-routing/
```
Ingen ruter å definere. Ingen konfigurasjon å oppdatere. Bare filer og mapper.
## Fordeler
- **Intuitivt** Hvis du kan navigere mapper, forstår du routingen
- **Refaktorvennlig** Å flytte innhold betyr å flytte mapper
- **Ingen ødelagte lenker** URL-er matcher filsystemet
- **Rask** Ingen rutematching overhead, direkte filoppslag
- **Forutsigbar** Det du ser er det du får

View file

@ -0,0 +1,3 @@
title = "File-Based Routing"
summary = "Your folder structure is your URL structure—no configuration needed"
date = "2024-11-26"

View file

@ -0,0 +1,5 @@
# Examples
This section demonstrates different content types and features you can use in FolderWeb.
Each example below shows a different capability. Click through to see the source and how it works.

View file

@ -0,0 +1,11 @@
# Mix Formats
FolderWeb lets you **combine different file types** in the same directory. Markdown, HTML, and PHP files can coexist and render together on a single page.
## Supported Formats
- **`.md`** Markdown for content (this file!)
- **`.html`** Static HTML snippets
- **`.php`** Dynamic PHP components
All files in the same directory are rendered in **alphabetical order**. Use number prefixes (like `00-`, `01-`, `02-`) to control the sequence.

View file

@ -0,0 +1,11 @@
# Bland formater
FolderWeb lar deg **kombinere forskjellige filtyper** i samme katalog. Markdown, HTML og PHP-filer kan eksistere side om side og rendres sammen på en enkelt side.
## Støttede formater
- **`.md`** Markdown for innhold (denne filen!)
- **`.html`** Statiske HTML-snippets
- **`.php`** Dynamiske PHP-komponenter
Alle filer i samme katalog rendres i **alfabetisk rekkefølge**. Bruk talprefikser (som `00-`, `01-`, `02-`) for å kontrollere sekvensen.

View file

@ -0,0 +1,11 @@
<section style="background: var(--color-accent-light); padding: var(--space-m); border-radius: 0.5rem; margin-block: var(--space-m);">
<h2 style="margin-block-start: 0;">HTML Component</h2>
<p>This section is rendered from <code>01-html-example.html</code> a static HTML file with inline styles.</p>
<p>HTML files are perfect for:</p>
<ul>
<li>Custom-styled sections</li>
<li>Embedded media (video, audio, iframes)</li>
<li>Interactive elements (forms, details/summary)</li>
<li>SVG graphics and diagrams</li>
</ul>
</section>

View file

@ -0,0 +1,11 @@
<section style="background: var(--color-accent-light); padding: var(--space-m); border-radius: 0.5rem; margin-block: var(--space-m);">
<h2 style="margin-block-start: 0;">HTML-komponent</h2>
<p>Denne seksjonen rendres fra <code>01-html-example.no.html</code> en statisk HTML-fil med inline stiler.</p>
<p>HTML-filer er perfekte for:</p>
<ul>
<li>Egendefinerte stilte seksjoner</li>
<li>Innebygde medier (video, lyd, iframes)</li>
<li>Interaktive elementer (skjemaer, detaljer/sammendrag)</li>
<li>SVG-grafikk og diagrammer</li>
</ul>
</section>

View file

@ -0,0 +1,41 @@
<?php
$currentTime = date('H:i:s');
$serverInfo = [
'PHP-versjon' => PHP_VERSION,
'Servertid' => $currentTime,
'Filer i denne katalogen' => count(glob(__DIR__ . '/*')),
];
?>
<section style="background: linear-gradient(135deg, oklch(95% 0.08 150), oklch(98% 0.04 150)); padding: var(--space-m); border-radius: 0.5rem; margin-block: var(--space-m);">
<h2 style="margin-block-start: 0;">PHP-komponent</h2>
<p>Denne seksjonen rendres fra <code>02-dynamic.no.php</code> en dynamisk PHP-fil som kjører serversiden.</p>
<table style="width: 100%; margin-block: var(--space-s);">
<thead>
<tr>
<th>Egenskap</th>
<th>Verdi</th>
</tr>
</thead>
<tbody>
<?php foreach ($serverInfo as $key => $value): ?>
<tr>
<td><?= htmlspecialchars($key) ?></td>
<td><strong><?= htmlspecialchars($value) ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>PHP-filer kan:</p>
<ul>
<li>Generere dynamisk innhold</li>
<li>Spørre databaser</li>
<li>Lese fra filer eller API-er</li>
<li>Beregne verdier i farten</li>
<li>Inkludere egendefinert styling og logikk</li>
</ul>
<p><em>Last inn denne siden nytt for å se servertiden oppdatere seg!</em></p>
</section>

View file

@ -0,0 +1,41 @@
<?php
$currentTime = date('H:i:s');
$serverInfo = [
'PHP Version' => PHP_VERSION,
'Server Time' => $currentTime,
'Files in this directory' => count(glob(__DIR__ . '/*')),
];
?>
<section style="background: linear-gradient(135deg, oklch(95% 0.08 150), oklch(98% 0.04 150)); padding: var(--space-m); border-radius: 0.5rem; margin-block: var(--space-m);">
<h2 style="margin-block-start: 0;">PHP Component</h2>
<p>This section is rendered from <code>02-dynamic.php</code> a dynamic PHP file that executes server-side.</p>
<table style="width: 100%; margin-block: var(--space-s);">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($serverInfo as $key => $value): ?>
<tr>
<td><?= htmlspecialchars($key) ?></td>
<td><strong><?= htmlspecialchars($value) ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>PHP files can:</p>
<ul>
<li>Generate dynamic content</li>
<li>Query databases</li>
<li>Read from files or APIs</li>
<li>Calculate values on-the-fly</li>
<li>Include custom styling and logic</li>
</ul>
<p><em>Refresh this page to see the server time update!</em></p>
</section>

View file

@ -0,0 +1,38 @@
## Why Mix Formats?
Different content types have different needs:
**Markdown** Fast authoring, readable source, perfect for articles and documentation
**HTML** Full control over structure, ideal for custom layouts and embedded media
**PHP** Dynamic content, server-side logic, database queries, real-time data
By mixing them freely, you get the best of all worlds. Write most content in Markdown for speed, add HTML for custom sections, and use PHP when you need dynamic behavior.
## This Page's Structure
This very page demonstrates format mixing:
```
/examples/mix-formats/
├── 00-intro.md (Markdown introduction)
├── 01-html-example.html (Static HTML component)
├── 02-dynamic.php (Dynamic PHP component)
├── 03-conclusion.md (Markdown conclusion)
└── metadata.ini (Page metadata)
```
All four content files render seamlessly as one cohesive page. The numbered prefixes ensure they appear in the intended order.
## Use Cases
**Blog with special sections** Markdown posts with custom HTML callouts or PHP-generated stats
**Documentation with demos** Markdown explanations with live PHP examples
**Landing pages** Mix markdown content with custom HTML heroes and PHP dynamic elements
**Portfolio sites** Markdown project descriptions with HTML galleries and PHP filtering
The flexibility is yours.

View file

@ -0,0 +1,38 @@
## Hvorfor blande formater?
Forskjellige innholdstyper har forskjellige behov:
**Markdown** Rask redigering, lesbar kilde, perfekt for artikler og dokumentasjon
**HTML** Full kontroll over struktur, ideelt for egendefinerte layout og innebygde medier
**PHP** Dynamisk innhold, server-side logikk, databasespørringer, sanntidsdata
Ved å blande dem fritt, får du det beste fra alle verdener. Skriv mesteparten av innholdet i Markdown for hastighet, legg til HTML for egendefinerte seksjoner, og bruk PHP når du trenger dynamisk oppførsel.
## Denne sidens struktur
Denne siden demonstrerer formatblanding:
```
/examples/mix-formats/
├── 00-intro.no.md (Markdown-introduksjon)
├── 01-html-example.no.html (Statisk HTML-komponent)
├── 02-dynamic.no.php (Dynamisk PHP-komponent)
├── 03-conclusion.no.md (Markdown-konklusjon)
└── metadata.ini (Sidemetadata)
```
Alle fire innholdsfilene rendres sømløst som én sammenhengende side. De nummererte prefiksene sikrer at de vises i ønsket rekkefølge.
## Bruksområder
**Blogg med spesialseksjoner** Markdown-innlegg med egendefinerte HTML-callouts eller PHP-generert statistikk
**Dokumentasjon med demoer** Markdown-forklaringer med live PHP-eksempler
**Landingssider** Bland markdown-innhold med egendefinerte HTML-heroer og PHP-dynamiske elementer
**Porteføljesider** Markdown-prosjektbeskrivelser med HTML-gallerier og PHP-filtrering
Fleksibiliteten er din.

View file

@ -0,0 +1,3 @@
title = "Mix Formats"
summary = "Combine Markdown, HTML, and PHP files to create rich, dynamic pages"
date = "2024-11-26"

View file

@ -0,0 +1,5 @@
# Nested Content
FolderWeb supports content nested as deep as you need. Your folder structure is your URL structure.
This section demonstrates multi-level nesting. Explore the subdirectories to see how it works.

View file

@ -0,0 +1,5 @@
# Level Two
This is two levels deep: `/examples/nested/level-two/`
You can organize content in hierarchies that make sense for your site structure.

View file

@ -0,0 +1,5 @@
# Level Three
This is three levels deep: `/examples/nested/level-two/level-three/`
The URL structure automatically follows your folder organization. No routing configuration needed.

View file

@ -0,0 +1,29 @@
# Level Four
Four levels deep: `/examples/nested/level-two/level-three/level-four/`
## How Deep Can You Go?
As deep as your filesystem allows. Each folder can contain:
- Content files (`.md`, `.html`, `.php`)
- Subdirectories (which become sub-URLs)
- Assets (images, PDFs, etc.)
- A `metadata.ini` file
- A `styles.css` file for page-specific styles
## Navigation
There's no automatic breadcrumb generation in the default theme, but you can add it in a custom template by parsing the URL path.
## Use Cases
**Documentation sites** Organize by category/subcategory/topic
**Photo galleries** Year/month/album/photo
**Course content** Course/module/lesson/exercise
**Product catalogs** Category/subcategory/product
The simplicity of file-based routing means your content organization is transparent and portable.

View file

@ -0,0 +1,47 @@
# No Build Step
Modern web development often involves complex build pipelines: transpilers, bundlers, minifiers, watchers, and more. FolderWeb takes a different approach: **edit a file, refresh the page, see the result**.
## What This Means
**No npm install** No package.json, no node_modules folder, no dependency hell
**No compilation** Write PHP, HTML, CSS, and Markdown directly
**No watching** No background processes monitoring file changes
**No bundling** Files are served as-is, leveraging HTTP/2 multiplexing
**No transpiling** Modern PHP 8.4+ and CSS work in all browsers
## The Development Flow
Traditional workflow:
```
Edit file → Save → Wait for build → Reload → See changes
```
FolderWeb workflow:
```
Edit file → Save → Reload → See changes
```
That's it. No waiting. No build errors. No cache invalidation headaches.
## But What About...
**Performance?** PHP is fast. No JavaScript means pages load instantly. CSS is cacheable with MD5 versioning.
**Minification?** HTTP/2 compression handles this. Serve from behind a CDN if needed.
**Modern CSS?** CSS nesting, custom properties, oklch() colors, grid—all native, all supported.
**Development speed?** Instant feedback loop beats any hot reload system.
## Why This Matters
Build steps add complexity. Every dependency is a potential breaking change. Every tool is another thing to learn, configure, and maintain.
FolderWeb will work the same way in 5 years, 10 years, even 20 years. Your content is just files. Your templates are just PHP. Your styles are just CSS.
**Simple lasts.**

View file

@ -0,0 +1,47 @@
# Uten byggesteg
Moderne webutvikling involverer ofte komplekse byggepipelines: transpilere, bundlere, minifisere, overvåkere og mer. FolderWeb tar en annen tilnærming: **rediger en fil, oppdater siden, se resultatet**.
## Hva dette betyr
**Ingen npm install** Ingen package.json, ingen node_modules-mappe, ingen avhengighetshelvete
**Ingen kompilering** Skriv PHP, HTML, CSS og Markdown direkte
**Ingen overvåkning** Ingen bakgrunnsprosesser som overvåker filendringer
**Ingen bundling** Filer serveres som de er, ved å utnytte HTTP/2-multipleksing
**Ingen transpilering** Moderne PHP 8.4+ og CSS fungerer i alle nettlesere
## Utviklingsflyten
Tradisjonell arbeidsflyt:
```
Rediger fil → Lagre → Vent på bygg → Last inn på nytt → Se endringer
```
FolderWeb-arbeidsflyt:
```
Rediger fil → Lagre → Last inn på nytt → Se endringer
```
Det er det. Ingen venting. Ingen byggefeil. Ingen cache-invalideringshodebryet.
## Men hva med...
**Ytelse?** PHP er rask. Ingen JavaScript betyr at sider lastes øyeblikkelig. CSS er cachebar med MD5-versjonering.
**Minifisering?** HTTP/2-kompresjon håndterer dette. Server fra bak en CDN om nødvendig.
**Moderne CSS?** CSS-nesting, custom properties, oklch()-farger, grid—alt native, alt støttet.
**Utviklingshastighet?** Øyeblikkelig tilbakemeldingssløyfe slår ethvert hot reload-system.
## Hvorfor dette betyr noe
Byggesteg legger til kompleksitet. Hver avhengighet er en potensiell breaking change. Hvert verktøy er noe nytt å lære, konfigurere og vedlikeholde.
FolderWeb vil fungere på samme måte om 5 år, 10 år, til og med 20 år. Innholdet ditt er bare filer. Malene dine er bare PHP. Stilene dine er bare CSS.
**Enkelt varer.**

View file

@ -0,0 +1,3 @@
title = "No Build Step"
summary = "Edit, save, refresh—that's it. No compilation, no bundling, no waiting"
date = "2024-11-26"

View file

@ -0,0 +1,3 @@
# Oldest Post
The oldest post in this compact list.

View file

@ -0,0 +1,3 @@
# Older Post
An older post showing date ordering.

View file

@ -0,0 +1,3 @@
# Recent Post
A recent example post.

View file

@ -0,0 +1,3 @@
# Compact Template
This section uses `list-compact.php` for a minimal, clean list view.

View file

@ -0,0 +1,4 @@
title = "Compact Template Example"
[settings]
page_template = "list-compact"

View file

@ -0,0 +1,3 @@
# Grid Template
This section uses `list-grid.php` to display items in a responsive grid with cards.

View file

@ -0,0 +1,3 @@
# Grid Item One
This is an example item in the grid layout.

View file

@ -0,0 +1 @@
summary = "First item demonstrating grid layout"

View file

@ -0,0 +1,3 @@
# Grid Item Three
A third item to show the grid layout with multiple items.

View file

@ -0,0 +1 @@
summary = "Third grid item"

View file

@ -0,0 +1,3 @@
# Grid Item Two
Another example showing how items appear in the grid.

View file

@ -0,0 +1 @@
summary = "Second item in the grid"

View file

@ -0,0 +1,4 @@
title = "Grid Template Example"
[settings]
page_template = "list-grid"

View file

@ -0,0 +1,24 @@
# List Templates
FolderWeb includes several list templates you can use for different presentation styles.
This section demonstrates the different templates by using subdirectories. Each subdirectory below uses a different template via `metadata.ini`.
## Available Templates
**list.php** Default linear list with optional cover images
**list-grid.php** Grid layout with cards, great for galleries or portfolios
**list-compact.php** Minimal list showing just titles and dates
## Choosing a Template
In any folder's `metadata.ini`, add:
```ini
[settings]
page_template = "list-grid"
```
The three subdirectories below each use a different template. Compare them to see which fits your content best.

View file

@ -0,0 +1,53 @@
# Multilingual Support
**Currently viewing: English (EN)**
This page demonstrates FolderWeb's built-in language support. Use the language switcher in the header to toggle between **EN** and **NO** - the page content will change to match your selected language.
## How It Works
Creating multilingual content is straightforward:
1. **Add language codes to filenames** Create `index.en.md` and `index.no.md`
2. **Language switcher appears automatically** When multiple versions exist
3. **URLs get language prefixes** Non-default languages use `/en/page/`, `/no/page/`, etc.
4. **Share metadata** One `metadata.ini` file per directory works for all languages
## File Structure Example
```
/content/about/
├── index.en.md # English version
├── index.no.md # Norwegian version
└── metadata.ini # Shared metadata
```
## Configuration
Enable languages in `/app/default/config.ini`:
```ini
[languages]
default = "en"
available = "en,no"
```
## Translation Strings
UI text comes from language files in `/app/default/languages/`:
```ini
; en.ini
home = "Home"
read_more = "Read more"
; no.ini
home = "Hjem"
read_more = "Les mer"
```
Translation strings are optional—templates fall back to sensible defaults if missing.
## Try It
Use the language switcher in the header to toggle between English and Norwegian versions of this page. The content stays the same, just translated.

View file

@ -0,0 +1,53 @@
# Flerspråklig støtte
**Viser nå: Norsk (NO)**
Denne siden demonstrerer FolderWebs innebygde språkstøtte. Bruk språkvelgeren i headeren for å bytte mellom **EN** og **NO** - sideinnholdet vil endre seg til å matche det valgte språket.
## Slik fungerer det
Å opprette flerspråklig innhold er enkelt:
1. **Legg til språkkoder i filnavn** Opprett `index.en.md` og `index.no.md`
2. **Språkvelgeren vises automatisk** Når flere versjoner finnes
3. **URL-er får språkprefikser** Ikke-standardspråk bruker `/en/side/`, `/no/side/`, osv.
4. **Del metadata** Én `metadata.ini`-fil per katalog fungerer for alle språk
## Eksempel på filstruktur
```
/content/om/
├── index.en.md # Engelsk versjon
├── index.no.md # Norsk versjon
└── metadata.ini # Delt metadata
```
## Konfigurasjon
Aktiver språk i `/app/default/config.ini`:
```ini
[languages]
default = "en"
available = "en,no"
```
## Oversettelsesstrenger
UI-tekst kommer fra språkfiler i `/app/default/languages/`:
```ini
; en.ini
home = "Home"
read_more = "Read more"
; no.ini
home = "Hjem"
read_more = "Les mer"
```
Oversettelsesstrenger er valgfrie—maler faller tilbake til fornuftige standardverdier hvis de mangler.
## Prøv det
Bruk språkvelgeren i headeren for å bytte mellom engelske og norske versjoner av denne siden. Innholdet forblir det samme, bare oversatt.

View file

@ -0,0 +1,2 @@
title = "Multilingual Support"
summary = "Learn how to create content in multiple languages"

View file

@ -1,34 +1,112 @@
/* Page-specific styles for homepage */ /* Hero section for frontpage */
.hero { .hero {
display: flex; background: linear-gradient(135deg, oklch(95% 0.05 250) 0%, oklch(98% 0.02 250) 100%);
flex-direction: column; border-radius: 0.75rem;
justify-content: flex-end; padding: var(--space-m);
padding: 1rem .4rem; margin-block: var(--space-s);
background-color: oklch(0.85 0.05 250); text-align: center;
min-height: 40vh;
text-align: center; & .hero-title {
font-size: clamp(2rem, 5vw, 2.5rem);
margin-block: 0 var(--space-xs);
background: linear-gradient(135deg, oklch(40% 0.15 250), oklch(30% 0.2 280));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
}
& .hero-subtitle {
font-size: clamp(1rem, 2vw, 1.125rem);
color: var(--color-muted);
margin-block-end: var(--space-m);
max-width: 32rem;
margin-inline: auto;
}
& .cta-button {
display: inline-block;
background: var(--color-accent);
color: white;
padding: var(--space-xs) var(--space-m);
border-radius: 0.5rem;
text-decoration: none;
font-weight: 600;
margin-block-start: var(--space-xs);
&:hover {
background: oklch(45% 0.15 250);
color: white;
}
}
} }
.hero h1 { .features {
font-size: clamp(2.5rem, 6vw, 4rem); display: grid;
margin: 0; grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
gap: var(--space-s);
margin-block: var(--space-m);
& .feature-card {
background: var(--color-background);
padding: var(--space-s);
border-radius: 0.5rem;
border: 1px solid var(--color-border);
text-align: center;
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
border-color: var(--color-accent);
}
& .feature-icon {
font-size: 2rem;
display: block;
margin-block-end: var(--space-xs);
}
& .feature-title {
font-weight: bold;
margin-block: var(--space-xs);
color: var(--color-text);
font-size: 0.9rem;
}
& .feature-description {
font-size: 0.85rem;
color: var(--color-muted);
margin: 0;
}
}
} }
.cta-section { .stats {
background: linear-gradient(135deg, oklch(0.65 0.15 250), oklch(0.50 0.12 250)); display: flex;
padding: 2rem 1rem; justify-content: center;
margin-top: 3rem; gap: var(--space-m);
text-align: center; flex-wrap: wrap;
color: white; margin-block-start: var(--space-m);
} padding-block-start: var(--space-s);
border-block-start: 1px solid var(--color-border);
.cta-section p { & .stat {
color: white; text-align: center;
font-size: clamp(1.1rem, 3vw, 1.3rem);
}
.cta-content { & .stat-value {
max-width: 42rem; display: block;
margin: 0 auto; font-size: 1.25rem;
font-weight: bold;
color: var(--color-accent);
}
& .stat-label {
font-size: 0.8rem;
color: var(--color-muted);
}
}
} }

View file

@ -1,13 +1,13 @@
; English translations ; English translations
; These strings are used in templates and can be customized
; If a string is missing, the key name will be used as fallback
home = "Home" home = "Home"
categories = "Categories" categories = "Categories"
tags = "Tags" tags = "Tags"
read_more = "Read more" read_more = "Read more"
read_article = "Read article" read_article = "Read article"
read_full_answer = "Read full answer"
download_pdf = "Download PDF"
summary = "Summary" summary = "Summary"
footer_text = "Footer content goes here" footer_handcoded = "Generated in"
footer_handcoded = "This page was generated in"
footer_page_time = "ms" footer_page_time = "ms"
months = "January,February,March,April,May,June,July,August,September,October,November,December" months = "January,February,March,April,May,June,July,August,September,October,November,December"

View file

@ -1,13 +1,13 @@
; Norwegian translations ; Norwegian translations
; These strings are used in templates and can be customized
; If a string is missing, English fallback will be used
home = "Hjem" home = "Hjem"
categories = "Kategorier" categories = "Kategorier"
tags = "Stikkord" tags = "Emneknagger"
read_more = "Les mer" read_more = "Les mer"
read_article = "Les artikkel" read_article = "Les artikkel"
read_full_answer = "Les hele svaret" summary = "Sammendrag"
download_pdf = "Last ned PDF" footer_handcoded = "Generert på"
summary = "Oppsummering"
footer_text = "Bunntekst her"
footer_handcoded = "Denne siden ble generert på"
footer_page_time = "ms" footer_page_time = "ms"
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember" months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"

View file

@ -1,191 +0,0 @@
/* MINIMAL CSS RESET*/
* { margin-bottom: 0; }
/* VARIABLES */
:root {
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-heading: Georgia, "Times New Roman", serif;
--color-primary: #4a90e2;
--color-primary: oklch(0.65 0.15 250);
--color-secondary: #2c5aa0;
--color-secondary: oklch(0.50 0.12 250);
--color-light: #f0f4f8;
--color-light: oklch(0.97 0.01 250);
--color-grey: #404040;
--color-grey: oklch(0.37 0 0);
}
/* GLOBAL */
html { font-family: var(--font-body); font-size: clamp(16px, 2.3vw, 20px); scroll-behavior: smooth; }
body { margin: 0; color: var(--color-grey) }
p, ul, ol, aside { line-height: 1.5em; hyphens: auto }
img { max-width: 100%; height: auto; }
h1 { color: var(--color-primary); font-size: 2.3rem }
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 400;
line-height: 1.3em;
margin-top: 1.3em;
text-wrap: pretty;
}
a {
color: var(--color-primary);
text-decoration: none;
&:hover { color: var(--color-secondary) }
}
.grid-container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "footer";
height: 100%;
width: 100%;
justify-content: center;
min-height: 100vh;
align-items: stretch;
}
.contain, :where(main>article, main>aside, main>section) {
display: grid;
grid-template-columns: minmax(.4rem, 1fr) minmax(0, 42rem) minmax(.3rem, 1fr);
> * {
grid-column: 2;
}
}
.escape {
grid-column: 1 / -1 !important;
}
/* HEADER */
header {
border-bottom: 3px #00000022 solid;
grid-area: header;
> div {
padding-bottom: .2rem;
display: flex;
.logo {
margin-right: .3rem;
svg {
width: 7rem;
height: 100%;
color: var(--color-primary);
}
}
nav {
display:flex;
align-items: center;
justify-content:flex-end;
flex: 1;
ul {
display: flex;
list-style: none;
flex-wrap: wrap;
margin-top: .4rem;
padding: 0;
justify-content: flex-end;
li {
margin-left:0.4rem;
margin-top:0.4rem;
}
}
}
}
}
/* MAIN */
main {
grid-area: main;
background-color: var(--color-light);
padding-bottom: 2rem;
aside { margin-top: 1.3em }
article {
.intro {
font-size: 1.2rem;
line-height: 1.35em;
}
}
.button {
margin-top: 1.3rem;
justify-self: start;
}
}
/* BUTTONS */
.button {
display: inline-block;
text-decoration: none;
border-radius: 2rem;
padding: 0.35rem 1rem;
background-color: transparent;
color: var(--color-grey);
outline: 0.08rem var(--color-grey) solid;
&:hover {
background-color: var(--color-grey);
color: white;
outline: none;
}
&:active, &.active {
background-color: var(--color-primary);
color: white;
outline: none;
}
&:focus {
background-color: var(--color-primary);
color: white;
outline: none;
}
&.inverted {
background-color: transparent;
color: white;
outline: 0.08rem white solid;
&:hover {
background-color: white;
color: var(--color-primary);
outline: none;
}
&:active, &.active {
background-color: var(--color-light);
color: var(--color-primary);
outline: none;
}
&:focus {
color: white;
background-color: var(--color-grey);
outline: none;
}
}
&.bigger {
font-size: 1.2em;
padding: calc(0.35rem * 1.2) calc(1rem * 1.2);
border-radius: calc(1rem * 1.2);
}
}
/* FOOTER */
footer {
color: var(--color-light);
a {
color: var(--color-light);
&:hover { color: white; text-decoration: underline }
}
background-color: var(--color-secondary);
grid-area: footer;
> div {
margin: 1rem 0;
text-align: center;
.generated { font-size: .6rem }
}
}

View file

@ -0,0 +1,188 @@
/* Variables */
:root {
--color-text: oklch(20% 0 0);
--color-background: oklch(98% 0 0);
--color-accent: oklch(50% 0.15 250);
--color-accent-light: oklch(95% 0.05 250);
--color-border: oklch(85% 0 0);
--color-muted: oklch(50% 0 0);
--space-xs: 0.5rem;
--space-s: 1rem;
--space-m: 2rem;
--space-l: 4rem;
--size-content: 65ch;
--size-constrained: 42rem;
--size-wide: 90rem;
}
/* Base */
* {
box-sizing: border-box;
}
html {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: var(--color-text);
background: var(--color-background);
}
body {
margin: 0;
display: grid;
grid-template-columns:
[full-start] minmax(var(--space-s), 1fr)
[content-start] minmax(0, var(--size-constrained))
[content-end] minmax(var(--space-s), 1fr)
[full-end];
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
margin-block: 1.5em 0.5em;
}
h1 { font-size: clamp(2rem, 5vw, 3rem); }
h2 { font-size: clamp(1.5rem, 4vw, 2rem); }
h3 { font-size: clamp(1.25rem, 3vw, 1.5rem); }
p, ul, ol, dl {
max-width: var(--size-content);
margin-block: 1em;
}
/* Links */
a {
color: var(--color-accent);
text-underline-offset: 0.2em;
&:hover {
text-decoration: none;
}
}
/* Layout */
header, main, footer {
grid-column: content;
}
header {
border-block-end: 1px solid var(--color-border);
padding-block: var(--space-s);
display: flex;
flex-wrap: wrap;
gap: var(--space-s);
align-items: center;
& nav {
display: flex;
gap: var(--space-s);
flex-wrap: wrap;
&:first-child {
flex: 1;
}
& a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
&[aria-current] {
font-weight: bold;
}
}
}
& .language-switcher {
margin-inline-start: auto;
}
}
main {
padding-block: var(--space-m);
}
footer {
border-block-start: 1px solid var(--color-border);
padding-block: var(--space-s);
font-size: 0.875rem;
color: var(--color-muted);
& nav {
display: flex;
gap: var(--space-s);
margin-block-end: var(--space-xs);
}
& p {
margin: 0;
}
}
/* Images */
img {
max-width: 100%;
height: auto;
display: block;
}
/* Code */
code {
background: var(--color-accent-light);
padding: 0.125em 0.25em;
border-radius: 0.25em;
font-size: 0.9em;
}
pre {
background: var(--color-accent-light);
padding: var(--space-s);
border-radius: 0.5em;
overflow-x: auto;
& code {
background: none;
padding: 0;
}
}
/* Tables */
table {
border-collapse: collapse;
width: 100%;
margin-block: var(--space-s);
}
th, td {
padding: var(--space-xs);
text-align: start;
border-block-end: 1px solid var(--color-border);
}
th {
font-weight: bold;
}
/* Blockquote */
blockquote {
margin-inline: 0;
padding-inline-start: var(--space-s);
border-inline-start: 4px solid var(--color-accent);
color: var(--color-muted);
}
/* Article metadata */
article {
& time {
color: var(--color-muted);
font-size: 0.875rem;
}
}

View file

@ -1,69 +1,55 @@
<?php
$startTime = microtime(true);
$publicDir = realpath($_SERVER['DOCUMENT_ROOT']);
$customCssPath = __DIR__ . '/../../custom/styles/base.css';
$defaultCssPath = __DIR__ . '/../styles/base.css';
$cssPath = file_exists($customCssPath) ? $customCssPath : $defaultCssPath;
$cssUrl = file_exists($customCssPath) ? '/app/styles/base.css' : '/app/default-styles/base.css';
$cssHash = file_exists($cssPath) ? hash_file('md5', $cssPath) : 'file_not_found';
if (isset($GLOBALS['_SERVER']['SCRIPT_FILENAME'])) { $includingFile = $_SERVER['SCRIPT_FILENAME']; }
if (!empty($includingFile)) { $pageName = pathinfo($includingFile, PATHINFO_FILENAME); }
if (!in_array(basename(dirname($includingFile)), ['latest', 'live', 'frozen']) && basename(dirname($includingFile)) !== '') { $dirName = basename(dirname($includingFile)); }
function getActiveClass($href) { return rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') === rtrim($href, '/') ? 'active' : ''; }
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>"> <html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
<head> <head>
<meta charset="UTF-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1">
<?php if (!empty($metaDescription)): ?> <title><?= htmlspecialchars($pageTitle ?? 'FolderWeb') ?></title>
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>"> <?php if (!empty($metaDescription)): ?>
<?php endif; ?> <meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
<link rel="stylesheet" href="<?= $cssUrl ?>?v=<?= $cssHash ?>"> <?php endif; ?>
<?php if (!empty($pageCssUrl)): ?> <?php if (!empty($socialImageUrl)): ?>
<link rel="stylesheet" href="<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>"> <meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
<?php endif; ?> <?php endif; ?>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"> <link rel="stylesheet" href="/app/default/styles/styles.css">
<link rel="icon" href="/favicon.png" type="image/png"> <?php if (!empty($pageCssUrl)): ?>
<title><?= htmlspecialchars($pageTitle ?? 'Site Title') ?></title> <link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
<?php endif; ?>
</head> </head>
<body>
<header>
<nav>
<a href="<?= htmlspecialchars($langPrefix ?? '') ?>/"><?= htmlspecialchars($homeLabel ?? ($translations['home'] ?? 'Home')) ?></a>
<?php if (!empty($navigation)): ?>
<?php foreach ($navigation as $item): ?>
<a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
<nav class="language-switcher" aria-label="Language">
<?php foreach ($languageUrls as $lang => $url): ?>
<a href="<?= htmlspecialchars($url) ?>" <?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>><?= htmlspecialchars(strtoupper($lang)) ?></a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
</header>
<body class="<?php if (isset($dirName)) echo 'section-' . $dirName . ' '; ?><?php if (isset($pageName)) echo 'page-' . $pageName; ?>"> <main>
<div class="grid-container"> <?= $content ?>
<header class="contain"> </main>
<div>
<div class="logo">
<a href="/">
<svg width="200" height="60" viewBox="0 0 200 60" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="40" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="currentColor">FolderWeb</text>
</svg>
</a>
</div>
<nav>
<ul>
<li><a href="/" class="button <?php echo getActiveClass('/'); ?>"><?= htmlspecialchars($translations['home'] ?? 'Home') ?></a></li>
<?php if (!empty($navigation)): ?>
<?php foreach ($navigation as $item): ?>
<li><a href="<?= htmlspecialchars($item['url']) ?>" class="button <?php echo getActiveClass($item['url']); ?>"><?= htmlspecialchars($item['title']) ?></a></li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</nav>
</div>
</header>
<main> <footer>
<?php echo $content ?? ''; ?> <nav>
</main> <a href="https://mastodon.social/@example" rel="me">Mastodon</a>
<a href="https://bsky.app/profile/example.bsky.social">Bluesky</a>
<footer> </nav>
<div class="contain"> <p>
<p><?= htmlspecialchars($translations['footer_text'] ?? 'Footer content goes here') ?></p> <?php if (!empty($translations['footer_handcoded'])): ?>
<?php $endTime = microtime(true); $pageLoadTime = round(($endTime - $startTime) * 1000, 2); ?> <?= htmlspecialchars($translations['footer_handcoded']) ?> <?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?><?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
<p class="generated"><?= htmlspecialchars($translations['footer_handcoded'] ?? 'This page was generated in') ?> <?php echo $pageLoadTime; ?><?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?></p> <?php else: ?>
</div> Generated in <?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?>ms
</footer> <?php endif; ?>
</div> </p>
</footer>
</body> </body>
</html> </html>

View file

@ -1,82 +0,0 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<section class="list-card-grid-wrapper">
<div class="list-card-grid">
<?php foreach ($items as $item): ?>
<article>
<?php if ($item['cover']): ?>
<a href="<?= htmlspecialchars($item['pdf'] ?? $item['url']) ?>">
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
</a>
<?php endif; ?>
<h1>
<a href="<?= htmlspecialchars($item['redirect'] ?? $item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h1>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<p><?= htmlspecialchars($item['date']) ?></p>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<div class="card-actions">
<?php if (!empty($item['pdf'])): ?>
<a href="<?= htmlspecialchars($item['pdf']) ?>" class="button" download><?= htmlspecialchars($translations['download_pdf'] ?? 'Download PDF') ?></a>
<?php endif; ?>
<?php if (!empty($item['redirect'])): ?>
<a href="<?= htmlspecialchars($item['redirect']) ?>" class="button"><?= htmlspecialchars($translations['read_article'] ?? 'Read article') ?></a>
<?php else: ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<style>
main > section.list-card-grid-wrapper {
margin-top: 1.3em;
.list-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(clamp(15rem, 45%, 20rem), 1fr));
gap: clamp(1rem, 3vw, 2rem);
}
.list-card-grid > article {
background-color: white;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
> :not(img, a) {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
margin-top: 1rem;
font-size: clamp(1.5rem, 4vw, 2rem);
}
.card-actions {
margin-top: auto;
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
padding-bottom: 1rem;
a {
margin-top: 1.3rem;
}
}
}
}
</style>

View file

@ -0,0 +1,61 @@
<?php if (!empty($pageContent)): ?>
<div class="list-intro">
<?= $pageContent ?>
</div>
<?php endif; ?>
<ul class="compact-list">
<?php foreach ($items as $item): ?>
<li>
<a href="<?= htmlspecialchars($item['url']) ?>">
<strong><?= htmlspecialchars($item['title']) ?></strong>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<time><?= htmlspecialchars($item['date']) ?></time>
<?php endif; ?>
</a>
<?php if (!empty($item['summary'])): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<style>
.compact-list {
list-style: none;
padding: 0;
max-width: var(--size-content);
& li {
border-block-end: 1px solid var(--color-border);
padding-block: var(--space-s);
&:first-child {
padding-block-start: 0;
}
}
& a {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-s);
text-decoration: none;
&:hover strong {
text-decoration: underline;
}
}
& time {
white-space: nowrap;
font-size: 0.875rem;
}
& p {
margin-block-start: var(--space-xs);
color: var(--color-muted);
font-size: 0.9rem;
}
}
</style>

View file

@ -1,141 +0,0 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<section class="list-faq-wrapper">
<div class="list-faq">
<?php foreach ($items as $item): ?>
<details class="faq-item">
<summary>
<h2><?= htmlspecialchars($item['title']) ?></h2>
<span class="toggle-icon" aria-hidden="true"></span>
</summary>
<div class="faq-content">
<?php if ($item['summary']): ?>
<p><strong><?= htmlspecialchars($translations['summary'] ?? 'Summary') ?>:</strong> <?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_full_answer'] ?? 'Read full answer') ?></a>
</div>
</details>
<?php endforeach; ?>
</div>
</section>
<style>
main > section.list-faq-wrapper {
margin-top: 1.3em;
.list-faq {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.faq-item {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 0.5rem;
overflow: hidden;
transition: border-color 0.2s ease;
&:hover {
border-color: var(--color-primary);
}
&[open] {
border-color: var(--color-primary);
}
}
summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
cursor: pointer;
list-style: none;
user-select: none;
&::-webkit-details-marker {
display: none;
}
h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 400;
color: var(--color-grey);
flex: 1;
}
.toggle-icon {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
position: relative;
margin-left: 1rem;
&::before,
&::after {
content: '';
position: absolute;
background-color: var(--color-primary);
transition: transform 0.3s ease;
}
&::before {
top: 50%;
left: 0;
right: 0;
height: 2px;
transform: translateY(-50%);
}
&::after {
left: 50%;
top: 0;
bottom: 0;
width: 2px;
transform: translateX(-50%);
}
}
&:hover h2 {
color: var(--color-primary);
}
}
.faq-item[open] summary .toggle-icon::after {
transform: translateX(-50%) rotate(90deg);
opacity: 0;
}
.faq-content {
padding: 0 1.5rem 1.5rem 1.5rem;
animation: slideDown 0.3s ease;
p {
margin-top: 0;
color: var(--color-grey);
line-height: 1.6;
}
.button {
margin-top: 1rem;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>

View file

@ -1,94 +1,48 @@
<?php if (!empty($pageContent)): ?> <?php if (!empty($pageContent)): ?>
<article class="list-intro"> <div class="list-intro">
<?= $pageContent ?> <?= $pageContent ?>
</article> </div>
<?php endif; ?> <?php endif; ?>
<section class="list-grid-wrapper"> <div class="grid">
<div class="list-grid"> <?php foreach ($items as $item): ?>
<?php foreach ($items as $item): ?> <article class="card">
<article> <?php if (!empty($item['cover'])): ?>
<?php if ($item['cover']): ?> <img src="<?= htmlspecialchars($item['cover']) ?>" alt="">
<a href="<?= htmlspecialchars($item['url']) ?>"> <?php endif; ?>
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
</a> <h3><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></h3>
<?php endif; ?>
<h1> <?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<a href="<?= htmlspecialchars($item['url']) ?>"> <time><?= htmlspecialchars($item['date']) ?></time>
<?= htmlspecialchars($item['title']) ?> <?php endif; ?>
</a>
</h1> <?php if (!empty($item['summary'])): ?>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?> <p><?= htmlspecialchars($item['summary']) ?></p>
<p><?= htmlspecialchars($item['date']) ?></p> <?php endif; ?>
<?php endif; ?> </article>
<?php if ($item['summary']): ?> <?php endforeach; ?>
<p><?= htmlspecialchars($item['summary']) ?></p> </div>
<?php endif; ?>
<div class="grid-actions">
<?php if (!empty($item['pdf'])): ?>
<a href="<?= htmlspecialchars($item['pdf']) ?>" class="button" download><?= htmlspecialchars($translations['download_pdf'] ?? 'Download PDF') ?></a>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<style> <style>
main > section.list-grid-wrapper { .grid {
margin-top: 1.3em; display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
gap: var(--space-m);
}
.list-grid { .card {
display: grid; border: 1px solid var(--color-border);
grid-template-columns: repeat(auto-fit, minmax(clamp(15rem, 45%, 20rem), 1fr)); border-radius: 0.5rem;
gap: clamp(1rem, 3vw, 2rem); padding: var(--space-s);
}
.list-grid > article { & img {
background-color: white; margin-block-end: var(--space-s);
padding: 0; border-radius: 0.25rem;
overflow: hidden; }
display: flex;
flex-direction: column;
> :not(img, a) { & h3 {
padding-left: 1rem; margin-block-start: 0;
padding-right: 1rem; }
}
h1 {
margin-top: 1rem;
font-size: clamp(1.5rem, 4vw, 2rem);
}
.grid-actions {
margin-top: auto;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding-bottom: 1rem;
a {
margin-top: 1.3rem;
border-radius: 1rem;
background-color: var(--color-grey);
padding: 0.35rem 1rem;
color: white;
text-decoration: none;
&:hover {
background-color: var(--color-primary);
color: white;
}
&:focus {
outline: 0.1rem var(--color-primary) solid;
color: var(--color-grey);
background-color: white;
}
}
}
}
} }
</style> </style>

View file

@ -1,55 +1,25 @@
<?php if (!empty($pageContent)): ?> <?php if (!empty($pageContent)): ?>
<article class="list-intro"> <div class="list-intro">
<?= $pageContent ?> <?= $pageContent ?>
</article> </div>
<?php endif; ?> <?php endif; ?>
<article> <div class="list">
<?php foreach ($items as $item): ?> <?php foreach ($items as $item): ?>
<article> <article class="list-item">
<?php if ($item['cover']): ?> <?php if (!empty($item['cover'])): ?>
<a href="<?= htmlspecialchars($item['url']) ?>"> <img src="<?= htmlspecialchars($item['cover']) ?>" alt="">
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>"> <?php endif; ?>
</a>
<?php endif; ?>
<h1>
<a href="<?= htmlspecialchars($item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h1>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<p><?= htmlspecialchars($item['date']) ?></p>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
</article>
<?php endforeach; ?>
</article>
<style> <h2><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></h2>
main > article {
> article {
background-color: white;
padding: 0;
padding-bottom: 1.3rem;
margin-bottom: 1.5rem;
overflow: hidden;
> :not(img, a) { <?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
padding-left: 1rem; <time><?= htmlspecialchars($item['date']) ?></time>
padding-right: 1rem; <?php endif; ?>
}
h1 { <?php if (!empty($item['summary'])): ?>
margin-top: 1rem; <p><?= htmlspecialchars($item['summary']) ?></p>
} <?php endif; ?>
</article>
> .button { <?php endforeach; ?>
margin-left: 1rem; </div>
margin-top: 1rem;
}
}
}
</style>

View file

@ -1,25 +1,3 @@
<?= $content ?> <article>
<?= $content ?>
<?php if ($pageMetadata && (isset($pageMetadata['tags']) || isset($pageMetadata['categories']))): ?> </article>
<aside class="metadata">
<?php if (!empty($pageMetadata['categories'])): ?>
<div class="categories">
<strong><?= htmlspecialchars($translations['categories'] ?? 'Categories') ?>:</strong>
<?php
$categories = array_map('trim', explode(',', $pageMetadata['categories']));
echo implode(', ', array_map('htmlspecialchars', $categories));
?>
</div>
<?php endif; ?>
<?php if (!empty($pageMetadata['tags'])): ?>
<div class="tags">
<strong><?= htmlspecialchars($translations['tags'] ?? 'Tags') ?>:</strong>
<?php
$tags = array_map('trim', explode(',', $pageMetadata['tags']));
echo implode(', ', array_map('htmlspecialchars', $tags));
?>
</div>
<?php endif; ?>
</aside>
<?php endif; ?>

View file

@ -490,5 +490,3 @@ content/
## See Also ## See Also
- [Plugin System Reference](plugin-system.md) - [Plugin System Reference](plugin-system.md)
- [How to Create Multi-Language Sites](../../docs/how-to/multi-language.md)
- [Configuration Reference](../../docs/reference/configuration.md)

View file

@ -46,7 +46,10 @@ Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dirOrType, strin
// Filter content files by language variant // Filter content files by language variant
if (is_array($data) && !empty($data) && isset($data[0]['path'])) { if (is_array($data) && !empty($data) && isset($data[0]['path'])) {
return filterFilesByLanguage($data, $dirOrType, $ctx); error_log("filterFilesByLanguage called with " . count($data) . " files, current lang: $currentLang");
$filtered = filterFilesByLanguage($data, $dirOrType, $ctx);
error_log("Filtered to " . count($filtered) . " files");
return $filtered;
} }
return $data; return $data;

View file

@ -24,6 +24,34 @@ if (file_exists($assetPath) && is_file($assetPath)) {
exit; exit;
} }
// Check for assets in content directory (CSS, images, etc.)
$contentAssetPath = $ctx->contentDir . '/' . $ctx->requestPath;
if (file_exists($contentAssetPath) && is_file($contentAssetPath)) {
$ext = pathinfo($contentAssetPath, PATHINFO_EXTENSION);
// Define MIME types for asset files
$mimeTypes = [
'css' => 'text/css',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'otf' => 'font/otf',
];
$extLower = strtolower($ext);
if (isset($mimeTypes[$extLower])) {
header('Content-Type: ' . $mimeTypes[$extLower]);
readfile($contentAssetPath);
exit;
}
}
// Handle frontpage // Handle frontpage
if (empty($ctx->requestPath)) { if (empty($ctx->requestPath)) {
$contentFiles = findAllContentFiles($ctx->contentDir); $contentFiles = findAllContentFiles($ctx->contentDir);

View file

@ -1,211 +0,0 @@
# FolderWeb Documentation
Welcome to the FolderWeb documentation! This comprehensive guide covers everything you need to know about building and maintaining websites with FolderWeb.
## 📚 Documentation Organization
This documentation follows the [Diataxis framework](https://diataxis.fr/), organizing content into four distinct types to help you find exactly what you need:
### 🎓 Tutorial (Learning-Oriented)
**Purpose**: Learn by doing
**For**: Newcomers to FolderWeb
- **[Getting Started](tutorial/00-getting-started.md)** - Build your first FolderWeb site from scratch in 10 minutes
Start here if you're new to FolderWeb. This hands-on tutorial walks you through creating a complete website with pages, blog posts, and custom styling.
### 📋 How-To Guides (Task-Oriented)
**Purpose**: Solve specific problems
**For**: Users with a specific goal
- **[Custom Templates](how-to/custom-templates.md)** - Override default templates with your own designs
- **[Custom Styles](how-to/custom-styles.md)** - Customize appearance using CSS
- **[Multi-Language Sites](how-to/multi-language.md)** - Set up and manage multiple languages
- **[Working with Metadata](how-to/working-with-metadata.md)** - Control content with metadata.ini files
Use these guides when you know what you want to accomplish and need step-by-step instructions.
### 📖 Reference (Information-Oriented)
**Purpose**: Look up technical details
**For**: Users who need precise information
- **[File Structure](reference/file-structure.md)** - Complete directory layout and file conventions
- **[Metadata](reference/metadata.md)** - All available metadata fields and their usage
- **[Templates](reference/templates.md)** - Template types and available variables
- **[Configuration](reference/configuration.md)** - Configuration options and format
- **[CSS Variables](reference/css-variables.md)** - All CSS custom properties for styling
Consult these documents when you need to look up specific technical details or API information.
### 💡 Explanation (Understanding-Oriented)
**Purpose**: Understand concepts and design decisions
**For**: Users who want to understand the "why"
- **[Philosophy](explanation/philosophy.md)** - Design principles and thinking behind FolderWeb
- **[Architecture](explanation/architecture.md)** - How FolderWeb works under the hood
Read these to gain deeper understanding of FolderWeb's design and architecture.
## 🚀 Quick Start
```bash
# 1. Create project directory
mkdir my-website && cd my-website
# 2. Copy framework files
cp -r /path/to/folderweb/app ./app
# 3. Create your first page
mkdir content
echo "# Welcome" > content/index.md
# 4. Start development server
php -S localhost:8000 -t . app/router.php
# 5. Open http://localhost:8000
```
**Next**: Follow the complete [Getting Started Tutorial](tutorial/00-getting-started.md)
## 🎯 Common Tasks
Quick links to frequently needed guides:
| Task | Guide |
|------|-------|
| Create a custom template | [Custom Templates](how-to/custom-templates.md) |
| Change colors and fonts | [Custom Styles](how-to/custom-styles.md) |
| Add multiple languages | [Multi-Language Sites](how-to/multi-language.md) |
| Configure page metadata | [Working with Metadata](how-to/working-with-metadata.md) |
| Look up all metadata fields | [Metadata Reference](reference/metadata.md) |
| Find template variables | [Templates Reference](reference/templates.md) |
| Understand file organization | [File Structure Reference](reference/file-structure.md) |
## 💡 Key Concepts
- **File-based routing**: Your folder structure defines your URL structure
- **Template fallback**: Custom templates automatically override defaults
- **Content types**: Single-file pages, multi-file pages, or list views
- **Language support**: Built-in multi-language with URL prefixes
- **Metadata control**: Configure behavior with simple INI files
- **No build process**: Save and refresh - see changes immediately
## 📋 Requirements
- **PHP**: 8.4 or higher
- **Web server**: Apache, Nginx, or PHP's built-in server
- **Extensions**: Standard PHP (no special extensions required)
## 🗂️ Documentation Files
### Complete File List
```
docs/
├── README.md # This file
├── index.md # Documentation homepage
├── tutorial/
│ └── 00-getting-started.md # Step-by-step tutorial
├── how-to/
│ ├── custom-templates.md # Override templates
│ ├── custom-styles.md # Customize CSS
│ ├── multi-language.md # Multi-language setup
│ └── working-with-metadata.md # Metadata usage
├── reference/
│ ├── file-structure.md # Directory layout
│ ├── metadata.md # Metadata fields
│ ├── templates.md # Template reference
│ ├── configuration.md # Config options
│ └── css-variables.md # CSS customization
└── explanation/
├── philosophy.md # Design principles
└── architecture.md # Technical architecture
```
## 📖 Reading Paths
Choose your path based on your needs:
### Path 1: Complete Beginner
1. [Getting Started Tutorial](tutorial/00-getting-started.md)
2. [Custom Styles](how-to/custom-styles.md)
3. [Working with Metadata](how-to/working-with-metadata.md)
4. [Philosophy](explanation/philosophy.md)
### Path 2: Experienced Developer
1. [Philosophy](explanation/philosophy.md)
2. [Architecture](explanation/architecture.md)
3. [File Structure Reference](reference/file-structure.md)
4. Browse How-To Guides as needed
### Path 3: Specific Task
1. Find your task in [How-To Guides](how-to/)
2. Consult [Reference](reference/) for details
3. Return to task completion
## 🤔 Getting Help
### Documentation Not Enough?
1. **Check the code**: FolderWeb is deliberately simple - reading the source is encouraged
2. **Review examples**: Look at the demo content in `/app/default/content/`
3. **Test locally**: Experiment with a test site to understand behavior
### Common Issues
| Problem | Solution |
|---------|----------|
| Styles not loading | Hard refresh browser (Ctrl+Shift+R) |
| 404 errors | Verify folder exists with content files |
| Language not working | Check `available` in config.ini |
| Metadata not showing | Validate INI syntax with PHP parser |
| Custom template ignored | Ensure file is in `/custom/templates/` |
## 🌟 Philosophy Highlights
FolderWeb embraces:
- **Simplicity**: Just enough, nothing more
- **Longevity**: Works today, works in 2035
- **Transparency**: Readable code, clear behavior
- **Files**: Your content, fully portable
- **No build**: Save and refresh workflow
Read the complete [Philosophy](explanation/philosophy.md) to understand FolderWeb's design principles.
## 📝 Contributing to Documentation
Documentation improvements are welcome:
- Fix typos or unclear explanations
- Add missing examples
- Improve existing guides
- Suggest new how-to guides
Keep documentation:
- Clear and concise
- Accurate and tested
- Organized according to Diataxis principles
## 🔗 External Resources
- [Diataxis Framework](https://diataxis.fr/) - Documentation organization system
- [PHP 8.4 Documentation](https://www.php.net/manual/en/) - PHP reference
- [Markdown Guide](https://www.markdownguide.org/) - Markdown syntax
- [MDN Web Docs](https://developer.mozilla.org/) - HTML and CSS reference
---
**Start here**: [Getting Started Tutorial](tutorial/00-getting-started.md)
**Main index**: [Documentation Index](index.md)

View file

@ -1,739 +0,0 @@
# Architecture
Understanding how FolderWeb works under the hood.
## High-Level Overview
FolderWeb follows a simple request-response flow:
```
HTTP Request
router.php (entry point)
Parse request path
Find content files
Determine content type (page/list/file)
Render content
Wrap in templates
HTTP Response
```
## Core Components
### 1. Router (`app/router.php`)
**Purpose**: Entry point for all requests, determines what to serve
**Responsibilities**:
- Receive HTTP requests
- Check for root-level assets (`/custom/assets/`)
- Parse request path
- Dispatch to appropriate renderer
- Handle redirects (trailing slashes)
- Serve 404 for missing content
**Key Flow**:
```php
// 1. Check for root-level assets
if (file_exists("/custom/assets/$path")) {
serve_static_file();
}
// 2. Empty path = home page (render all root content files)
if (empty($path)) {
render_all_files_in_root();
}
// 3. Parse request path
$result = parseRequestPath($ctx);
// 4. Handle based on type
match($result['type']) {
'page' => renderMultipleFiles(...),
'file' => renderFile(...),
'directory' => renderListView(...),
'not_found' => show_404()
};
```
**Location**: `/app/router.php` (lines 1-100+)
### 2. Content Discovery (`app/content.php`)
**Purpose**: Find and parse content files and directories
**Key Functions**:
#### `parseRequestPath($ctx)`
Analyzes request path and determines content type.
**Returns**:
```php
[
'type' => 'page' | 'file' | 'directory' | 'not_found',
'path' => '/full/system/path',
'files' => [...], // For page type
// ... other data
]
```
**Logic**:
1. Resolve translated slugs to real paths
2. Check if path exists
3. If directory:
- Has subdirectories? → `type: 'directory'` (list view)
- Has content files only? → `type: 'page'` (multi-file)
4. If matches file? → `type: 'file'`
5. Otherwise → `type: 'not_found'`
#### `findAllContentFiles($dir, $lang, $defaultLang, $availableLangs)`
Scans directory for content files.
**Process**:
1. Read directory contents
2. Filter for valid extensions (`.md`, `.html`, `.php`)
3. Parse filenames for language suffix
4. Filter by current language:
- Show `.{lang}.ext` files for that language
- Show default files (no suffix) only if no language variant
5. Sort alphanumerically
6. Return array of file paths
**Example**:
```php
// Directory contains:
// - index.md
// - index.no.md
// - about.md
// English request (lang=en, default=en):
findAllContentFiles() → ['index.md', 'about.md']
// Norwegian request (lang=no):
findAllContentFiles() → ['index.no.md', 'about.md']
```
#### `loadMetadata($dirPath, $lang, $defaultLang)`
Loads and merges metadata for a directory.
**Process**:
1. Check for `metadata.ini` in directory
2. Parse INI file with sections
3. Start with base values
4. Override with language-specific section if exists
5. Return merged array
**Example**:
```ini
title = "About"
summary = "Learn about us"
[no]
title = "Om"
summary = "Lær om oss"
```
For Norwegian request:
```php
loadMetadata(..., 'no', 'en') → [
'title' => 'Om', // Overridden
'summary' => 'Lær om oss' // Overridden
]
```
#### `resolveTranslatedPath($ctx, $requestPath)`
Maps translated slugs back to real directory names.
**Example**:
```ini
; In content/about/metadata.ini:
[no]
slug = "om-oss"
```
Request to `/no/om-oss/` resolves to `content/about/`.
**Process**:
1. Split path into segments
2. For each segment:
- Load metadata of parent directory
- Check if any subdirectory has matching translated slug
- Replace segment with real directory name
3. Return resolved path
### 3. Rendering Engine (`app/rendering.php`)
**Purpose**: Convert content to HTML and wrap in templates
**Key Functions**:
#### `renderContentFile($filePath)`
Converts a single content file to HTML.
**Process**:
```php
switch (extension) {
case 'md':
return Parsedown->text(file_contents);
case 'html':
return file_contents;
case 'php':
ob_start();
include $filePath; // $ctx available
return ob_get_clean();
}
```
#### `renderFile($ctx, $filePath)`
Renders single file wrapped in templates.
**Process**:
1. Convert file to HTML
2. Load metadata
3. Wrap in page template
4. Wrap in base template
5. Return HTML
#### `renderMultipleFiles($ctx, $filePaths, $pageDir)`
Renders multiple files as single page.
**Process**:
1. Convert each file to HTML
2. Concatenate HTML (in order)
3. Load metadata
4. Wrap in page template
5. Wrap in base template
6. Return HTML
**Used for**: Multi-file pages (documentation, long articles)
#### `renderTemplate($ctx, $content, $statusCode = 200)`
Wraps content in base template.
**Process**:
1. Extract variables for template
2. Set HTTP status code
3. Include base template
4. Return HTML
**Variables provided**:
- `$content` - Rendered HTML
- `$ctx` - Context object
- `$currentLang`, `$navigation`, `$homeLabel`, etc.
### 4. Context Object (`app/context.php`)
**Purpose**: Immutable request context with computed properties
**Implementation**:
```php
readonly class Context {
public function __construct(
public private(set) string $contentDir,
public private(set) string $currentLang,
// ... other properties
) {}
// Computed property (PHP 8.4 hook)
public string $langPrefix {
get => $this->currentLang !== $this->defaultLang
? "/{$this->currentLang}"
: '';
}
// Lazy-loaded computed property
public array $navigation {
get => buildNavigation($this);
}
}
```
**Benefits**:
- **Immutability**: Cannot be changed after creation
- **Type safety**: All properties typed
- **Computed values**: Calculated on-demand
- **No globals**: Passed explicitly
**Creation**:
```php
$ctx = createContext();
```
This function:
1. Loads configuration
2. Extracts language from URL
3. Determines content directory
4. Resolves template paths
5. Returns readonly Context object
### 5. Configuration (`app/config.php`)
**Purpose**: Load and merge configuration
**Process**:
1. Parse `/app/config.ini` (defaults)
2. Parse `/custom/config.ini` if exists
3. Merge arrays (custom overrides defaults)
4. Extract language settings
5. Validate configuration
**Configuration Used**:
```ini
[languages]
default = "en"
available = "en,no,fr"
```
### 6. Helper Functions (`app/helpers.php`)
**Purpose**: Utility functions used throughout
**Key Helpers**:
| Function | Purpose |
|----------|---------|
| `resolveTemplate($name, $type)` | Find custom or default template |
| `getSubdirectories($dir)` | List subdirectories only |
| `extractTitle($filePath, $lang, $defaultLang)` | Extract H1 from content |
| `formatNorwegianDate($date)` | Format date as "2. november 2025" |
| `extractDateFromFolder($name)` | Parse date from folder name |
| `findCoverImage($dir)` | Locate cover image |
| `findPdfFile($dir)` | Find first PDF |
### 7. Static File Server (`app/static.php`)
**Purpose**: Serve CSS, fonts, and other static assets
**Process**:
1. Validate path (prevent directory traversal)
2. Resolve real path
3. Check file exists and is readable
4. Determine MIME type
5. Set headers
6. Output file contents
**Routes**:
- `/app/styles/base.css` → Custom or default CSS
- `/app/default-styles/base.css` → Default CSS
- `/custom/fonts/*` → Custom fonts
## Data Flow
### Request Flow Diagram
```
1. HTTP Request: /blog/2025-11-02-post/
2. router.php receives request
3. createContext()
├─ Load config
├─ Extract language from URL
├─ Determine content directory
└─ Return Context object
4. parseRequestPath($ctx)
├─ resolveTranslatedPath() - map slug to real path
├─ Check path exists
├─ findAllContentFiles() - scan for content
└─ Return ['type' => 'file', 'path' => '...']
5. renderFile($ctx, $filePath)
├─ renderContentFile() - convert to HTML
├─ loadMetadata() - get metadata
├─ Apply page template
└─ Apply base template
6. HTTP Response: HTML
```
### List View Flow
```
1. Request: /blog/
2. parseRequestPath() → type: 'directory'
3. Load directory metadata
├─ Get page_template setting
└─ Get other directory metadata
4. getSubdirectories() - find all subdirs
5. For each subdirectory:
├─ loadMetadata() - get title, date, summary
├─ findCoverImage() - locate cover
├─ findPdfFile() - locate PDF
└─ Build item array
6. Render list template with $items
7. Wrap in base template
8. Return HTML
```
## File Organization
### Separation of Concerns
```
app/
├── router.php # Entry point, request handling
├── content.php # Content discovery, parsing
├── rendering.php # HTML generation, templates
├── context.php # Request context
├── config.php # Configuration loading
├── helpers.php # Utility functions
├── constants.php # Constants (extensions)
└── static.php # Static file serving
```
Each file has a single responsibility.
### Template Resolution
Templates use fallback chain:
```
1. /custom/templates/{name}.php
↓ (if not found)
2. /app/default/templates/{name}.php
```
**Implementation**:
```php
function resolveTemplate($name, $type = 'templates') {
$custom = __DIR__ . "/../custom/$type/$name";
$default = __DIR__ . "/default/$type/$name";
return file_exists($custom) ? $custom : $default;
}
```
This pattern applies to:
- Templates
- Styles
- Languages
- Any overridable resource
## Language Handling
### URL Structure
```
/ → Default language
/no/ → Norwegian
/fr/page/ → French page
```
### Language Extraction
From URL path:
```php
// Request: /no/blog/post/
$segments = explode('/', trim($path, '/'));
$firstSegment = $segments[0] ?? '';
if (in_array($firstSegment, $availableLangs)) {
$currentLang = $firstSegment;
$pathWithoutLang = implode('/', array_slice($segments, 1));
} else {
$currentLang = $defaultLang;
$pathWithoutLang = $path;
}
```
### Content Filtering
Files with language suffixes (`.{lang}.ext`) are filtered:
```php
// Parse filename
$parts = explode('.', $filename);
$lastPart = $parts[count($parts) - 2] ?? null;
// Check if second-to-last part is a language
if (in_array($lastPart, $availableLangs)) {
$fileLang = $lastPart;
} else {
$fileLang = $defaultLang;
}
// Include file if:
// - It matches current language, OR
// - It's default language AND no specific variant exists
```
## Navigation Building
### Process
```php
function buildNavigation($ctx) {
$items = [];
// 1. Scan content root for directories
$dirs = getSubdirectories($ctx->contentDir);
// 2. For each directory
foreach ($dirs as $dir) {
// Load metadata
$metadata = loadMetadata($dir, $ctx->currentLang, $ctx->defaultLang);
// Skip if menu = false
if (!($metadata['menu'] ?? false)) continue;
// Build item
$items[] = [
'title' => $metadata['title'] ?? basename($dir),
'url' => $ctx->langPrefix . '/' . basename($dir) . '/',
'order' => $metadata['menu_order'] ?? 999
];
}
// 3. Sort by menu_order
usort($items, fn($a, $b) => $a['order'] <=> $b['order']);
return $items;
}
```
### Caching
Navigation is a computed property, calculated once per request:
```php
public array $navigation {
get => buildNavigation($this);
}
```
PHP memoizes the result automatically.
## Performance Characteristics
### Time Complexity
| Operation | Complexity | Notes |
|-----------|------------|-------|
| Route resolution | O(1) | Direct file checks |
| Content file scan | O(n) | n = files in directory |
| Metadata loading | O(1) | Single file read |
| Template rendering | O(m) | m = content size |
| Navigation build | O(d) | d = top-level directories |
### Space Complexity
- **Memory**: O(c) where c = content size
- **No caching**: Each request independent
- **Stateless**: No session storage
### Optimization Points
1. **OPcache**: PHP bytecode caching (biggest impact)
2. **Web server cache**: Serve cached HTML
3. **Reverse proxy**: Varnish, Cloudflare
4. **Minimize file reads**: Context created once per request
## Security Architecture
### Path Validation
Multiple layers prevent directory traversal:
```php
// 1. Remove .. segments
$path = str_replace('..', '', $path);
// 2. Resolve to real path
$realPath = realpath($path);
// 3. Ensure within content directory
if (!str_starts_with($realPath, $contentDir)) {
return 404;
}
// 4. Check readable
if (!is_readable($realPath)) {
return 404;
}
```
### Output Escaping
All user-generated content escaped:
```php
<?= htmlspecialchars($metadata['title']) ?>
```
This prevents XSS attacks.
### MIME Type Validation
Static files served with correct MIME types:
```php
$mimeTypes = [
'css' => 'text/css',
'woff2' => 'font/woff2',
'jpg' => 'image/jpeg',
// ...
];
header('Content-Type: ' . $mimeTypes[$extension]);
```
## Error Handling
### HTTP Status Codes
- **200 OK**: Successful content render
- **301 Moved Permanently**: Missing trailing slash
- **404 Not Found**: Content doesn't exist
### 404 Handling
When content not found:
```php
renderTemplate($ctx, '<h1>404 Not Found</h1>', 404);
```
Base template rendered with 404 status.
## Extension Points
### Custom Templates
Override any template:
```php
// Framework checks:
$custom = '/custom/templates/list-my-custom.php';
if (file_exists($custom)) {
include $custom;
} else {
include '/app/default/templates/list.php';
}
```
### Custom Functions
Add your own in `/custom/functions.php`:
```php
<?php
// Custom helper functions
function myCustomFunction() {
// Your code
}
```
Include in router:
```php
if (file_exists(__DIR__ . '/../custom/functions.php')) {
require_once __DIR__ . '/../custom/functions.php';
}
```
### Content Files as PHP
`.php` content files have full access:
```php
<?php
// In content/dynamic/index.php
$currentTime = date('Y-m-d H:i:s');
?>
# Dynamic Content
Current time: <?= $currentTime ?>
The language is: <?= $ctx->currentLang ?>
```
## Testing Architecture
### Manual Testing
1. Create test content
2. Start dev server: `php -S localhost:8000 -t . app/router.php`
3. Visit URLs, verify output
4. Check different languages
5. Test edge cases (missing files, invalid paths)
### Automated Testing (Future)
Possible test structure:
```php
// tests/RouterTest.php
test('renders home page', function() {
$response = request('/');
expect($response->status)->toBe(200);
expect($response->body)->toContain('<h1>');
});
test('handles 404', function() {
$response = request('/nonexistent/');
expect($response->status)->toBe(404);
});
```
## Deployment Architecture
### Simple Deployment
```bash
# 1. Clone repository
git clone https://github.com/you/your-site
# 2. Point web server to directory
# Document root: /path/to/site
# Rewrite all requests to: /app/router.php
# 3. Done
```
### With Build Step (Optional)
```bash
# 1. Clone and build
git clone ...
cd site
# 2. Process custom styles (optional)
# E.g., PostCSS, autoprefixer
# 3. Deploy
rsync -av . server:/var/www/site/
```
### Zero-Downtime Deployment
```bash
# 1. Deploy to new directory
rsync -av . server:/var/www/site-new/
# 2. Symlink switch
ln -sfn /var/www/site-new /var/www/site-current
# 3. Web server serves from /var/www/site-current
```
## Related
- [Philosophy](philosophy.md)
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
- [File Structure Reference](../reference/file-structure.md)
- [Template Reference](../reference/templates.md)

View file

@ -1,496 +0,0 @@
# Philosophy
Understanding the principles and thinking behind FolderWeb.
## Core Idea
**Your file system is your content management system.**
FolderWeb embraces the simplest possible approach to web publishing: create a folder structure that mirrors your site hierarchy, drop files into folders, and they immediately become pages. No database, no admin panel, no build process.
## Design Principles
### 1. Just Enough, Nothing More
FolderWeb applies minimal PHP to enable modern conveniences while remaining maintainable for years or decades. Every feature must justify its existence by solving a real problem without creating new complexity.
**What this means:**
- No frameworks that might be abandoned
- No build tools that need maintenance
- No package managers introducing dependencies
- No abstractions unless they provide lasting value
**Example**: Instead of using a routing library, FolderWeb uses PHP's native file functions to map folders to URLs. This will work identically in 2025 and 2035.
### 2. Longevity Over Novelty
Code should outlive trends. FolderWeb prioritizes stability and backward compatibility over cutting-edge features.
**What this means:**
- Standard PHP (no exotic extensions)
- Plain HTML and CSS (no JavaScript required)
- Simple file formats (Markdown, INI)
- Conventions over configuration
**Why it matters**: A site built today should still work in 10 years without updates. The web's foundational technologies (HTML, CSS, PHP) change slowly and maintain backward compatibility.
### 3. Transparent and Readable
You should be able to open any file and immediately understand what it does. No magic, no hidden behavior.
**What this means:**
- Sparse, meaningful comments
- Descriptive function names
- Simple control flow
- Minimal abstraction layers
**Example**: Want to know how templates work? Open `app/rendering.php` and read 100 lines of straightforward PHP. No framework documentation needed.
### 4. Files Are Content
Your content lives in plain text files you can edit with any text editor. You own your content completely.
**What this means:**
- Content is portable (copy files, migrate easily)
- Version control friendly (Git tracks changes)
- No lock-in (files work without FolderWeb)
- Backup-friendly (copy a folder)
**Example**: Your entire site is a folder structure. Zip it, move it to another server, extract it, and it works. No database export/import, no migration scripts.
## What FolderWeb Is
### A File-Based Router
FolderWeb maps your folder structure to URLs:
```
content/blog/2025-11-02-post/ → yoursite.com/blog/2025-11-02-post/
```
That's it. No route definitions, no controllers, no configuration.
### A Content Renderer
FolderWeb converts Markdown to HTML, wraps it in templates, and serves it. Three steps:
1. Find content files
2. Convert to HTML
3. Wrap in template
### A Minimal Template System
FolderWeb provides just enough templating to avoid repetition:
- Base template for site structure
- Page template for content wrapper
- List templates for directory views
All using plain PHP includes. No template language to learn.
### A Convention Framework
FolderWeb establishes conventions that eliminate configuration:
- `metadata.ini` for structured data
- `cover.jpg` for images
- `YYYY-MM-DD-slug` for dates
- `filename.lang.ext` for translations
Learn the conventions once, apply them everywhere.
## What FolderWeb Is Not
### Not a CMS
No admin panel. Edit files directly with your text editor, commit to Git, deploy.
**Why**: Admin panels add complexity, require maintenance, create security risks, and limit what you can do. Text files are simpler and more powerful.
### Not a Static Site Generator
FolderWeb renders pages on request, not at build time.
**Why**: No build step means immediate feedback. Save a file, refresh your browser, see changes. No waiting for builds, no deployment pipelines required (though you can add them).
### Not a JavaScript Framework
Zero JavaScript in the framework. HTML and CSS only.
**Why**: JavaScript adds complexity, breaks without it, requires builds/transpilation, and changes rapidly. HTML and CSS are stable and sufficient for content sites.
### Not Opinionated About Design
FolderWeb provides minimal default styles. Your design is your own.
**Why**: Design trends change. FolderWeb gives you a clean foundation and gets out of the way. Override everything in `/custom/`.
## Design Decisions Explained
### Why PHP 8.4+?
**Modern features without complexity.**
PHP 8.4 provides:
- Readonly classes (immutability)
- Property hooks (computed properties)
- Arrow functions (concise code)
- Modern array functions
- Asymmetric visibility (controlled access)
These features make code clearer without adding dependencies or build steps. PHP 8.4 will be supported for years.
**Tradeoff**: Requires newer PHP, but gains clarity and performance.
### Why No Database?
**Files are simpler.**
Databases add:
- Setup complexity
- Backup complexity
- Migration complexity
- Performance tuning
- Additional failure points
For content sites, files provide:
- Version control integration
- Simple backups (copy folder)
- Portability
- Transparent storage
- No setup required
**When you might need a database**: User-generated content, real-time updates, complex queries, thousands of pages. For those cases, use a different tool.
### Why INI Files for Metadata?
**Simple, readable, PHP-native.**
INI format:
- No parsing library needed (built into PHP)
- Human-readable and editable
- Supports sections for languages
- Familiar format
**Alternatives considered**:
- **YAML**: Requires library, complex syntax
- **JSON**: Not as human-friendly, no comments
- **TOML**: Requires library, less familiar
- **Frontmatter**: Mixes content and metadata
### Why Markdown?
**Readable as plain text, converts to HTML.**
Markdown is:
- Easy to learn (15 minutes)
- Readable without rendering
- Widely supported
- Future-proof (plain text)
- Version control friendly
**Alternatives supported**: HTML (for complex layouts), PHP (for dynamic content).
### Why No Build Tools?
**Immediate feedback, zero setup.**
Build tools add:
- Installation steps
- Configuration files
- Waiting for builds
- Build failures to debug
- Another thing that can break
Without builds:
- Save file → refresh browser → see result
- No setup (just PHP)
- Nothing to configure
- Nothing to break
**Tradeoff**: Can't use Sass, TypeScript, etc. But you can use modern CSS, which is very capable.
### Why Trailing Slashes?
**Consistency and clarity.**
```
/blog/ # Directory (list view)
/blog # Redirects to /blog/
```
Trailing slashes clarify that URLs represent directories, not files. Consistent URLs prevent duplicate content and simplify routing.
### Why Language Prefixes?
**Clear, hackable URLs.**
```
yoursite.com/en/about/ # English
yoursite.com/no/about/ # Norwegian
```
Language in URL:
- User sees current language
- Can manually change URL
- Bookmarkable per language
- SEO-friendly (clear language signal)
**Default language has no prefix** (shorter, cleaner URLs for primary audience).
## Architectural Patterns
### Immutable Context
The `Context` object is readonly (PHP 8.4):
```php
readonly class Context {
public function __construct(
public private(set) string $contentDir,
public private(set) string $currentLang,
// ...
) {}
}
```
**Why**: Prevents accidental mutation, makes code predictable, enables safe sharing of state.
### Computed Properties
Properties calculate values on-demand:
```php
public string $langPrefix {
get => $this->currentLang !== $this->defaultLang
? "/{$this->currentLang}"
: '';
}
```
**Why**: Keeps related logic together, avoids storing derived data, updates automatically.
### Function-Based API
Core functionality exposed as functions, not classes:
```php
renderFile($ctx, $filePath);
findAllContentFiles($dir, $lang, $defaultLang, $availableLangs);
```
**Why**: Simple to understand, easy to test, no object lifecycle to manage.
### Template Fallback
Check custom, fall back to default:
```php
$custom = "/custom/templates/$name.php";
$default = "/app/default/templates/$name.php";
return file_exists($custom) ? $custom : $default;
```
**Why**: Never modify defaults, always override. Clean separation between framework and customization.
## Performance Philosophy
### Performance Through Simplicity
FolderWeb is fast because it does less:
- No database queries
- No heavy frameworks
- No JavaScript parsing
- Minimal file reads
- Direct PHP includes
**Measured performance**: Page load time displayed in footer. Pride in speed through simplicity.
### Caching Strategy
FolderWeb doesn't implement caching. Instead:
- Use OPcache (PHP bytecode cache)
- Use web server caching (Apache/Nginx)
- Use reverse proxy (Varnish, Cloudflare)
- CSS versioned automatically (MD5 hash)
**Why**: Let specialized tools handle caching. FolderWeb focuses on core functionality.
### Optimization Priorities
1. **Avoid work**: Don't render what's not needed
2. **Use native functions**: PHP's file functions are optimized
3. **Minimal abstraction**: Fewer layers = less overhead
## Security Philosophy
### Defense in Depth
Multiple security layers:
- Path validation (prevent directory traversal)
- Realpath checks (resolve symlinks, verify paths)
- Content root enforcement (files must be in document root)
- Output escaping (prevent XSS)
- MIME type validation (proper content types)
### Simplicity Is Security
Less code = smaller attack surface:
- No database (no SQL injection)
- No user input rendering (no XSS in content)
- No file uploads (no upload vulnerabilities)
- No authentication (no auth bypasses)
**For user-generated content**: Use a different tool. FolderWeb is for static content you control.
## Maintenance Philosophy
### Code Should Age Gracefully
FolderWeb aims to require zero maintenance:
- Standard PHP (no exotic dependencies)
- Minimal third-party code (one library: Parsedown)
- Stable APIs (PHP doesn't break backward compatibility)
- No framework upgrades needed
**Goal**: Deploy once, forget about it. Check in years later, it still works.
### Convention Over Configuration
Fewer configuration options = less to maintain:
- File conventions replace config
- Sensible defaults for everything
- Only configure what's necessary (languages)
### Documentation Is Core
Documentation is part of the project:
- Comprehensive reference
- Clear examples
- Explanation of decisions
- How-to guides for common tasks
**Why**: Future you (or someone else) will thank present you.
## When to Use FolderWeb
### Ideal For
- **Blogs**: Write Markdown, publish immediately
- **Documentation**: Multi-file pages, clear structure
- **Portfolios**: Grid layouts, cover images
- **Marketing sites**: Static content, fast loading
- **Personal sites**: Simple, maintainable
- **Long-term projects**: Will work for decades
### Not Ideal For
- **User-generated content**: No database, no auth
- **E-commerce**: Needs dynamic inventory, checkout
- **Social networks**: Real-time updates, complex data
- **SPAs**: JavaScript-heavy, API-driven
- **Large-scale sites**: Thousands of pages (consider static generation)
### Perfect Fit Scenario
You want a blog or content site that:
- You control all content
- Loads fast
- Requires minimal maintenance
- Will work for years without updates
- Integrates with Git workflow
- Gives you complete control
## Comparison to Alternatives
### vs WordPress
**WordPress**: Full-featured CMS, database-driven, plugin ecosystem, admin panel, requires regular updates
**FolderWeb**: File-based, no database, no plugins, no admin, zero maintenance
**Choose WordPress if**: You need plugins, non-technical editors, or a proven ecosystem
**Choose FolderWeb if**: You want simplicity, longevity, and complete control
### vs Jekyll/Hugo (Static Generators)
**Static Generators**: Build at deploy time, generate HTML files, fast serving, requires builds
**FolderWeb**: Renders on request, no build step, immediate feedback, simpler workflow
**Choose Static Generator if**: You want maximum performance, have build infrastructure
**Choose FolderWeb if**: You want immediate feedback, simpler deployment, dynamic capabilities
### vs Laravel/Symfony (PHP Frameworks)
**Frameworks**: Full-stack, MVC architecture, ORM, routing, complex features
**FolderWeb**: Minimal, file-based routing, no ORM, single purpose
**Choose Framework if**: You're building a complex web application
**Choose FolderWeb if**: You're publishing content and want simplicity
## Future Direction
### Stability Over Features
FolderWeb aims to reach "done" status:
- Core functionality complete
- No major features needed
- Focus on documentation and examples
- Bug fixes and security updates only
### Possible Additions
Only if they maintain simplicity:
- More template examples
- Additional default styles (opt-in)
- Performance optimizations
- Better error messages
### Will Never Add
Features that contradict philosophy:
- JavaScript requirement
- Database integration
- Build process
- Admin panel
- User authentication
- Complex plugin system
## Contributing to FolderWeb
### Align With Philosophy
Proposed changes should:
- Maintain simplicity
- Avoid new dependencies
- Work with PHP 8.4+
- Be maintainable long-term
- Solve real problems
### Ideal Contributions
- Bug fixes
- Performance improvements
- Better documentation
- Example templates
- Test cases
- Clarification of existing code
### Before Adding Features
Ask:
1. Can this be solved in userland (custom templates/code)?
2. Does this add complexity for all users?
3. Will this need maintenance in 5 years?
4. Is this truly necessary?
## Conclusion
FolderWeb is deliberately simple. It does one thing—publishes content from files—and does it well. It resists feature creep, embraces constraints, and prioritizes longevity.
This isn't the right tool for every project. But for content sites that value simplicity, maintainability, and longevity, it might be perfect.
The code you write today should work in 2035. FolderWeb is built on that principle.
## Related
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
- [Architecture Overview](architecture.md)
- [File Structure Reference](../reference/file-structure.md)

View file

@ -1,398 +0,0 @@
# How to Customize Styles
This guide shows you how to override the default styles with your own CSS.
## Overview
FolderWeb uses a fallback system for styles:
1. Check `/custom/styles/base.css`
2. Fall back to `/app/default/styles/base.css`
The framework automatically versions CSS files with MD5 hashes for cache busting.
## Quick Start
### Step 1: Create Custom Stylesheet
```bash
mkdir -p custom/styles
touch custom/styles/base.css
```
### Step 2: Override CSS Variables
The easiest way to customize is to override CSS custom properties:
```css
:root {
--color-primary: oklch(0.65 0.20 30); /* Orange */
--color-secondary: oklch(0.50 0.18 30); /* Dark orange */
--color-light: oklch(0.98 0.01 30); /* Warm white */
--color-grey: oklch(0.40 0 0); /* Grey */
--font-body: "Helvetica Neue", Arial, sans-serif;
--font-heading: "Georgia", serif;
--spacing-unit: 1.5rem;
--border-radius: 8px;
}
```
### Step 3: Test Your Changes
Refresh your browser. If changes don't appear, do a hard refresh (Ctrl+Shift+R or Cmd+Shift+R).
## Available CSS Variables
### Colors
```css
--color-primary: oklch(0.65 0.15 250); /* Primary blue */
--color-secondary: oklch(0.50 0.12 250); /* Dark blue */
--color-light: oklch(0.97 0.01 250); /* Off-white */
--color-grey: oklch(0.37 0 0); /* Dark grey */
```
**Note**: FolderWeb uses OKLCH colors for perceptually uniform color spaces. You can also use hex, rgb, or hsl if preferred.
### Typography
```css
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-heading: Georgia, "Times New Roman", serif;
--font-size-base: 1.125rem; /* 18px */
--font-size-small: 0.875rem; /* 14px */
--line-height-base: 1.6;
--line-height-heading: 1.2;
```
### Spacing
```css
--spacing-unit: 1.5rem; /* Base spacing (24px) */
--spacing-small: 0.75rem; /* 12px */
--spacing-large: 3rem; /* 48px */
```
### Layout
```css
--max-width: 70rem; /* Content max-width */
--border-radius: 4px; /* Corner rounding */
```
## Adding Custom Fonts
### Step 1: Add Font Files
```bash
mkdir -p custom/fonts
# Copy your .woff2 files here
cp ~/Downloads/MyFont-Regular.woff2 custom/fonts/
cp ~/Downloads/MyFont-Bold.woff2 custom/fonts/
```
### Step 2: Declare Font Faces
In `custom/styles/base.css`:
```css
@font-face {
font-family: 'MyFont';
src: url('/custom/fonts/MyFont-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'MyFont';
src: url('/custom/fonts/MyFont-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
:root {
--font-body: 'MyFont', sans-serif;
}
```
**Note**: Font files are automatically served by FolderWeb's static file handler.
## Page-Specific Styling
FolderWeb adds dynamic CSS classes to the `<body>` element:
```html
<body class="section-blog page-2025-11-02-my-post">
```
Use these for targeted styling:
```css
/* Style all blog pages */
.section-blog {
--color-primary: oklch(0.60 0.15 150); /* Green for blog */
}
/* Style a specific page */
.page-about {
font-size: 1.25rem;
}
/* Combine for precision */
.section-docs.page-installation {
background: var(--color-light);
}
```
## Responsive Design
FolderWeb uses modern CSS features for responsiveness. Use `clamp()` for fluid typography:
```css
:root {
--font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
--spacing-unit: clamp(1rem, 0.8rem + 1vw, 2rem);
}
h1 {
font-size: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}
```
Use container queries for component responsiveness:
```css
.card-grid {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--spacing-unit);
}
@container (min-width: 600px) {
.card {
padding: var(--spacing-large);
}
}
```
## Dark Mode
Add a dark mode using CSS custom properties and media queries:
```css
:root {
--color-bg: oklch(0.97 0.01 250);
--color-text: oklch(0.20 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: oklch(0.20 0 0);
--color-text: oklch(0.95 0 0);
--color-light: oklch(0.25 0 0);
--color-primary: oklch(0.70 0.15 250);
}
}
body {
background: var(--color-bg);
color: var(--color-text);
}
```
## List Template Styling
Style the different list templates:
### Grid Layout
```css
.list-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-unit);
}
.list-grid article {
border: 1px solid var(--color-light);
border-radius: var(--border-radius);
overflow: hidden;
}
.list-grid img {
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
```
### Card Grid
```css
.card-grid .card {
background: var(--color-light);
padding: var(--spacing-unit);
border-radius: var(--border-radius);
transition: transform 0.2s ease;
}
.card-grid .card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
```
### FAQ Layout
```css
.faq details {
border: 1px solid var(--color-light);
border-radius: var(--border-radius);
padding: var(--spacing-unit);
margin-block-end: var(--spacing-small);
}
.faq summary {
cursor: pointer;
font-weight: 700;
user-select: none;
}
.faq summary:hover {
color: var(--color-primary);
}
```
## Modern CSS Features
FolderWeb encourages use of modern CSS:
### CSS Nesting
```css
.article {
padding: var(--spacing-unit);
& h2 {
color: var(--color-primary);
margin-block-start: var(--spacing-large);
}
& a {
color: var(--color-secondary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
```
### Logical Properties
Use logical properties for internationalization:
```css
/* Instead of: margin-left, margin-right */
article {
margin-inline: auto;
padding-inline: var(--spacing-unit);
padding-block: var(--spacing-large);
}
/* Instead of: text-align: left */
.content {
text-align: start;
}
```
### Modern Color Functions
```css
:root {
/* OKLCH: lightness, chroma, hue */
--primary: oklch(0.65 0.15 250);
/* Adjust lightness for hover */
--primary-hover: oklch(0.55 0.15 250);
/* Or use color-mix */
--primary-light: color-mix(in oklch, var(--primary), white 20%);
}
```
## Performance Tips
### Minimize Custom Styles
Override only what's necessary. The default stylesheet is already optimized.
### Use CSS Variables
Variables reduce repetition and improve maintainability:
```css
/* Good */
:root {
--card-padding: var(--spacing-unit);
}
.card { padding: var(--card-padding); }
.box { padding: var(--card-padding); }
/* Less maintainable */
.card { padding: 1.5rem; }
.box { padding: 1.5rem; }
```
### Avoid `!important`
FolderWeb uses low-specificity selectors, so you shouldn't need `!important`.
## Debugging Styles
### Check Which Stylesheet is Loaded
View source and look for:
```html
<link rel="stylesheet" href="/app/styles/base.css?v=abc123...">
```
If you see `/app/styles/`, your custom stylesheet is being used.
If you see `/app/default-styles/`, the default is being used.
### Browser DevTools
1. Right-click element → Inspect
2. Check "Computed" tab to see which properties are applied
3. Check "Sources" tab to verify your CSS file is loaded
4. Use "Network" tab to ensure CSS isn't cached with old version
### Hard Refresh
Always do a hard refresh after CSS changes:
- **Chrome/Firefox**: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
- **Safari**: Cmd+Option+R
## Complete Override
If you want complete control, you can replace the entire stylesheet. Copy the default:
```bash
cp app/default/styles/base.css custom/styles/base.css
```
Then edit freely. Remember: you're responsible for all styles when you do this.
## Related
- [Custom Templates](custom-templates.md)
- [CSS Reference](../reference/css-variables.md)
- [File Structure Reference](../reference/file-structure.md)

View file

@ -1,288 +0,0 @@
# How to Create Custom Templates
This guide shows you how to override default templates with your own custom designs.
## Overview
FolderWeb uses a template fallback system:
1. Check `/custom/templates/` for custom version
2. Fall back to `/app/default/templates/` if not found
**Important**: Never modify files in `/app/default/` — always create custom versions in `/custom/`.
## Available Templates
- **base.php** - HTML wrapper (header, navigation, footer)
- **page.php** - Single page/article wrapper
- **list.php** - Simple list view (default)
- **list-grid.php** - Grid layout with images
- **list-card-grid.php** - Card grid (supports PDFs, external links)
- **list-faq.php** - Expandable FAQ/Q&A format
## Customizing the Base Template
The base template controls your entire site layout.
### Step 1: Copy the Default
```bash
mkdir -p custom/templates
cp app/default/templates/base.php custom/templates/base.php
```
### Step 2: Edit Your Copy
The base template has access to these variables:
```php
$content // The rendered page content (HTML)
$currentLang // Current language code (e.g., "en", "no")
$navigation // Array of navigation items
$homeLabel // Site title
$translations // Translation strings
$pageTitle // Current page title
$dirName // Parent directory name (for CSS classes)
$pageName // Current page name (for CSS classes)
```
### Example: Add a Custom Header
```php
<header class="site-header">
<div class="contain">
<a href="<?= $ctx->langPrefix ?>/" class="logo">
<img src="/custom/assets/logo.svg" alt="<?= $homeLabel ?>">
</a>
<nav>
<ul>
<?php foreach ($navigation as $item): ?>
<li>
<a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a>
</li>
<?php endforeach; ?>
</ul>
</nav>
</div>
</header>
```
## Customizing the Page Template
The page template wraps individual articles and pages.
### Step 1: Copy the Default
```bash
cp app/default/templates/page.php custom/templates/page.php
```
### Step 2: Customize
Available variables:
```php
$content // Main content HTML
$pageMetadata // Array of metadata (tags, categories, etc.)
$translations // Translation strings
```
### Example: Add Author Information
```php
<article>
<?php if (isset($pageMetadata['author'])): ?>
<div class="author-info">
<p>Written by <?= htmlspecialchars($pageMetadata['author']) ?></p>
</div>
<?php endif; ?>
<?= $content ?>
<?php if (isset($pageMetadata['tags'])): ?>
<div class="tags">
<strong><?= $translations['tags'] ?>:</strong>
<?php foreach ($pageMetadata['tags'] as $tag): ?>
<span class="tag"><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
```
## Creating a Custom List Template
List templates control how directories with subdirectories are displayed.
### Step 1: Create Your Template
```bash
touch custom/templates/list-custom.php
```
### Step 2: Use List Template Variables
All list templates receive:
```php
$items // Array of subdirectories
$metadata // Directory metadata
$pageContent // Optional intro content
$translations // Translation strings
```
Each item in `$items` has:
```php
[
'title' => 'Post Title',
'date' => '2. november 2025',
'url' => '/blog/2025-11-02-post/',
'cover' => '/blog/2025-11-02-post/cover.jpg',
'summary' => 'Brief description',
'pdf' => '/blog/2025-11-02-post/document.pdf',
'redirect' => 'https://external-site.com'
]
```
### Example: Timeline Template
```php
<?php if (!empty($pageContent)): ?>
<div class="page-intro">
<?= $pageContent ?>
</div>
<?php endif; ?>
<div class="timeline">
<?php foreach ($items as $item): ?>
<article class="timeline-item">
<time><?= $item['date'] ?></time>
<h2>
<a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a>
</h2>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
```
### Step 3: Apply Your Template
Create a `metadata.ini` in the directory:
```ini
page_template = "list-custom"
```
## Template Best Practices
### Always Escape Output
Prevent XSS attacks by escaping user-generated content:
```php
<?= htmlspecialchars($item['title']) ?>
```
### Use Short Echo Tags
FolderWeb uses modern PHP, so short tags are always available:
```php
<?= $variable ?> // Good
<?php echo $variable; ?> // Also works, but verbose
```
### Check Before Using
Always check if variables exist:
```php
<?php if (isset($item['cover']) && $item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php endif; ?>
```
### Leverage CSS Classes
The base template adds dynamic classes to `<body>`:
```php
<body class="section-<?= $dirName ?> page-<?= $pageName ?>">
```
Use these for page-specific styling without JavaScript.
## Advanced: Accessing the Context Object
Templates can access the full context object `$ctx`:
```php
<?php
// Available properties:
$ctx->contentDir // Path to content directory
$ctx->currentLang // Current language
$ctx->defaultLang // Default language
$ctx->availableLangs // Array of available languages
$ctx->langPrefix // URL prefix (e.g., "/en" or "")
$ctx->requestPath // Current request path
$ctx->hasTrailingSlash // Boolean
$ctx->navigation // Navigation array (computed property)
$ctx->homeLabel // Site title (computed property)
$ctx->translations // Translation array (computed property)
?>
```
## Example: Breadcrumb Navigation
Add breadcrumbs to your page template:
```php
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="<?= $ctx->langPrefix ?>/">Home</a></li>
<?php
$parts = array_filter(explode('/', trim($ctx->requestPath, '/')));
$path = '';
foreach ($parts as $i => $part):
$path .= '/' . $part;
$isLast = ($i === count($parts) - 1);
?>
<li<?= $isLast ? ' aria-current="page"' : '' ?>>
<?php if ($isLast): ?>
<?= htmlspecialchars($part) ?>
<?php else: ?>
<a href="<?= $ctx->langPrefix . $path ?>/">
<?= htmlspecialchars($part) ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</nav>
```
## Testing Your Templates
1. Clear your browser cache
2. Reload the page
3. Check browser console for errors
4. Validate HTML with W3C validator
## Reverting Changes
To revert to default templates, simply delete your custom version:
```bash
rm custom/templates/base.php
```
FolderWeb will automatically fall back to the default.
## Related
- [Customizing Styles](custom-styles.md)
- [Template Reference](../reference/templates.md)
- [Metadata Reference](../reference/metadata.md)

View file

@ -1,425 +0,0 @@
# How to Create a Multi-Language Site
This guide shows you how to set up and manage a multi-language website with FolderWeb.
## Overview
FolderWeb supports multiple languages through:
- Language prefixes in URLs
- Language-specific content files
- Translated slugs and metadata
- Translation files for UI strings
## Configuration
### Step 1: Configure Available Languages
Create or edit `custom/config.ini`:
```ini
[languages]
default = "en"
available = "en,no,fr"
```
- **default**: The primary language (no URL prefix)
- **available**: Comma-separated list of all supported languages
### Step 2: Create Translation Files
Create translation files for each language in `custom/languages/`:
```bash
mkdir -p custom/languages
```
**English** (`custom/languages/en.ini`):
```ini
home = "Home"
read_more = "Read more"
categories = "Categories"
tags = "Tags"
footer_text = "Made with FolderWeb"
footer_handcoded = "Generated in"
footer_page_time = "ms"
```
**Norwegian** (`custom/languages/no.ini`):
```ini
home = "Hjem"
read_more = "Les mer"
categories = "Kategorier"
tags = "Stikkord"
footer_text = "Laget med FolderWeb"
footer_handcoded = "Generert på"
footer_page_time = "ms"
```
**French** (`custom/languages/fr.ini`):
```ini
home = "Accueil"
read_more = "Lire la suite"
categories = "Catégories"
tags = "Étiquettes"
footer_text = "Créé avec FolderWeb"
footer_handcoded = "Généré en"
footer_page_time = "ms"
```
## URL Structure
With the configuration above:
- **English** (default): `yoursite.com/about/`
- **Norwegian**: `yoursite.com/no/about/`
- **French**: `yoursite.com/fr/about/`
The default language never has a URL prefix.
## Creating Language-Specific Content
### Method 1: Separate Files Per Language
Use language suffixes in filenames: `filename.{lang}.ext`
**Example structure**:
```
content/about/
├── index.md # Default language (English)
├── index.no.md # Norwegian version
└── index.fr.md # French version
```
**Rules**:
- Language-specific files (`.lang.ext`) show only for that language
- Default files (no language suffix) show only if no language variant exists
- Files are automatically filtered based on current language
### Example Content
**content/about/index.md** (English):
```markdown
# About Us
We are a company dedicated to simplicity.
```
**content/about/index.no.md** (Norwegian):
```markdown
# Om Oss
Vi er et selskap dedikert til enkelhet.
```
**content/about/index.fr.md** (French):
```markdown
# À Propos
Nous sommes une entreprise dédiée à la simplicité.
```
Now when users visit:
- `/about/` → Shows English (index.md)
- `/no/about/` → Shows Norwegian (index.no.md)
- `/fr/about/` → Shows French (index.fr.md)
### Method 2: Language-Specific Folders
For blog posts and articles, you can create separate folders:
```
content/blog/
├── 2025-11-01-english-post/
│ └── index.md
├── 2025-11-01-norsk-innlegg/
│ └── index.no.md
└── 2025-11-01-article-francais/
└── index.fr.md
```
## Translated Slugs and Titles
Use `metadata.ini` to provide translated slugs and metadata:
**content/about/metadata.ini**:
```ini
; Default (English)
title = "About Us"
slug = "about"
[no]
title = "Om Oss"
slug = "om-oss"
[fr]
title = "À Propos"
slug = "a-propos"
```
Now URLs become:
- English: `/about/`
- Norwegian: `/no/om-oss/`
- French: `/fr/a-propos/`
The actual folder is still named `about/`, but FolderWeb maps the translated slug to the real folder.
## Blog Posts with Translations
**Structure**:
```
content/blog/
└── 2025-11-02-my-post/
├── index.md
├── index.no.md
├── index.fr.md
├── cover.jpg
└── metadata.ini
```
**metadata.ini**:
```ini
; Default language
title = "My First Post"
summary = "An introduction to multilingual blogging."
date = "2025-11-02"
[no]
title = "Mitt Første Innlegg"
summary = "En introduksjon til flerspråklig blogging."
[fr]
title = "Mon Premier Article"
summary = "Une introduction au blogging multilingue."
```
**Important**: Date is global, cover image is shared across languages.
## Navigation and Menus
Navigation is automatically built with translations. In `metadata.ini` for each top-level directory:
**content/blog/metadata.ini**:
```ini
menu = true
menu_order = 1
title = "Blog"
[no]
title = "Blogg"
[fr]
title = "Blog"
```
**content/about/metadata.ini**:
```ini
menu = true
menu_order = 2
title = "About"
[no]
title = "Om"
[fr]
title = "À Propos"
```
Navigation automatically includes language prefix in URLs.
## Using Translations in Templates
### In Default Templates
Translations are automatically available as `$translations` array:
```php
<a href="<?= $ctx->langPrefix ?>/">
<?= $translations['home'] ?>
</a>
<p><?= $translations['footer_text'] ?></p>
```
### In Custom Templates
Access translations the same way:
```php
<button><?= $translations['read_more'] ?></button>
```
### Access Current Language
```php
<?php if ($ctx->currentLang === 'no'): ?>
<p>Dette er norsk innhold.</p>
<?php else: ?>
<p>This is English content.</p>
<?php endif; ?>
```
## Language Switcher
Create a language switcher in your custom base template:
```php
<nav class="language-switcher">
<?php foreach ($ctx->availableLangs as $lang): ?>
<?php
$url = $lang === $ctx->defaultLang
? '/' . trim($ctx->requestPath, '/')
: '/' . $lang . '/' . trim($ctx->requestPath, '/');
$current = $lang === $ctx->currentLang;
?>
<a href="<?= $url ?>"
<?= $current ? 'aria-current="true"' : '' ?>
class="<?= $current ? 'active' : '' ?>">
<?= strtoupper($lang) ?>
</a>
<?php endforeach; ?>
</nav>
```
Style it:
```css
.language-switcher {
display: flex;
gap: 0.5rem;
}
.language-switcher a {
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius);
text-decoration: none;
}
.language-switcher a.active {
background: var(--color-primary);
color: white;
}
```
## List Views with Multiple Languages
When displaying blog listings, FolderWeb automatically filters items by language:
```
content/blog/
├── 2025-11-01-english-article/
│ └── index.md # Shows in English
├── 2025-11-02-norsk-artikkel/
│ └── index.no.md # Shows only in Norwegian
└── 2025-11-03-universal/
├── index.md # Shows in English
├── index.no.md # Shows in Norwegian
└── index.fr.md # Shows in French
```
When viewing `/blog/`:
- Shows "english-article" and "universal"
When viewing `/no/blog/`:
- Shows "norsk-artikkel" and "universal"
When viewing `/fr/blog/`:
- Shows only "universal"
## Handling Missing Translations
### Default Fallback
If a translation is missing, FolderWeb uses the default language automatically.
### Show Different Content
You can use PHP in your content files:
```php
<?php if ($ctx->currentLang === 'en'): ?>
# Welcome
This page is only in English.
<?php else: ?>
# Under Construction
This page is not yet translated.
<?php endif; ?>
```
## SEO Considerations
### Add hreflang Tags
In your custom base template:
```php
<head>
<!-- ... other head content ... -->
<?php foreach ($ctx->availableLangs as $lang): ?>
<?php
$url = $lang === $ctx->defaultLang
? 'https://yoursite.com/' . trim($ctx->requestPath, '/')
: 'https://yoursite.com/' . $lang . '/' . trim($ctx->requestPath, '/');
?>
<link rel="alternate" hreflang="<?= $lang ?>" href="<?= $url ?>">
<?php endforeach; ?>
<link rel="alternate" hreflang="x-default"
href="https://yoursite.com/<?= trim($ctx->requestPath, '/') ?>">
</head>
```
### Language-Specific Metadata
Add language attributes:
```php
<html lang="<?= $ctx->currentLang ?>">
```
## Testing Your Multi-Language Site
1. **Visit default language**: `http://localhost:8000/about/`
2. **Visit Norwegian**: `http://localhost:8000/no/about/`
3. **Visit French**: `http://localhost:8000/fr/about/`
4. **Check navigation**: Ensure links include language prefix
5. **Test translation strings**: Verify UI text changes per language
6. **Check blog listings**: Confirm language-specific posts appear correctly
## Common Patterns
### Blog in Multiple Languages
Structure:
```
content/blog/
├── metadata.ini # List template config
└── [date]-[slug]/
├── index.{lang}.md # One file per language
├── cover.jpg # Shared assets
└── metadata.ini # Translated metadata
```
### Documentation in Multiple Languages
Structure:
```
content/docs/
├── metadata.ini # Template config
├── 00-intro.md # Default language
├── 00-intro.no.md # Norwegian
├── 01-setup.md
├── 01-setup.no.md
└── ...
```
### Mixed Content Strategy
Not everything needs translation. You can have:
- English-only blog posts (no language suffix)
- Multi-language main pages (with language suffixes)
- Shared images and assets
## Related
- [Metadata Reference](../reference/metadata.md)
- [Configuration Reference](../reference/configuration.md)
- [Template Variables Reference](../reference/templates.md)

Some files were not shown because too many files have changed in this diff Show more