Compare commits
No commits in common. "0e190404732137e26cee8ea9c7d1e5b843781f92" and "bb38245218c62c3b6723382b7f08f2b8711b1771" have entirely different histories.
0e19040473
...
bb38245218
108 changed files with 7544 additions and 1591 deletions
3
app/config.ini
Normal file
3
app/config.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[languages]
|
||||||
|
default = "no"
|
||||||
|
available = "no,en"
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
function createContext(): Context {
|
function createContext(): Context {
|
||||||
// Load configuration with fallback: custom -> default
|
// Load configuration
|
||||||
$defaultConfig = __DIR__ . '/default/config.ini';
|
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
|
||||||
$customConfig = __DIR__ . '/../custom/config.ini';
|
? __DIR__ . '/../custom/config.ini'
|
||||||
|
: __DIR__ . '/config.ini';
|
||||||
// Start with default config
|
$config = parse_ini_file($configFile, true);
|
||||||
$config = file_exists($defaultConfig) ? parse_ini_file($defaultConfig, true) : [];
|
|
||||||
|
|
||||||
// Merge with custom config if it exists
|
|
||||||
if (file_exists($customConfig)) {
|
|
||||||
$config = array_replace_recursive($config, parse_ini_file($customConfig, true) ?: []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load global plugins
|
// Load global plugins
|
||||||
getPluginManager()->loadGlobalPlugins($config);
|
getPluginManager()->loadGlobalPlugins($config);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
[languages]
|
[languages]
|
||||||
default = "en"
|
default = "en"
|
||||||
available = "no,en"
|
available = "en"
|
||||||
|
|
||||||
[plugins]
|
|
||||||
enabled = "languages"
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?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>
|
|
||||||
6
app/default/content/00-welcome.php
Normal file
6
app/default/content/00-welcome.php
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<header style="text-align: center; padding: 4rem 0 2rem;">
|
||||||
|
<h1 style="font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">Welcome to FolderWeb</h1>
|
||||||
|
<p style="font-size: clamp(1.125rem, 2vw, 1.5rem); color: oklch(0.5 0 0); max-width: 60ch; margin: 0 auto;">
|
||||||
|
A minimalist PHP framework that turns folders into websites. No JavaScript, no build tools, just simple files.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
7
app/default/content/00a-getting-started.md
Normal file
7
app/default/content/00a-getting-started.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This is demo content to help you understand how FolderWeb works. To replace it with your own content:
|
||||||
|
|
||||||
|
1. Create a `/content` folder in your project root
|
||||||
|
2. Add your content files (`.md`, `.html`, or `.php`)
|
||||||
|
3. This demo will automatically disappear
|
||||||
15
app/default/content/01-core-concepts.md
Normal file
15
app/default/content/01-core-concepts.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### File-Based Routing
|
||||||
|
|
||||||
|
Drop a file in a folder and it's instantly accessible at a URL matching that path. Your folder structure becomes your URL structure.
|
||||||
|
|
||||||
|
### Multiple Content Types
|
||||||
|
|
||||||
|
- **Markdown** - Write in `.md` files, automatically converted to HTML
|
||||||
|
- **HTML** - Pure HTML files for complete control
|
||||||
|
- **PHP** - Dynamic content when you need it
|
||||||
|
|
||||||
|
### Multi-File Pages
|
||||||
|
|
||||||
|
Any folder without subfolders renders all content files (`.md`, `.html`, `.php`) in alphanumerical order. Mix formats freely!
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<!-- 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.
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<!-- 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.
|
|
||||||
13
app/default/content/02-features.html
Normal file
13
app/default/content/02-features.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<article>
|
||||||
|
<h2>Smart Features</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Metadata</strong> - Use <code>metadata.ini</code> files for titles, dates, summaries</li>
|
||||||
|
<li><strong>Date extraction</strong> - Folder names like <code>2025-11-01-title</code> automatically show dates</li>
|
||||||
|
<li><strong>Cover images</strong> - Add <code>cover.jpg</code> for list view thumbnails</li>
|
||||||
|
<li><strong>Templates</strong> - Custom templates in <code>/custom/templates/</code> override defaults</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Explore the Demo</h2>
|
||||||
|
<p>Check out the <a href="/articles/">Articles</a> and <a href="/about/">About</a> pages to see different content types in action.</p>
|
||||||
|
</article>
|
||||||
11
app/default/content/03-this-page-demo.md
Normal file
11
app/default/content/03-this-page-demo.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
## About This Frontpage
|
||||||
|
|
||||||
|
**This frontpage demonstrates the multi-file approach!** It's composed of:
|
||||||
|
|
||||||
|
1. `00-welcome.php` - Hero header (PHP/HTML)
|
||||||
|
2. `00a-getting-started.md` - Getting started guide (Markdown)
|
||||||
|
3. `01-core-concepts.md` - Core concepts (Markdown)
|
||||||
|
4. `02-features.html` - Features and links (HTML)
|
||||||
|
5. `03-this-page-demo.md` - This explanation (Markdown)
|
||||||
|
|
||||||
|
All files in the root `/content` folder are rendered together, just like pages in subfolders. Name your files anything you want—they'll render in alphanumerical order.
|
||||||
15
app/default/content/about/00-introduction.md
Normal file
15
app/default/content/about/00-introduction.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# About FolderWeb
|
||||||
|
|
||||||
|
FolderWeb is a minimalist PHP framework designed for simplicity, longevity, and maintainability. It's built on a simple philosophy: **just enough, nothing more**.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
Modern web development has become unnecessarily complex. Build tools, package managers, JavaScript frameworks that change every few months—it's exhausting and unsustainable.
|
||||||
|
|
||||||
|
FolderWeb is different. It's built to:
|
||||||
|
|
||||||
|
- **Work for decades** without requiring constant updates
|
||||||
|
- **Be understandable** by reading a few hundred lines of code
|
||||||
|
- **Stay maintainable** without specialized knowledge
|
||||||
|
- **Load fast** with no JavaScript overhead
|
||||||
|
- **Just work** without configuration or setup
|
||||||
15
app/default/content/about/01-design-principles.html
Normal file
15
app/default/content/about/01-design-principles.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<article>
|
||||||
|
<h2>Design Principles</h2>
|
||||||
|
|
||||||
|
<h3>Minimalism</h3>
|
||||||
|
<p>Use only what is strictly necessary. No frameworks, no build tools, no package managers for frontend code. Every line of code must justify its existence.</p>
|
||||||
|
|
||||||
|
<h3>File-Based Everything</h3>
|
||||||
|
<p>Your folder structure is your URL structure. Drop a file in a folder and it's instantly accessible. No routes to configure, no databases to set up.</p>
|
||||||
|
|
||||||
|
<h3>Override, Never Modify</h3>
|
||||||
|
<p>Custom templates and styles go in <code>/custom/</code> and automatically override defaults. The core files in <code>/app/default/</code> remain untouched and updateable.</p>
|
||||||
|
|
||||||
|
<h3>Modern Standards</h3>
|
||||||
|
<p>Use modern PHP 8.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>
|
||||||
21
app/default/content/about/02-technology-stack.php
Normal file
21
app/default/content/about/02-technology-stack.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<article>
|
||||||
|
<h2>Technology Stack</h2>
|
||||||
|
|
||||||
|
<h3>Backend</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>PHP 8.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>
|
||||||
19
app/default/content/about/03-what-it-is-not.md
Normal file
19
app/default/content/about/03-what-it-is-not.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
## What It's Not
|
||||||
|
|
||||||
|
FolderWeb is **not**:
|
||||||
|
|
||||||
|
- A CMS with an admin panel
|
||||||
|
- A single-page application framework
|
||||||
|
- A solution for complex web applications
|
||||||
|
- Trying to scale to millions of users
|
||||||
|
- Following current trends and fads
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
|
||||||
|
FolderWeb **is**:
|
||||||
|
|
||||||
|
- A simple way to publish content
|
||||||
|
- A foundation that will work for decades
|
||||||
|
- A teaching tool for web fundamentals
|
||||||
|
- A protest against unnecessary complexity
|
||||||
|
- Perfect for documentation, blogs, portfolios, small business sites
|
||||||
21
app/default/content/about/04-get-started.md
Normal file
21
app/default/content/about/04-get-started.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
Ready to build something simple and lasting?
|
||||||
|
|
||||||
|
1. Create a `/content` folder
|
||||||
|
2. Add your first `.md` file
|
||||||
|
3. That's it—you're publishing
|
||||||
|
|
||||||
|
No build step. No npm install. No configuration files. Just content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**This page demonstrates FolderWeb's multi-file approach.** Notice how this page is built from multiple files that render in alphanumerical order:
|
||||||
|
|
||||||
|
- `00-introduction.md` - Markdown content
|
||||||
|
- `01-design-principles.html` - Static HTML
|
||||||
|
- `02-technology-stack.php` - Dynamic PHP (shows current date)
|
||||||
|
- `03-what-it-is-not.md` - More Markdown
|
||||||
|
- `04-get-started.md` - This section
|
||||||
|
|
||||||
|
Mix file types freely. They all render together seamlessly!
|
||||||
3
app/default/content/about/metadata.ini
Normal file
3
app/default/content/about/metadata.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "About FolderWeb"
|
||||||
|
menu = true
|
||||||
|
menu_order = 2
|
||||||
19
app/default/content/about/styles.css
Normal file
19
app/default/content/about/styles.css
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
# 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
|
||||||
|

|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Markdown Guide"
|
||||||
|
date = "2025-10-15"
|
||||||
|
summary = "Master Markdown syntax for beautiful, semantic content in FolderWeb."
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Templates and Customization"
|
||||||
|
date = "2025-10-28"
|
||||||
|
summary = "Customize your FolderWeb site with templates, styles, and metadata options."
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
# 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/`
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Getting Started with FolderWeb"
|
||||||
|
date = "2025-11-01"
|
||||||
|
summary = "Learn the basics of FolderWeb and create your first content in minutes."
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Multi-File Content Pages
|
||||||
|
|
||||||
|
One of FolderWeb's most powerful features is the ability to compose a single page from multiple content files. This gives you flexibility in how you organize and author your content.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When a folder contains **no subdirectories**, FolderWeb treats it as a **page-type folder**. All `.md`, `.html`, and `.php` files in that folder are rendered in **alphanumerical order**.
|
||||||
|
|
||||||
|
This means you can:
|
||||||
|
|
||||||
|
- Break long content into manageable sections
|
||||||
|
- Mix file formats freely (Markdown, HTML, PHP)
|
||||||
|
- Reorder sections by renaming files
|
||||||
|
- Include dynamic PHP content alongside static content
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
## File Naming Examples
|
||||||
|
|
||||||
|
Here are some example naming patterns:
|
||||||
|
|
||||||
|
```
|
||||||
|
/my-page/
|
||||||
|
00-introduction.md
|
||||||
|
01-getting-started.md
|
||||||
|
02-advanced-topics.html
|
||||||
|
03-conclusion.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Files render in this order:
|
||||||
|
1. `00-introduction.md`
|
||||||
|
2. `01-getting-started.md`
|
||||||
|
3. `02-advanced-topics.html`
|
||||||
|
4. `03-conclusion.php`
|
||||||
|
|
||||||
|
## Folder Types
|
||||||
|
|
||||||
|
FolderWeb automatically determines folder type:
|
||||||
|
|
||||||
|
- **Page-type folder**: No subdirectories → Renders all content files as a single page
|
||||||
|
- **Article-type folder**: Has subdirectories → Shows list view with links to subdirectories
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<article>
|
||||||
|
<h2>Use Cases</h2>
|
||||||
|
|
||||||
|
<h3>Long Documentation</h3>
|
||||||
|
<p>Break lengthy documentation into logical sections. Each section gets its own file, making editing and maintenance easier.</p>
|
||||||
|
|
||||||
|
<h3>Mixed Content Types</h3>
|
||||||
|
<p>Use Markdown for simple text, HTML for complex layouts, and PHP for dynamic content—all on the same page.</p>
|
||||||
|
|
||||||
|
<h3>Collaborative Editing</h3>
|
||||||
|
<p>Multiple authors can work on different sections simultaneously without merge conflicts.</p>
|
||||||
|
|
||||||
|
<h3>Progressive Enhancement</h3>
|
||||||
|
<p>Start with simple Markdown files. Later, enhance specific sections with HTML or PHP without restructuring.</p>
|
||||||
|
</article>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<article>
|
||||||
|
<h2>Dynamic Content Example</h2>
|
||||||
|
|
||||||
|
<p>This section is a PHP file that generates dynamic content. Here are some examples:</p>
|
||||||
|
|
||||||
|
<div style="background: #f0f9ff; border: 1px solid #0284c7; padding: 1.5rem; border-radius: 8px; margin: 1.5rem 0;">
|
||||||
|
<h3>Server Information</h3>
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
<li><strong>Current Time:</strong> <?= date('H:i:s') ?></li>
|
||||||
|
<li><strong>Today's Date:</strong> <?= date('l, F j, Y') ?></li>
|
||||||
|
<li><strong>PHP Version:</strong> <?= PHP_VERSION ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>PHP files can access all the same variables and functions available throughout FolderWeb, making it easy to create dynamic, data-driven content.</p>
|
||||||
|
</article>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Use Descriptive Prefixes
|
||||||
|
|
||||||
|
Number your files with two-digit prefixes (`00-`, `01-`, `02-`) to maintain clear ordering:
|
||||||
|
|
||||||
|
- Allows up to 100 sections before needing three digits
|
||||||
|
- Keeps files sorted in file managers
|
||||||
|
- Makes reordering easy (just rename)
|
||||||
|
|
||||||
|
### Choose the Right Format
|
||||||
|
|
||||||
|
- **Markdown (`.md`)** - For most content. Simple, clean, readable.
|
||||||
|
- **HTML (`.html`)** - For complex layouts or embedded media.
|
||||||
|
- **PHP (`.php`)** - For dynamic content, calculations, or data display.
|
||||||
|
|
||||||
|
### Keep It Simple
|
||||||
|
|
||||||
|
Don't overcomplicate. If your page works well as a single file, keep it that way. Use multiple files when they genuinely make maintenance easier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**This article itself demonstrates the multi-file approach.** View the source folder to see how it's structured!
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Multi-File Content Pages"
|
||||||
|
date = "2025-11-02"
|
||||||
|
summary = "Learn how to create pages from multiple content files in any format"
|
||||||
4
app/default/content/articles/metadata.ini
Normal file
4
app/default/content/articles/metadata.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
title = "Articles"
|
||||||
|
menu = true
|
||||||
|
menu_order = 1
|
||||||
|
page_template = "list"
|
||||||
3
app/default/content/articles/page.md
Normal file
3
app/default/content/articles/page.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Articles
|
||||||
|
|
||||||
|
A collection of guides and tutorials to help you get the most out of FolderWeb.
|
||||||
22
app/default/content/articles/styles.css
Normal file
22
app/default/content/articles/styles.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 324 B |
|
|
@ -1,39 +0,0 @@
|
||||||
# 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
|
|
||||||

|
|
||||||
[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.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
title = "Cover Images and Assets"
|
|
||||||
summary = "Learn how cover images and static assets work in FolderWeb"
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
title = "Working with Metadata"
|
|
||||||
date = "2024-11-20"
|
|
||||||
summary = "Learn how to use metadata.ini files to control page information and behavior"
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
# 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):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
# 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):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
title = "Markdown Demonstration"
|
|
||||||
summary = "Shows all the Markdown formatting features available through Parsedown"
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
title = "File-Based Routing"
|
|
||||||
summary = "Your folder structure is your URL structure—no configuration needed"
|
|
||||||
date = "2024-11-26"
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<?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 på 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 på nytt for å se servertiden oppdatere seg!</em></p>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
## 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.
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
## 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.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
title = "Mix Formats"
|
|
||||||
summary = "Combine Markdown, HTML, and PHP files to create rich, dynamic pages"
|
|
||||||
date = "2024-11-26"
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Level Two
|
|
||||||
|
|
||||||
This is two levels deep: `/examples/nested/level-two/`
|
|
||||||
|
|
||||||
You can organize content in hierarchies that make sense for your site structure.
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# 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.**
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# 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.**
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
title = "No Build Step"
|
|
||||||
summary = "Edit, save, refresh—that's it. No compilation, no bundling, no waiting"
|
|
||||||
date = "2024-11-26"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Oldest Post
|
|
||||||
|
|
||||||
The oldest post in this compact list.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Older Post
|
|
||||||
|
|
||||||
An older post showing date ordering.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Recent Post
|
|
||||||
|
|
||||||
A recent example post.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Compact Template
|
|
||||||
|
|
||||||
This section uses `list-compact.php` for a minimal, clean list view.
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
title = "Compact Template Example"
|
|
||||||
|
|
||||||
[settings]
|
|
||||||
page_template = "list-compact"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Grid Template
|
|
||||||
|
|
||||||
This section uses `list-grid.php` to display items in a responsive grid with cards.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Grid Item One
|
|
||||||
|
|
||||||
This is an example item in the grid layout.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
summary = "First item demonstrating grid layout"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Grid Item Three
|
|
||||||
|
|
||||||
A third item to show the grid layout with multiple items.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
summary = "Third grid item"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Grid Item Two
|
|
||||||
|
|
||||||
Another example showing how items appear in the grid.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
summary = "Second item in the grid"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
title = "Grid Template Example"
|
|
||||||
|
|
||||||
[settings]
|
|
||||||
page_template = "list-grid"
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
title = "Multilingual Support"
|
|
||||||
summary = "Learn how to create content in multiple languages"
|
|
||||||
|
|
@ -1,112 +1,34 @@
|
||||||
/* Hero section for frontpage */
|
/* Page-specific styles for homepage */
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
background: linear-gradient(135deg, oklch(95% 0.05 250) 0%, oklch(98% 0.02 250) 100%);
|
display: flex;
|
||||||
border-radius: 0.75rem;
|
flex-direction: column;
|
||||||
padding: var(--space-m);
|
justify-content: flex-end;
|
||||||
margin-block: var(--space-s);
|
padding: 1rem .4rem;
|
||||||
text-align: center;
|
background-color: oklch(0.85 0.05 250);
|
||||||
|
min-height: 40vh;
|
||||||
& .hero-title {
|
text-align: center;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.features {
|
.hero h1 {
|
||||||
display: grid;
|
font-size: clamp(2.5rem, 6vw, 4rem);
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
|
margin: 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.cta-section {
|
||||||
display: flex;
|
background: linear-gradient(135deg, oklch(0.65 0.15 250), oklch(0.50 0.12 250));
|
||||||
justify-content: center;
|
padding: 2rem 1rem;
|
||||||
gap: var(--space-m);
|
margin-top: 3rem;
|
||||||
flex-wrap: wrap;
|
text-align: center;
|
||||||
margin-block-start: var(--space-m);
|
color: white;
|
||||||
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);
|
||||||
|
}
|
||||||
& .stat-value {
|
|
||||||
display: block;
|
.cta-content {
|
||||||
font-size: 1.25rem;
|
max-width: 42rem;
|
||||||
font-weight: bold;
|
margin: 0 auto;
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .stat-label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_handcoded = "Generated in"
|
footer_text = "Footer content goes here"
|
||||||
|
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"
|
||||||
|
|
|
||||||
|
|
@ -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 = "Emneknagger"
|
tags = "Stikkord"
|
||||||
read_more = "Les mer"
|
read_more = "Les mer"
|
||||||
read_article = "Les artikkel"
|
read_article = "Les artikkel"
|
||||||
summary = "Sammendrag"
|
read_full_answer = "Les hele svaret"
|
||||||
footer_handcoded = "Generert på"
|
download_pdf = "Last ned PDF"
|
||||||
|
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"
|
||||||
|
|
|
||||||
191
app/default/styles/base.css
Normal file
191
app/default/styles/base.css
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
/* 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
/* Variables */
|
|
||||||
:root {
|
|
||||||
--color-text: oklch(20% 0 0);
|
|
||||||
--color-background: oklch(98% 0 0);
|
|
||||||
--color-accent: oklch(50% 0.15 250);
|
|
||||||
--color-accent-light: oklch(95% 0.05 250);
|
|
||||||
--color-border: oklch(85% 0 0);
|
|
||||||
--color-muted: oklch(50% 0 0);
|
|
||||||
|
|
||||||
--space-xs: 0.5rem;
|
|
||||||
--space-s: 1rem;
|
|
||||||
--space-m: 2rem;
|
|
||||||
--space-l: 4rem;
|
|
||||||
|
|
||||||
--size-content: 65ch;
|
|
||||||
--size-constrained: 42rem;
|
|
||||||
--size-wide: 90rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns:
|
|
||||||
[full-start] minmax(var(--space-s), 1fr)
|
|
||||||
[content-start] minmax(0, var(--size-constrained))
|
|
||||||
[content-end] minmax(var(--space-s), 1fr)
|
|
||||||
[full-end];
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-block: 1.5em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: clamp(2rem, 5vw, 3rem); }
|
|
||||||
h2 { font-size: clamp(1.5rem, 4vw, 2rem); }
|
|
||||||
h3 { font-size: clamp(1.25rem, 3vw, 1.5rem); }
|
|
||||||
|
|
||||||
p, ul, ol, dl {
|
|
||||||
max-width: var(--size-content);
|
|
||||||
margin-block: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
a {
|
|
||||||
color: var(--color-accent);
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
header, main, footer {
|
|
||||||
grid-column: content;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
border-block-end: 1px solid var(--color-border);
|
|
||||||
padding-block: var(--space-s);
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-s);
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& nav {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-s);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& a {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-current] {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .language-switcher {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding-block: var(--space-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
border-block-start: 1px solid var(--color-border);
|
|
||||||
padding-block: var(--space-s);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
|
|
||||||
& nav {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-s);
|
|
||||||
margin-block-end: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
& p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code */
|
|
||||||
code {
|
|
||||||
background: var(--color-accent-light);
|
|
||||||
padding: 0.125em 0.25em;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: var(--color-accent-light);
|
|
||||||
padding: var(--space-s);
|
|
||||||
border-radius: 0.5em;
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
& code {
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
margin-block: var(--space-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: var(--space-xs);
|
|
||||||
text-align: start;
|
|
||||||
border-block-end: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquote */
|
|
||||||
blockquote {
|
|
||||||
margin-inline: 0;
|
|
||||||
padding-inline-start: var(--space-s);
|
|
||||||
border-inline-start: 4px solid var(--color-accent);
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Article metadata */
|
|
||||||
article {
|
|
||||||
& time {
|
|
||||||
color: var(--color-muted);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +1,69 @@
|
||||||
|
<?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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?= htmlspecialchars($pageTitle ?? 'FolderWeb') ?></title>
|
<?php if (!empty($metaDescription)): ?>
|
||||||
<?php if (!empty($metaDescription)): ?>
|
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||||
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<link rel="stylesheet" href="<?= $cssUrl ?>?v=<?= $cssHash ?>">
|
||||||
<?php if (!empty($socialImageUrl)): ?>
|
<?php if (!empty($pageCssUrl)): ?>
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
|
<link rel="stylesheet" href="<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<link rel="stylesheet" href="/app/default/styles/styles.css">
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
<?php if (!empty($pageCssUrl)): ?>
|
<link rel="icon" href="/favicon.png" type="image/png">
|
||||||
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
<title><?= htmlspecialchars($pageTitle ?? 'Site Title') ?></title>
|
||||||
<?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>
|
|
||||||
|
|
||||||
<main>
|
<body class="<?php if (isset($dirName)) echo 'section-' . $dirName . ' '; ?><?php if (isset($pageName)) echo 'page-' . $pageName; ?>">
|
||||||
<?= $content ?>
|
<div class="grid-container">
|
||||||
</main>
|
<header class="contain">
|
||||||
|
<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>
|
||||||
|
|
||||||
<footer>
|
<main>
|
||||||
<nav>
|
<?php echo $content ?? ''; ?>
|
||||||
<a href="https://mastodon.social/@example" rel="me">Mastodon</a>
|
</main>
|
||||||
<a href="https://bsky.app/profile/example.bsky.social">Bluesky</a>
|
|
||||||
</nav>
|
<footer>
|
||||||
<p>
|
<div class="contain">
|
||||||
<?php if (!empty($translations['footer_handcoded'])): ?>
|
<p><?= htmlspecialchars($translations['footer_text'] ?? 'Footer content goes here') ?></p>
|
||||||
<?= htmlspecialchars($translations['footer_handcoded']) ?> <?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?><?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
|
<?php $endTime = microtime(true); $pageLoadTime = round(($endTime - $startTime) * 1000, 2); ?>
|
||||||
<?php else: ?>
|
<p class="generated"><?= htmlspecialchars($translations['footer_handcoded'] ?? 'This page was generated in') ?> <?php echo $pageLoadTime; ?><?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?></p>
|
||||||
Generated in <?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?>ms
|
</div>
|
||||||
<?php endif; ?>
|
</footer>
|
||||||
</p>
|
</div>
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
82
app/default/templates/list-card-grid.php
Normal file
82
app/default/templates/list-card-grid.php
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<?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>
|
|
||||||
141
app/default/templates/list-faq.php
Normal file
141
app/default/templates/list-faq.php
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -1,48 +1,94 @@
|
||||||
<?php if (!empty($pageContent)): ?>
|
<?php if (!empty($pageContent)): ?>
|
||||||
<div class="list-intro">
|
<article class="list-intro">
|
||||||
<?= $pageContent ?>
|
<?= $pageContent ?>
|
||||||
</div>
|
</article>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="grid">
|
<section class="list-grid-wrapper">
|
||||||
<?php foreach ($items as $item): ?>
|
<div class="list-grid">
|
||||||
<article class="card">
|
<?php foreach ($items as $item): ?>
|
||||||
<?php if (!empty($item['cover'])): ?>
|
<article>
|
||||||
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="">
|
<?php if ($item['cover']): ?>
|
||||||
<?php endif; ?>
|
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||||
|
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
|
||||||
<h3><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></h3>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
|
<h1>
|
||||||
<time><?= htmlspecialchars($item['date']) ?></time>
|
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||||
<?php endif; ?>
|
<?= htmlspecialchars($item['title']) ?>
|
||||||
|
</a>
|
||||||
<?php if (!empty($item['summary'])): ?>
|
</h1>
|
||||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
|
||||||
<?php endif; ?>
|
<p><?= htmlspecialchars($item['date']) ?></p>
|
||||||
</article>
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php if ($item['summary']): ?>
|
||||||
</div>
|
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||||
|
<?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>
|
||||||
.grid {
|
main > section.list-grid-wrapper {
|
||||||
display: grid;
|
margin-top: 1.3em;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
|
|
||||||
gap: var(--space-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.list-grid {
|
||||||
border: 1px solid var(--color-border);
|
display: grid;
|
||||||
border-radius: 0.5rem;
|
grid-template-columns: repeat(auto-fit, minmax(clamp(15rem, 45%, 20rem), 1fr));
|
||||||
padding: var(--space-s);
|
gap: clamp(1rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
& img {
|
.list-grid > article {
|
||||||
margin-block-end: var(--space-s);
|
background-color: white;
|
||||||
border-radius: 0.25rem;
|
padding: 0;
|
||||||
}
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
& h3 {
|
> :not(img, a) {
|
||||||
margin-block-start: 0;
|
padding-left: 1rem;
|
||||||
}
|
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>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,55 @@
|
||||||
<?php if (!empty($pageContent)): ?>
|
<?php if (!empty($pageContent)): ?>
|
||||||
<div class="list-intro">
|
<article class="list-intro">
|
||||||
<?= $pageContent ?>
|
<?= $pageContent ?>
|
||||||
</div>
|
</article>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="list">
|
<article>
|
||||||
<?php foreach ($items as $item): ?>
|
<?php foreach ($items as $item): ?>
|
||||||
<article class="list-item">
|
<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>
|
||||||
|
<?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>
|
||||||
|
|
||||||
<h2><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></h2>
|
<style>
|
||||||
|
main > article {
|
||||||
|
> article {
|
||||||
|
background-color: white;
|
||||||
|
padding: 0;
|
||||||
|
padding-bottom: 1.3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
|
> :not(img, a) {
|
||||||
<time><?= htmlspecialchars($item['date']) ?></time>
|
padding-left: 1rem;
|
||||||
<?php endif; ?>
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
<?php if (!empty($item['summary'])): ?>
|
h1 {
|
||||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
margin-top: 1rem;
|
||||||
<?php endif; ?>
|
}
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
> .button {
|
||||||
</div>
|
margin-left: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,25 @@
|
||||||
<article>
|
<?= $content ?>
|
||||||
<?= $content ?>
|
|
||||||
</article>
|
<?php if ($pageMetadata && (isset($pageMetadata['tags']) || isset($pageMetadata['categories']))): ?>
|
||||||
|
<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; ?>
|
||||||
|
|
|
||||||
|
|
@ -490,3 +490,5 @@ content/
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [Plugin System Reference](plugin-system.md)
|
- [Plugin System Reference](plugin-system.md)
|
||||||
|
- [How to Create Multi-Language Sites](../../docs/how-to/multi-language.md)
|
||||||
|
- [Configuration Reference](../../docs/reference/configuration.md)
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,7 @@ Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dirOrType, strin
|
||||||
|
|
||||||
// Filter content files by language variant
|
// Filter content files by language variant
|
||||||
if (is_array($data) && !empty($data) && isset($data[0]['path'])) {
|
if (is_array($data) && !empty($data) && isset($data[0]['path'])) {
|
||||||
error_log("filterFilesByLanguage called with " . count($data) . " files, current lang: $currentLang");
|
return filterFilesByLanguage($data, $dirOrType, $ctx);
|
||||||
$filtered = filterFilesByLanguage($data, $dirOrType, $ctx);
|
|
||||||
error_log("Filtered to " . count($filtered) . " files");
|
|
||||||
return $filtered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
|
|
|
||||||
|
|
@ -24,34 +24,6 @@ if (file_exists($assetPath) && is_file($assetPath)) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for assets in content directory (CSS, images, etc.)
|
|
||||||
$contentAssetPath = $ctx->contentDir . '/' . $ctx->requestPath;
|
|
||||||
if (file_exists($contentAssetPath) && is_file($contentAssetPath)) {
|
|
||||||
$ext = pathinfo($contentAssetPath, PATHINFO_EXTENSION);
|
|
||||||
// Define MIME types for asset files
|
|
||||||
$mimeTypes = [
|
|
||||||
'css' => 'text/css',
|
|
||||||
'jpg' => 'image/jpeg',
|
|
||||||
'jpeg' => 'image/jpeg',
|
|
||||||
'png' => 'image/png',
|
|
||||||
'gif' => 'image/gif',
|
|
||||||
'webp' => 'image/webp',
|
|
||||||
'svg' => 'image/svg+xml',
|
|
||||||
'pdf' => 'application/pdf',
|
|
||||||
'woff' => 'font/woff',
|
|
||||||
'woff2' => 'font/woff2',
|
|
||||||
'ttf' => 'font/ttf',
|
|
||||||
'otf' => 'font/otf',
|
|
||||||
];
|
|
||||||
|
|
||||||
$extLower = strtolower($ext);
|
|
||||||
if (isset($mimeTypes[$extLower])) {
|
|
||||||
header('Content-Type: ' . $mimeTypes[$extLower]);
|
|
||||||
readfile($contentAssetPath);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle frontpage
|
// Handle frontpage
|
||||||
if (empty($ctx->requestPath)) {
|
if (empty($ctx->requestPath)) {
|
||||||
$contentFiles = findAllContentFiles($ctx->contentDir);
|
$contentFiles = findAllContentFiles($ctx->contentDir);
|
||||||
|
|
|
||||||
211
docs/README.md
Normal file
211
docs/README.md
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
# 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)
|
||||||
739
docs/explanation/architecture.md
Normal file
739
docs/explanation/architecture.md
Normal file
|
|
@ -0,0 +1,739 @@
|
||||||
|
# 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)
|
||||||
496
docs/explanation/philosophy.md
Normal file
496
docs/explanation/philosophy.md
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
# 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)
|
||||||
398
docs/how-to/custom-styles.md
Normal file
398
docs/how-to/custom-styles.md
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
# 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)
|
||||||
288
docs/how-to/custom-templates.md
Normal file
288
docs/how-to/custom-templates.md
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
# 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)
|
||||||
425
docs/how-to/multi-language.md
Normal file
425
docs/how-to/multi-language.md
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
# How to Create a Multi-Language Site
|
||||||
|
|
||||||
|
This guide shows you how to set up and manage a multi-language website with FolderWeb.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
FolderWeb supports multiple languages through:
|
||||||
|
- Language prefixes in URLs
|
||||||
|
- Language-specific content files
|
||||||
|
- Translated slugs and metadata
|
||||||
|
- Translation files for UI strings
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Step 1: Configure Available Languages
|
||||||
|
|
||||||
|
Create or edit `custom/config.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[languages]
|
||||||
|
default = "en"
|
||||||
|
available = "en,no,fr"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **default**: The primary language (no URL prefix)
|
||||||
|
- **available**: Comma-separated list of all supported languages
|
||||||
|
|
||||||
|
### Step 2: Create Translation Files
|
||||||
|
|
||||||
|
Create translation files for each language in `custom/languages/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p custom/languages
|
||||||
|
```
|
||||||
|
|
||||||
|
**English** (`custom/languages/en.ini`):
|
||||||
|
```ini
|
||||||
|
home = "Home"
|
||||||
|
read_more = "Read more"
|
||||||
|
categories = "Categories"
|
||||||
|
tags = "Tags"
|
||||||
|
footer_text = "Made with FolderWeb"
|
||||||
|
footer_handcoded = "Generated in"
|
||||||
|
footer_page_time = "ms"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Norwegian** (`custom/languages/no.ini`):
|
||||||
|
```ini
|
||||||
|
home = "Hjem"
|
||||||
|
read_more = "Les mer"
|
||||||
|
categories = "Kategorier"
|
||||||
|
tags = "Stikkord"
|
||||||
|
footer_text = "Laget med FolderWeb"
|
||||||
|
footer_handcoded = "Generert på"
|
||||||
|
footer_page_time = "ms"
|
||||||
|
```
|
||||||
|
|
||||||
|
**French** (`custom/languages/fr.ini`):
|
||||||
|
```ini
|
||||||
|
home = "Accueil"
|
||||||
|
read_more = "Lire la suite"
|
||||||
|
categories = "Catégories"
|
||||||
|
tags = "Étiquettes"
|
||||||
|
footer_text = "Créé avec FolderWeb"
|
||||||
|
footer_handcoded = "Généré en"
|
||||||
|
footer_page_time = "ms"
|
||||||
|
```
|
||||||
|
|
||||||
|
## URL Structure
|
||||||
|
|
||||||
|
With the configuration above:
|
||||||
|
|
||||||
|
- **English** (default): `yoursite.com/about/`
|
||||||
|
- **Norwegian**: `yoursite.com/no/about/`
|
||||||
|
- **French**: `yoursite.com/fr/about/`
|
||||||
|
|
||||||
|
The default language never has a URL prefix.
|
||||||
|
|
||||||
|
## Creating Language-Specific Content
|
||||||
|
|
||||||
|
### Method 1: Separate Files Per Language
|
||||||
|
|
||||||
|
Use language suffixes in filenames: `filename.{lang}.ext`
|
||||||
|
|
||||||
|
**Example structure**:
|
||||||
|
```
|
||||||
|
content/about/
|
||||||
|
├── index.md # Default language (English)
|
||||||
|
├── index.no.md # Norwegian version
|
||||||
|
└── index.fr.md # French version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules**:
|
||||||
|
- Language-specific files (`.lang.ext`) show only for that language
|
||||||
|
- Default files (no language suffix) show only if no language variant exists
|
||||||
|
- Files are automatically filtered based on current language
|
||||||
|
|
||||||
|
### Example Content
|
||||||
|
|
||||||
|
**content/about/index.md** (English):
|
||||||
|
```markdown
|
||||||
|
# About Us
|
||||||
|
|
||||||
|
We are a company dedicated to simplicity.
|
||||||
|
```
|
||||||
|
|
||||||
|
**content/about/index.no.md** (Norwegian):
|
||||||
|
```markdown
|
||||||
|
# Om Oss
|
||||||
|
|
||||||
|
Vi er et selskap dedikert til enkelhet.
|
||||||
|
```
|
||||||
|
|
||||||
|
**content/about/index.fr.md** (French):
|
||||||
|
```markdown
|
||||||
|
# À Propos
|
||||||
|
|
||||||
|
Nous sommes une entreprise dédiée à la simplicité.
|
||||||
|
```
|
||||||
|
|
||||||
|
Now when users visit:
|
||||||
|
- `/about/` → Shows English (index.md)
|
||||||
|
- `/no/about/` → Shows Norwegian (index.no.md)
|
||||||
|
- `/fr/about/` → Shows French (index.fr.md)
|
||||||
|
|
||||||
|
### Method 2: Language-Specific Folders
|
||||||
|
|
||||||
|
For blog posts and articles, you can create separate folders:
|
||||||
|
|
||||||
|
```
|
||||||
|
content/blog/
|
||||||
|
├── 2025-11-01-english-post/
|
||||||
|
│ └── index.md
|
||||||
|
├── 2025-11-01-norsk-innlegg/
|
||||||
|
│ └── index.no.md
|
||||||
|
└── 2025-11-01-article-francais/
|
||||||
|
└── index.fr.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translated Slugs and Titles
|
||||||
|
|
||||||
|
Use `metadata.ini` to provide translated slugs and metadata:
|
||||||
|
|
||||||
|
**content/about/metadata.ini**:
|
||||||
|
```ini
|
||||||
|
; Default (English)
|
||||||
|
title = "About Us"
|
||||||
|
slug = "about"
|
||||||
|
|
||||||
|
[no]
|
||||||
|
title = "Om Oss"
|
||||||
|
slug = "om-oss"
|
||||||
|
|
||||||
|
[fr]
|
||||||
|
title = "À Propos"
|
||||||
|
slug = "a-propos"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now URLs become:
|
||||||
|
- English: `/about/`
|
||||||
|
- Norwegian: `/no/om-oss/`
|
||||||
|
- French: `/fr/a-propos/`
|
||||||
|
|
||||||
|
The actual folder is still named `about/`, but FolderWeb maps the translated slug to the real folder.
|
||||||
|
|
||||||
|
## Blog Posts with Translations
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```
|
||||||
|
content/blog/
|
||||||
|
└── 2025-11-02-my-post/
|
||||||
|
├── index.md
|
||||||
|
├── index.no.md
|
||||||
|
├── index.fr.md
|
||||||
|
├── cover.jpg
|
||||||
|
└── metadata.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
**metadata.ini**:
|
||||||
|
```ini
|
||||||
|
; Default language
|
||||||
|
title = "My First Post"
|
||||||
|
summary = "An introduction to multilingual blogging."
|
||||||
|
date = "2025-11-02"
|
||||||
|
|
||||||
|
[no]
|
||||||
|
title = "Mitt Første Innlegg"
|
||||||
|
summary = "En introduksjon til flerspråklig blogging."
|
||||||
|
|
||||||
|
[fr]
|
||||||
|
title = "Mon Premier Article"
|
||||||
|
summary = "Une introduction au blogging multilingue."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Date is global, cover image is shared across languages.
|
||||||
|
|
||||||
|
## Navigation and Menus
|
||||||
|
|
||||||
|
Navigation is automatically built with translations. In `metadata.ini` for each top-level directory:
|
||||||
|
|
||||||
|
**content/blog/metadata.ini**:
|
||||||
|
```ini
|
||||||
|
menu = true
|
||||||
|
menu_order = 1
|
||||||
|
|
||||||
|
title = "Blog"
|
||||||
|
[no]
|
||||||
|
title = "Blogg"
|
||||||
|
[fr]
|
||||||
|
title = "Blog"
|
||||||
|
```
|
||||||
|
|
||||||
|
**content/about/metadata.ini**:
|
||||||
|
```ini
|
||||||
|
menu = true
|
||||||
|
menu_order = 2
|
||||||
|
|
||||||
|
title = "About"
|
||||||
|
[no]
|
||||||
|
title = "Om"
|
||||||
|
[fr]
|
||||||
|
title = "À Propos"
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigation automatically includes language prefix in URLs.
|
||||||
|
|
||||||
|
## Using Translations in Templates
|
||||||
|
|
||||||
|
### In Default Templates
|
||||||
|
|
||||||
|
Translations are automatically available as `$translations` array:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<a href="<?= $ctx->langPrefix ?>/">
|
||||||
|
<?= $translations['home'] ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p><?= $translations['footer_text'] ?></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Custom Templates
|
||||||
|
|
||||||
|
Access translations the same way:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<button><?= $translations['read_more'] ?></button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Current Language
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php if ($ctx->currentLang === 'no'): ?>
|
||||||
|
<p>Dette er norsk innhold.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>This is English content.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Language Switcher
|
||||||
|
|
||||||
|
Create a language switcher in your custom base template:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<nav class="language-switcher">
|
||||||
|
<?php foreach ($ctx->availableLangs as $lang): ?>
|
||||||
|
<?php
|
||||||
|
$url = $lang === $ctx->defaultLang
|
||||||
|
? '/' . trim($ctx->requestPath, '/')
|
||||||
|
: '/' . $lang . '/' . trim($ctx->requestPath, '/');
|
||||||
|
$current = $lang === $ctx->currentLang;
|
||||||
|
?>
|
||||||
|
<a href="<?= $url ?>"
|
||||||
|
<?= $current ? 'aria-current="true"' : '' ?>
|
||||||
|
class="<?= $current ? 'active' : '' ?>">
|
||||||
|
<?= strtoupper($lang) ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
Style it:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.language-switcher {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher a {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher a.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## List Views with Multiple Languages
|
||||||
|
|
||||||
|
When displaying blog listings, FolderWeb automatically filters items by language:
|
||||||
|
|
||||||
|
```
|
||||||
|
content/blog/
|
||||||
|
├── 2025-11-01-english-article/
|
||||||
|
│ └── index.md # Shows in English
|
||||||
|
├── 2025-11-02-norsk-artikkel/
|
||||||
|
│ └── index.no.md # Shows only in Norwegian
|
||||||
|
└── 2025-11-03-universal/
|
||||||
|
├── index.md # Shows in English
|
||||||
|
├── index.no.md # Shows in Norwegian
|
||||||
|
└── index.fr.md # Shows in French
|
||||||
|
```
|
||||||
|
|
||||||
|
When viewing `/blog/`:
|
||||||
|
- Shows "english-article" and "universal"
|
||||||
|
|
||||||
|
When viewing `/no/blog/`:
|
||||||
|
- Shows "norsk-artikkel" and "universal"
|
||||||
|
|
||||||
|
When viewing `/fr/blog/`:
|
||||||
|
- Shows only "universal"
|
||||||
|
|
||||||
|
## Handling Missing Translations
|
||||||
|
|
||||||
|
### Default Fallback
|
||||||
|
|
||||||
|
If a translation is missing, FolderWeb uses the default language automatically.
|
||||||
|
|
||||||
|
### Show Different Content
|
||||||
|
|
||||||
|
You can use PHP in your content files:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php if ($ctx->currentLang === 'en'): ?>
|
||||||
|
# Welcome
|
||||||
|
This page is only in English.
|
||||||
|
<?php else: ?>
|
||||||
|
# Under Construction
|
||||||
|
This page is not yet translated.
|
||||||
|
<?php endif; ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEO Considerations
|
||||||
|
|
||||||
|
### Add hreflang Tags
|
||||||
|
|
||||||
|
In your custom base template:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<head>
|
||||||
|
<!-- ... other head content ... -->
|
||||||
|
|
||||||
|
<?php foreach ($ctx->availableLangs as $lang): ?>
|
||||||
|
<?php
|
||||||
|
$url = $lang === $ctx->defaultLang
|
||||||
|
? 'https://yoursite.com/' . trim($ctx->requestPath, '/')
|
||||||
|
: 'https://yoursite.com/' . $lang . '/' . trim($ctx->requestPath, '/');
|
||||||
|
?>
|
||||||
|
<link rel="alternate" hreflang="<?= $lang ?>" href="<?= $url ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="x-default"
|
||||||
|
href="https://yoursite.com/<?= trim($ctx->requestPath, '/') ?>">
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Language-Specific Metadata
|
||||||
|
|
||||||
|
Add language attributes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<html lang="<?= $ctx->currentLang ?>">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Your Multi-Language Site
|
||||||
|
|
||||||
|
1. **Visit default language**: `http://localhost:8000/about/`
|
||||||
|
2. **Visit Norwegian**: `http://localhost:8000/no/about/`
|
||||||
|
3. **Visit French**: `http://localhost:8000/fr/about/`
|
||||||
|
4. **Check navigation**: Ensure links include language prefix
|
||||||
|
5. **Test translation strings**: Verify UI text changes per language
|
||||||
|
6. **Check blog listings**: Confirm language-specific posts appear correctly
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Blog in Multiple Languages
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
content/blog/
|
||||||
|
├── metadata.ini # List template config
|
||||||
|
└── [date]-[slug]/
|
||||||
|
├── index.{lang}.md # One file per language
|
||||||
|
├── cover.jpg # Shared assets
|
||||||
|
└── metadata.ini # Translated metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation in Multiple Languages
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
content/docs/
|
||||||
|
├── metadata.ini # Template config
|
||||||
|
├── 00-intro.md # Default language
|
||||||
|
├── 00-intro.no.md # Norwegian
|
||||||
|
├── 01-setup.md
|
||||||
|
├── 01-setup.no.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed Content Strategy
|
||||||
|
|
||||||
|
Not everything needs translation. You can have:
|
||||||
|
- English-only blog posts (no language suffix)
|
||||||
|
- Multi-language main pages (with language suffixes)
|
||||||
|
- Shared images and assets
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Metadata Reference](../reference/metadata.md)
|
||||||
|
- [Configuration Reference](../reference/configuration.md)
|
||||||
|
- [Template Variables Reference](../reference/templates.md)
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue