Compare commits

..

No commits in common. "ad516600bb15791123a462f38649a2317dfcc919" and "673c02d2377be9ee7e2bf9f2d2582ce07ae93914" have entirely different histories.

16 changed files with 6 additions and 6093 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/custom* /custom*
/content* /content*
/docs*

View file

@ -1,3 +1,8 @@
; PnP Framework Configuration
[languages] [languages]
; Default language (used when no language prefix in URL)
default = "no" default = "no"
; Available languages (comma-separated language codes)
available = "no,en" available = "no,en"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,196 +0,0 @@
# Getting Started
Welcome to FolderWeb! This tutorial will guide you through creating your first website from scratch in just a few minutes.
## What You'll Build
By the end of this tutorial, you'll have a simple website with:
- A home page
- An about page
- A blog with multiple posts
- Custom styling
## Prerequisites
- PHP 8.4 or higher
- A web server (Apache, Nginx, or PHP's built-in server)
- Basic understanding of file systems
## Step 1: Set Up Your Project
First, create your project directory:
```bash
mkdir my-website
cd my-website
```
Copy the FolderWeb framework files into your project:
```bash
# Copy app/ directory with all framework files
cp -r /path/to/folderweb/app ./app
# Create your content directory
mkdir content
```
## Step 2: Create Your First Page
Create a simple home page by adding a Markdown file to your content directory:
```bash
echo "# Welcome to My Website" > content/index.md
echo "" >> content/index.md
echo "This is my first page built with FolderWeb." >> content/index.md
```
## Step 3: Start the Server
Use PHP's built-in server to view your site:
```bash
php -S localhost:8000 -t . app/router.php
```
Open your browser and navigate to `http://localhost:8000`. You should see your home page!
## Step 4: Add Another Page
Create an "about" page. FolderWeb uses folder-based routing, so the folder name becomes the URL:
```bash
mkdir content/about
echo "# About Me" > content/about/index.md
echo "" >> content/about/index.md
echo "I'm learning FolderWeb, a minimalist PHP framework." >> content/about/index.md
```
Visit `http://localhost:8000/about/` to see your new page.
## Step 5: Create a Blog
Now let's create a blog with multiple posts. When a folder contains subdirectories, FolderWeb automatically creates a list view:
```bash
# Create blog directory
mkdir -p content/blog
# Create first post
mkdir content/blog/2025-11-01-my-first-post
echo "# My First Post" > content/blog/2025-11-01-my-first-post/index.md
echo "" >> content/blog/2025-11-01-my-first-post/index.md
echo "This is my first blog post!" >> content/blog/2025-11-01-my-first-post/index.md
# Create second post
mkdir content/blog/2025-11-02-second-post
echo "# Second Post" > content/blog/2025-11-02-second-post/index.md
echo "" >> content/blog/2025-11-02-second-post/index.md
echo "Already on my second post!" >> content/blog/2025-11-02-second-post/index.md
```
Visit `http://localhost:8000/blog/` to see your blog listing. Notice how FolderWeb automatically:
- Extracted dates from the folder names (2025-11-01, 2025-11-02)
- Created clickable links to each post
- Formatted the dates in Norwegian style
## Step 6: Add Metadata
Let's enhance your blog posts with metadata. Create a `metadata.ini` file in one of your posts:
```bash
cat > content/blog/2025-11-01-my-first-post/metadata.ini << 'EOF'
title = "My Amazing First Post"
date = "2025-11-01"
summary = "A brief introduction to my blogging journey with FolderWeb."
EOF
```
Refresh `http://localhost:8000/blog/` and you'll see the summary appears in the listing.
## Step 7: Add a Cover Image
Make your blog more visual by adding a cover image:
```bash
# Copy or download an image as cover.jpg in your post folder
# For this example, we'll just note where it should go:
# content/blog/2025-11-01-my-first-post/cover.jpg
```
If you add a `cover.jpg`, `cover.png`, or `cover.webp` file to a post folder, it will automatically appear in the blog listing.
## Step 8: Customize Your Blog Layout
Change how your blog is displayed by setting a different template. Create metadata for the blog directory:
```bash
cat > content/blog/metadata.ini << 'EOF'
title = "My Blog"
page_template = "list-grid"
EOF
```
Refresh `http://localhost:8000/blog/` to see a grid layout instead of a simple list.
Available list templates:
- `list` (default) - Simple list
- `list-grid` - Grid with cover images
- `list-card-grid` - Card-style grid
- `list-faq` - Expandable Q&A format
## Step 9: Add Custom Styles
Create a custom stylesheet to override the default styles:
```bash
mkdir -p custom/styles
cat > custom/styles/base.css << 'EOF'
:root {
--color-primary: oklch(0.55 0.20 30); /* Orange instead of blue */
--font-heading: "Arial", sans-serif; /* Sans-serif headings */
}
EOF
```
Refresh your browser to see your custom colors.
## Step 10: Multi-File Pages
FolderWeb can combine multiple files into a single page. This is useful for long documentation:
```bash
# Create a documentation page with multiple sections
mkdir content/docs
echo "# Introduction" > content/docs/00-intro.md
echo "Welcome to the docs!" >> content/docs/00-intro.md
echo "# Installation" > content/docs/01-install.md
echo "How to install the software." >> content/docs/01-install.md
echo "# Configuration" > content/docs/02-config.md
echo "How to configure it." >> content/docs/02-config.md
```
Visit `http://localhost:8000/docs/` and all three files render as one continuous page.
## What You've Learned
Congratulations! You now know how to:
- ✓ Create pages using Markdown files
- ✓ Use folder-based routing
- ✓ Create blog listings with automatic date extraction
- ✓ Add metadata to customize content
- ✓ Use cover images
- ✓ Switch between different list templates
- ✓ Apply custom styling
- ✓ Create multi-file pages
## Next Steps
- Explore the [How-To Guides](../how-to/) for specific tasks
- Read the [Reference Documentation](../reference/) for complete feature details
- Understand the [Philosophy](../explanation/philosophy.md) behind FolderWeb
- Learn about [multi-language support](../how-to/multi-language.md)
- Discover [template customization](../how-to/custom-templates.md)