This commit is contained in:
Ruben 2025-11-02 13:46:47 +01:00
parent b97b2f5503
commit ad516600bb
14 changed files with 6093 additions and 0 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,481 @@
# 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)