Add getting started documentation

Add tutorial on adding content

Add tutorial on styling

Add tutorial on templates

Add configuration reference

Add metadata reference

Add template variables reference

Add internationalization reference

Add plugin system documentation

Add creating templates documentation

Add index page
This commit is contained in:
Ruben 2025-11-27 23:01:02 +01:00
parent 0e19040473
commit 76697e4656
11 changed files with 4724 additions and 0 deletions

View file

@ -0,0 +1,304 @@
# Configuration Reference
FolderWeb uses INI files for configuration. The configuration system follows a simple hierarchy with sensible defaults.
## Configuration Files
```
app/default/config.ini # Framework defaults (don't modify)
custom/config.ini # Your overrides (create this)
```
**How it works:**
1. FolderWeb loads `app/default/config.ini`
2. If `custom/config.ini` exists, its values override the defaults
3. Only override what you need—missing values fall back to defaults
## Creating Your Configuration
**custom/config.ini:**
```ini
[languages]
default = "en"
available = "en,no,de"
[plugins]
enabled = "languages,my-custom-plugin"
[site]
title = "My Website"
```
That's it. Only add what you need to change.
## Available Configuration Options
### `[languages]`
Controls multilingual support (requires the `languages` plugin).
```ini
[languages]
default = "en" # Default language code
available = "en,no,de" # Comma-separated list of available languages
```
**Values:**
- `default` — Language code used when no language is specified in URL
- `available` — Comma-separated list of language codes (ISO 639-1)
**Example:**
```ini
[languages]
default = "no"
available = "no,en"
```
### `[plugins]`
Controls which plugins are loaded.
```ini
[plugins]
enabled = "languages,analytics,custom-plugin"
```
**Values:**
- `enabled` — Comma-separated list of plugin names (without `.php` extension)
**Plugin loading order:**
1. `app/plugins/global/` — Built-in global plugins
2. `custom/plugins/global/` — Your global plugins
3. `app/plugins/page/` — Built-in page plugins (not yet used)
4. `custom/plugins/page/` — Your page plugins (not yet used)
**Example:**
```ini
[plugins]
enabled = "languages"
```
To disable all plugins, leave the value empty:
```ini
[plugins]
enabled = ""
```
### Custom Sections
Add your own configuration sections for custom plugins:
```ini
[analytics]
tracking_id = "UA-12345678-1"
enabled = true
[social]
twitter = "@myhandle"
github = "myusername"
[api]
endpoint = "https://api.example.com"
key = "secret-key-here"
```
Access in plugins via the `$config` parameter:
```php
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
$trackingId = $config['analytics']['tracking_id'] ?? null;
// Use the config value...
return $ctx;
});
```
## Default Configuration
Here's what's included in `app/default/config.ini`:
```ini
[languages]
default = "en"
available = "en,no"
[plugins]
enabled = "languages"
```
These values are active unless you override them in `custom/config.ini`.
## Configuration Best Practices
### 1. Only Override What Changes
**Bad:**
```ini
[languages]
default = "en"
available = "en,no"
[plugins]
enabled = "languages"
```
**Good:**
```ini
# Only change the default language
[languages]
default = "no"
```
### 2. Use Comments
```ini
[languages]
default = "no" # Norwegian site
available = "no,en,de" # Also support English and German
[plugins]
enabled = "languages,analytics" # Google Analytics plugin
```
### 3. Keep Secrets Separate
Don't commit API keys and secrets to version control. Use environment-specific config or `.gitignore`:
```ini
[api]
key = "dev-key-here" # Override in production
```
### 4. Organize by Purpose
```ini
# Multilingual settings
[languages]
default = "en"
available = "en,no"
# Third-party services
[analytics]
enabled = true
tracking_id = "UA-12345678-1"
# Custom features
[reading_time]
words_per_minute = 200
```
## Environment-Specific Configuration
FolderWeb doesn't have built-in environment detection, but you can handle it manually:
**Option 1: Different files**
```bash
# Development
ln -s custom/config.dev.ini custom/config.ini
# Production
ln -s custom/config.prod.ini custom/config.ini
```
**Option 2: Server-side includes**
**custom/config.ini:**
```ini
[languages]
default = "en"
```
**custom/config.prod.ini:**
```ini
[api]
key = "production-key"
```
Load production config in your deployment script:
```bash
cat custom/config.prod.ini >> custom/config.ini
```
**Option 3: Environment variables**
Read from environment variables in a custom plugin:
```php
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
// Override config with environment variables
$apiKey = getenv('API_KEY') ?: ($config['api']['key'] ?? null);
$ctx->set('api_key', $apiKey);
return $ctx;
});
```
## Accessing Configuration in Code
Configuration is passed to plugin hooks:
```php
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
// Access configuration
$defaultLang = $config['languages']['default'] ?? 'en';
$plugins = $config['plugins']['enabled'] ?? '';
// Use it
$ctx->set('site_lang', $defaultLang);
return $ctx;
});
```
Configuration is **not** directly available in templates. If you need config values in templates, set them via a plugin hook:
```php
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
global $config;
$vars['siteTitle'] = $config['site']['title'] ?? 'My Site';
$vars['socialLinks'] = [
'twitter' => $config['social']['twitter'] ?? null,
'github' => $config['social']['github'] ?? null,
];
return $vars;
});
```
## Configuration Schema
FolderWeb doesn't enforce a schema—you can add any sections and keys you need. However, these are the recognized built-in options:
| Section | Key | Type | Default | Description |
|---------|-----|------|---------|-------------|
| `languages` | `default` | string | `"en"` | Default language code |
| `languages` | `available` | string | `"en,no"` | Comma-separated language codes |
| `plugins` | `enabled` | string | `"languages"` | Comma-separated plugin names |
All other sections are custom and plugin-specific.
## Debugging Configuration
To see the active configuration, create a debug page:
**content/debug.php:**
```php
<?php
global $config;
echo '<pre>';
print_r($config);
echo '</pre>';
?>
```
Visit `/debug/` to see the merged configuration array.
**Remember to delete this page** before deploying to production.
## What's Next?
- **[Metadata Reference](#)** — Configure individual pages with `metadata.ini`
- **[Template Variables](#)** — Access configuration in templates
- **[Creating Plugins](#)** — Use configuration in custom plugins

View file

@ -0,0 +1,447 @@
# Metadata Reference
Metadata files (`metadata.ini`) configure individual pages and directories. They control titles, URLs, templates, navigation, and more.
## Basic Structure
```ini
title = "Page Title"
summary = "Short description"
date = "2024-12-15"
search_description = "SEO-friendly description"
```
Place `metadata.ini` in any content directory:
```
content/blog/my-post/
├── index.md
└── metadata.ini
```
## Core Fields
### `title`
The page or item title.
```ini
title = "My Blog Post"
```
**Default:** Extracted from first `# Heading` in Markdown, or folder name
**Used in:** Page `<title>`, list items, navigation menu
**Type:** String
### `summary`
Short description shown in list views.
```ini
summary = "A brief introduction to this topic"
```
**Default:** None (empty)
**Used in:** List item previews, RSS feeds, social media cards
**Type:** String
**Recommended length:** 150-200 characters
### `date`
Publication or modification date.
```ini
date = "2024-12-15"
```
**Default:** Extracted from folder name (`YYYY-MM-DD-title`)
**Format:** `YYYY-MM-DD` (ISO 8601)
**Used in:** List sorting, date displays, `<time>` elements
**Type:** String (date)
### `search_description`
SEO meta description for search engines.
```ini
search_description = "Learn how to build fast, maintainable websites with FolderWeb"
```
**Default:** Uses `summary` if not set
**Used in:** `<meta name="description">` tag
**Type:** String
**Recommended length:** 150-160 characters
### `slug`
Custom URL slug (overrides folder name).
```ini
slug = "custom-url"
```
**Default:** Folder name (with date prefix removed)
**Used in:** URL generation
**Type:** String (alphanumeric, hyphens, underscores)
**Example:**
```
Folder: content/blog/2024-12-15-very-long-title/
Slug: short-title
URL: /blog/short-title/
```
### `menu`
Show this page in the navigation menu.
```ini
menu = 1
```
**Default:** `0` (not in menu)
**Values:** `1` (show) or `0` (hide)
**Type:** Integer
### `menu_order`
Position in navigation menu (lower numbers first).
```ini
menu = 1
menu_order = 10
```
**Default:** `999` (last)
**Used in:** Navigation sorting
**Type:** Integer
**Example:**
```ini
# Home
menu = 1
menu_order = 1
# About
menu = 1
menu_order = 10
# Blog
menu = 1
menu_order = 20
# Contact
menu = 1
menu_order = 30
```
## Settings Section
Advanced settings go in a `[settings]` section:
```ini
title = "My Page"
[settings]
page_template = "list-grid"
show_date = true
hide_list = false
```
### `page_template`
Which template to use for list views.
```ini
[settings]
page_template = "list-grid"
```
**Default:** `list` (uses `list.php`)
**Available templates:**
- `list` — Simple vertical list
- `list-grid` — Card grid layout
- `list-compact` — Minimal compact list
- Custom templates you create
**Used in:** List view rendering
**Type:** String (template name without `.php`)
### `show_date`
Display the date on the page.
```ini
[settings]
show_date = true
```
**Default:** `true`
**Values:** `true` or `false`
**Type:** Boolean
### `hide_list`
Don't show list view even if directory has subdirectories.
```ini
[settings]
hide_list = true
```
**Default:** `false`
**Values:** `true` or `false`
**Type:** Boolean
**Use case:** Section landing pages that should show content instead of list
## Language-Specific Overrides
Add language-specific sections to override fields:
```ini
title = "About Us"
summary = "Learn about our company"
slug = "about"
[no]
title = "Om oss"
summary = "Les om bedriften vår"
slug = "om"
[de]
title = "Über uns"
summary = "Erfahren Sie mehr über unser Unternehmen"
slug = "uber-uns"
```
**Supported fields in language sections:**
- `title`
- `summary`
- `search_description`
- `slug`
**Language codes:** Must match your configured languages (`config.ini`).
**URLs with language-specific slugs:**
- English: `/about/`
- Norwegian: `/no/om/`
- German: `/de/uber-uns/`
## Custom Fields
Add any custom fields you need:
```ini
title = "Project X"
author = "Jane Doe"
client = "ACME Corp"
project_url = "https://example.com"
featured = true
tags = "web,design,portfolio"
```
Access custom fields in templates:
```php
<?php if (isset($metadata['author'])): ?>
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
<?php endif; ?>
<?php if (isset($metadata['project_url'])): ?>
<a href="<?= htmlspecialchars($metadata['project_url']) ?>">
View Project
</a>
<?php endif; ?>
```
Or in plugins:
```php
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
$metadata = $ctx->get('metadata', []);
if (isset($metadata['tags'])) {
$vars['tags'] = explode(',', $metadata['tags']);
}
return $vars;
});
```
## Complete Example
**content/blog/2024-12-15-building-fast-sites/metadata.ini:**
```ini
# Core fields
title = "Building Fast Websites"
summary = "Learn how to optimize your site for speed and performance"
date = "2024-12-15"
search_description = "A comprehensive guide to building fast, performant websites in 2024"
# Navigation
menu = 0 # Don't show in main menu
menu_order = 999
# Custom slug
slug = "fast-sites"
# Settings
[settings]
show_date = true
# Custom fields
author = "Jane Doe"
category = "Web Performance"
tags = "performance,optimization,web"
estimated_reading_time = 8
# Norwegian translation
[no]
title = "Bygge raske nettsider"
summary = "Lær hvordan du optimaliserer nettstedet ditt for hastighet og ytelse"
search_description = "En omfattende guide til å bygge raske, effektive nettsteder i 2024"
slug = "raske-sider"
```
## Metadata Priority
When determining values, FolderWeb follows this priority:
1. **Language-specific metadata** (e.g., `[no]` section)
2. **Root metadata** (e.g., `title = "..."`)
3. **Auto-extracted values** (e.g., first heading, folder date)
4. **Defaults** (e.g., folder name)
**Example:**
```
Folder: content/blog/2024-12-15-my-post/
```
**Title resolution:**
1. Check `metadata.ini` for `[en] title = "..."`
2. Check `metadata.ini` for `title = "..."`
3. Extract from first `# Heading` in Markdown
4. Use folder name: "my-post"
## Metadata in List Items
When rendering list views, each item receives these metadata fields:
```php
$item = [
'url' => '/blog/my-post/',
'path' => '/content/blog/2024-12-15-my-post',
'title' => 'My Post',
'summary' => 'Short description',
'date' => '2024-12-15',
'formatted_date' => '15. desember 2024', // Language-specific
'cover_image' => '/blog/my-post/cover.jpg', // If exists
// All custom metadata fields...
'author' => 'Jane Doe',
'tags' => 'web,design',
];
```
Access in list templates:
```php
<?php foreach ($items as $item): ?>
<article>
<h2><?= htmlspecialchars($item['title']) ?></h2>
<?php if (isset($item['author'])): ?>
<p>By <?= htmlspecialchars($item['author']) ?></p>
<?php endif; ?>
<?php if (isset($item['summary'])): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
```
## Best Practices
### 1. Use Consistent Field Names
```ini
# Good: consistent naming
author = "Jane Doe"
published_date = "2024-12-15"
featured = true
# Bad: inconsistent naming
Author = "Jane Doe"
PublishDate = "2024-12-15"
is_featured = true
```
### 2. Keep Summaries Concise
```ini
# Good
summary = "Learn to optimize website performance in 5 steps"
# Too long
summary = "This comprehensive article will teach you everything you need to know about optimizing website performance, including lazy loading, code splitting, image optimization, and much more in detailed steps with examples"
```
### 3. Use Semantic Custom Fields
```ini
# Good: clear purpose
author = "Jane Doe"
category = "Tutorial"
difficulty = "Beginner"
# Bad: unclear purpose
field1 = "Jane Doe"
field2 = "Tutorial"
field3 = "Beginner"
```
### 4. Add Comments
```ini
# SEO and social media
title = "Building Fast Websites"
search_description = "A guide to web performance optimization"
# Author and categorization
author = "Jane Doe"
category = "Performance"
# Custom display options
featured = true # Show in featured section
priority = 10 # Higher = more prominent
```
## Debugging Metadata
To see parsed metadata, create a debug template:
**custom/templates/page.php:**
```php
<pre><?php print_r($metadata); ?></pre>
<hr>
<?= $content ?>
```
Or in list templates:
```php
<pre><?php print_r($items); ?></pre>
<hr>
<?= $pageContent ?>
```
**Remember to revert** before deploying to production.
## What's Next?
- **[Template Variables](#)** — See how metadata is used in templates
- **[Internationalization](#)** — Use language-specific metadata
- **[Creating Plugins](#)** — Process metadata in custom plugins

View file

@ -0,0 +1,474 @@
# Template Variables Reference
Templates have access to a set of variables provided by FolderWeb and its plugins. This reference documents all available variables and their types.
## Base Template Variables
Available in `base.php`:
### `$content`
The fully rendered HTML content from the page or list template.
**Type:** String (HTML)
**Example:**
```php
<main>
<?= $content ?>
</main>
```
### `$pageTitle`
The page title for the `<title>` tag.
**Type:** String
**Default:** `"FolderWeb"`
**Example:**
```php
<title><?= htmlspecialchars($pageTitle ?? 'FolderWeb') ?></title>
```
**Source:**
1. Language-specific metadata `[lang] title`
2. Root metadata `title`
3. First heading in content
4. Folder name
### `$metaDescription`
SEO description for the `<meta name="description">` tag.
**Type:** String
**Optional:** May be empty
**Example:**
```php
<?php if (!empty($metaDescription)): ?>
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
<?php endif; ?>
```
**Source:**
1. Metadata `search_description`
2. Metadata `summary`
3. Empty if not set
### `$socialImageUrl`
URL to cover image for social media meta tags.
**Type:** String (URL)
**Optional:** May be empty
**Example:**
```php
<?php if (!empty($socialImageUrl)): ?>
<meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
<?php endif; ?>
```
**Source:** First `cover.*` image found in content directory
### `$navigation`
Array of navigation menu items.
**Type:** Array of associative arrays
**Structure:**
```php
[
['url' => '/about/', 'title' => 'About'],
['url' => '/blog/', 'title' => 'Blog'],
['url' => '/contact/', 'title' => 'Contact'],
]
```
**Example:**
```php
<?php if (!empty($navigation)): ?>
<nav>
<?php foreach ($navigation as $item): ?>
<a href="<?= htmlspecialchars($item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
```
**Source:** Pages with `menu = 1` in metadata, sorted by `menu_order`
### `$homeLabel`
Text for the home link.
**Type:** String
**Default:** `"Home"`
**Example:**
```php
<a href="/"><?= htmlspecialchars($homeLabel ?? 'Home') ?></a>
```
**Source:** Translation string `translations['home']` or fallback "Home"
### `$currentLang`
Current language code.
**Type:** String
**Default:** From `config.ini` `languages.default`
**Example:**
```php
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
```
**Values:** ISO 639-1 language codes (`en`, `no`, `de`, etc.)
### `$langPrefix`
URL prefix for the current language.
**Type:** String
**Default:** Empty string for default language
**Example:**
```php
<a href="<?= htmlspecialchars($langPrefix ?? '') ?>/">Home</a>
```
**Values:**
- `""` (empty) for default language
- `"/no"` for Norwegian
- `"/de"` for German
- etc.
### `$languageUrls`
URLs to switch between available languages.
**Type:** Associative array
**Structure:**
```php
[
'en' => '/page/',
'no' => '/no/side/',
'de' => '/de/seite/',
]
```
**Example:**
```php
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
<nav class="language-switcher">
<?php foreach ($languageUrls as $lang => $url): ?>
<a href="<?= htmlspecialchars($url) ?>"
<?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>>
<?= htmlspecialchars(strtoupper($lang)) ?>
</a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
```
### `$translations`
Translated UI strings for the current language.
**Type:** Associative array
**Structure:**
```php
[
'home' => 'Home',
'footer_handcoded' => 'Generated in',
'footer_page_time' => 'ms',
'months' => 'January,February,March,...',
]
```
**Example:**
```php
<p><?= htmlspecialchars($translations['home'] ?? 'Home') ?></p>
```
**Source:** Language files in `custom/languages/[lang].ini` or `app/default/languages/[lang].ini`
### `$pageCssUrl`
URL to page-specific CSS file.
**Type:** String (URL)
**Optional:** Only set if `styles.css` exists in content directory
**Example:**
```php
<?php if (!empty($pageCssUrl)): ?>
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
<?php endif; ?>
```
### `$pageCssHash`
MD5 hash of page-specific CSS for cache busting.
**Type:** String (MD5 hash)
**Optional:** Only set if `$pageCssUrl` exists
**Example:** See `$pageCssUrl` above
## Page Template Variables
Available in `page.php`:
### `$content`
The fully rendered HTML content from the page.
**Type:** String (HTML)
**Example:**
```php
<article>
<?= $content ?>
</article>
```
### `$metadata`
All metadata for the current page.
**Type:** Associative array
**Structure:**
```php
[
'title' => 'Page Title',
'summary' => 'Short description',
'date' => '2024-12-15',
'formatted_date' => '15. desember 2024',
'show_date' => true,
'author' => 'Jane Doe', // Custom fields
'tags' => 'web,design',
// ... all other metadata fields
]
```
**Example:**
```php
<?php if (isset($metadata['title'])): ?>
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
<?php endif; ?>
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
<time datetime="<?= $metadata['date'] ?>">
<?= $metadata['formatted_date'] ?? $metadata['date'] ?>
</time>
<?php endif; ?>
```
## List Template Variables
Available in `list.php`, `list-grid.php`, `list-compact.php`, etc.:
### `$pageContent`
Optional intro content from the directory's own files.
**Type:** String (HTML)
**Optional:** May be empty
**Example:**
```php
<?php if ($pageContent): ?>
<div class="intro">
<?= $pageContent ?>
</div>
<?php endif; ?>
```
**Source:** Content files in the list directory itself (not subdirectories)
### `$items`
Array of items to display in the list.
**Type:** Array of associative arrays
**Structure:**
```php
[
[
'url' => '/blog/my-post/',
'path' => '/content/blog/2024-12-15-my-post',
'title' => 'My Post',
'summary' => 'Short description',
'date' => '2024-12-15',
'formatted_date' => '15. desember 2024',
'cover_image' => '/blog/my-post/cover.jpg',
// All custom metadata fields...
'author' => 'Jane Doe',
'category' => 'Tutorial',
],
// ... more items
]
```
**Example:**
```php
<?php foreach ($items as $item): ?>
<article>
<?php if (isset($item['cover_image'])): ?>
<img src="<?= $item['cover_image'] ?>"
alt="<?= htmlspecialchars($item['title']) ?>">
<?php endif; ?>
<h2>
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h2>
<?php if (isset($item['date'])): ?>
<time datetime="<?= $item['date'] ?>">
<?= $item['formatted_date'] ?? $item['date'] ?>
</time>
<?php endif; ?>
<?php if (isset($item['summary'])): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?>
```
### `$metadata`
Metadata for the list directory itself.
**Type:** Associative array
**Structure:** Same as page metadata
**Example:**
```php
<?php if (isset($metadata['title'])): ?>
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
<?php endif; ?>
```
## Item Properties
Each item in `$items` has these properties:
| Property | Type | Description | Optional |
|----------|------|-------------|----------|
| `url` | String | Full URL to the item | No |
| `path` | String | Filesystem path to item | No |
| `title` | String | Item title | No |
| `summary` | String | Short description | Yes |
| `date` | String | ISO date (YYYY-MM-DD) | Yes |
| `formatted_date` | String | Localized date string | Yes |
| `cover_image` | String | URL to cover image | Yes |
| Custom fields | Mixed | Any metadata fields | Yes |
## Adding Custom Variables
Use the `Hook::TEMPLATE_VARS` hook to add custom variables:
```php
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
// Add a custom variable
$vars['siteName'] = 'My Website';
// Add computed values
$vars['currentYear'] = date('Y');
// Add from context
$vars['userName'] = $ctx->get('user_name', 'Guest');
return $vars;
});
```
Then use in templates:
```php
<p>&copy; <?= $currentYear ?> <?= htmlspecialchars($siteName) ?></p>
```
## Variable Availability by Template
| Variable | `base.php` | `page.php` | `list.php` |
|----------|------------|------------|------------|
| `$content` | ✓ | ✓ | — |
| `$pageTitle` | ✓ | — | — |
| `$metaDescription` | ✓ | — | — |
| `$socialImageUrl` | ✓ | — | — |
| `$navigation` | ✓ | — | — |
| `$homeLabel` | ✓ | — | — |
| `$currentLang` | ✓ | — | — |
| `$langPrefix` | ✓ | — | — |
| `$languageUrls` | ✓ | — | — |
| `$translations` | ✓ | — | — |
| `$pageCssUrl` | ✓ | — | — |
| `$pageCssHash` | ✓ | — | — |
| `$metadata` | — | ✓ | ✓ |
| `$pageContent` | — | — | ✓ |
| `$items` | — | — | ✓ |
**Note:** All variables are technically available everywhere via plugin hooks, but this table shows the default availability.
## Escaping Output
**Always escape user content** to prevent XSS attacks:
```php
<!-- Good -->
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
<p><?= htmlspecialchars($item['summary']) ?></p>
<!-- Bad -->
<h1><?= $metadata['title'] ?></h1>
<p><?= $item['summary'] ?></p>
```
**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown):
```php
<!-- Good (content is already HTML) -->
<div class="content">
<?= $content ?>
</div>
```
## Checking Variable Existence
Always check if optional variables exist:
```php
<!-- Good -->
<?php if (isset($metadata['author'])): ?>
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
<?php endif; ?>
<!-- Bad (may cause warnings) -->
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
```
Use null coalescing for defaults:
```php
<p><?= htmlspecialchars($metadata['author'] ?? 'Anonymous') ?></p>
```
## Debugging Variables
To see all available variables in a template:
```php
<pre><?php var_dump(get_defined_vars()); ?></pre>
```
Or specific variables:
```php
<pre><?php print_r($metadata); ?></pre>
<pre><?php print_r($items); ?></pre>
```
**Remember to remove debug code** before deploying to production.
## What's Next?
- **[Internationalization](#)** — Use language-specific variables
- **[Creating Plugins](#)** — Add custom template variables
- **[Template Tutorial](#)** — See variables in action

View file

@ -0,0 +1,526 @@
# Internationalization (i18n)
FolderWeb supports multilingual websites through the built-in `languages` plugin. This guide covers everything you need to build sites in multiple languages.
## How It Works
The language plugin provides URL-based language selection:
- **Default language:** `/about/` (no language prefix)
- **Other languages:** `/no/om/`, `/de/uber-uns/`
Language is determined from the URL, and content files, metadata, and translations adapt automatically.
## Configuration
Enable and configure languages in `custom/config.ini`:
```ini
[languages]
default = "en" # Default language (no URL prefix)
available = "en,no,de" # Comma-separated language codes
[plugins]
enabled = "languages" # Enable the language plugin
```
**Language codes:** Use ISO 639-1 two-letter codes (`en`, `no`, `de`, `fr`, `es`, etc.).
## Language-Specific Content Files
Create language variants of content files using the naming pattern `name.lang.ext`:
```
content/about/
├── index.md # Default language (English)
├── index.no.md # Norwegian version
└── index.de.md # German version
```
**How it works:**
- URL `/about/` → Shows `index.md`
- URL `/no/om/` → Shows `index.no.md`
- URL `/de/uber-uns/` → Shows `index.de.md`
**Fallback behavior:** If no language-specific file exists, the default file is shown.
### Multiple Files Per Page
Language variants work with multiple content files:
```
content/portfolio/
├── 00-hero.php
├── 00-hero.no.php
├── 01-intro.md
├── 01-intro.no.md
├── 02-projects.html
└── 02-projects.no.html
```
- URL `/portfolio/` → Shows `00-hero.php` + `01-intro.md` + `02-projects.html`
- URL `/no/portfolio/` → Shows `00-hero.no.php` + `01-intro.no.md` + `02-projects.no.html`
## Language-Specific Metadata
Override metadata fields for each language using sections in `metadata.ini`:
```ini
# Default (English)
title = "About Us"
summary = "Learn about our company"
slug = "about"
# Norwegian
[no]
title = "Om oss"
summary = "Les om bedriften vår"
slug = "om"
# German
[de]
title = "Über uns"
summary = "Erfahren Sie mehr über unser Unternehmen"
slug = "uber-uns"
```
**Supported fields:**
- `title` — Page/item title
- `summary` — Short description
- `search_description` — SEO description
- `slug` — Custom URL slug
**Result:**
- `/about/` — Title: "About Us"
- `/no/om/` — Title: "Om oss"
- `/de/uber-uns/` — Title: "Über uns"
## Translation Files
UI strings (home link, footer text, month names) are translated using language files:
```
custom/languages/
├── en.ini
├── no.ini
└── de.ini
```
### Creating Translation Files
**custom/languages/en.ini:**
```ini
home = "Home"
footer_handcoded = "Generated in"
footer_page_time = "ms"
months = "January,February,March,April,May,June,July,August,September,October,November,December"
```
**custom/languages/no.ini:**
```ini
home = "Hjem"
footer_handcoded = "Generert på"
footer_page_time = "ms"
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"
```
**custom/languages/de.ini:**
```ini
home = "Startseite"
footer_handcoded = "Generiert in"
footer_page_time = "ms"
months = "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember"
```
### Using Translations in Templates
Access translations via the `$translations` variable:
```php
<a href="/">
<?= htmlspecialchars($translations['home'] ?? 'Home') ?>
</a>
<footer>
<p>
<?= htmlspecialchars($translations['footer_handcoded'] ?? 'Generated in') ?>
<?= number_format($pageLoadTime, 4) ?>
<?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
</p>
</footer>
```
### Adding Custom Translation Strings
Add any strings you need:
**custom/languages/en.ini:**
```ini
read_more = "Read more"
posted_on = "Posted on"
by_author = "by"
categories = "Categories"
tags = "Tags"
```
**custom/languages/no.ini:**
```ini
read_more = "Les mer"
posted_on = "Publisert"
by_author = "av"
categories = "Kategorier"
tags = "Tagger"
```
Use in templates:
```php
<a href="<?= $item['url'] ?>">
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?>
</a>
<p>
<?= htmlspecialchars($translations['posted_on'] ?? 'Posted on') ?>
<?= $item['formatted_date'] ?>
</p>
```
## Language Switcher
The language plugin automatically provides language switcher URLs in the `$languageUrls` variable.
**In base.php:**
```php
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
<nav class="language-switcher" aria-label="Language">
<?php foreach ($languageUrls as $lang => $url): ?>
<a href="<?= htmlspecialchars($url) ?>"
<?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>>
<?= htmlspecialchars(strtoupper($lang)) ?>
</a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
```
**How it works:**
- The switcher links to the **same page** in different languages
- Language-specific slugs are automatically resolved
- Current language is marked with `aria-current="true"`
**Example URLs:**
- On `/about/`: EN → `/about/`, NO → `/no/om/`, DE → `/de/uber-uns/`
- On `/no/om/`: EN → `/about/`, NO → `/no/om/`, DE → `/de/uber-uns/`
## Date Formatting
Dates are automatically formatted using translated month names.
**With `months` in language files:**
```ini
# en.ini
months = "January,February,March,April,May,June,July,August,September,October,November,December"
# no.ini
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"
```
**Result:**
- English: "15. December 2024"
- Norwegian: "15. desember 2024"
**Date format:** `[day]. [month] [year]` (e.g., "15. December 2024")
## Complete Multilingual Example
### Directory Structure
```
content/
├── metadata.ini
├── index.md
├── index.no.md
└── blog/
├── metadata.ini
├── 2024-12-15-first-post/
│ ├── metadata.ini
│ ├── index.md
│ ├── index.no.md
│ └── cover.jpg
└── 2024-12-20-second-post/
├── metadata.ini
├── index.md
└── index.no.md
```
### Root Metadata
**content/metadata.ini:**
```ini
title = "My Site"
[no]
title = "Min Side"
```
### Blog Metadata
**content/blog/metadata.ini:**
```ini
title = "Blog"
summary = "Latest articles and updates"
[no]
title = "Blogg"
summary = "Siste artikler og oppdateringer"
```
### Post Metadata
**content/blog/2024-12-15-first-post/metadata.ini:**
```ini
title = "My First Post"
summary = "An introduction to my blog"
slug = "first-post"
[no]
title = "Mitt første innlegg"
summary = "En introduksjon til bloggen min"
slug = "forste-innlegg"
```
### URLs Generated
**English (default):**
- Home: `/`
- Blog: `/blog/`
- Post: `/blog/first-post/`
**Norwegian:**
- Home: `/no/`
- Blog: `/no/blogg/`
- Post: `/no/blogg/forste-innlegg/`
## Language-Aware Navigation
Navigation menus automatically use language-specific titles:
**content/about/metadata.ini:**
```ini
title = "About"
menu = 1
menu_order = 10
[no]
title = "Om"
```
**Result in navigation:**
- English site: "About"
- Norwegian site: "Om"
## Template Variables for i18n
The language plugin provides these template variables:
| Variable | Type | Description |
|----------|------|-------------|
| `$currentLang` | String | Current language code (e.g., `"en"`, `"no"`) |
| `$defaultLang` | String | Default language from config |
| `$langPrefix` | String | URL prefix (e.g., `""`, `"/no"`) |
| `$languageUrls` | Array | URLs to switch languages |
| `$translations` | Array | Translated UI strings |
| `$availableLangs` | Array | All available language codes |
**Example usage:**
```php
<html lang="<?= htmlspecialchars($currentLang) ?>">
<a href="<?= htmlspecialchars($langPrefix) ?>/">
<?= htmlspecialchars($translations['home'] ?? 'Home') ?>
</a>
<nav>
<?php foreach ($navigation as $item): ?>
<a href="<?= htmlspecialchars($langPrefix . $item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
<?php endforeach; ?>
</nav>
```
## Right-to-Left (RTL) Languages
For RTL languages (Arabic, Hebrew, etc.), set the `dir` attribute:
**custom/templates/base.php:**
```php
<?php
$rtlLangs = ['ar', 'he', 'fa', 'ur'];
$dir = in_array($currentLang, $rtlLangs) ? 'rtl' : 'ltr';
?>
<html lang="<?= htmlspecialchars($currentLang) ?>" dir="<?= $dir ?>">
```
Use logical CSS properties for proper RTL support:
```css
/* Good: logical properties */
.card {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
}
/* Bad: directional properties */
.card {
margin-left: 1rem;
padding-right: 2rem;
}
```
## Best Practices
### 1. Always Provide Fallbacks
```php
<!-- Good -->
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?>
<!-- Bad -->
<?= htmlspecialchars($translations['read_more']) ?>
```
### 2. Use Language Codes Consistently
```ini
# Good
[languages]
available = "en,no,de" # Lowercase, ISO 639-1
# Bad
available = "EN,nb-NO,de-DE" # Mixed case, non-standard
```
### 3. Translate Everything
Don't mix languages on the same page:
```php
<!-- Good -->
<p><?= htmlspecialchars($translations['posted_on']) ?> <?= $item['formatted_date'] ?></p>
<!-- Bad -->
<p>Posted on <?= $item['formatted_date'] ?></p> <!-- English hardcoded -->
```
### 4. Test All Languages
Verify:
- Content files load correctly
- Metadata overrides work
- Language switcher links are correct
- Navigation uses translated titles
- Dates format properly
### 5. Handle Missing Translations Gracefully
```php
<?php if (isset($item['summary'])): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php else: ?>
<p><?= htmlspecialchars($translations['no_summary'] ?? 'No description available') ?></p>
<?php endif; ?>
```
## Limitations
### No Automatic Translation
FolderWeb doesn't translate content automatically. You must:
- Create separate content files for each language
- Manually translate all metadata
- Provide all translation strings
### No Language Detection
FolderWeb doesn't detect browser language. Users must:
- Click the language switcher
- Visit a language-specific URL directly
You can add browser detection with a custom plugin if needed.
### Fixed URL Structure
All languages share the same folder structure. You cannot have:
- Content in `/en/blog/` and `/no/nyheter/` (different folder names)
You must use:
- Content in `/blog/` with language-specific slugs and content files
## Troubleshooting
### Language Switcher Shows Wrong URLs
**Problem:** Language switcher links to incorrect pages.
**Solution:** Check that language-specific slugs are set in metadata:
```ini
slug = "about"
[no]
slug = "om" # Must be set
```
### Content Not Changing Language
**Problem:** Same content appears in all languages.
**Solution:** Verify file naming:
- ✓ `index.no.md` (correct)
- ✗ `index-no.md` (wrong)
- ✗ `index_no.md` (wrong)
### Dates Not Translating
**Problem:** Dates show in English for all languages.
**Solution:** Add `months` to language files:
```ini
months = "January,February,March,April,May,June,July,August,September,October,November,December"
```
### Navigation Shows English Titles
**Problem:** Menu items use English even in other languages.
**Solution:** Add language sections to metadata:
```ini
title = "About"
menu = 1
[no]
title = "Om"
```
## What's Next?
- **[Configuration Reference](#)** — Configure available languages
- **[Metadata Reference](#)** — Set language-specific metadata
- **[Template Variables](#)** — Use i18n variables in templates
- **[Creating Plugins](#)** — Extend i18n functionality