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
This commit is contained in:
Ruben 2025-11-27 21:31:47 +01:00
parent f0d50ff8bf
commit 0e19040473
101 changed files with 1356 additions and 7532 deletions

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

@ -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

@ -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)

View file

@ -1,481 +0,0 @@
# How to Work with Metadata
This guide shows you how to use `metadata.ini` files to control page behavior, appearance, and content.
## What is Metadata?
Metadata provides structured information about your content directories without cluttering your content files. It's stored in `metadata.ini` files using the INI format.
## Basic Metadata File
Create `metadata.ini` in any content directory:
```ini
title = "My Page Title"
date = "2025-11-02"
summary = "A brief description of this page."
```
## Common Metadata Fields
### Title
Controls the displayed title (overrides automatic title extraction):
```ini
title = "Custom Page Title"
```
If not provided, FolderWeb extracts the title from:
1. First H1 heading in content (`# Title` in Markdown)
2. Folder name (as fallback)
### Date
Set an explicit date (overrides folder name date extraction):
```ini
date = "2025-11-02"
```
Format: `YYYY-MM-DD`
FolderWeb automatically formats this in Norwegian style: "2. november 2025"
### Summary
Add a summary for list views:
```ini
summary = "This appears in blog listings and card grids."
```
Summaries are displayed in:
- List views
- Grid layouts
- Card grids
## Navigation Control
### Adding to Menu
```ini
menu = true
menu_order = 1
```
- **menu**: Set to `true` to include in site navigation
- **menu_order**: Controls order (lower numbers appear first)
**Example** - Setting up main navigation:
**content/blog/metadata.ini**:
```ini
menu = true
menu_order = 1
title = "Blog"
```
**content/about/metadata.ini**:
```ini
menu = true
menu_order = 2
title = "About"
```
**content/contact/metadata.ini**:
```ini
menu = true
menu_order = 3
title = "Contact"
```
Result: Navigation shows "Blog", "About", "Contact" in that order.
## Template Control
### Choosing List Template
For directories with subdirectories, control which list template is used:
```ini
page_template = "list-grid"
```
Available templates:
- `list` - Simple list (default)
- `list-grid` - Grid with cover images
- `list-card-grid` - Card-style grid (supports PDFs, external links)
- `list-faq` - Expandable FAQ format
**Example** - Blog with grid layout:
**content/blog/metadata.ini**:
```ini
title = "Blog"
page_template = "list-grid"
```
## External Redirects
Make a directory item link externally (used with `list-card-grid`):
```ini
redirect = "https://example.com"
```
**Example** - Portfolio with external links:
**content/portfolio/project-live-site/metadata.ini**:
```ini
title = "Visit Live Site"
summary = "Check out the deployed project."
redirect = "https://myproject.com"
```
When using the `list-card-grid` template, this creates a card that links to the external URL instead of an internal page.
## Multi-Language Metadata
Use sections for language-specific overrides:
```ini
; Default language values
title = "About Us"
summary = "Learn more about our company."
[no]
title = "Om Oss"
summary = "Lær mer om vårt selskap."
slug = "om-oss"
[fr]
title = "À Propos"
summary = "Découvrez notre entreprise."
slug = "a-propos"
```
Language sections override base values for that language.
### Translated Slugs
The `slug` field in language sections changes the URL:
```ini
[no]
slug = "om-oss"
```
Now the Norwegian version is accessible at `/no/om-oss/` instead of `/no/about/`.
## Custom Metadata Fields
You can add any custom fields you need:
```ini
title = "Article Title"
author = "Jane Doe"
reading_time = "5 min"
difficulty = "intermediate"
featured = true
```
Access these in custom templates:
```php
<?php if (isset($metadata['author'])): ?>
<p class="author">By <?= htmlspecialchars($metadata['author']) ?></p>
<?php endif; ?>
<?php if (isset($metadata['reading_time'])): ?>
<span class="reading-time"><?= htmlspecialchars($metadata['reading_time']) ?></span>
<?php endif; ?>
```
## Arrays in Metadata
INI format supports arrays using repeated keys:
```ini
tags[] = "PHP"
tags[] = "Web Development"
tags[] = "Tutorial"
categories[] = "Programming"
categories[] = "Backend"
```
Access in templates:
```php
<?php if (!empty($metadata['tags'])): ?>
<div class="tags">
<?php foreach ($metadata['tags'] as $tag): ?>
<span class="tag"><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
```
## Boolean Values
Use `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`:
```ini
menu = true
featured = yes
draft = false
```
## Comments
Add comments with `;` or `#`:
```ini
; This is a comment
title = "My Page"
# This is also a comment
date = "2025-11-02"
```
## Metadata Inheritance
Metadata does **not** inherit from parent directories. Each directory needs its own `metadata.ini`.
## Metadata for List Items
When a directory is displayed in a list view, FolderWeb loads its metadata:
**content/blog/2025-11-01-first-post/metadata.ini**:
```ini
title = "My First Blog Post"
date = "2025-11-01"
summary = "An introduction to blogging with FolderWeb."
```
This metadata appears in the blog listing at `/blog/`.
## Complete Example: Blog Setup
### Blog Directory Metadata
**content/blog/metadata.ini**:
```ini
title = "Blog"
menu = true
menu_order = 1
page_template = "list-grid"
[no]
title = "Blogg"
slug = "blogg"
```
### Individual Post Metadata
**content/blog/2025-11-02-web-performance/metadata.ini**:
```ini
title = "Optimizing Web Performance"
date = "2025-11-02"
summary = "Learn techniques to make your website faster."
author = "Jane Developer"
reading_time = "8 min"
tags[] = "Performance"
tags[] = "Web Development"
tags[] = "Optimization"
categories[] = "Technical"
categories[] = "Tutorial"
[no]
title = "Optimalisering av Nettsideytelse"
summary = "Lær teknikker for å gjøre nettsiden din raskere."
```
## Accessing Metadata in Templates
### In Page Templates
```php
<?php
// Page-specific metadata
$metadata = $pageMetadata;
// Access fields
$author = $metadata['author'] ?? 'Unknown';
$tags = $metadata['tags'] ?? [];
?>
<article>
<header>
<h1><?= htmlspecialchars($metadata['title'] ?? '') ?></h1>
<p class="author">By <?= htmlspecialchars($author) ?></p>
</header>
<?= $content ?>
<?php if (!empty($tags)): ?>
<footer class="tags">
<?php foreach ($tags as $tag): ?>
<span><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</footer>
<?php endif; ?>
</article>
```
### In List Templates
Each item in `$items` includes its metadata:
```php
<?php foreach ($items as $item): ?>
<article>
<h2>
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h2>
<?php if ($item['date']): ?>
<time><?= $item['date'] ?></time>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<?php if ($item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php endif; ?>
<?php if ($item['pdf']): ?>
<a href="<?= $item['pdf'] ?>" download>Download PDF</a>
<?php endif; ?>
<?php if ($item['redirect']): ?>
<a href="<?= $item['redirect'] ?>" target="_blank" rel="noopener">
Visit External Link
</a>
<?php endif; ?>
</article>
<?php endforeach; ?>
```
## Debugging Metadata
### Check if Metadata is Loaded
In your template, dump the metadata:
```php
<pre><?php var_dump($metadata); ?></pre>
```
Or for list items:
```php
<pre><?php var_dump($items); ?></pre>
```
### Verify INI Syntax
Use PHP to test your INI file:
```bash
php -r "print_r(parse_ini_file('content/blog/metadata.ini', true));"
```
This shows parsed values and helps identify syntax errors.
## Best Practices
### Use Consistent Field Names
Stick to standard fields for common data:
- `title` for titles
- `date` for dates
- `summary` for summaries
- `author` for authors
### Escape Output
Always escape metadata in templates:
```php
<?= htmlspecialchars($metadata['title']) ?>
```
### Provide Defaults
Use null coalescing for missing fields:
```php
$author = $metadata['author'] ?? 'Anonymous';
$date = $metadata['date'] ?? 'Unknown date';
```
### Keep It Simple
Only add metadata fields you actually use. Don't over-engineer.
### Use Comments
Document non-obvious metadata:
```ini
; Featured articles appear at the top of the homepage
featured = true
; Legacy field kept for backwards compatibility
old_url = "/blog/old-slug/"
```
## Common Patterns
### Blog Post
```ini
title = "Post Title"
date = "2025-11-02"
summary = "Brief description"
author = "Author Name"
tags[] = "tag1"
tags[] = "tag2"
```
### Documentation Page
```ini
title = "API Reference"
menu = true
menu_order = 3
page_template = "list"
```
### Portfolio Item
```ini
title = "Project Name"
date = "2025-11-02"
summary = "Project description"
redirect = "https://live-demo.com"
```
### FAQ Section
```ini
title = "Frequently Asked Questions"
menu = true
menu_order = 4
page_template = "list-faq"
```
## Related
- [Multi-Language Guide](multi-language.md)
- [Custom Templates](custom-templates.md)
- [Metadata Reference](../reference/metadata.md)
- [Template Variables Reference](../reference/templates.md)

View file

@ -1,309 +0,0 @@
# FolderWeb Documentation
Complete documentation for FolderWeb, a minimalist file-based PHP framework for content websites.
## What is FolderWeb?
FolderWeb is a file-based content publishing framework. Drop Markdown files in folders, and they become pages. No database, no build process, no JavaScript required. Just PHP, HTML, and CSS.
**Core principle**: Your file system is your content management system.
## Documentation Structure
This documentation follows the [Diataxis framework](https://diataxis.fr/), organizing content into four types:
### 🎓 Tutorial
**Learning-oriented**: Get started with FolderWeb
- [Getting Started](tutorial/00-getting-started.md) - Build your first site in 10 minutes
**Start here** if you're new to FolderWeb.
### 📋 How-To Guides
**Task-oriented**: Solve specific problems
- [Custom Templates](how-to/custom-templates.md) - Override default templates
- [Custom Styles](how-to/custom-styles.md) - Customize appearance with CSS
- [Multi-Language Sites](how-to/multi-language.md) - Set up multiple languages
- [Working with Metadata](how-to/working-with-metadata.md) - Use metadata.ini files
**Use these** when you need to accomplish a specific task.
### 📖 Reference
**Information-oriented**: Look up technical details
- [File Structure](reference/file-structure.md) - Complete directory layout
- [Metadata](reference/metadata.md) - All metadata fields
- [Templates](reference/templates.md) - Template variables and usage
- [Configuration](reference/configuration.md) - Configuration options
- [CSS Variables](reference/css-variables.md) - Styling customization
**Consult these** when you need precise technical information.
### 💡 Explanation
**Understanding-oriented**: Understand concepts and design
- [Philosophy](explanation/philosophy.md) - Design principles and thinking
- [Architecture](explanation/architecture.md) - How FolderWeb works
**Read these** to understand why FolderWeb works the way it does.
## Quick Links
### Common Tasks
- **Create a page**: Drop `index.md` in a folder
- **Create a blog**: Make a folder with subdirectories
- **Add navigation**: Set `menu = true` in `metadata.ini`
- **Customize look**: Override `/custom/styles/base.css`
- **Use custom template**: Set `page_template = "template-name"` in metadata
- **Multi-language**: Configure languages and add `.{lang}.md` files
### Key Concepts
- **File-based routing**: `content/blog/post/``yoursite.com/blog/post/`
- **Template fallback**: Custom templates override defaults
- **Language prefixes**: `/en/page/` for English, `/no/page/` for Norwegian
- **Metadata inheritance**: None - each directory has its own `metadata.ini`
- **Content types**: Single-file, multi-file, or list view
## Quick Start
```bash
# 1. Create project
mkdir my-site && cd my-site
# 2. Copy framework files
cp -r /path/to/folderweb/app ./app
# 3. Create content
mkdir content
echo "# Hello World" > content/index.md
# 4. Start server
php -S localhost:8000 -t . app/router.php
# 5. Visit http://localhost:8000
```
## System Requirements
- **PHP**: 8.4 or higher
- **Web server**: Apache, Nginx, or PHP built-in server
- **Extensions**: Standard PHP (no special extensions needed)
## File Structure Overview
```
project/
├── app/ # Framework (never modify)
│ ├── router.php # Entry point
│ ├── content.php # Content discovery
│ ├── rendering.php # Template rendering
│ └── default/ # Default templates, styles, languages
├── content/ # Your website content
│ ├── index.md # Home page
│ ├── about/ # About page
│ └── blog/ # Blog with posts
└── custom/ # Your customizations
├── templates/ # Custom templates
├── styles/ # Custom CSS
├── languages/ # Custom translations
└── config.ini # Configuration overrides
```
## Core Features
### File-Based Routing
Your folder structure defines your URLs:
```
content/blog/2025-11-02-post/ → /blog/2025-11-02-post/
```
No route configuration needed.
### Multiple Content Types
- **Single-file page**: One file in a directory
- **Multi-file page**: Multiple files combined into one page
- **List view**: Directory with subdirectories becomes a listing
### Template System
Six templates included:
- `base.php` - HTML wrapper
- `page.php` - Page wrapper
- `list.php` - Simple list
- `list-grid.php` - Grid with images
- `list-card-grid.php` - Card grid
- `list-faq.php` - Expandable FAQ
Override any template in `/custom/templates/`.
### Multi-Language Support
Configure languages:
```ini
[languages]
default = "en"
available = "en,no,fr"
```
Create language-specific files:
```
index.md # English (default)
index.no.md # Norwegian
index.fr.md # French
```
URLs automatically prefixed: `/`, `/no/`, `/fr/`
### Metadata System
Control behavior with `metadata.ini`:
```ini
title = "My Page"
date = "2025-11-02"
summary = "Page description"
menu = true
menu_order = 1
page_template = "list-grid"
```
### Modern CSS
Default styles use:
- CSS custom properties (variables)
- CSS nesting
- OKLCH colors
- Grid layouts
- Fluid typography with `clamp()`
- Logical properties
Override in `/custom/styles/base.css`.
## Philosophy Highlights
- **Just enough, nothing more**: Minimal, maintainable code
- **Longevity over novelty**: Works today, works in 2035
- **Files are content**: Portable, version-controllable
- **No JavaScript required**: Pure HTML and CSS
- **No build process**: Immediate feedback
Read the full [Philosophy](explanation/philosophy.md) for more.
## Example Use Cases
### Personal Blog
```
content/
├── index.md # About me
├── blog/ # Blog posts
│ ├── metadata.ini # page_template = "list-grid"
│ ├── 2025-11-01-post/
│ └── 2025-11-02-post/
└── contact/ # Contact page
└── index.md
```
### Documentation Site
```
content/
├── index.md # Introduction
├── getting-started/ # Multi-file tutorial
│ ├── 00-install.md
│ ├── 01-setup.md
│ └── 02-first-steps.md
└── reference/ # API reference
├── metadata.ini # page_template = "list"
├── functions/
└── classes/
```
### Portfolio
```
content/
├── index.md # Homepage
├── projects/ # Project grid
│ ├── metadata.ini # page_template = "list-card-grid"
│ ├── project-1/
│ │ ├── index.md
│ │ ├── cover.jpg
│ │ └── metadata.ini # redirect = "https://project.com"
│ └── project-2/
└── about/
└── index.md
```
## When to Use FolderWeb
### ✅ Ideal For
- Blogs and content sites
- Documentation
- Portfolios
- Marketing sites
- Personal websites
- Projects requiring longevity
### ❌ Not Ideal For
- User-generated content (no database/auth)
- E-commerce (use dedicated platform)
- Social networks (need real-time features)
- JavaScript-heavy SPAs
- Sites with thousands of pages (consider static generators)
## Getting Help
### Documentation
- Follow the [Tutorial](tutorial/00-getting-started.md) step-by-step
- Check [How-To Guides](how-to/) for specific tasks
- Consult [Reference](reference/) for technical details
- Read [Explanation](explanation/) for concepts
### Common Issues
**Styles not loading**: Hard refresh (Ctrl+Shift+R or Cmd+Shift+R)
**404 errors**: Check folder exists and has content files
**Language not working**: Ensure language is in `available` config
**Metadata not appearing**: Verify INI syntax with `parse_ini_file()`
**Templates not found**: Check file exists in `/custom/templates/`
## Contributing
FolderWeb prioritizes stability and simplicity. Contributions should:
- Maintain simplicity
- Avoid dependencies
- Solve real problems
- Be maintainable long-term
## License
Check the project repository for license information.
## Next Steps
1. **New user?** Start with the [Getting Started Tutorial](tutorial/00-getting-started.md)
2. **Need to do something specific?** Browse [How-To Guides](how-to/)
3. **Want technical details?** Explore [Reference Documentation](reference/)
4. **Curious about design?** Read [Philosophy](explanation/philosophy.md) and [Architecture](explanation/architecture.md)
---
**FolderWeb**: Simple, file-based content publishing for the long term.

View file

@ -1,400 +0,0 @@
# Configuration Reference
Complete reference for FolderWeb configuration options.
## Configuration File
**Location**: `/custom/config.ini` (or `/app/config.ini` for defaults)
**Format**: INI format with sections
**Override Behavior**: Custom config values override defaults
## Configuration Sections
### [languages]
Controls language support and defaults.
```ini
[languages]
default = "en"
available = "en,no,fr"
```
#### default
**Type**: String
**Required**: Yes
**Default**: `"en"`
The primary language of your site. This language:
- Has no URL prefix
- Is used as fallback for missing translations
- Should match a translation file in `/custom/languages/` or `/app/default/languages/`
**Examples**:
```ini
default = "en" ; English
default = "no" ; Norwegian
default = "fr" ; French
default = "de" ; German
```
#### available
**Type**: Comma-separated string
**Required**: Yes
**Default**: `"en,no"`
List of all languages supported by your site. Must include the default language.
**Examples**:
```ini
; English only
available = "en"
; English and Norwegian
available = "en,no"
; Multiple languages
available = "en,no,fr,de,es"
```
**URL Structure**:
- Default language: `yoursite.com/page/`
- Other languages: `yoursite.com/fr/page/`, `yoursite.com/de/page/`
## File Structure
### Default Configuration
**Location**: `/app/config.ini`
```ini
[languages]
default = "no"
available = "no,en"
```
**Note**: Never modify `/app/config.ini` directly.
### Custom Configuration
**Location**: `/custom/config.ini`
Create this file to override defaults:
```ini
[languages]
default = "en"
available = "en,fr,de"
```
Only include settings you want to override.
## Configuration Loading
Configuration is loaded in this order:
1. Load `/app/config.ini` (defaults)
2. Load `/custom/config.ini` if exists
3. Merge, with custom values overriding defaults
Example:
**app/config.ini**:
```ini
[languages]
default = "no"
available = "no,en"
```
**custom/config.ini**:
```ini
[languages]
default = "en"
```
**Result**:
```ini
[languages]
default = "en" ; From custom
available = "no,en" ; From default (not overridden)
```
## Complete Configuration Examples
### Single Language Site
```ini
[languages]
default = "en"
available = "en"
```
URLs: All at root level (`/page/`, `/blog/`, etc.)
### Bilingual Site (English/Norwegian)
```ini
[languages]
default = "en"
available = "en,no"
```
URLs:
- English: `/page/`, `/blog/`
- Norwegian: `/no/page/`, `/no/blog/`
### Multilingual Site
```ini
[languages]
default = "en"
available = "en,no,fr,de,es"
```
URLs:
- English (default): `/page/`
- Norwegian: `/no/page/`
- French: `/fr/page/`
- German: `/de/page/`
- Spanish: `/es/page/`
## Language Codes
Use ISO 639-1 two-letter codes:
| Code | Language |
|------|----------|
| `en` | English |
| `no` | Norwegian |
| `fr` | French |
| `de` | German |
| `es` | Spanish |
| `it` | Italian |
| `pt` | Portuguese |
| `nl` | Dutch |
| `sv` | Swedish |
| `da` | Danish |
| `fi` | Finnish |
| `pl` | Polish |
| `ru` | Russian |
| `ja` | Japanese |
| `zh` | Chinese |
| `ko` | Korean |
| `ar` | Arabic |
Full list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
## Related Configuration
### Translation Files
For each language in `available`, create a translation file:
**Pattern**: `/custom/languages/{lang}.ini`
**Example** (with `available = "en,no,fr"`):
```
custom/languages/
├── en.ini
├── no.ini
└── fr.ini
```
See [Translation Reference](translations.md) for details.
### Content Files
Language-specific content uses the same codes:
**Pattern**: `filename.{lang}.ext`
**Examples**:
- `index.md` - Default language
- `index.no.md` - Norwegian
- `index.fr.md` - French
See [Multi-Language Guide](../how-to/multi-language.md) for details.
## Validation
### Check Configuration
Verify configuration is loaded correctly:
**test-config.php**:
```php
<?php
require 'app/config.php';
$ctx = createContext();
echo "Default language: " . $ctx->defaultLang . "\n";
echo "Available languages: " . implode(', ', $ctx->availableLangs) . "\n";
echo "Current language: " . $ctx->currentLang . "\n";
echo "Language prefix: " . $ctx->langPrefix . "\n";
```
Run:
```bash
php test-config.php
```
### Common Errors
**Missing default in available**:
```ini
; Wrong - default must be in available
[languages]
default = "en"
available = "no,fr"
; Correct
[languages]
default = "en"
available = "en,no,fr"
```
**Invalid language codes**:
```ini
; Avoid - use ISO codes
available = "english,norwegian"
; Correct - ISO 639-1 codes
available = "en,no"
```
**Typos in section names**:
```ini
; Wrong
[language]
default = "en"
; Correct
[languages]
default = "en"
```
## Future Configuration Options
FolderWeb is minimal by design. Currently, only language settings are configurable.
Possible future additions:
- Date format preferences
- Timezone settings
- Content directory override
- Cache settings
For now, these are handled through code or conventions.
## Environment-Specific Configuration
To use different configs per environment:
**Option 1: Conditional Loading**
**custom/config.ini**:
```ini
[languages]
default = "en"
available = "en,no"
```
**custom/config.dev.ini**:
```ini
[languages]
default = "en"
available = "en"
```
Modify `app/config.php` to load based on environment.
**Option 2: Separate Deployments**
Use different `custom/config.ini` files per deployment:
- Development: `/custom/config.ini` with dev settings
- Production: Different `/custom/config.ini` with prod settings
## Configuration in Templates
Access configuration through context object:
```php
<!-- Current language -->
<html lang="<?= $ctx->currentLang ?>">
<!-- Available languages -->
<nav class="language-switcher">
<?php foreach ($ctx->availableLangs as $lang): ?>
<a href="/<?= $lang ?>/"><?= strtoupper($lang) ?></a>
<?php endforeach; ?>
</nav>
<!-- Default language check -->
<?php if ($ctx->currentLang === $ctx->defaultLang): ?>
<p>Viewing in default language</p>
<?php endif; ?>
<!-- Language prefix for URLs -->
<a href="<?= $ctx->langPrefix ?>/about/">About</a>
```
## Best Practices
### Keep It Simple
Only configure what's necessary. FolderWeb embraces sensible defaults.
### Match Translation Files
Ensure translation files exist for all languages:
```ini
[languages]
available = "en,no,fr"
```
Requires:
- `custom/languages/en.ini`
- `custom/languages/no.ini`
- `custom/languages/fr.ini`
### Choose Appropriate Default
Your default language should be:
- Your primary audience's language
- The language with most content
- The language you'll maintain long-term
### Document Your Choices
Add comments to explain configuration:
```ini
; Site uses English as primary language (most content)
; Norwegian and French are secondary translations
[languages]
default = "en"
available = "en,no,fr"
```
## Testing Configuration Changes
After changing configuration:
1. **Clear browser cache** (Ctrl+Shift+R or Cmd+Shift+R)
2. **Test default language**: Visit `/`
3. **Test other languages**: Visit `/no/`, `/fr/`, etc.
4. **Check navigation**: Ensure menu links include language prefix
5. **Verify translations**: Check UI strings change per language
6. **Test language switcher**: Confirm switching works
## Related
- [Multi-Language Guide](../how-to/multi-language.md)
- [Translation Reference](translations.md)
- [Metadata Reference](metadata.md)
- [Context Object Reference](templates.md#context-object)

View file

@ -1,538 +0,0 @@
# CSS Variables Reference
Complete reference for all CSS custom properties available in FolderWeb.
## Overview
FolderWeb uses CSS custom properties (variables) for theming. Override these in `/custom/styles/base.css` to customize your site's appearance.
## Color Variables
### Primary Colors
```css
:root {
--color-primary: oklch(0.65 0.15 250);
--color-secondary: oklch(0.50 0.12 250);
--color-light: oklch(0.97 0.01 250);
--color-grey: oklch(0.37 0 0);
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--color-primary` | Blue (OKLCH) | Primary brand color, links, buttons |
| `--color-secondary` | Dark blue (OKLCH) | Secondary accents, hover states |
| `--color-light` | Off-white (OKLCH) | Background, light sections |
| `--color-grey` | Dark grey | Body text, headings |
### OKLCH Color Space
FolderWeb uses OKLCH for perceptually uniform colors:
```css
oklch(lightness chroma hue)
```
- **Lightness**: 0 (black) to 1 (white)
- **Chroma**: 0 (grey) to ~0.4 (vibrant)
- **Hue**: 0-360 degrees
**Examples**:
```css
/* Blue hues (250°) */
--color-primary: oklch(0.65 0.15 250);
/* Orange hues (30°) */
--color-primary: oklch(0.65 0.20 30);
/* Green hues (150°) */
--color-primary: oklch(0.60 0.15 150);
/* Red hues (0°) */
--color-primary: oklch(0.60 0.20 0);
/* Purple hues (300°) */
--color-primary: oklch(0.60 0.18 300);
```
### Alternative Color Formats
You can use hex, rgb, or hsl instead:
```css
:root {
/* Hex */
--color-primary: #4169E1;
--color-secondary: #1E3A8A;
/* RGB */
--color-primary: rgb(65, 105, 225);
--color-secondary: rgb(30, 58, 138);
/* HSL */
--color-primary: hsl(225, 73%, 57%);
--color-secondary: hsl(225, 64%, 33%);
}
```
## Typography Variables
### Font Families
```css
:root {
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-heading: Georgia, "Times New Roman", serif;
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--font-body` | System sans-serif stack | Body text, paragraphs |
| `--font-heading` | Serif stack | Headings (h1-h6) |
**Custom Fonts**:
```css
@font-face {
font-family: 'MyFont';
src: url('/custom/fonts/MyFont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
--font-body: 'MyFont', sans-serif;
}
```
### Font Sizes
```css
:root {
--font-size-base: 1.125rem; /* 18px */
--font-size-small: 0.875rem; /* 14px */
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--font-size-base` | 1.125rem (18px) | Body text size |
| `--font-size-small` | 0.875rem (14px) | Small text, metadata |
**Responsive Sizing**:
```css
:root {
/* Fluid typography */
--font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}
h1 {
font-size: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}
```
### Line Heights
```css
:root {
--line-height-base: 1.6;
--line-height-heading: 1.2;
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--line-height-base` | 1.6 | Body text line height |
| `--line-height-heading` | 1.2 | Heading line height |
## Spacing Variables
```css
:root {
--spacing-unit: 1.5rem; /* 24px */
--spacing-small: 0.75rem; /* 12px */
--spacing-large: 3rem; /* 48px */
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--spacing-unit` | 1.5rem (24px) | Base spacing unit |
| `--spacing-small` | 0.75rem (12px) | Small gaps |
| `--spacing-large` | 3rem (48px) | Large gaps, section spacing |
**Usage**:
```css
.card {
padding: var(--spacing-unit);
margin-block-end: var(--spacing-large);
}
.tag {
padding: var(--spacing-small);
}
```
**Responsive Spacing**:
```css
:root {
--spacing-unit: clamp(1rem, 0.8rem + 1vw, 2rem);
}
```
## Layout Variables
```css
:root {
--max-width: 70rem; /* 1120px */
--border-radius: 4px;
}
```
| Variable | Default | Description |
|----------|---------|-------------|
| `--max-width` | 70rem (1120px) | Content max-width |
| `--border-radius` | 4px | Corner rounding |
**Usage**:
```css
.contain {
max-inline-size: var(--max-width);
margin-inline: auto;
}
.card {
border-radius: var(--border-radius);
}
```
## Complete Variable List
```css
:root {
/* Colors */
--color-primary: oklch(0.65 0.15 250);
--color-secondary: oklch(0.50 0.12 250);
--color-light: oklch(0.97 0.01 250);
--color-grey: oklch(0.37 0 0);
/* Typography */
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-heading: Georgia, "Times New Roman", serif;
--font-size-base: 1.125rem;
--font-size-small: 0.875rem;
--line-height-base: 1.6;
--line-height-heading: 1.2;
/* Spacing */
--spacing-unit: 1.5rem;
--spacing-small: 0.75rem;
--spacing-large: 3rem;
/* Layout */
--max-width: 70rem;
--border-radius: 4px;
}
```
## Customization Examples
### Orange Theme
```css
:root {
--color-primary: oklch(0.65 0.20 30);
--color-secondary: oklch(0.50 0.18 30);
--color-light: oklch(0.97 0.01 30);
}
```
### Dark Mode
```css
@media (prefers-color-scheme: dark) {
:root {
--color-primary: oklch(0.70 0.15 250);
--color-secondary: oklch(0.80 0.12 250);
--color-light: oklch(0.25 0 0);
--color-grey: oklch(0.90 0 0);
}
}
```
### Large Text
```css
:root {
--font-size-base: 1.25rem; /* 20px */
--line-height-base: 1.7;
--spacing-unit: 2rem;
}
```
### Tight Layout
```css
:root {
--max-width: 50rem; /* 800px */
--spacing-unit: 1rem; /* 16px */
--spacing-large: 2rem; /* 32px */
}
```
### Rounded Design
```css
:root {
--border-radius: 12px;
}
```
## Using Variables
### In Your Styles
```css
.card {
background: var(--color-light);
color: var(--color-grey);
padding: var(--spacing-unit);
border-radius: var(--border-radius);
}
.button {
background: var(--color-primary);
color: white;
padding: var(--spacing-small) var(--spacing-unit);
border-radius: var(--border-radius);
}
.button:hover {
background: var(--color-secondary);
}
```
### With Fallbacks
Provide fallbacks for older browsers:
```css
.card {
background: #F5F5F5; /* Fallback */
background: var(--color-light); /* Variable */
}
```
### With calc()
Combine with calculations:
```css
.card {
padding: calc(var(--spacing-unit) * 2);
margin-block-end: calc(var(--spacing-large) - 1rem);
}
```
### With color-mix()
Create variations:
```css
.button {
background: var(--color-primary);
}
.button:hover {
background: color-mix(in oklch, var(--color-primary), black 10%);
}
.button-light {
background: color-mix(in oklch, var(--color-primary), white 80%);
}
```
## Adding Custom Variables
Define your own variables:
```css
:root {
/* Custom color palette */
--color-accent: oklch(0.70 0.15 180);
--color-warning: oklch(0.70 0.20 60);
--color-danger: oklch(0.60 0.20 10);
--color-success: oklch(0.65 0.15 140);
/* Custom spacing */
--spacing-xs: 0.25rem;
--spacing-xl: 4rem;
--spacing-2xl: 6rem;
/* Custom typography */
--font-mono: 'Monaco', 'Courier New', monospace;
--font-size-large: 1.5rem;
--font-size-xlarge: 2rem;
/* Custom layout */
--sidebar-width: 20rem;
--header-height: 4rem;
--content-gap: 2rem;
}
```
Use them:
```css
.sidebar {
width: var(--sidebar-width);
background: var(--color-light);
}
code {
font-family: var(--font-mono);
background: var(--color-accent);
padding: var(--spacing-xs);
}
```
## Scoped Variables
Override variables for specific sections:
```css
/* Global defaults */
:root {
--color-primary: oklch(0.65 0.15 250);
}
/* Blog section uses green */
.section-blog {
--color-primary: oklch(0.60 0.15 150);
}
/* About page uses orange */
.page-about {
--color-primary: oklch(0.65 0.20 30);
}
/* Variables cascade to children */
.section-blog .button {
background: var(--color-primary); /* Green in blog */
}
```
## Responsive Variables
Change variables at breakpoints:
```css
:root {
--spacing-unit: 1rem;
--font-size-base: 1rem;
--max-width: 60rem;
}
@media (min-width: 768px) {
:root {
--spacing-unit: 1.5rem;
--font-size-base: 1.125rem;
--max-width: 70rem;
}
}
@media (min-width: 1200px) {
:root {
--spacing-unit: 2rem;
--font-size-base: 1.25rem;
--max-width: 80rem;
}
}
```
## Browser Support
CSS custom properties are supported in all modern browsers:
- Chrome 49+
- Firefox 31+
- Safari 9.1+
- Edge 15+
For older browsers, provide fallbacks or use PostCSS with custom properties plugin.
## Debugging Variables
Inspect variables in browser DevTools:
1. Right-click element → Inspect
2. Check "Computed" tab
3. Scroll to custom properties section
4. See resolved values
Or log in console:
```javascript
getComputedStyle(document.documentElement).getPropertyValue('--color-primary')
```
## Best Practices
### Use Semantic Names
```css
/* Good - semantic */
--color-primary
--color-text
--color-background
/* Avoid - non-semantic */
--color-blue
--color-444
```
### Group Related Variables
```css
:root {
/* Colors */
--color-primary: ...;
--color-secondary: ...;
/* Typography */
--font-body: ...;
--font-heading: ...;
/* Spacing */
--spacing-unit: ...;
}
```
### Document Your Variables
```css
:root {
/* Brand colors from design system */
--color-primary: oklch(0.65 0.15 250); /* Blue - primary CTA */
--color-secondary: oklch(0.50 0.12 250); /* Dark blue - accents */
/* Layout constraints */
--max-width: 70rem; /* 1120px - content max width */
}
```
### Provide Fallbacks
```css
.card {
background: #F5F5F5;
background: var(--color-light);
}
```
## Related
- [Custom Styles Guide](../how-to/custom-styles.md)
- [Template Reference](templates.md)
- [File Structure Reference](file-structure.md)

View file

@ -1,394 +0,0 @@
# File Structure Reference
Complete reference for FolderWeb's file and directory structure.
## Root Structure
```
project/
├── app/ # Framework core (never modify)
├── content/ # Your website content
├── custom/ # Your customizations
└── .htaccess # Web server configuration (optional)
```
## App Directory (Framework Core)
```
app/
├── router.php # Main entry point and request router
├── content.php # Content discovery and parsing functions
├── rendering.php # Template rendering engine
├── context.php # Context object (readonly class)
├── config.php # Configuration loader
├── helpers.php # Utility functions
├── constants.php # File extension constants
├── static.php # Static file server
├── config.ini # Default configuration
├── default/ # Default files (fallback)
│ ├── templates/ # Default templates
│ │ ├── base.php # HTML wrapper
│ │ ├── page.php # Page wrapper
│ │ ├── list.php # Simple list
│ │ ├── list-grid.php # Grid layout
│ │ ├── list-card-grid.php # Card grid
│ │ └── list-faq.php # FAQ layout
│ ├── styles/ # Default CSS
│ │ └── base.css # Main stylesheet
│ ├── languages/ # Default translations
│ │ ├── en.ini # English
│ │ └── no.ini # Norwegian
│ └── content/ # Demo content (fallback)
└── vendor/ # Third-party libraries
└── Parsedown.php # Markdown parser
```
**Important**: Never modify files in `/app/`. All customization goes in `/custom/`.
## Custom Directory
```
custom/
├── templates/ # Override templates
│ ├── base.php # Custom base template
│ ├── page.php # Custom page template
│ ├── list-*.php # Custom list templates
│ └── [custom].php # Your custom templates
├── styles/ # Override styles
│ └── base.css # Custom stylesheet
├── languages/ # Override translations
│ ├── en.ini # English translations
│ ├── no.ini # Norwegian translations
│ └── [lang].ini # Other languages
├── fonts/ # Custom web fonts
│ └── *.woff2 # Font files
├── assets/ # Root-level assets
│ ├── favicon.ico # Site favicon
│ ├── robots.txt # Robots file
│ ├── logo.svg # Logo
│ └── [any file] # Served at root level
└── config.ini # Configuration overrides
```
## Content Directory
Your content directory contains all your website pages and assets.
### Basic Structure
```
content/
├── index.md # Home page
├── about/ # About page
│ ├── index.md # Page content
│ ├── metadata.ini # Page metadata
│ └── team-photo.jpg # Page asset
├── blog/ # Blog (list view)
│ ├── metadata.ini # Blog configuration
│ ├── 2025-11-01-first-post/
│ │ ├── index.md # Post content
│ │ ├── cover.jpg # Cover image
│ │ └── metadata.ini # Post metadata
│ └── 2025-11-02-second-post/
│ ├── index.md
│ ├── cover.webp
│ └── metadata.ini
└── docs/ # Multi-file page
├── 00-intro.md # Section 1
├── 01-setup.md # Section 2
├── 02-usage.md # Section 3
└── metadata.ini # Page metadata
```
### Content Types
#### Single-File Page
```
content/about/
└── index.md
```
URL: `/about/`
#### Multi-File Page
```
content/docs/
├── 00-intro.md
├── 01-setup.md
└── 02-usage.md
```
URL: `/docs/` (all files render as one page)
#### List View (Directory with Subdirectories)
```
content/blog/
├── metadata.ini
├── 2025-11-01-post/
│ └── index.md
└── 2025-11-02-post/
└── index.md
```
URL: `/blog/` (shows list of posts)
## File Naming Conventions
### Content Files
Supported extensions:
- `.md` - Markdown (parsed with Parsedown)
- `.html` - HTML (included as-is)
- `.php` - PHP (executed with access to `$ctx`)
### Language-Specific Files
Format: `filename.{lang}.ext`
Examples:
- `index.md` - Default language
- `index.no.md` - Norwegian
- `index.fr.md` - French
- `about.en.md` - English
### Date Prefixes
Format: `YYYY-MM-DD-slug`
Examples:
- `2025-11-01-my-post`
- `2025-11-02-another-post`
Dates are automatically extracted and formatted.
### Cover Images
Filename: `cover.{ext}`
Supported formats:
- `cover.jpg`
- `cover.jpeg`
- `cover.png`
- `cover.webp`
- `cover.gif`
Automatically detected in list views.
### PDF Files
Any `.pdf` file in a directory is automatically linked in grid layouts.
### Metadata Files
Filename: `metadata.ini`
Format: INI with optional language sections.
## File Discovery Order
### Content File Priority
For multi-file pages, files are rendered in alphanumerical order:
```
content/docs/
├── 00-intro.md # First
├── 01-setup.md # Second
├── 02-usage.md # Third
└── 99-appendix.md # Last
```
Use numerical prefixes to control order.
### Template Resolution
Templates are resolved with custom fallback:
1. `/custom/templates/{name}.php`
2. `/app/default/templates/{name}.php`
### CSS Resolution
Stylesheets are resolved with custom fallback:
1. `/custom/styles/base.css`
2. `/app/default/styles/base.css`
### Translation Resolution
Translations are resolved with custom fallback:
1. `/custom/languages/{lang}.ini`
2. `/app/default/languages/{lang}.ini`
### Configuration Resolution
Configuration is merged:
1. Load `/app/config.ini`
2. Merge with `/custom/config.ini` if exists
Custom values override defaults.
## URL Mapping
### Basic Mapping
```
/content/about/index.md → /about/
/content/blog/ → /blog/
/content/docs/ → /docs/
```
### Language Prefixes
Default language (no prefix):
```
/content/about/index.md → /about/
```
Other languages (with prefix):
```
/content/about/index.no.md → /no/about/
/content/about/index.fr.md → /fr/about/
```
### Translated Slugs
With metadata slug overrides:
```
content/about/metadata.ini:
[no]
slug = "om-oss"
[fr]
slug = "a-propos"
```
URLs become:
- `/about/` (English)
- `/no/om-oss/` (Norwegian)
- `/fr/a-propos/` (French)
### Trailing Slashes
FolderWeb requires trailing slashes for directories. Missing slashes trigger 301 redirects:
```
/blog → 301 redirect to → /blog/
```
## Special Files and Directories
### System Files (Ignored)
These files are automatically ignored:
- `.htaccess`
- `.git/`
- `.DS_Store`
- `node_modules/`
- Hidden files/directories (starting with `.`)
### Index Files
`index.md`, `index.html`, `index.php` are treated as directory content, not separate routes.
### Metadata Files
`metadata.ini` files are configuration, never rendered as content.
## Asset Serving
### Root-Level Assets
Files in `/custom/assets/` are served at site root:
```
/custom/assets/robots.txt → yoursite.com/robots.txt
/custom/assets/favicon.ico → yoursite.com/favicon.ico
/custom/assets/logo.svg → yoursite.com/logo.svg
```
### Content Assets
Files in content directories are accessible at their directory URL:
```
/content/blog/2025-11-01-post/cover.jpg
→ yoursite.com/blog/2025-11-01-post/cover.jpg
/content/about/team-photo.jpg
→ yoursite.com/about/team-photo.jpg
```
### CSS Files
CSS is served with version hashing:
```
/custom/styles/base.css
→ yoursite.com/app/styles/base.css?v=abc123def456
```
### Font Files
Fonts in `/custom/fonts/` are accessible:
```
/custom/fonts/MyFont.woff2
→ yoursite.com/custom/fonts/MyFont.woff2
```
## File Permissions
### Readable Files
The web server must have read access to:
- All files in `/app/`
- All files in `/content/`
- All files in `/custom/`
### Writable Files
FolderWeb is read-only. No files require write access.
### Security
- Path validation prevents directory traversal
- Files must be within document root
- Realpath checks ensure proper resolution
## Size Limits
- **Read Tool**: Files larger than 50KB are truncated
- **No upload limits**: FolderWeb doesn't handle uploads
- **No execution limits**: Standard PHP limits apply
## Caching
### CSS Versioning
CSS files are versioned with MD5 hash:
```html
<link rel="stylesheet" href="/app/styles/base.css?v=abc123def456">
```
Hash updates when file content changes.
### No Built-in Cache
FolderWeb doesn't implement content caching. Use:
- Web server caching (Apache, Nginx)
- Reverse proxy (Varnish, Cloudflare)
- PHP OPcache for code
## Related
- [Metadata Reference](metadata.md)
- [Configuration Reference](configuration.md)
- [How to Customize Templates](../how-to/custom-templates.md)
- [How to Customize Styles](../how-to/custom-styles.md)

View file

@ -1,625 +0,0 @@
# Metadata Reference
Complete reference for all metadata fields and their usage in `metadata.ini` files.
## File Format
Metadata files use INI format:
```ini
; Comments start with semicolon
key = "value"
array[] = "value1"
array[] = "value2"
[section]
key = "section value"
```
## Standard Fields
### title
**Type**: String
**Used in**: All content types
**Purpose**: Override automatic title extraction
```ini
title = "Custom Page Title"
```
If not provided, FolderWeb extracts title from:
1. First H1 heading (`# Title` in Markdown)
2. Folder name (as fallback)
**Multi-language**:
```ini
title = "English Title"
[no]
title = "Norsk Tittel"
[fr]
title = "Titre Français"
```
### date
**Type**: Date string (YYYY-MM-DD)
**Used in**: Blog posts, articles
**Purpose**: Override automatic date extraction
```ini
date = "2025-11-02"
```
If not provided, FolderWeb extracts date from folder names like `2025-11-02-post-title`.
Dates are automatically formatted in Norwegian style: "2. november 2025"
### summary
**Type**: String
**Used in**: List views
**Purpose**: Brief description for cards and listings
```ini
summary = "A concise description that appears in blog listings."
```
**Multi-language**:
```ini
summary = "English summary"
[no]
summary = "Norsk sammendrag"
```
### menu
**Type**: Boolean
**Used in**: Top-level directories
**Purpose**: Include in site navigation
```ini
menu = true
```
Accepted values: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`
### menu_order
**Type**: Integer
**Used in**: Navigation items
**Purpose**: Control navigation order (lower numbers first)
```ini
menu = true
menu_order = 1
```
### page_template
**Type**: String
**Used in**: Directories with subdirectories
**Purpose**: Choose list template
```ini
page_template = "list-grid"
```
Available values:
- `list` - Simple list (default)
- `list-grid` - Grid with cover images
- `list-card-grid` - Card-style grid
- `list-faq` - Expandable FAQ format
- Any custom template name (without `.php` extension)
### slug
**Type**: String
**Used in**: Language sections
**Purpose**: Translate URL segments
```ini
[no]
slug = "om-oss"
[fr]
slug = "a-propos"
```
The actual folder is `about/`, but URLs become:
- `/about/` (English)
- `/no/om-oss/` (Norwegian)
- `/fr/a-propos/` (French)
### hide_list
**Type**: Boolean
**Used in**: Directories with subdirectories
**Purpose**: Hide list of subfolders and show only page content
```ini
hide_list = true
```
When enabled, directories with subfolders display as regular pages instead of showing a list view. Useful when you need subfolders for organization but want to present a single page to visitors.
Accepted values: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off`
### redirect
**Type**: URL string
**Used in**: List items (with `list-card-grid` template)
**Purpose**: Link to external site instead of internal page
```ini
redirect = "https://example.com"
```
Creates an external link card in card grid layouts.
## Custom Fields
You can add any custom fields for use in your templates:
### Common Custom Fields
```ini
; Author information
author = "Jane Doe"
author_email = "jane@example.com"
author_url = "https://janedoe.com"
; Content metadata
reading_time = "5 min"
difficulty = "intermediate"
featured = true
; Categorization
tags[] = "PHP"
tags[] = "Tutorial"
tags[] = "Web Development"
categories[] = "Programming"
categories[] = "Backend"
; SEO
meta_description = "Complete guide to FolderWeb metadata"
meta_keywords = "metadata, ini, folderweb"
; Social sharing
og_image = "/blog/post/social-card.jpg"
twitter_card = "summary_large_image"
; Version tracking
version = "1.2.0"
last_updated = "2025-11-02"
; Display options
hide_date = true
hide_author = false
hide_list = true
show_toc = true
; External references
github_url = "https://github.com/user/repo"
demo_url = "https://demo.example.com"
download_url = "/files/document.pdf"
```
## Array Fields
Use `[]` syntax for array values:
```ini
tags[] = "PHP"
tags[] = "Web Development"
tags[] = "Tutorial"
authors[] = "Jane Doe"
authors[] = "John Smith"
related_posts[] = "/blog/post-1/"
related_posts[] = "/blog/post-2/"
```
Access in templates:
```php
<?php foreach ($metadata['tags'] as $tag): ?>
<span><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
```
## Boolean Values
Accepted boolean formats:
```ini
; True values
featured = true
featured = 1
featured = yes
featured = on
; False values
draft = false
draft = 0
draft = no
draft = off
```
## Language Sections
Use `[lang]` sections for multi-language overrides:
```ini
; Base values (default language)
title = "About Us"
summary = "Learn about our company"
slug = "about"
; Norwegian overrides
[no]
title = "Om Oss"
summary = "Lær om vårt selskap"
slug = "om-oss"
; French overrides
[fr]
title = "À Propos"
summary = "Découvrez notre entreprise"
slug = "a-propos"
; Fields not overridden inherit base values
```
Language-specific fields override base fields for that language.
## Comments
Use `;` or `#` for comments:
```ini
; This is a comment
title = "My Page"
# This is also a comment
date = "2025-11-02"
; Comments can be on same line as values
menu = true ; Include in navigation
```
## Special Characters
### Quotes
Use quotes for values with special characters:
```ini
; Optional for simple values
title = Simple Title
title = "Simple Title"
; Required for values with spaces at start/end
title = " Padded Title "
; Required for values with special characters
summary = "Use \"quotes\" for nested quotes"
summary = 'Single quotes work too'
```
### Escape Sequences
Standard INI escape sequences:
```ini
; Newline
text = "First line\nSecond line"
; Tab
text = "Indented\ttext"
; Quote
text = "He said \"Hello\""
```
## Metadata Location
### Directory Metadata
Place `metadata.ini` in the directory it describes:
```
content/blog/metadata.ini # Blog configuration
content/about/metadata.ini # About page metadata
```
### Item Metadata
Place `metadata.ini` in each subdirectory:
```
content/blog/2025-11-01-post/metadata.ini # Post metadata
content/blog/2025-11-02-post/metadata.ini # Post metadata
```
## Metadata Scope
Metadata applies only to its directory. **No inheritance** from parent directories.
## Complete Examples
### Blog Configuration
**content/blog/metadata.ini**:
```ini
; Display settings
title = "Blog"
page_template = "list-grid"
; Navigation
menu = true
menu_order = 1
; Multi-language
[no]
title = "Blogg"
slug = "blogg"
[fr]
title = "Blog"
slug = "blog"
```
### Blog Post
**content/blog/2025-11-02-web-performance/metadata.ini**:
```ini
; Basic information
title = "Optimizing Web Performance"
date = "2025-11-02"
summary = "Learn techniques to make your website faster."
; Author information
author = "Jane Developer"
author_url = "https://jane.dev"
; Content metadata
reading_time = "8 min"
difficulty = "intermediate"
featured = true
; Categorization
tags[] = "Performance"
tags[] = "Web Development"
tags[] = "Optimization"
categories[] = "Technical"
categories[] = "Tutorial"
; SEO
meta_description = "Complete guide to web performance optimization"
; Multi-language versions
[no]
title = "Optimalisering av Nettsideytelse"
summary = "Lær teknikker for å gjøre nettsiden din raskere."
[fr]
title = "Optimisation des Performances Web"
summary = "Apprenez à accélérer votre site web."
```
### Documentation Page
**content/docs/metadata.ini**:
```ini
title = "Documentation"
menu = true
menu_order = 2
page_template = "list"
; Custom fields
show_toc = true
github_url = "https://github.com/user/repo"
[no]
title = "Dokumentasjon"
slug = "dokumentasjon"
```
### Portfolio Project
**content/portfolio/project-name/metadata.ini**:
```ini
title = "Project Name"
date = "2025-11-02"
summary = "Brief project description"
; External links
redirect = "https://project-demo.com"
github_url = "https://github.com/user/project"
; Project details
client = "Company Name"
role = "Lead Developer"
technologies[] = "PHP"
technologies[] = "HTML"
technologies[] = "CSS"
[no]
title = "Prosjektnavn"
summary = "Kort prosjektbeskrivelse"
```
### FAQ Section
**content/faq/metadata.ini**:
```ini
title = "Frequently Asked Questions"
menu = true
menu_order = 4
page_template = "list-faq"
[no]
title = "Ofte Stilte Spørsmål"
slug = "oss"
[fr]
title = "Questions Fréquemment Posées"
slug = "faq"
```
## Accessing Metadata in Templates
### In Page Templates
Variable: `$pageMetadata`
```php
<?php
$title = $pageMetadata['title'] ?? 'Untitled';
$author = $pageMetadata['author'] ?? null;
$tags = $pageMetadata['tags'] ?? [];
?>
<article>
<h1><?= htmlspecialchars($title) ?></h1>
<?php if ($author): ?>
<p class="author">By <?= htmlspecialchars($author) ?></p>
<?php endif; ?>
<?= $content ?>
<?php if (!empty($tags)): ?>
<div class="tags">
<?php foreach ($tags as $tag): ?>
<span class="tag"><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
```
### In List Templates
Variable: `$metadata` (directory metadata), `$items` (item metadata)
```php
<!-- Directory metadata -->
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
<!-- Items with their metadata -->
<?php foreach ($items as $item): ?>
<article>
<h2>
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h2>
<?php if ($item['date']): ?>
<time><?= $item['date'] ?></time>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
```
## Validation
### Check Syntax
Test INI file parsing:
```bash
php -r "print_r(parse_ini_file('content/blog/metadata.ini', true));"
```
### Common Errors
**Unquoted special characters**:
```ini
; Wrong
title = Title with: special characters
; Correct
title = "Title with: special characters"
```
**Missing array brackets**:
```ini
; Wrong (only last value kept)
tags = "PHP"
tags = "Tutorial"
; Correct (array created)
tags[] = "PHP"
tags[] = "Tutorial"
```
**Invalid section names**:
```ini
; Wrong
[language.no]
; Correct
[no]
```
## Best Practices
### Always Escape Output
```php
<?= htmlspecialchars($metadata['title']) ?>
```
### Provide Defaults
```php
$author = $metadata['author'] ?? 'Anonymous';
$tags = $metadata['tags'] ?? [];
```
### Check Before Using
```php
<?php if (isset($metadata['author'])): ?>
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
<?php endif; ?>
```
### Use Consistent Field Names
Stick to standard names across your site:
- `author` not `writer` or `by`
- `tags` not `keywords` or `topics`
- `summary` not `description` or `excerpt`
### Document Custom Fields
Add comments explaining non-obvious fields:
```ini
; Featured articles appear at top of homepage
featured = true
; External demo link (overrides internal page)
demo_url = "https://demo.example.com"
```
## Related
- [Multi-Language Guide](../how-to/multi-language.md)
- [Working with Metadata](../how-to/working-with-metadata.md)
- [Template Variables Reference](templates.md)
- [Configuration Reference](configuration.md)

View file

@ -1,608 +0,0 @@
# Template Reference
Complete reference for all templates and available variables in FolderWeb.
## Template System Overview
FolderWeb uses a fallback template system:
1. Check `/custom/templates/{name}.php`
2. Fall back to `/app/default/templates/{name}.php`
Templates are plain PHP files with access to specific variables and the context object.
## Core Templates
### base.php
**Purpose**: HTML wrapper for all pages (header, navigation, footer)
**Used**: On every page render
**Customizable**: Yes
**Available Variables**:
| Variable | Type | Description |
|----------|------|-------------|
| `$content` | string | Rendered page content (HTML) |
| `$ctx` | Context | Full context object |
| `$currentLang` | string | Current language code (e.g., "en", "no") |
| `$navigation` | array | Navigation menu items |
| `$homeLabel` | string | Site title |
| `$translations` | array | UI translation strings |
| `$pageTitle` | string | Current page title |
| `$dirName` | string | Parent directory name |
| `$pageName` | string | Current page filename |
**Example**:
```php
<!DOCTYPE html>
<html lang="<?= $ctx->currentLang ?>">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($pageTitle) ?> | <?= htmlspecialchars($homeLabel) ?></title>
<link rel="stylesheet" href="/app/styles/base.css?v=<?= md5_file(resolveTemplate('base.css', 'styles')) ?>">
</head>
<body class="section-<?= $dirName ?> page-<?= $pageName ?>">
<header>
<nav>
<a href="<?= $ctx->langPrefix ?>/"><?= $homeLabel ?></a>
<?php foreach ($navigation as $item): ?>
<a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a>
<?php endforeach; ?>
</nav>
</header>
<main>
<?= $content ?>
</main>
<footer>
<p><?= $translations['footer_text'] ?></p>
</footer>
</body>
</html>
```
### page.php
**Purpose**: Wrapper for single pages and articles
**Used**: For file and multi-file pages
**Customizable**: Yes
**Available Variables**:
| Variable | Type | Description |
|----------|------|-------------|
| `$content` | string | Rendered content (HTML) |
| `$pageMetadata` | array | Page metadata from metadata.ini |
| `$translations` | array | UI translation strings |
**Example**:
```php
<article>
<?= $content ?>
<?php if (!empty($pageMetadata['tags'])): ?>
<footer class="tags">
<strong><?= $translations['tags'] ?>:</strong>
<?php foreach ($pageMetadata['tags'] as $tag): ?>
<span class="tag"><?= htmlspecialchars($tag) ?></span>
<?php endforeach; ?>
</footer>
<?php endif; ?>
</article>
```
## List Templates
### list.php
**Purpose**: Simple list view (default)
**Used**: Directories with subdirectories
**Customizable**: Yes
**Available Variables**:
| Variable | Type | Description |
|----------|------|-------------|
| `$items` | array | Array of subdirectory items |
| `$metadata` | array | Directory metadata |
| `$pageContent` | string | Optional intro content (HTML) |
| `$translations` | array | UI translation strings |
**Item Structure**:
Each item in `$items` has:
```php
[
'title' => 'Item Title', // From metadata or H1
'date' => '2. november 2025', // Formatted date
'url' => '/blog/post-slug/', // Full URL with language prefix
'cover' => '/path/to/cover.jpg', // Cover image path or null
'summary' => 'Brief description', // From metadata or null
'pdf' => '/path/to/file.pdf', // PDF file path or null
'redirect' => 'https://...', // External URL or null
]
```
**Example**:
```php
<?php if (!empty($pageContent)): ?>
<div class="page-intro">
<?= $pageContent ?>
</div>
<?php endif; ?>
<div class="list">
<?php foreach ($items as $item): ?>
<article>
<h2>
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h2>
<?php if ($item['date']): ?>
<time><?= $item['date'] ?></time>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
```
### list-grid.php
**Purpose**: Grid layout with cover images
**Used**: Visual blog/portfolio listings
**Customizable**: Yes
**Same variables as list.php**
Features:
- Grid layout
- Cover images
- PDF download links
- "Read more" buttons
**Example**:
```php
<div class="list-grid">
<?php foreach ($items as $item): ?>
<article>
<?php if ($item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php endif; ?>
<h2>
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h2>
<?php if ($item['date']): ?>
<time><?= $item['date'] ?></time>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<div class="actions">
<a href="<?= $item['url'] ?>" class="button">
<?= $translations['read_more'] ?>
</a>
<?php if ($item['pdf']): ?>
<a href="<?= $item['pdf'] ?>" download class="button secondary">
Download PDF
</a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
```
### list-card-grid.php
**Purpose**: Card-style grid with external link support
**Used**: Portfolios, resource lists
**Customizable**: Yes
**Same variables as list.php**
Features:
- Card-style layout
- PDF download support
- External redirect support
- Cover images
**Example**:
```php
<div class="card-grid">
<?php foreach ($items as $item): ?>
<article class="card">
<?php if ($item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php endif; ?>
<h2><?= htmlspecialchars($item['title']) ?></h2>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<?php if ($item['redirect']): ?>
<a href="<?= $item['redirect'] ?>"
target="_blank"
rel="noopener"
class="button">
Visit Site
</a>
<?php elseif ($item['pdf']): ?>
<a href="<?= $item['pdf'] ?>" download class="button">
Download PDF
</a>
<?php else: ?>
<a href="<?= $item['url'] ?>" class="button">
View Details
</a>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
```
### list-faq.php
**Purpose**: Expandable FAQ/Q&A format
**Used**: FAQ sections, documentation
**Customizable**: Yes
**Same variables as list.php**
Features:
- Collapsible `<details>` elements
- Semantic HTML
- Keyboard accessible
**Example**:
```php
<?php if (!empty($pageContent)): ?>
<div class="page-intro">
<?= $pageContent ?>
</div>
<?php endif; ?>
<div class="faq">
<?php foreach ($items as $item): ?>
<details>
<summary><?= htmlspecialchars($item['title']) ?></summary>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<a href="<?= $item['url'] ?>">
<?= $translations['read_more'] ?>
</a>
</details>
<?php endforeach; ?>
</div>
```
## Context Object
All templates have access to `$ctx` (Context object):
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `$ctx->contentDir` | string | Path to content directory |
| `$ctx->currentLang` | string | Current language code |
| `$ctx->defaultLang` | string | Default language code |
| `$ctx->availableLangs` | array | Available language codes |
| `$ctx->langPrefix` | string | URL language prefix (e.g., "/en" or "") |
| `$ctx->requestPath` | string | Current request path |
| `$ctx->hasTrailingSlash` | bool | Whether path has trailing slash |
| `$ctx->navigation` | array | Navigation menu items (computed) |
| `$ctx->homeLabel` | string | Site title (computed) |
| `$ctx->translations` | array | UI translations (computed) |
### Example Usage
```php
<!-- Language switcher -->
<?php foreach ($ctx->availableLangs as $lang): ?>
<?php
$url = $lang === $ctx->defaultLang
? '/' . trim($ctx->requestPath, '/')
: '/' . $lang . '/' . trim($ctx->requestPath, '/');
?>
<a href="<?= $url ?>" <?= $lang === $ctx->currentLang ? 'aria-current="true"' : '' ?>>
<?= strtoupper($lang) ?>
</a>
<?php endforeach; ?>
<!-- Breadcrumbs -->
<nav aria-label="Breadcrumb">
<ol>
<li><a href="<?= $ctx->langPrefix ?>/"><?= $ctx->homeLabel ?></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>
```
## Navigation Array
Structure of `$navigation` items:
```php
[
[
'title' => 'Blog',
'url' => '/blog/',
'order' => 1
],
[
'title' => 'About',
'url' => '/about/',
'order' => 2
],
// ...
]
```
Already sorted by `menu_order` field.
## Translation Array
Structure of `$translations`:
```php
[
'home' => 'Home',
'read_more' => 'Read more',
'categories' => 'Categories',
'tags' => 'Tags',
'footer_text' => 'Made with FolderWeb',
'footer_handcoded' => 'Generated in',
'footer_page_time' => 'ms',
// ... custom translations
]
```
## Helper Functions Available in Templates
### resolveTemplate()
Find custom or default template:
```php
$templatePath = resolveTemplate('base', 'templates');
$cssPath = resolveTemplate('base.css', 'styles');
```
### htmlspecialchars()
Escape output (always use for user content):
```php
<?= htmlspecialchars($variable) ?>
```
### Other PHP Functions
All standard PHP functions are available:
- `isset()`, `empty()`
- `count()`, `array_filter()`
- `date()`, `time()`
- String functions
- etc.
## Creating Custom Templates
### Step 1: Create Template File
```bash
touch custom/templates/my-custom-list.php
```
### Step 2: Use Standard Variables
Custom list templates receive `$items`, `$metadata`, `$pageContent`, `$translations`.
### Step 3: Apply in Metadata
**content/my-section/metadata.ini**:
```ini
page_template = "my-custom-list"
```
Note: Omit `.php` extension.
## Template Best Practices
### Always Escape Output
```php
<!-- Good -->
<?= htmlspecialchars($item['title']) ?>
<!-- Bad (XSS vulnerability) -->
<?= $item['title'] ?>
```
### Check Variables Before Use
```php
<?php if (isset($item['cover']) && $item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php endif; ?>
```
### Use Null Coalescing
```php
$author = $metadata['author'] ?? 'Anonymous';
```
### Semantic HTML
```php
<!-- Good -->
<article>
<h2><a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a></h2>
<time datetime="2025-11-02"><?= $item['date'] ?></time>
</article>
<!-- Avoid -->
<div>
<span><a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a></span>
<span><?= $item['date'] ?></span>
</div>
```
### Accessibility
```php
<!-- Proper alt text -->
<img src="<?= $item['cover'] ?>" alt="<?= htmlspecialchars($item['title']) ?>">
<!-- ARIA labels -->
<nav aria-label="Main navigation">
<!-- Semantic elements -->
<main>
<header>
<footer>
<article>
<aside>
```
### Short Echo Tags
```php
<!-- Good (modern PHP) -->
<?= $variable ?>
<!-- Verbose -->
<?php echo $variable; ?>
```
### Keep Logic Minimal
Prepare data in functions, not templates:
```php
<!-- Avoid complex logic in templates -->
<?php
// Don't do heavy processing here
$processedData = someComplexFunction($items);
?>
<!-- Keep templates simple -->
<?php foreach ($items as $item): ?>
<?= htmlspecialchars($item['title']) ?>
<?php endforeach; ?>
```
## Common Patterns
### Card with Fallback Content
```php
<article class="card">
<?php if ($item['cover']): ?>
<img src="<?= $item['cover'] ?>" alt="">
<?php else: ?>
<div class="placeholder">No image</div>
<?php endif; ?>
<h2><?= htmlspecialchars($item['title'] ?? 'Untitled') ?></h2>
<p><?= htmlspecialchars($item['summary'] ?? 'No description available.') ?></p>
</article>
```
### Conditional Links
```php
<?php if ($item['redirect']): ?>
<a href="<?= $item['redirect'] ?>" target="_blank" rel="noopener">
External Link
</a>
<?php else: ?>
<a href="<?= $item['url'] ?>">
Read More
</a>
<?php endif; ?>
```
### Date Formatting
```php
<!-- Use provided formatted date -->
<?php if ($item['date']): ?>
<time><?= $item['date'] ?></time>
<?php endif; ?>
```
Date is already formatted in Norwegian style by FolderWeb.
## Debugging Templates
### Dump Variables
```php
<pre><?php var_dump($items); ?></pre>
<pre><?php var_dump($metadata); ?></pre>
<pre><?php var_dump($ctx); ?></pre>
```
### Check Template Resolution
Verify which template is used:
```php
<?php
$templatePath = resolveTemplate('base', 'templates');
echo "Using template: $templatePath";
?>
```
### PHP Error Reporting
Enable in development:
```php
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
?>
```
## Related
- [Custom Templates Guide](../how-to/custom-templates.md)
- [Metadata Reference](metadata.md)
- [File Structure Reference](file-structure.md)
- [CSS Variables Reference](css-variables.md)

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