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:
parent
0e19040473
commit
76697e4656
11 changed files with 4724 additions and 0 deletions
304
docs/03-reference/01-configuration.md
Normal file
304
docs/03-reference/01-configuration.md
Normal 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
|
||||
447
docs/03-reference/02-metadata.md
Normal file
447
docs/03-reference/02-metadata.md
Normal 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
|
||||
474
docs/03-reference/03-template-variables.md
Normal file
474
docs/03-reference/03-template-variables.md
Normal 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>© <?= $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
|
||||
526
docs/03-reference/04-internationalization.md
Normal file
526
docs/03-reference/04-internationalization.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue