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

View file

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

View file

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

610
docs/reference/metadata.md Normal file
View file

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

608
docs/reference/templates.md Normal file
View file

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