Compare commits
3 commits
673c02d237
...
ad516600bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad516600bb | ||
|
|
b97b2f5503 | ||
|
|
eec90a4cd8 |
16 changed files with 6093 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
||||||
/custom*
|
/custom*
|
||||||
/content*
|
/content*
|
||||||
/docs*
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,3 @@
|
||||||
; PnP Framework Configuration
|
|
||||||
|
|
||||||
[languages]
|
[languages]
|
||||||
; Default language (used when no language prefix in URL)
|
|
||||||
default = "no"
|
default = "no"
|
||||||
|
|
||||||
; Available languages (comma-separated language codes)
|
|
||||||
available = "no,en"
|
available = "no,en"
|
||||||
|
|
|
||||||
211
docs/README.md
Normal file
211
docs/README.md
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
# FolderWeb Documentation
|
||||||
|
|
||||||
|
Welcome to the FolderWeb documentation! This comprehensive guide covers everything you need to know about building and maintaining websites with FolderWeb.
|
||||||
|
|
||||||
|
## 📚 Documentation Organization
|
||||||
|
|
||||||
|
This documentation follows the [Diataxis framework](https://diataxis.fr/), organizing content into four distinct types to help you find exactly what you need:
|
||||||
|
|
||||||
|
### 🎓 Tutorial (Learning-Oriented)
|
||||||
|
|
||||||
|
**Purpose**: Learn by doing
|
||||||
|
**For**: Newcomers to FolderWeb
|
||||||
|
|
||||||
|
- **[Getting Started](tutorial/00-getting-started.md)** - Build your first FolderWeb site from scratch in 10 minutes
|
||||||
|
|
||||||
|
Start here if you're new to FolderWeb. This hands-on tutorial walks you through creating a complete website with pages, blog posts, and custom styling.
|
||||||
|
|
||||||
|
### 📋 How-To Guides (Task-Oriented)
|
||||||
|
|
||||||
|
**Purpose**: Solve specific problems
|
||||||
|
**For**: Users with a specific goal
|
||||||
|
|
||||||
|
- **[Custom Templates](how-to/custom-templates.md)** - Override default templates with your own designs
|
||||||
|
- **[Custom Styles](how-to/custom-styles.md)** - Customize appearance using CSS
|
||||||
|
- **[Multi-Language Sites](how-to/multi-language.md)** - Set up and manage multiple languages
|
||||||
|
- **[Working with Metadata](how-to/working-with-metadata.md)** - Control content with metadata.ini files
|
||||||
|
|
||||||
|
Use these guides when you know what you want to accomplish and need step-by-step instructions.
|
||||||
|
|
||||||
|
### 📖 Reference (Information-Oriented)
|
||||||
|
|
||||||
|
**Purpose**: Look up technical details
|
||||||
|
**For**: Users who need precise information
|
||||||
|
|
||||||
|
- **[File Structure](reference/file-structure.md)** - Complete directory layout and file conventions
|
||||||
|
- **[Metadata](reference/metadata.md)** - All available metadata fields and their usage
|
||||||
|
- **[Templates](reference/templates.md)** - Template types and available variables
|
||||||
|
- **[Configuration](reference/configuration.md)** - Configuration options and format
|
||||||
|
- **[CSS Variables](reference/css-variables.md)** - All CSS custom properties for styling
|
||||||
|
|
||||||
|
Consult these documents when you need to look up specific technical details or API information.
|
||||||
|
|
||||||
|
### 💡 Explanation (Understanding-Oriented)
|
||||||
|
|
||||||
|
**Purpose**: Understand concepts and design decisions
|
||||||
|
**For**: Users who want to understand the "why"
|
||||||
|
|
||||||
|
- **[Philosophy](explanation/philosophy.md)** - Design principles and thinking behind FolderWeb
|
||||||
|
- **[Architecture](explanation/architecture.md)** - How FolderWeb works under the hood
|
||||||
|
|
||||||
|
Read these to gain deeper understanding of FolderWeb's design and architecture.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create project directory
|
||||||
|
mkdir my-website && cd my-website
|
||||||
|
|
||||||
|
# 2. Copy framework files
|
||||||
|
cp -r /path/to/folderweb/app ./app
|
||||||
|
|
||||||
|
# 3. Create your first page
|
||||||
|
mkdir content
|
||||||
|
echo "# Welcome" > content/index.md
|
||||||
|
|
||||||
|
# 4. Start development server
|
||||||
|
php -S localhost:8000 -t . app/router.php
|
||||||
|
|
||||||
|
# 5. Open http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next**: Follow the complete [Getting Started Tutorial](tutorial/00-getting-started.md)
|
||||||
|
|
||||||
|
## 🎯 Common Tasks
|
||||||
|
|
||||||
|
Quick links to frequently needed guides:
|
||||||
|
|
||||||
|
| Task | Guide |
|
||||||
|
|------|-------|
|
||||||
|
| Create a custom template | [Custom Templates](how-to/custom-templates.md) |
|
||||||
|
| Change colors and fonts | [Custom Styles](how-to/custom-styles.md) |
|
||||||
|
| Add multiple languages | [Multi-Language Sites](how-to/multi-language.md) |
|
||||||
|
| Configure page metadata | [Working with Metadata](how-to/working-with-metadata.md) |
|
||||||
|
| Look up all metadata fields | [Metadata Reference](reference/metadata.md) |
|
||||||
|
| Find template variables | [Templates Reference](reference/templates.md) |
|
||||||
|
| Understand file organization | [File Structure Reference](reference/file-structure.md) |
|
||||||
|
|
||||||
|
## 💡 Key Concepts
|
||||||
|
|
||||||
|
- **File-based routing**: Your folder structure defines your URL structure
|
||||||
|
- **Template fallback**: Custom templates automatically override defaults
|
||||||
|
- **Content types**: Single-file pages, multi-file pages, or list views
|
||||||
|
- **Language support**: Built-in multi-language with URL prefixes
|
||||||
|
- **Metadata control**: Configure behavior with simple INI files
|
||||||
|
- **No build process**: Save and refresh - see changes immediately
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- **PHP**: 8.4 or higher
|
||||||
|
- **Web server**: Apache, Nginx, or PHP's built-in server
|
||||||
|
- **Extensions**: Standard PHP (no special extensions required)
|
||||||
|
|
||||||
|
## 🗂️ Documentation Files
|
||||||
|
|
||||||
|
### Complete File List
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # This file
|
||||||
|
├── index.md # Documentation homepage
|
||||||
|
│
|
||||||
|
├── tutorial/
|
||||||
|
│ └── 00-getting-started.md # Step-by-step tutorial
|
||||||
|
│
|
||||||
|
├── how-to/
|
||||||
|
│ ├── custom-templates.md # Override templates
|
||||||
|
│ ├── custom-styles.md # Customize CSS
|
||||||
|
│ ├── multi-language.md # Multi-language setup
|
||||||
|
│ └── working-with-metadata.md # Metadata usage
|
||||||
|
│
|
||||||
|
├── reference/
|
||||||
|
│ ├── file-structure.md # Directory layout
|
||||||
|
│ ├── metadata.md # Metadata fields
|
||||||
|
│ ├── templates.md # Template reference
|
||||||
|
│ ├── configuration.md # Config options
|
||||||
|
│ └── css-variables.md # CSS customization
|
||||||
|
│
|
||||||
|
└── explanation/
|
||||||
|
├── philosophy.md # Design principles
|
||||||
|
└── architecture.md # Technical architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Reading Paths
|
||||||
|
|
||||||
|
Choose your path based on your needs:
|
||||||
|
|
||||||
|
### Path 1: Complete Beginner
|
||||||
|
|
||||||
|
1. [Getting Started Tutorial](tutorial/00-getting-started.md)
|
||||||
|
2. [Custom Styles](how-to/custom-styles.md)
|
||||||
|
3. [Working with Metadata](how-to/working-with-metadata.md)
|
||||||
|
4. [Philosophy](explanation/philosophy.md)
|
||||||
|
|
||||||
|
### Path 2: Experienced Developer
|
||||||
|
|
||||||
|
1. [Philosophy](explanation/philosophy.md)
|
||||||
|
2. [Architecture](explanation/architecture.md)
|
||||||
|
3. [File Structure Reference](reference/file-structure.md)
|
||||||
|
4. Browse How-To Guides as needed
|
||||||
|
|
||||||
|
### Path 3: Specific Task
|
||||||
|
|
||||||
|
1. Find your task in [How-To Guides](how-to/)
|
||||||
|
2. Consult [Reference](reference/) for details
|
||||||
|
3. Return to task completion
|
||||||
|
|
||||||
|
## 🤔 Getting Help
|
||||||
|
|
||||||
|
### Documentation Not Enough?
|
||||||
|
|
||||||
|
1. **Check the code**: FolderWeb is deliberately simple - reading the source is encouraged
|
||||||
|
2. **Review examples**: Look at the demo content in `/app/default/content/`
|
||||||
|
3. **Test locally**: Experiment with a test site to understand behavior
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
| Problem | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| Styles not loading | Hard refresh browser (Ctrl+Shift+R) |
|
||||||
|
| 404 errors | Verify folder exists with content files |
|
||||||
|
| Language not working | Check `available` in config.ini |
|
||||||
|
| Metadata not showing | Validate INI syntax with PHP parser |
|
||||||
|
| Custom template ignored | Ensure file is in `/custom/templates/` |
|
||||||
|
|
||||||
|
## 🌟 Philosophy Highlights
|
||||||
|
|
||||||
|
FolderWeb embraces:
|
||||||
|
|
||||||
|
- **Simplicity**: Just enough, nothing more
|
||||||
|
- **Longevity**: Works today, works in 2035
|
||||||
|
- **Transparency**: Readable code, clear behavior
|
||||||
|
- **Files**: Your content, fully portable
|
||||||
|
- **No build**: Save and refresh workflow
|
||||||
|
|
||||||
|
Read the complete [Philosophy](explanation/philosophy.md) to understand FolderWeb's design principles.
|
||||||
|
|
||||||
|
## 📝 Contributing to Documentation
|
||||||
|
|
||||||
|
Documentation improvements are welcome:
|
||||||
|
|
||||||
|
- Fix typos or unclear explanations
|
||||||
|
- Add missing examples
|
||||||
|
- Improve existing guides
|
||||||
|
- Suggest new how-to guides
|
||||||
|
|
||||||
|
Keep documentation:
|
||||||
|
- Clear and concise
|
||||||
|
- Accurate and tested
|
||||||
|
- Organized according to Diataxis principles
|
||||||
|
|
||||||
|
## 🔗 External Resources
|
||||||
|
|
||||||
|
- [Diataxis Framework](https://diataxis.fr/) - Documentation organization system
|
||||||
|
- [PHP 8.4 Documentation](https://www.php.net/manual/en/) - PHP reference
|
||||||
|
- [Markdown Guide](https://www.markdownguide.org/) - Markdown syntax
|
||||||
|
- [MDN Web Docs](https://developer.mozilla.org/) - HTML and CSS reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Start here**: [Getting Started Tutorial](tutorial/00-getting-started.md)
|
||||||
|
**Main index**: [Documentation Index](index.md)
|
||||||
739
docs/explanation/architecture.md
Normal file
739
docs/explanation/architecture.md
Normal file
|
|
@ -0,0 +1,739 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Understanding how FolderWeb works under the hood.
|
||||||
|
|
||||||
|
## High-Level Overview
|
||||||
|
|
||||||
|
FolderWeb follows a simple request-response flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP Request
|
||||||
|
↓
|
||||||
|
router.php (entry point)
|
||||||
|
↓
|
||||||
|
Parse request path
|
||||||
|
↓
|
||||||
|
Find content files
|
||||||
|
↓
|
||||||
|
Determine content type (page/list/file)
|
||||||
|
↓
|
||||||
|
Render content
|
||||||
|
↓
|
||||||
|
Wrap in templates
|
||||||
|
↓
|
||||||
|
HTTP Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### 1. Router (`app/router.php`)
|
||||||
|
|
||||||
|
**Purpose**: Entry point for all requests, determines what to serve
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Receive HTTP requests
|
||||||
|
- Check for root-level assets (`/custom/assets/`)
|
||||||
|
- Parse request path
|
||||||
|
- Dispatch to appropriate renderer
|
||||||
|
- Handle redirects (trailing slashes)
|
||||||
|
- Serve 404 for missing content
|
||||||
|
|
||||||
|
**Key Flow**:
|
||||||
|
```php
|
||||||
|
// 1. Check for root-level assets
|
||||||
|
if (file_exists("/custom/assets/$path")) {
|
||||||
|
serve_static_file();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Empty path = home page (render all root content files)
|
||||||
|
if (empty($path)) {
|
||||||
|
render_all_files_in_root();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Parse request path
|
||||||
|
$result = parseRequestPath($ctx);
|
||||||
|
|
||||||
|
// 4. Handle based on type
|
||||||
|
match($result['type']) {
|
||||||
|
'page' => renderMultipleFiles(...),
|
||||||
|
'file' => renderFile(...),
|
||||||
|
'directory' => renderListView(...),
|
||||||
|
'not_found' => show_404()
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Location**: `/app/router.php` (lines 1-100+)
|
||||||
|
|
||||||
|
### 2. Content Discovery (`app/content.php`)
|
||||||
|
|
||||||
|
**Purpose**: Find and parse content files and directories
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
|
||||||
|
#### `parseRequestPath($ctx)`
|
||||||
|
Analyzes request path and determines content type.
|
||||||
|
|
||||||
|
**Returns**:
|
||||||
|
```php
|
||||||
|
[
|
||||||
|
'type' => 'page' | 'file' | 'directory' | 'not_found',
|
||||||
|
'path' => '/full/system/path',
|
||||||
|
'files' => [...], // For page type
|
||||||
|
// ... other data
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logic**:
|
||||||
|
1. Resolve translated slugs to real paths
|
||||||
|
2. Check if path exists
|
||||||
|
3. If directory:
|
||||||
|
- Has subdirectories? → `type: 'directory'` (list view)
|
||||||
|
- Has content files only? → `type: 'page'` (multi-file)
|
||||||
|
4. If matches file? → `type: 'file'`
|
||||||
|
5. Otherwise → `type: 'not_found'`
|
||||||
|
|
||||||
|
#### `findAllContentFiles($dir, $lang, $defaultLang, $availableLangs)`
|
||||||
|
Scans directory for content files.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Read directory contents
|
||||||
|
2. Filter for valid extensions (`.md`, `.html`, `.php`)
|
||||||
|
3. Parse filenames for language suffix
|
||||||
|
4. Filter by current language:
|
||||||
|
- Show `.{lang}.ext` files for that language
|
||||||
|
- Show default files (no suffix) only if no language variant
|
||||||
|
5. Sort alphanumerically
|
||||||
|
6. Return array of file paths
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```php
|
||||||
|
// Directory contains:
|
||||||
|
// - index.md
|
||||||
|
// - index.no.md
|
||||||
|
// - about.md
|
||||||
|
|
||||||
|
// English request (lang=en, default=en):
|
||||||
|
findAllContentFiles() → ['index.md', 'about.md']
|
||||||
|
|
||||||
|
// Norwegian request (lang=no):
|
||||||
|
findAllContentFiles() → ['index.no.md', 'about.md']
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `loadMetadata($dirPath, $lang, $defaultLang)`
|
||||||
|
Loads and merges metadata for a directory.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Check for `metadata.ini` in directory
|
||||||
|
2. Parse INI file with sections
|
||||||
|
3. Start with base values
|
||||||
|
4. Override with language-specific section if exists
|
||||||
|
5. Return merged array
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```ini
|
||||||
|
title = "About"
|
||||||
|
summary = "Learn about us"
|
||||||
|
|
||||||
|
[no]
|
||||||
|
title = "Om"
|
||||||
|
summary = "Lær om oss"
|
||||||
|
```
|
||||||
|
|
||||||
|
For Norwegian request:
|
||||||
|
```php
|
||||||
|
loadMetadata(..., 'no', 'en') → [
|
||||||
|
'title' => 'Om', // Overridden
|
||||||
|
'summary' => 'Lær om oss' // Overridden
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `resolveTranslatedPath($ctx, $requestPath)`
|
||||||
|
Maps translated slugs back to real directory names.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```ini
|
||||||
|
; In content/about/metadata.ini:
|
||||||
|
[no]
|
||||||
|
slug = "om-oss"
|
||||||
|
```
|
||||||
|
|
||||||
|
Request to `/no/om-oss/` resolves to `content/about/`.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Split path into segments
|
||||||
|
2. For each segment:
|
||||||
|
- Load metadata of parent directory
|
||||||
|
- Check if any subdirectory has matching translated slug
|
||||||
|
- Replace segment with real directory name
|
||||||
|
3. Return resolved path
|
||||||
|
|
||||||
|
### 3. Rendering Engine (`app/rendering.php`)
|
||||||
|
|
||||||
|
**Purpose**: Convert content to HTML and wrap in templates
|
||||||
|
|
||||||
|
**Key Functions**:
|
||||||
|
|
||||||
|
#### `renderContentFile($filePath)`
|
||||||
|
Converts a single content file to HTML.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
```php
|
||||||
|
switch (extension) {
|
||||||
|
case 'md':
|
||||||
|
return Parsedown->text(file_contents);
|
||||||
|
case 'html':
|
||||||
|
return file_contents;
|
||||||
|
case 'php':
|
||||||
|
ob_start();
|
||||||
|
include $filePath; // $ctx available
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `renderFile($ctx, $filePath)`
|
||||||
|
Renders single file wrapped in templates.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Convert file to HTML
|
||||||
|
2. Load metadata
|
||||||
|
3. Wrap in page template
|
||||||
|
4. Wrap in base template
|
||||||
|
5. Return HTML
|
||||||
|
|
||||||
|
#### `renderMultipleFiles($ctx, $filePaths, $pageDir)`
|
||||||
|
Renders multiple files as single page.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Convert each file to HTML
|
||||||
|
2. Concatenate HTML (in order)
|
||||||
|
3. Load metadata
|
||||||
|
4. Wrap in page template
|
||||||
|
5. Wrap in base template
|
||||||
|
6. Return HTML
|
||||||
|
|
||||||
|
**Used for**: Multi-file pages (documentation, long articles)
|
||||||
|
|
||||||
|
#### `renderTemplate($ctx, $content, $statusCode = 200)`
|
||||||
|
Wraps content in base template.
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Extract variables for template
|
||||||
|
2. Set HTTP status code
|
||||||
|
3. Include base template
|
||||||
|
4. Return HTML
|
||||||
|
|
||||||
|
**Variables provided**:
|
||||||
|
- `$content` - Rendered HTML
|
||||||
|
- `$ctx` - Context object
|
||||||
|
- `$currentLang`, `$navigation`, `$homeLabel`, etc.
|
||||||
|
|
||||||
|
### 4. Context Object (`app/context.php`)
|
||||||
|
|
||||||
|
**Purpose**: Immutable request context with computed properties
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```php
|
||||||
|
readonly class Context {
|
||||||
|
public function __construct(
|
||||||
|
public private(set) string $contentDir,
|
||||||
|
public private(set) string $currentLang,
|
||||||
|
// ... other properties
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Computed property (PHP 8.4 hook)
|
||||||
|
public string $langPrefix {
|
||||||
|
get => $this->currentLang !== $this->defaultLang
|
||||||
|
? "/{$this->currentLang}"
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy-loaded computed property
|
||||||
|
public array $navigation {
|
||||||
|
get => buildNavigation($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- **Immutability**: Cannot be changed after creation
|
||||||
|
- **Type safety**: All properties typed
|
||||||
|
- **Computed values**: Calculated on-demand
|
||||||
|
- **No globals**: Passed explicitly
|
||||||
|
|
||||||
|
**Creation**:
|
||||||
|
```php
|
||||||
|
$ctx = createContext();
|
||||||
|
```
|
||||||
|
|
||||||
|
This function:
|
||||||
|
1. Loads configuration
|
||||||
|
2. Extracts language from URL
|
||||||
|
3. Determines content directory
|
||||||
|
4. Resolves template paths
|
||||||
|
5. Returns readonly Context object
|
||||||
|
|
||||||
|
### 5. Configuration (`app/config.php`)
|
||||||
|
|
||||||
|
**Purpose**: Load and merge configuration
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Parse `/app/config.ini` (defaults)
|
||||||
|
2. Parse `/custom/config.ini` if exists
|
||||||
|
3. Merge arrays (custom overrides defaults)
|
||||||
|
4. Extract language settings
|
||||||
|
5. Validate configuration
|
||||||
|
|
||||||
|
**Configuration Used**:
|
||||||
|
```ini
|
||||||
|
[languages]
|
||||||
|
default = "en"
|
||||||
|
available = "en,no,fr"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Helper Functions (`app/helpers.php`)
|
||||||
|
|
||||||
|
**Purpose**: Utility functions used throughout
|
||||||
|
|
||||||
|
**Key Helpers**:
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `resolveTemplate($name, $type)` | Find custom or default template |
|
||||||
|
| `getSubdirectories($dir)` | List subdirectories only |
|
||||||
|
| `extractTitle($filePath, $lang, $defaultLang)` | Extract H1 from content |
|
||||||
|
| `formatNorwegianDate($date)` | Format date as "2. november 2025" |
|
||||||
|
| `extractDateFromFolder($name)` | Parse date from folder name |
|
||||||
|
| `findCoverImage($dir)` | Locate cover image |
|
||||||
|
| `findPdfFile($dir)` | Find first PDF |
|
||||||
|
|
||||||
|
### 7. Static File Server (`app/static.php`)
|
||||||
|
|
||||||
|
**Purpose**: Serve CSS, fonts, and other static assets
|
||||||
|
|
||||||
|
**Process**:
|
||||||
|
1. Validate path (prevent directory traversal)
|
||||||
|
2. Resolve real path
|
||||||
|
3. Check file exists and is readable
|
||||||
|
4. Determine MIME type
|
||||||
|
5. Set headers
|
||||||
|
6. Output file contents
|
||||||
|
|
||||||
|
**Routes**:
|
||||||
|
- `/app/styles/base.css` → Custom or default CSS
|
||||||
|
- `/app/default-styles/base.css` → Default CSS
|
||||||
|
- `/custom/fonts/*` → Custom fonts
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Request Flow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
1. HTTP Request: /blog/2025-11-02-post/
|
||||||
|
↓
|
||||||
|
2. router.php receives request
|
||||||
|
↓
|
||||||
|
3. createContext()
|
||||||
|
├─ Load config
|
||||||
|
├─ Extract language from URL
|
||||||
|
├─ Determine content directory
|
||||||
|
└─ Return Context object
|
||||||
|
↓
|
||||||
|
4. parseRequestPath($ctx)
|
||||||
|
├─ resolveTranslatedPath() - map slug to real path
|
||||||
|
├─ Check path exists
|
||||||
|
├─ findAllContentFiles() - scan for content
|
||||||
|
└─ Return ['type' => 'file', 'path' => '...']
|
||||||
|
↓
|
||||||
|
5. renderFile($ctx, $filePath)
|
||||||
|
├─ renderContentFile() - convert to HTML
|
||||||
|
├─ loadMetadata() - get metadata
|
||||||
|
├─ Apply page template
|
||||||
|
└─ Apply base template
|
||||||
|
↓
|
||||||
|
6. HTTP Response: HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
### List View Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Request: /blog/
|
||||||
|
↓
|
||||||
|
2. parseRequestPath() → type: 'directory'
|
||||||
|
↓
|
||||||
|
3. Load directory metadata
|
||||||
|
├─ Get page_template setting
|
||||||
|
└─ Get other directory metadata
|
||||||
|
↓
|
||||||
|
4. getSubdirectories() - find all subdirs
|
||||||
|
↓
|
||||||
|
5. For each subdirectory:
|
||||||
|
├─ loadMetadata() - get title, date, summary
|
||||||
|
├─ findCoverImage() - locate cover
|
||||||
|
├─ findPdfFile() - locate PDF
|
||||||
|
└─ Build item array
|
||||||
|
↓
|
||||||
|
6. Render list template with $items
|
||||||
|
↓
|
||||||
|
7. Wrap in base template
|
||||||
|
↓
|
||||||
|
8. Return HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
### Separation of Concerns
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── router.php # Entry point, request handling
|
||||||
|
├── content.php # Content discovery, parsing
|
||||||
|
├── rendering.php # HTML generation, templates
|
||||||
|
├── context.php # Request context
|
||||||
|
├── config.php # Configuration loading
|
||||||
|
├── helpers.php # Utility functions
|
||||||
|
├── constants.php # Constants (extensions)
|
||||||
|
└── static.php # Static file serving
|
||||||
|
```
|
||||||
|
|
||||||
|
Each file has a single responsibility.
|
||||||
|
|
||||||
|
### Template Resolution
|
||||||
|
|
||||||
|
Templates use fallback chain:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. /custom/templates/{name}.php
|
||||||
|
↓ (if not found)
|
||||||
|
2. /app/default/templates/{name}.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```php
|
||||||
|
function resolveTemplate($name, $type = 'templates') {
|
||||||
|
$custom = __DIR__ . "/../custom/$type/$name";
|
||||||
|
$default = __DIR__ . "/default/$type/$name";
|
||||||
|
return file_exists($custom) ? $custom : $default;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This pattern applies to:
|
||||||
|
- Templates
|
||||||
|
- Styles
|
||||||
|
- Languages
|
||||||
|
- Any overridable resource
|
||||||
|
|
||||||
|
## Language Handling
|
||||||
|
|
||||||
|
### URL Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/ → Default language
|
||||||
|
/no/ → Norwegian
|
||||||
|
/fr/page/ → French page
|
||||||
|
```
|
||||||
|
|
||||||
|
### Language Extraction
|
||||||
|
|
||||||
|
From URL path:
|
||||||
|
```php
|
||||||
|
// Request: /no/blog/post/
|
||||||
|
$segments = explode('/', trim($path, '/'));
|
||||||
|
$firstSegment = $segments[0] ?? '';
|
||||||
|
|
||||||
|
if (in_array($firstSegment, $availableLangs)) {
|
||||||
|
$currentLang = $firstSegment;
|
||||||
|
$pathWithoutLang = implode('/', array_slice($segments, 1));
|
||||||
|
} else {
|
||||||
|
$currentLang = $defaultLang;
|
||||||
|
$pathWithoutLang = $path;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Filtering
|
||||||
|
|
||||||
|
Files with language suffixes (`.{lang}.ext`) are filtered:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Parse filename
|
||||||
|
$parts = explode('.', $filename);
|
||||||
|
$lastPart = $parts[count($parts) - 2] ?? null;
|
||||||
|
|
||||||
|
// Check if second-to-last part is a language
|
||||||
|
if (in_array($lastPart, $availableLangs)) {
|
||||||
|
$fileLang = $lastPart;
|
||||||
|
} else {
|
||||||
|
$fileLang = $defaultLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include file if:
|
||||||
|
// - It matches current language, OR
|
||||||
|
// - It's default language AND no specific variant exists
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation Building
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|
```php
|
||||||
|
function buildNavigation($ctx) {
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
// 1. Scan content root for directories
|
||||||
|
$dirs = getSubdirectories($ctx->contentDir);
|
||||||
|
|
||||||
|
// 2. For each directory
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
// Load metadata
|
||||||
|
$metadata = loadMetadata($dir, $ctx->currentLang, $ctx->defaultLang);
|
||||||
|
|
||||||
|
// Skip if menu = false
|
||||||
|
if (!($metadata['menu'] ?? false)) continue;
|
||||||
|
|
||||||
|
// Build item
|
||||||
|
$items[] = [
|
||||||
|
'title' => $metadata['title'] ?? basename($dir),
|
||||||
|
'url' => $ctx->langPrefix . '/' . basename($dir) . '/',
|
||||||
|
'order' => $metadata['menu_order'] ?? 999
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sort by menu_order
|
||||||
|
usort($items, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
Navigation is a computed property, calculated once per request:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public array $navigation {
|
||||||
|
get => buildNavigation($this);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
PHP memoizes the result automatically.
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Time Complexity
|
||||||
|
|
||||||
|
| Operation | Complexity | Notes |
|
||||||
|
|-----------|------------|-------|
|
||||||
|
| Route resolution | O(1) | Direct file checks |
|
||||||
|
| Content file scan | O(n) | n = files in directory |
|
||||||
|
| Metadata loading | O(1) | Single file read |
|
||||||
|
| Template rendering | O(m) | m = content size |
|
||||||
|
| Navigation build | O(d) | d = top-level directories |
|
||||||
|
|
||||||
|
### Space Complexity
|
||||||
|
|
||||||
|
- **Memory**: O(c) where c = content size
|
||||||
|
- **No caching**: Each request independent
|
||||||
|
- **Stateless**: No session storage
|
||||||
|
|
||||||
|
### Optimization Points
|
||||||
|
|
||||||
|
1. **OPcache**: PHP bytecode caching (biggest impact)
|
||||||
|
2. **Web server cache**: Serve cached HTML
|
||||||
|
3. **Reverse proxy**: Varnish, Cloudflare
|
||||||
|
4. **Minimize file reads**: Context created once per request
|
||||||
|
|
||||||
|
## Security Architecture
|
||||||
|
|
||||||
|
### Path Validation
|
||||||
|
|
||||||
|
Multiple layers prevent directory traversal:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 1. Remove .. segments
|
||||||
|
$path = str_replace('..', '', $path);
|
||||||
|
|
||||||
|
// 2. Resolve to real path
|
||||||
|
$realPath = realpath($path);
|
||||||
|
|
||||||
|
// 3. Ensure within content directory
|
||||||
|
if (!str_starts_with($realPath, $contentDir)) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check readable
|
||||||
|
if (!is_readable($realPath)) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Escaping
|
||||||
|
|
||||||
|
All user-generated content escaped:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?= htmlspecialchars($metadata['title']) ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents XSS attacks.
|
||||||
|
|
||||||
|
### MIME Type Validation
|
||||||
|
|
||||||
|
Static files served with correct MIME types:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$mimeTypes = [
|
||||||
|
'css' => 'text/css',
|
||||||
|
'woff2' => 'font/woff2',
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
header('Content-Type: ' . $mimeTypes[$extension]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
- **200 OK**: Successful content render
|
||||||
|
- **301 Moved Permanently**: Missing trailing slash
|
||||||
|
- **404 Not Found**: Content doesn't exist
|
||||||
|
|
||||||
|
### 404 Handling
|
||||||
|
|
||||||
|
When content not found:
|
||||||
|
|
||||||
|
```php
|
||||||
|
renderTemplate($ctx, '<h1>404 Not Found</h1>', 404);
|
||||||
|
```
|
||||||
|
|
||||||
|
Base template rendered with 404 status.
|
||||||
|
|
||||||
|
## Extension Points
|
||||||
|
|
||||||
|
### Custom Templates
|
||||||
|
|
||||||
|
Override any template:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Framework checks:
|
||||||
|
$custom = '/custom/templates/list-my-custom.php';
|
||||||
|
if (file_exists($custom)) {
|
||||||
|
include $custom;
|
||||||
|
} else {
|
||||||
|
include '/app/default/templates/list.php';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Functions
|
||||||
|
|
||||||
|
Add your own in `/custom/functions.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// Custom helper functions
|
||||||
|
|
||||||
|
function myCustomFunction() {
|
||||||
|
// Your code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Include in router:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (file_exists(__DIR__ . '/../custom/functions.php')) {
|
||||||
|
require_once __DIR__ . '/../custom/functions.php';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Files as PHP
|
||||||
|
|
||||||
|
`.php` content files have full access:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// In content/dynamic/index.php
|
||||||
|
$currentTime = date('Y-m-d H:i:s');
|
||||||
|
?>
|
||||||
|
|
||||||
|
# Dynamic Content
|
||||||
|
|
||||||
|
Current time: <?= $currentTime ?>
|
||||||
|
|
||||||
|
The language is: <?= $ctx->currentLang ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
1. Create test content
|
||||||
|
2. Start dev server: `php -S localhost:8000 -t . app/router.php`
|
||||||
|
3. Visit URLs, verify output
|
||||||
|
4. Check different languages
|
||||||
|
5. Test edge cases (missing files, invalid paths)
|
||||||
|
|
||||||
|
### Automated Testing (Future)
|
||||||
|
|
||||||
|
Possible test structure:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// tests/RouterTest.php
|
||||||
|
test('renders home page', function() {
|
||||||
|
$response = request('/');
|
||||||
|
expect($response->status)->toBe(200);
|
||||||
|
expect($response->body)->toContain('<h1>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles 404', function() {
|
||||||
|
$response = request('/nonexistent/');
|
||||||
|
expect($response->status)->toBe(404);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Architecture
|
||||||
|
|
||||||
|
### Simple Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone repository
|
||||||
|
git clone https://github.com/you/your-site
|
||||||
|
|
||||||
|
# 2. Point web server to directory
|
||||||
|
# Document root: /path/to/site
|
||||||
|
# Rewrite all requests to: /app/router.php
|
||||||
|
|
||||||
|
# 3. Done
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Build Step (Optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone and build
|
||||||
|
git clone ...
|
||||||
|
cd site
|
||||||
|
|
||||||
|
# 2. Process custom styles (optional)
|
||||||
|
# E.g., PostCSS, autoprefixer
|
||||||
|
|
||||||
|
# 3. Deploy
|
||||||
|
rsync -av . server:/var/www/site/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zero-Downtime Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Deploy to new directory
|
||||||
|
rsync -av . server:/var/www/site-new/
|
||||||
|
|
||||||
|
# 2. Symlink switch
|
||||||
|
ln -sfn /var/www/site-new /var/www/site-current
|
||||||
|
|
||||||
|
# 3. Web server serves from /var/www/site-current
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Philosophy](philosophy.md)
|
||||||
|
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
|
||||||
|
- [File Structure Reference](../reference/file-structure.md)
|
||||||
|
- [Template Reference](../reference/templates.md)
|
||||||
496
docs/explanation/philosophy.md
Normal file
496
docs/explanation/philosophy.md
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
# Philosophy
|
||||||
|
|
||||||
|
Understanding the principles and thinking behind FolderWeb.
|
||||||
|
|
||||||
|
## Core Idea
|
||||||
|
|
||||||
|
**Your file system is your content management system.**
|
||||||
|
|
||||||
|
FolderWeb embraces the simplest possible approach to web publishing: create a folder structure that mirrors your site hierarchy, drop files into folders, and they immediately become pages. No database, no admin panel, no build process.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
### 1. Just Enough, Nothing More
|
||||||
|
|
||||||
|
FolderWeb applies minimal PHP to enable modern conveniences while remaining maintainable for years or decades. Every feature must justify its existence by solving a real problem without creating new complexity.
|
||||||
|
|
||||||
|
**What this means:**
|
||||||
|
- No frameworks that might be abandoned
|
||||||
|
- No build tools that need maintenance
|
||||||
|
- No package managers introducing dependencies
|
||||||
|
- No abstractions unless they provide lasting value
|
||||||
|
|
||||||
|
**Example**: Instead of using a routing library, FolderWeb uses PHP's native file functions to map folders to URLs. This will work identically in 2025 and 2035.
|
||||||
|
|
||||||
|
### 2. Longevity Over Novelty
|
||||||
|
|
||||||
|
Code should outlive trends. FolderWeb prioritizes stability and backward compatibility over cutting-edge features.
|
||||||
|
|
||||||
|
**What this means:**
|
||||||
|
- Standard PHP (no exotic extensions)
|
||||||
|
- Plain HTML and CSS (no JavaScript required)
|
||||||
|
- Simple file formats (Markdown, INI)
|
||||||
|
- Conventions over configuration
|
||||||
|
|
||||||
|
**Why it matters**: A site built today should still work in 10 years without updates. The web's foundational technologies (HTML, CSS, PHP) change slowly and maintain backward compatibility.
|
||||||
|
|
||||||
|
### 3. Transparent and Readable
|
||||||
|
|
||||||
|
You should be able to open any file and immediately understand what it does. No magic, no hidden behavior.
|
||||||
|
|
||||||
|
**What this means:**
|
||||||
|
- Sparse, meaningful comments
|
||||||
|
- Descriptive function names
|
||||||
|
- Simple control flow
|
||||||
|
- Minimal abstraction layers
|
||||||
|
|
||||||
|
**Example**: Want to know how templates work? Open `app/rendering.php` and read 100 lines of straightforward PHP. No framework documentation needed.
|
||||||
|
|
||||||
|
### 4. Files Are Content
|
||||||
|
|
||||||
|
Your content lives in plain text files you can edit with any text editor. You own your content completely.
|
||||||
|
|
||||||
|
**What this means:**
|
||||||
|
- Content is portable (copy files, migrate easily)
|
||||||
|
- Version control friendly (Git tracks changes)
|
||||||
|
- No lock-in (files work without FolderWeb)
|
||||||
|
- Backup-friendly (copy a folder)
|
||||||
|
|
||||||
|
**Example**: Your entire site is a folder structure. Zip it, move it to another server, extract it, and it works. No database export/import, no migration scripts.
|
||||||
|
|
||||||
|
## What FolderWeb Is
|
||||||
|
|
||||||
|
### A File-Based Router
|
||||||
|
|
||||||
|
FolderWeb maps your folder structure to URLs:
|
||||||
|
```
|
||||||
|
content/blog/2025-11-02-post/ → yoursite.com/blog/2025-11-02-post/
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. No route definitions, no controllers, no configuration.
|
||||||
|
|
||||||
|
### A Content Renderer
|
||||||
|
|
||||||
|
FolderWeb converts Markdown to HTML, wraps it in templates, and serves it. Three steps:
|
||||||
|
1. Find content files
|
||||||
|
2. Convert to HTML
|
||||||
|
3. Wrap in template
|
||||||
|
|
||||||
|
### A Minimal Template System
|
||||||
|
|
||||||
|
FolderWeb provides just enough templating to avoid repetition:
|
||||||
|
- Base template for site structure
|
||||||
|
- Page template for content wrapper
|
||||||
|
- List templates for directory views
|
||||||
|
|
||||||
|
All using plain PHP includes. No template language to learn.
|
||||||
|
|
||||||
|
### A Convention Framework
|
||||||
|
|
||||||
|
FolderWeb establishes conventions that eliminate configuration:
|
||||||
|
- `metadata.ini` for structured data
|
||||||
|
- `cover.jpg` for images
|
||||||
|
- `YYYY-MM-DD-slug` for dates
|
||||||
|
- `filename.lang.ext` for translations
|
||||||
|
|
||||||
|
Learn the conventions once, apply them everywhere.
|
||||||
|
|
||||||
|
## What FolderWeb Is Not
|
||||||
|
|
||||||
|
### Not a CMS
|
||||||
|
|
||||||
|
No admin panel. Edit files directly with your text editor, commit to Git, deploy.
|
||||||
|
|
||||||
|
**Why**: Admin panels add complexity, require maintenance, create security risks, and limit what you can do. Text files are simpler and more powerful.
|
||||||
|
|
||||||
|
### Not a Static Site Generator
|
||||||
|
|
||||||
|
FolderWeb renders pages on request, not at build time.
|
||||||
|
|
||||||
|
**Why**: No build step means immediate feedback. Save a file, refresh your browser, see changes. No waiting for builds, no deployment pipelines required (though you can add them).
|
||||||
|
|
||||||
|
### Not a JavaScript Framework
|
||||||
|
|
||||||
|
Zero JavaScript in the framework. HTML and CSS only.
|
||||||
|
|
||||||
|
**Why**: JavaScript adds complexity, breaks without it, requires builds/transpilation, and changes rapidly. HTML and CSS are stable and sufficient for content sites.
|
||||||
|
|
||||||
|
### Not Opinionated About Design
|
||||||
|
|
||||||
|
FolderWeb provides minimal default styles. Your design is your own.
|
||||||
|
|
||||||
|
**Why**: Design trends change. FolderWeb gives you a clean foundation and gets out of the way. Override everything in `/custom/`.
|
||||||
|
|
||||||
|
## Design Decisions Explained
|
||||||
|
|
||||||
|
### Why PHP 8.4+?
|
||||||
|
|
||||||
|
**Modern features without complexity.**
|
||||||
|
|
||||||
|
PHP 8.4 provides:
|
||||||
|
- Readonly classes (immutability)
|
||||||
|
- Property hooks (computed properties)
|
||||||
|
- Arrow functions (concise code)
|
||||||
|
- Modern array functions
|
||||||
|
- Asymmetric visibility (controlled access)
|
||||||
|
|
||||||
|
These features make code clearer without adding dependencies or build steps. PHP 8.4 will be supported for years.
|
||||||
|
|
||||||
|
**Tradeoff**: Requires newer PHP, but gains clarity and performance.
|
||||||
|
|
||||||
|
### Why No Database?
|
||||||
|
|
||||||
|
**Files are simpler.**
|
||||||
|
|
||||||
|
Databases add:
|
||||||
|
- Setup complexity
|
||||||
|
- Backup complexity
|
||||||
|
- Migration complexity
|
||||||
|
- Performance tuning
|
||||||
|
- Additional failure points
|
||||||
|
|
||||||
|
For content sites, files provide:
|
||||||
|
- Version control integration
|
||||||
|
- Simple backups (copy folder)
|
||||||
|
- Portability
|
||||||
|
- Transparent storage
|
||||||
|
- No setup required
|
||||||
|
|
||||||
|
**When you might need a database**: User-generated content, real-time updates, complex queries, thousands of pages. For those cases, use a different tool.
|
||||||
|
|
||||||
|
### Why INI Files for Metadata?
|
||||||
|
|
||||||
|
**Simple, readable, PHP-native.**
|
||||||
|
|
||||||
|
INI format:
|
||||||
|
- No parsing library needed (built into PHP)
|
||||||
|
- Human-readable and editable
|
||||||
|
- Supports sections for languages
|
||||||
|
- Familiar format
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- **YAML**: Requires library, complex syntax
|
||||||
|
- **JSON**: Not as human-friendly, no comments
|
||||||
|
- **TOML**: Requires library, less familiar
|
||||||
|
- **Frontmatter**: Mixes content and metadata
|
||||||
|
|
||||||
|
### Why Markdown?
|
||||||
|
|
||||||
|
**Readable as plain text, converts to HTML.**
|
||||||
|
|
||||||
|
Markdown is:
|
||||||
|
- Easy to learn (15 minutes)
|
||||||
|
- Readable without rendering
|
||||||
|
- Widely supported
|
||||||
|
- Future-proof (plain text)
|
||||||
|
- Version control friendly
|
||||||
|
|
||||||
|
**Alternatives supported**: HTML (for complex layouts), PHP (for dynamic content).
|
||||||
|
|
||||||
|
### Why No Build Tools?
|
||||||
|
|
||||||
|
**Immediate feedback, zero setup.**
|
||||||
|
|
||||||
|
Build tools add:
|
||||||
|
- Installation steps
|
||||||
|
- Configuration files
|
||||||
|
- Waiting for builds
|
||||||
|
- Build failures to debug
|
||||||
|
- Another thing that can break
|
||||||
|
|
||||||
|
Without builds:
|
||||||
|
- Save file → refresh browser → see result
|
||||||
|
- No setup (just PHP)
|
||||||
|
- Nothing to configure
|
||||||
|
- Nothing to break
|
||||||
|
|
||||||
|
**Tradeoff**: Can't use Sass, TypeScript, etc. But you can use modern CSS, which is very capable.
|
||||||
|
|
||||||
|
### Why Trailing Slashes?
|
||||||
|
|
||||||
|
**Consistency and clarity.**
|
||||||
|
|
||||||
|
```
|
||||||
|
/blog/ # Directory (list view)
|
||||||
|
/blog # Redirects to /blog/
|
||||||
|
```
|
||||||
|
|
||||||
|
Trailing slashes clarify that URLs represent directories, not files. Consistent URLs prevent duplicate content and simplify routing.
|
||||||
|
|
||||||
|
### Why Language Prefixes?
|
||||||
|
|
||||||
|
**Clear, hackable URLs.**
|
||||||
|
|
||||||
|
```
|
||||||
|
yoursite.com/en/about/ # English
|
||||||
|
yoursite.com/no/about/ # Norwegian
|
||||||
|
```
|
||||||
|
|
||||||
|
Language in URL:
|
||||||
|
- User sees current language
|
||||||
|
- Can manually change URL
|
||||||
|
- Bookmarkable per language
|
||||||
|
- SEO-friendly (clear language signal)
|
||||||
|
|
||||||
|
**Default language has no prefix** (shorter, cleaner URLs for primary audience).
|
||||||
|
|
||||||
|
## Architectural Patterns
|
||||||
|
|
||||||
|
### Immutable Context
|
||||||
|
|
||||||
|
The `Context` object is readonly (PHP 8.4):
|
||||||
|
```php
|
||||||
|
readonly class Context {
|
||||||
|
public function __construct(
|
||||||
|
public private(set) string $contentDir,
|
||||||
|
public private(set) string $currentLang,
|
||||||
|
// ...
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Prevents accidental mutation, makes code predictable, enables safe sharing of state.
|
||||||
|
|
||||||
|
### Computed Properties
|
||||||
|
|
||||||
|
Properties calculate values on-demand:
|
||||||
|
```php
|
||||||
|
public string $langPrefix {
|
||||||
|
get => $this->currentLang !== $this->defaultLang
|
||||||
|
? "/{$this->currentLang}"
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Keeps related logic together, avoids storing derived data, updates automatically.
|
||||||
|
|
||||||
|
### Function-Based API
|
||||||
|
|
||||||
|
Core functionality exposed as functions, not classes:
|
||||||
|
```php
|
||||||
|
renderFile($ctx, $filePath);
|
||||||
|
findAllContentFiles($dir, $lang, $defaultLang, $availableLangs);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Simple to understand, easy to test, no object lifecycle to manage.
|
||||||
|
|
||||||
|
### Template Fallback
|
||||||
|
|
||||||
|
Check custom, fall back to default:
|
||||||
|
```php
|
||||||
|
$custom = "/custom/templates/$name.php";
|
||||||
|
$default = "/app/default/templates/$name.php";
|
||||||
|
return file_exists($custom) ? $custom : $default;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Never modify defaults, always override. Clean separation between framework and customization.
|
||||||
|
|
||||||
|
## Performance Philosophy
|
||||||
|
|
||||||
|
### Performance Through Simplicity
|
||||||
|
|
||||||
|
FolderWeb is fast because it does less:
|
||||||
|
- No database queries
|
||||||
|
- No heavy frameworks
|
||||||
|
- No JavaScript parsing
|
||||||
|
- Minimal file reads
|
||||||
|
- Direct PHP includes
|
||||||
|
|
||||||
|
**Measured performance**: Page load time displayed in footer. Pride in speed through simplicity.
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
|
||||||
|
FolderWeb doesn't implement caching. Instead:
|
||||||
|
- Use OPcache (PHP bytecode cache)
|
||||||
|
- Use web server caching (Apache/Nginx)
|
||||||
|
- Use reverse proxy (Varnish, Cloudflare)
|
||||||
|
- CSS versioned automatically (MD5 hash)
|
||||||
|
|
||||||
|
**Why**: Let specialized tools handle caching. FolderWeb focuses on core functionality.
|
||||||
|
|
||||||
|
### Optimization Priorities
|
||||||
|
|
||||||
|
1. **Avoid work**: Don't render what's not needed
|
||||||
|
2. **Use native functions**: PHP's file functions are optimized
|
||||||
|
3. **Minimal abstraction**: Fewer layers = less overhead
|
||||||
|
|
||||||
|
## Security Philosophy
|
||||||
|
|
||||||
|
### Defense in Depth
|
||||||
|
|
||||||
|
Multiple security layers:
|
||||||
|
- Path validation (prevent directory traversal)
|
||||||
|
- Realpath checks (resolve symlinks, verify paths)
|
||||||
|
- Content root enforcement (files must be in document root)
|
||||||
|
- Output escaping (prevent XSS)
|
||||||
|
- MIME type validation (proper content types)
|
||||||
|
|
||||||
|
### Simplicity Is Security
|
||||||
|
|
||||||
|
Less code = smaller attack surface:
|
||||||
|
- No database (no SQL injection)
|
||||||
|
- No user input rendering (no XSS in content)
|
||||||
|
- No file uploads (no upload vulnerabilities)
|
||||||
|
- No authentication (no auth bypasses)
|
||||||
|
|
||||||
|
**For user-generated content**: Use a different tool. FolderWeb is for static content you control.
|
||||||
|
|
||||||
|
## Maintenance Philosophy
|
||||||
|
|
||||||
|
### Code Should Age Gracefully
|
||||||
|
|
||||||
|
FolderWeb aims to require zero maintenance:
|
||||||
|
- Standard PHP (no exotic dependencies)
|
||||||
|
- Minimal third-party code (one library: Parsedown)
|
||||||
|
- Stable APIs (PHP doesn't break backward compatibility)
|
||||||
|
- No framework upgrades needed
|
||||||
|
|
||||||
|
**Goal**: Deploy once, forget about it. Check in years later, it still works.
|
||||||
|
|
||||||
|
### Convention Over Configuration
|
||||||
|
|
||||||
|
Fewer configuration options = less to maintain:
|
||||||
|
- File conventions replace config
|
||||||
|
- Sensible defaults for everything
|
||||||
|
- Only configure what's necessary (languages)
|
||||||
|
|
||||||
|
### Documentation Is Core
|
||||||
|
|
||||||
|
Documentation is part of the project:
|
||||||
|
- Comprehensive reference
|
||||||
|
- Clear examples
|
||||||
|
- Explanation of decisions
|
||||||
|
- How-to guides for common tasks
|
||||||
|
|
||||||
|
**Why**: Future you (or someone else) will thank present you.
|
||||||
|
|
||||||
|
## When to Use FolderWeb
|
||||||
|
|
||||||
|
### Ideal For
|
||||||
|
|
||||||
|
- **Blogs**: Write Markdown, publish immediately
|
||||||
|
- **Documentation**: Multi-file pages, clear structure
|
||||||
|
- **Portfolios**: Grid layouts, cover images
|
||||||
|
- **Marketing sites**: Static content, fast loading
|
||||||
|
- **Personal sites**: Simple, maintainable
|
||||||
|
- **Long-term projects**: Will work for decades
|
||||||
|
|
||||||
|
### Not Ideal For
|
||||||
|
|
||||||
|
- **User-generated content**: No database, no auth
|
||||||
|
- **E-commerce**: Needs dynamic inventory, checkout
|
||||||
|
- **Social networks**: Real-time updates, complex data
|
||||||
|
- **SPAs**: JavaScript-heavy, API-driven
|
||||||
|
- **Large-scale sites**: Thousands of pages (consider static generation)
|
||||||
|
|
||||||
|
### Perfect Fit Scenario
|
||||||
|
|
||||||
|
You want a blog or content site that:
|
||||||
|
- You control all content
|
||||||
|
- Loads fast
|
||||||
|
- Requires minimal maintenance
|
||||||
|
- Will work for years without updates
|
||||||
|
- Integrates with Git workflow
|
||||||
|
- Gives you complete control
|
||||||
|
|
||||||
|
## Comparison to Alternatives
|
||||||
|
|
||||||
|
### vs WordPress
|
||||||
|
|
||||||
|
**WordPress**: Full-featured CMS, database-driven, plugin ecosystem, admin panel, requires regular updates
|
||||||
|
|
||||||
|
**FolderWeb**: File-based, no database, no plugins, no admin, zero maintenance
|
||||||
|
|
||||||
|
**Choose WordPress if**: You need plugins, non-technical editors, or a proven ecosystem
|
||||||
|
|
||||||
|
**Choose FolderWeb if**: You want simplicity, longevity, and complete control
|
||||||
|
|
||||||
|
### vs Jekyll/Hugo (Static Generators)
|
||||||
|
|
||||||
|
**Static Generators**: Build at deploy time, generate HTML files, fast serving, requires builds
|
||||||
|
|
||||||
|
**FolderWeb**: Renders on request, no build step, immediate feedback, simpler workflow
|
||||||
|
|
||||||
|
**Choose Static Generator if**: You want maximum performance, have build infrastructure
|
||||||
|
|
||||||
|
**Choose FolderWeb if**: You want immediate feedback, simpler deployment, dynamic capabilities
|
||||||
|
|
||||||
|
### vs Laravel/Symfony (PHP Frameworks)
|
||||||
|
|
||||||
|
**Frameworks**: Full-stack, MVC architecture, ORM, routing, complex features
|
||||||
|
|
||||||
|
**FolderWeb**: Minimal, file-based routing, no ORM, single purpose
|
||||||
|
|
||||||
|
**Choose Framework if**: You're building a complex web application
|
||||||
|
|
||||||
|
**Choose FolderWeb if**: You're publishing content and want simplicity
|
||||||
|
|
||||||
|
## Future Direction
|
||||||
|
|
||||||
|
### Stability Over Features
|
||||||
|
|
||||||
|
FolderWeb aims to reach "done" status:
|
||||||
|
- Core functionality complete
|
||||||
|
- No major features needed
|
||||||
|
- Focus on documentation and examples
|
||||||
|
- Bug fixes and security updates only
|
||||||
|
|
||||||
|
### Possible Additions
|
||||||
|
|
||||||
|
Only if they maintain simplicity:
|
||||||
|
- More template examples
|
||||||
|
- Additional default styles (opt-in)
|
||||||
|
- Performance optimizations
|
||||||
|
- Better error messages
|
||||||
|
|
||||||
|
### Will Never Add
|
||||||
|
|
||||||
|
Features that contradict philosophy:
|
||||||
|
- JavaScript requirement
|
||||||
|
- Database integration
|
||||||
|
- Build process
|
||||||
|
- Admin panel
|
||||||
|
- User authentication
|
||||||
|
- Complex plugin system
|
||||||
|
|
||||||
|
## Contributing to FolderWeb
|
||||||
|
|
||||||
|
### Align With Philosophy
|
||||||
|
|
||||||
|
Proposed changes should:
|
||||||
|
- Maintain simplicity
|
||||||
|
- Avoid new dependencies
|
||||||
|
- Work with PHP 8.4+
|
||||||
|
- Be maintainable long-term
|
||||||
|
- Solve real problems
|
||||||
|
|
||||||
|
### Ideal Contributions
|
||||||
|
|
||||||
|
- Bug fixes
|
||||||
|
- Performance improvements
|
||||||
|
- Better documentation
|
||||||
|
- Example templates
|
||||||
|
- Test cases
|
||||||
|
- Clarification of existing code
|
||||||
|
|
||||||
|
### Before Adding Features
|
||||||
|
|
||||||
|
Ask:
|
||||||
|
1. Can this be solved in userland (custom templates/code)?
|
||||||
|
2. Does this add complexity for all users?
|
||||||
|
3. Will this need maintenance in 5 years?
|
||||||
|
4. Is this truly necessary?
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
FolderWeb is deliberately simple. It does one thing—publishes content from files—and does it well. It resists feature creep, embraces constraints, and prioritizes longevity.
|
||||||
|
|
||||||
|
This isn't the right tool for every project. But for content sites that value simplicity, maintainability, and longevity, it might be perfect.
|
||||||
|
|
||||||
|
The code you write today should work in 2035. FolderWeb is built on that principle.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
|
||||||
|
- [Architecture Overview](architecture.md)
|
||||||
|
- [File Structure Reference](../reference/file-structure.md)
|
||||||
398
docs/how-to/custom-styles.md
Normal file
398
docs/how-to/custom-styles.md
Normal 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)
|
||||||
288
docs/how-to/custom-templates.md
Normal file
288
docs/how-to/custom-templates.md
Normal 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)
|
||||||
425
docs/how-to/multi-language.md
Normal file
425
docs/how-to/multi-language.md
Normal 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)
|
||||||
481
docs/how-to/working-with-metadata.md
Normal file
481
docs/how-to/working-with-metadata.md
Normal 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)
|
||||||
309
docs/index.md
Normal file
309
docs/index.md
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
# FolderWeb Documentation
|
||||||
|
|
||||||
|
Complete documentation for FolderWeb, a minimalist file-based PHP framework for content websites.
|
||||||
|
|
||||||
|
## What is FolderWeb?
|
||||||
|
|
||||||
|
FolderWeb is a file-based content publishing framework. Drop Markdown files in folders, and they become pages. No database, no build process, no JavaScript required. Just PHP, HTML, and CSS.
|
||||||
|
|
||||||
|
**Core principle**: Your file system is your content management system.
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
This documentation follows the [Diataxis framework](https://diataxis.fr/), organizing content into four types:
|
||||||
|
|
||||||
|
### 🎓 Tutorial
|
||||||
|
|
||||||
|
**Learning-oriented**: Get started with FolderWeb
|
||||||
|
|
||||||
|
- [Getting Started](tutorial/00-getting-started.md) - Build your first site in 10 minutes
|
||||||
|
|
||||||
|
**Start here** if you're new to FolderWeb.
|
||||||
|
|
||||||
|
### 📋 How-To Guides
|
||||||
|
|
||||||
|
**Task-oriented**: Solve specific problems
|
||||||
|
|
||||||
|
- [Custom Templates](how-to/custom-templates.md) - Override default templates
|
||||||
|
- [Custom Styles](how-to/custom-styles.md) - Customize appearance with CSS
|
||||||
|
- [Multi-Language Sites](how-to/multi-language.md) - Set up multiple languages
|
||||||
|
- [Working with Metadata](how-to/working-with-metadata.md) - Use metadata.ini files
|
||||||
|
|
||||||
|
**Use these** when you need to accomplish a specific task.
|
||||||
|
|
||||||
|
### 📖 Reference
|
||||||
|
|
||||||
|
**Information-oriented**: Look up technical details
|
||||||
|
|
||||||
|
- [File Structure](reference/file-structure.md) - Complete directory layout
|
||||||
|
- [Metadata](reference/metadata.md) - All metadata fields
|
||||||
|
- [Templates](reference/templates.md) - Template variables and usage
|
||||||
|
- [Configuration](reference/configuration.md) - Configuration options
|
||||||
|
- [CSS Variables](reference/css-variables.md) - Styling customization
|
||||||
|
|
||||||
|
**Consult these** when you need precise technical information.
|
||||||
|
|
||||||
|
### 💡 Explanation
|
||||||
|
|
||||||
|
**Understanding-oriented**: Understand concepts and design
|
||||||
|
|
||||||
|
- [Philosophy](explanation/philosophy.md) - Design principles and thinking
|
||||||
|
- [Architecture](explanation/architecture.md) - How FolderWeb works
|
||||||
|
|
||||||
|
**Read these** to understand why FolderWeb works the way it does.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
### Common Tasks
|
||||||
|
|
||||||
|
- **Create a page**: Drop `index.md` in a folder
|
||||||
|
- **Create a blog**: Make a folder with subdirectories
|
||||||
|
- **Add navigation**: Set `menu = true` in `metadata.ini`
|
||||||
|
- **Customize look**: Override `/custom/styles/base.css`
|
||||||
|
- **Use custom template**: Set `page_template = "template-name"` in metadata
|
||||||
|
- **Multi-language**: Configure languages and add `.{lang}.md` files
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
- **File-based routing**: `content/blog/post/` → `yoursite.com/blog/post/`
|
||||||
|
- **Template fallback**: Custom templates override defaults
|
||||||
|
- **Language prefixes**: `/en/page/` for English, `/no/page/` for Norwegian
|
||||||
|
- **Metadata inheritance**: None - each directory has its own `metadata.ini`
|
||||||
|
- **Content types**: Single-file, multi-file, or list view
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create project
|
||||||
|
mkdir my-site && cd my-site
|
||||||
|
|
||||||
|
# 2. Copy framework files
|
||||||
|
cp -r /path/to/folderweb/app ./app
|
||||||
|
|
||||||
|
# 3. Create content
|
||||||
|
mkdir content
|
||||||
|
echo "# Hello World" > content/index.md
|
||||||
|
|
||||||
|
# 4. Start server
|
||||||
|
php -S localhost:8000 -t . app/router.php
|
||||||
|
|
||||||
|
# 5. Visit http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **PHP**: 8.4 or higher
|
||||||
|
- **Web server**: Apache, Nginx, or PHP built-in server
|
||||||
|
- **Extensions**: Standard PHP (no special extensions needed)
|
||||||
|
|
||||||
|
## File Structure Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── app/ # Framework (never modify)
|
||||||
|
│ ├── router.php # Entry point
|
||||||
|
│ ├── content.php # Content discovery
|
||||||
|
│ ├── rendering.php # Template rendering
|
||||||
|
│ └── default/ # Default templates, styles, languages
|
||||||
|
├── content/ # Your website content
|
||||||
|
│ ├── index.md # Home page
|
||||||
|
│ ├── about/ # About page
|
||||||
|
│ └── blog/ # Blog with posts
|
||||||
|
└── custom/ # Your customizations
|
||||||
|
├── templates/ # Custom templates
|
||||||
|
├── styles/ # Custom CSS
|
||||||
|
├── languages/ # Custom translations
|
||||||
|
└── config.ini # Configuration overrides
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### File-Based Routing
|
||||||
|
|
||||||
|
Your folder structure defines your URLs:
|
||||||
|
|
||||||
|
```
|
||||||
|
content/blog/2025-11-02-post/ → /blog/2025-11-02-post/
|
||||||
|
```
|
||||||
|
|
||||||
|
No route configuration needed.
|
||||||
|
|
||||||
|
### Multiple Content Types
|
||||||
|
|
||||||
|
- **Single-file page**: One file in a directory
|
||||||
|
- **Multi-file page**: Multiple files combined into one page
|
||||||
|
- **List view**: Directory with subdirectories becomes a listing
|
||||||
|
|
||||||
|
### Template System
|
||||||
|
|
||||||
|
Six templates included:
|
||||||
|
- `base.php` - HTML wrapper
|
||||||
|
- `page.php` - Page wrapper
|
||||||
|
- `list.php` - Simple list
|
||||||
|
- `list-grid.php` - Grid with images
|
||||||
|
- `list-card-grid.php` - Card grid
|
||||||
|
- `list-faq.php` - Expandable FAQ
|
||||||
|
|
||||||
|
Override any template in `/custom/templates/`.
|
||||||
|
|
||||||
|
### Multi-Language Support
|
||||||
|
|
||||||
|
Configure languages:
|
||||||
|
```ini
|
||||||
|
[languages]
|
||||||
|
default = "en"
|
||||||
|
available = "en,no,fr"
|
||||||
|
```
|
||||||
|
|
||||||
|
Create language-specific files:
|
||||||
|
```
|
||||||
|
index.md # English (default)
|
||||||
|
index.no.md # Norwegian
|
||||||
|
index.fr.md # French
|
||||||
|
```
|
||||||
|
|
||||||
|
URLs automatically prefixed: `/`, `/no/`, `/fr/`
|
||||||
|
|
||||||
|
### Metadata System
|
||||||
|
|
||||||
|
Control behavior with `metadata.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
title = "My Page"
|
||||||
|
date = "2025-11-02"
|
||||||
|
summary = "Page description"
|
||||||
|
menu = true
|
||||||
|
menu_order = 1
|
||||||
|
page_template = "list-grid"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modern CSS
|
||||||
|
|
||||||
|
Default styles use:
|
||||||
|
- CSS custom properties (variables)
|
||||||
|
- CSS nesting
|
||||||
|
- OKLCH colors
|
||||||
|
- Grid layouts
|
||||||
|
- Fluid typography with `clamp()`
|
||||||
|
- Logical properties
|
||||||
|
|
||||||
|
Override in `/custom/styles/base.css`.
|
||||||
|
|
||||||
|
## Philosophy Highlights
|
||||||
|
|
||||||
|
- **Just enough, nothing more**: Minimal, maintainable code
|
||||||
|
- **Longevity over novelty**: Works today, works in 2035
|
||||||
|
- **Files are content**: Portable, version-controllable
|
||||||
|
- **No JavaScript required**: Pure HTML and CSS
|
||||||
|
- **No build process**: Immediate feedback
|
||||||
|
|
||||||
|
Read the full [Philosophy](explanation/philosophy.md) for more.
|
||||||
|
|
||||||
|
## Example Use Cases
|
||||||
|
|
||||||
|
### Personal Blog
|
||||||
|
|
||||||
|
```
|
||||||
|
content/
|
||||||
|
├── index.md # About me
|
||||||
|
├── blog/ # Blog posts
|
||||||
|
│ ├── metadata.ini # page_template = "list-grid"
|
||||||
|
│ ├── 2025-11-01-post/
|
||||||
|
│ └── 2025-11-02-post/
|
||||||
|
└── contact/ # Contact page
|
||||||
|
└── index.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Site
|
||||||
|
|
||||||
|
```
|
||||||
|
content/
|
||||||
|
├── index.md # Introduction
|
||||||
|
├── getting-started/ # Multi-file tutorial
|
||||||
|
│ ├── 00-install.md
|
||||||
|
│ ├── 01-setup.md
|
||||||
|
│ └── 02-first-steps.md
|
||||||
|
└── reference/ # API reference
|
||||||
|
├── metadata.ini # page_template = "list"
|
||||||
|
├── functions/
|
||||||
|
└── classes/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Portfolio
|
||||||
|
|
||||||
|
```
|
||||||
|
content/
|
||||||
|
├── index.md # Homepage
|
||||||
|
├── projects/ # Project grid
|
||||||
|
│ ├── metadata.ini # page_template = "list-card-grid"
|
||||||
|
│ ├── project-1/
|
||||||
|
│ │ ├── index.md
|
||||||
|
│ │ ├── cover.jpg
|
||||||
|
│ │ └── metadata.ini # redirect = "https://project.com"
|
||||||
|
│ └── project-2/
|
||||||
|
└── about/
|
||||||
|
└── index.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use FolderWeb
|
||||||
|
|
||||||
|
### ✅ Ideal For
|
||||||
|
|
||||||
|
- Blogs and content sites
|
||||||
|
- Documentation
|
||||||
|
- Portfolios
|
||||||
|
- Marketing sites
|
||||||
|
- Personal websites
|
||||||
|
- Projects requiring longevity
|
||||||
|
|
||||||
|
### ❌ Not Ideal For
|
||||||
|
|
||||||
|
- User-generated content (no database/auth)
|
||||||
|
- E-commerce (use dedicated platform)
|
||||||
|
- Social networks (need real-time features)
|
||||||
|
- JavaScript-heavy SPAs
|
||||||
|
- Sites with thousands of pages (consider static generators)
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Follow the [Tutorial](tutorial/00-getting-started.md) step-by-step
|
||||||
|
- Check [How-To Guides](how-to/) for specific tasks
|
||||||
|
- Consult [Reference](reference/) for technical details
|
||||||
|
- Read [Explanation](explanation/) for concepts
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Styles not loading**: Hard refresh (Ctrl+Shift+R or Cmd+Shift+R)
|
||||||
|
|
||||||
|
**404 errors**: Check folder exists and has content files
|
||||||
|
|
||||||
|
**Language not working**: Ensure language is in `available` config
|
||||||
|
|
||||||
|
**Metadata not appearing**: Verify INI syntax with `parse_ini_file()`
|
||||||
|
|
||||||
|
**Templates not found**: Check file exists in `/custom/templates/`
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
FolderWeb prioritizes stability and simplicity. Contributions should:
|
||||||
|
- Maintain simplicity
|
||||||
|
- Avoid dependencies
|
||||||
|
- Solve real problems
|
||||||
|
- Be maintainable long-term
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Check the project repository for license information.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **New user?** Start with the [Getting Started Tutorial](tutorial/00-getting-started.md)
|
||||||
|
2. **Need to do something specific?** Browse [How-To Guides](how-to/)
|
||||||
|
3. **Want technical details?** Explore [Reference Documentation](reference/)
|
||||||
|
4. **Curious about design?** Read [Philosophy](explanation/philosophy.md) and [Architecture](explanation/architecture.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**FolderWeb**: Simple, file-based content publishing for the long term.
|
||||||
400
docs/reference/configuration.md
Normal file
400
docs/reference/configuration.md
Normal 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)
|
||||||
538
docs/reference/css-variables.md
Normal file
538
docs/reference/css-variables.md
Normal 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)
|
||||||
394
docs/reference/file-structure.md
Normal file
394
docs/reference/file-structure.md
Normal 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
610
docs/reference/metadata.md
Normal 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
608
docs/reference/templates.md
Normal 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)
|
||||||
196
docs/tutorial/00-getting-started.md
Normal file
196
docs/tutorial/00-getting-started.md
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Welcome to FolderWeb! This tutorial will guide you through creating your first website from scratch in just a few minutes.
|
||||||
|
|
||||||
|
## What You'll Build
|
||||||
|
|
||||||
|
By the end of this tutorial, you'll have a simple website with:
|
||||||
|
- A home page
|
||||||
|
- An about page
|
||||||
|
- A blog with multiple posts
|
||||||
|
- Custom styling
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- PHP 8.4 or higher
|
||||||
|
- A web server (Apache, Nginx, or PHP's built-in server)
|
||||||
|
- Basic understanding of file systems
|
||||||
|
|
||||||
|
## Step 1: Set Up Your Project
|
||||||
|
|
||||||
|
First, create your project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir my-website
|
||||||
|
cd my-website
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the FolderWeb framework files into your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy app/ directory with all framework files
|
||||||
|
cp -r /path/to/folderweb/app ./app
|
||||||
|
|
||||||
|
# Create your content directory
|
||||||
|
mkdir content
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Create Your First Page
|
||||||
|
|
||||||
|
Create a simple home page by adding a Markdown file to your content directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "# Welcome to My Website" > content/index.md
|
||||||
|
echo "" >> content/index.md
|
||||||
|
echo "This is my first page built with FolderWeb." >> content/index.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Start the Server
|
||||||
|
|
||||||
|
Use PHP's built-in server to view your site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -S localhost:8000 -t . app/router.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Open your browser and navigate to `http://localhost:8000`. You should see your home page!
|
||||||
|
|
||||||
|
## Step 4: Add Another Page
|
||||||
|
|
||||||
|
Create an "about" page. FolderWeb uses folder-based routing, so the folder name becomes the URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir content/about
|
||||||
|
echo "# About Me" > content/about/index.md
|
||||||
|
echo "" >> content/about/index.md
|
||||||
|
echo "I'm learning FolderWeb, a minimalist PHP framework." >> content/about/index.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:8000/about/` to see your new page.
|
||||||
|
|
||||||
|
## Step 5: Create a Blog
|
||||||
|
|
||||||
|
Now let's create a blog with multiple posts. When a folder contains subdirectories, FolderWeb automatically creates a list view:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create blog directory
|
||||||
|
mkdir -p content/blog
|
||||||
|
|
||||||
|
# Create first post
|
||||||
|
mkdir content/blog/2025-11-01-my-first-post
|
||||||
|
echo "# My First Post" > content/blog/2025-11-01-my-first-post/index.md
|
||||||
|
echo "" >> content/blog/2025-11-01-my-first-post/index.md
|
||||||
|
echo "This is my first blog post!" >> content/blog/2025-11-01-my-first-post/index.md
|
||||||
|
|
||||||
|
# Create second post
|
||||||
|
mkdir content/blog/2025-11-02-second-post
|
||||||
|
echo "# Second Post" > content/blog/2025-11-02-second-post/index.md
|
||||||
|
echo "" >> content/blog/2025-11-02-second-post/index.md
|
||||||
|
echo "Already on my second post!" >> content/blog/2025-11-02-second-post/index.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:8000/blog/` to see your blog listing. Notice how FolderWeb automatically:
|
||||||
|
- Extracted dates from the folder names (2025-11-01, 2025-11-02)
|
||||||
|
- Created clickable links to each post
|
||||||
|
- Formatted the dates in Norwegian style
|
||||||
|
|
||||||
|
## Step 6: Add Metadata
|
||||||
|
|
||||||
|
Let's enhance your blog posts with metadata. Create a `metadata.ini` file in one of your posts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > content/blog/2025-11-01-my-first-post/metadata.ini << 'EOF'
|
||||||
|
title = "My Amazing First Post"
|
||||||
|
date = "2025-11-01"
|
||||||
|
summary = "A brief introduction to my blogging journey with FolderWeb."
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Refresh `http://localhost:8000/blog/` and you'll see the summary appears in the listing.
|
||||||
|
|
||||||
|
## Step 7: Add a Cover Image
|
||||||
|
|
||||||
|
Make your blog more visual by adding a cover image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy or download an image as cover.jpg in your post folder
|
||||||
|
# For this example, we'll just note where it should go:
|
||||||
|
# content/blog/2025-11-01-my-first-post/cover.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
If you add a `cover.jpg`, `cover.png`, or `cover.webp` file to a post folder, it will automatically appear in the blog listing.
|
||||||
|
|
||||||
|
## Step 8: Customize Your Blog Layout
|
||||||
|
|
||||||
|
Change how your blog is displayed by setting a different template. Create metadata for the blog directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > content/blog/metadata.ini << 'EOF'
|
||||||
|
title = "My Blog"
|
||||||
|
page_template = "list-grid"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Refresh `http://localhost:8000/blog/` to see a grid layout instead of a simple list.
|
||||||
|
|
||||||
|
Available list templates:
|
||||||
|
- `list` (default) - Simple list
|
||||||
|
- `list-grid` - Grid with cover images
|
||||||
|
- `list-card-grid` - Card-style grid
|
||||||
|
- `list-faq` - Expandable Q&A format
|
||||||
|
|
||||||
|
## Step 9: Add Custom Styles
|
||||||
|
|
||||||
|
Create a custom stylesheet to override the default styles:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p custom/styles
|
||||||
|
cat > custom/styles/base.css << 'EOF'
|
||||||
|
:root {
|
||||||
|
--color-primary: oklch(0.55 0.20 30); /* Orange instead of blue */
|
||||||
|
--font-heading: "Arial", sans-serif; /* Sans-serif headings */
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Refresh your browser to see your custom colors.
|
||||||
|
|
||||||
|
## Step 10: Multi-File Pages
|
||||||
|
|
||||||
|
FolderWeb can combine multiple files into a single page. This is useful for long documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a documentation page with multiple sections
|
||||||
|
mkdir content/docs
|
||||||
|
echo "# Introduction" > content/docs/00-intro.md
|
||||||
|
echo "Welcome to the docs!" >> content/docs/00-intro.md
|
||||||
|
|
||||||
|
echo "# Installation" > content/docs/01-install.md
|
||||||
|
echo "How to install the software." >> content/docs/01-install.md
|
||||||
|
|
||||||
|
echo "# Configuration" > content/docs/02-config.md
|
||||||
|
echo "How to configure it." >> content/docs/02-config.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:8000/docs/` and all three files render as one continuous page.
|
||||||
|
|
||||||
|
## What You've Learned
|
||||||
|
|
||||||
|
Congratulations! You now know how to:
|
||||||
|
|
||||||
|
- ✓ Create pages using Markdown files
|
||||||
|
- ✓ Use folder-based routing
|
||||||
|
- ✓ Create blog listings with automatic date extraction
|
||||||
|
- ✓ Add metadata to customize content
|
||||||
|
- ✓ Use cover images
|
||||||
|
- ✓ Switch between different list templates
|
||||||
|
- ✓ Apply custom styling
|
||||||
|
- ✓ Create multi-file pages
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Explore the [How-To Guides](../how-to/) for specific tasks
|
||||||
|
- Read the [Reference Documentation](../reference/) for complete feature details
|
||||||
|
- Understand the [Philosophy](../explanation/philosophy.md) behind FolderWeb
|
||||||
|
- Learn about [multi-language support](../how-to/multi-language.md)
|
||||||
|
- Discover [template customization](../how-to/custom-templates.md)
|
||||||
Loading…
Add table
Reference in a new issue