Add getting started documentation
Add tutorial on adding content Add tutorial on styling Add tutorial on templates Add configuration reference Add metadata reference Add template variables reference Add internationalization reference Add plugin system documentation Add creating templates documentation Add index page
This commit is contained in:
parent
0e19040473
commit
76697e4656
11 changed files with 4724 additions and 0 deletions
165
docs/01-getting-started/index.md
Normal file
165
docs/01-getting-started/index.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Getting Started with FolderWeb
|
||||
|
||||
Welcome to FolderWeb—a delightfully minimal PHP framework that turns your folder structure into a website. No build steps, no package managers, no JavaScript frameworks. Just files and folders doing what they do best.
|
||||
|
||||
## What You Need
|
||||
|
||||
- **PHP 8.4+** — Modern PHP with all the good stuff
|
||||
- **A web server** — Apache, Nginx, or just PHP's built-in server for local development
|
||||
- **A text editor** — Whatever makes you happy
|
||||
|
||||
That's it. No npm, no webpack, no node_modules folder the size of the observable universe.
|
||||
|
||||
## Quick Start (5 Minutes)
|
||||
|
||||
### 1. Get the Code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/folderweb.git
|
||||
cd folderweb
|
||||
```
|
||||
|
||||
### 2. Set Up Your Custom Directory
|
||||
|
||||
FolderWeb separates framework code (`app/`) from your customizations (`custom/`). Copy the defaults to get started:
|
||||
|
||||
**Unix/Linux/macOS:**
|
||||
```bash
|
||||
cp -r app/default custom
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
Copy-Item -Recurse app\default custom
|
||||
```
|
||||
|
||||
**Or just:** Copy the `app/default` folder and rename it to `custom` using your file manager.
|
||||
|
||||
### 3. Create Your Content Directory
|
||||
|
||||
```bash
|
||||
mkdir content
|
||||
```
|
||||
|
||||
This is where your actual website content lives—separate from the framework.
|
||||
|
||||
### 4. Fire It Up Locally
|
||||
|
||||
**Using Podman (Recommended):**
|
||||
|
||||
If you have [Podman](https://podman.io/) installed, there's a ready-made setup:
|
||||
|
||||
```bash
|
||||
cd devel
|
||||
podman-compose up
|
||||
```
|
||||
|
||||
Visit `http://localhost:8080` and you're live.
|
||||
|
||||
**Using PHP's Built-in Server:**
|
||||
|
||||
```bash
|
||||
php -S localhost:8080 -t .
|
||||
```
|
||||
|
||||
Simple, but you'll need to configure routing manually for production.
|
||||
|
||||
### 5. Make Your First Page
|
||||
|
||||
Create a file at `content/hello.md`:
|
||||
|
||||
```markdown
|
||||
# Hello, World!
|
||||
|
||||
This is my first page. Look ma, no build step!
|
||||
```
|
||||
|
||||
Visit `http://localhost:8080/hello/` and there it is.
|
||||
|
||||
**Pro tip:** Notice the trailing slash? FolderWeb enforces them. Folders are folders, after all.
|
||||
|
||||
## What Just Happened?
|
||||
|
||||
FolderWeb looked at your request (`/hello/`), found `content/hello.md`, processed the Markdown into HTML, wrapped it in a template, and served it. All in milliseconds.
|
||||
|
||||
The magic is simple:
|
||||
- **Folders = URLs:** Your directory structure is your site structure
|
||||
- **Files = Content:** Drop a `.md`, `.html`, or `.php` file and it renders
|
||||
- **Templates = Presentation:** HTML wrappers that make everything pretty
|
||||
- **Metadata = Configuration:** Optional `.ini` files for titles, dates, and settings
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you're up and running, you can:
|
||||
|
||||
1. **[Add more content](#)** — Learn about Markdown files, metadata, and organizing pages
|
||||
2. **[Customize the design](#)** — Edit templates and CSS to make it yours
|
||||
3. **[Deploy to production](#)** — Get your site online with a web host
|
||||
|
||||
Or just keep making pages. It's your website—do what makes you happy.
|
||||
|
||||
---
|
||||
|
||||
## Deploying to Production
|
||||
|
||||
When you're ready to go live, you'll need a web host with PHP support. Look for:
|
||||
|
||||
- **PHP 8.4+** (or at least 8.0, but why settle?)
|
||||
- **Apache or Nginx** with mod_rewrite or equivalent
|
||||
- **SSH access** (optional, but makes life easier)
|
||||
|
||||
Most shared hosting providers offer this. Avoid anything that says "managed WordPress only"—you're too cool for that.
|
||||
|
||||
### Deployment with Symlinks (Recommended)
|
||||
|
||||
Keep the framework separate from the web root for easy upgrades:
|
||||
|
||||
```bash
|
||||
# Your server structure:
|
||||
/home/yourusername/
|
||||
├── folderweb/ # Git repo (not public)
|
||||
│ ├── app/
|
||||
│ └── custom/
|
||||
├── content/ # Your content (not public)
|
||||
└── public_html/ # Web root (public)
|
||||
├── app -> ../folderweb/app/ # Symlink
|
||||
├── custom -> ../folderweb/custom/ # Symlink
|
||||
└── content -> ../content/ # Symlink
|
||||
```
|
||||
|
||||
**Why?** When you update FolderWeb, just `git pull` and you're done. No copying files, no risk of overwriting customizations.
|
||||
|
||||
### Apache Configuration
|
||||
|
||||
If your host lets you use `.htaccess`, FolderWeb will handle routing automatically. Otherwise, add this to your Apache config:
|
||||
|
||||
```apache
|
||||
<Directory /path/to/public_html>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ /app/router.php [L,QSA]
|
||||
</Directory>
|
||||
```
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /app/router.php?$query_string;
|
||||
}
|
||||
```
|
||||
|
||||
That's it. Upload your files, point your domain, and you're live.
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
Ready to dive deeper? Head to the [Tutorial](#) to learn how to:
|
||||
- Organize content with folders and metadata
|
||||
- Customize templates and styles
|
||||
- Add multilingual support
|
||||
- Create list views and navigation menus
|
||||
|
||||
Or jump straight to the [Reference](#) if you're the "read the manual" type.
|
||||
308
docs/02-tutorial/01-adding-content.md
Normal file
308
docs/02-tutorial/01-adding-content.md
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
# Adding Content
|
||||
|
||||
FolderWeb turns your folder structure into a website. That's not marketing speak—it's literally how it works. Let's explore what that means in practice.
|
||||
|
||||
## The Basic Idea
|
||||
|
||||
```
|
||||
content/
|
||||
├── about.md → yoursite.com/about/
|
||||
├── blog/
|
||||
│ └── index.md → yoursite.com/blog/
|
||||
└── contact.html → yoursite.com/contact/
|
||||
```
|
||||
|
||||
Every file becomes a page. Every folder becomes a URL path. No configuration required.
|
||||
|
||||
## File Types
|
||||
|
||||
FolderWeb recognizes three content types:
|
||||
|
||||
### Markdown (`.md`)
|
||||
|
||||
The bread and butter of content creation. Write in Markdown, get HTML.
|
||||
|
||||
```markdown
|
||||
# My Page Title
|
||||
|
||||
This is a paragraph with **bold** and *italic* text.
|
||||
|
||||
- Lists work
|
||||
- And so do [links](https://example.com)
|
||||
|
||||
## Subheading
|
||||
|
||||
More content here.
|
||||
```
|
||||
|
||||
FolderWeb uses [Parsedown](https://parsedown.org/) to convert Markdown to HTML, with caching for performance.
|
||||
|
||||
### HTML (`.html`)
|
||||
|
||||
When you need more control or already have HTML:
|
||||
|
||||
```html
|
||||
<h1>My Page</h1>
|
||||
<p>This is plain HTML.</p>
|
||||
<div class="custom-widget">
|
||||
Whatever you want.
|
||||
</div>
|
||||
```
|
||||
|
||||
### PHP (`.php`)
|
||||
|
||||
For dynamic content:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$currentYear = date('Y');
|
||||
echo "<p>Copyright {$currentYear}</p>";
|
||||
?>
|
||||
|
||||
<h2>Latest Posts</h2>
|
||||
<?php
|
||||
$posts = ['Post 1', 'Post 2', 'Post 3'];
|
||||
foreach ($posts as $post) {
|
||||
echo "<li>{$post}</li>";
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
**Important:** PHP files are executed, so be careful with user input and security.
|
||||
|
||||
## Folder Structure as URL Structure
|
||||
|
||||
Your folder hierarchy determines your URLs:
|
||||
|
||||
```
|
||||
content/
|
||||
├── index.md → /
|
||||
├── about.md → /about/
|
||||
├── blog/
|
||||
│ ├── index.md → /blog/
|
||||
│ ├── 2024-12-15-first-post/
|
||||
│ │ └── index.md → /blog/first-post/
|
||||
│ └── 2024-12-20-second-post/
|
||||
│ └── index.md → /blog/second-post/
|
||||
└── projects/
|
||||
├── index.md → /projects/
|
||||
└── my-project/
|
||||
└── index.md → /projects/my-project/
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Folder names become URL slugs
|
||||
- Date prefixes (`YYYY-MM-DD-`) are stripped from URLs but preserved for sorting
|
||||
- Trailing slashes are enforced (FolderWeb redirects `/about` → `/about/`)
|
||||
|
||||
## Multiple Files in One Page
|
||||
|
||||
You can combine multiple content files in a single directory. They render in **alphabetical order**:
|
||||
|
||||
```
|
||||
content/portfolio/
|
||||
├── 00-hero.php # Renders first
|
||||
├── 01-intro.md # Renders second
|
||||
├── 02-gallery.html # Renders third
|
||||
└── 03-contact.md # Renders last
|
||||
```
|
||||
|
||||
All four files render as one page at `/portfolio/`.
|
||||
|
||||
**Use case:** Build pages from modular components—header, content, footer, etc.
|
||||
|
||||
## Dates in Folder Names
|
||||
|
||||
FolderWeb automatically extracts dates from folder names:
|
||||
|
||||
```
|
||||
content/blog/
|
||||
├── 2024-12-15-my-first-post/ # Date: December 15, 2024
|
||||
├── 2024-12-20-another-post/ # Date: December 20, 2024
|
||||
└── 2025-01-01-new-year-post/ # Date: January 1, 2025
|
||||
```
|
||||
|
||||
The date format is `YYYY-MM-DD-` and it's **stripped from the URL**:
|
||||
|
||||
- Folder: `2024-12-15-my-first-post`
|
||||
- URL: `/blog/my-first-post/`
|
||||
- Date: Extracted and available in templates
|
||||
|
||||
Dates are automatically formatted based on your language (e.g., "15. desember 2024" in Norwegian).
|
||||
|
||||
## Adding Assets (Images, Files)
|
||||
|
||||
Drop assets directly in your content folders:
|
||||
|
||||
```
|
||||
content/blog/my-post/
|
||||
├── index.md
|
||||
├── cover.jpg # Cover image (automatic)
|
||||
├── diagram.png # Referenced in content
|
||||
└── styles.css # Page-specific styles
|
||||
```
|
||||
|
||||
**Reference in Markdown:**
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
**Cover images:** Name your cover image `cover.jpg`, `cover.png`, or `cover.webp`. FolderWeb automatically uses it in list views and social media meta tags.
|
||||
|
||||
## Metadata Files
|
||||
|
||||
Add a `metadata.ini` file to configure pages:
|
||||
|
||||
```
|
||||
content/blog/my-post/
|
||||
├── index.md
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
**Basic metadata:**
|
||||
|
||||
```ini
|
||||
title = "My Awesome Post"
|
||||
summary = "A short description for list views"
|
||||
date = "2024-12-15"
|
||||
```
|
||||
|
||||
**More options:**
|
||||
|
||||
```ini
|
||||
title = "My Post"
|
||||
summary = "Short description"
|
||||
date = "2024-12-15"
|
||||
search_description = "SEO-friendly description for search engines"
|
||||
menu = 1 # Show in navigation menu
|
||||
menu_order = 10 # Menu position (lower = first)
|
||||
|
||||
[settings]
|
||||
show_date = true # Display date on page
|
||||
hide_list = false # Don't show list view even if subdirectories exist
|
||||
```
|
||||
|
||||
See the [Metadata Reference](#) for all available options.
|
||||
|
||||
## List Views vs. Page Views
|
||||
|
||||
FolderWeb automatically decides whether to show a **list** or a **page**:
|
||||
|
||||
**List view** (directory has subdirectories):
|
||||
```
|
||||
content/blog/
|
||||
├── index.md # Intro content
|
||||
├── metadata.ini # List configuration
|
||||
├── 2024-12-15-first-post/
|
||||
└── 2024-12-20-second-post/
|
||||
```
|
||||
|
||||
Result: `/blog/` shows a list of posts with the intro content at the top.
|
||||
|
||||
**Page view** (directory has only files):
|
||||
```
|
||||
content/about/
|
||||
└── index.md
|
||||
```
|
||||
|
||||
Result: `/about/` shows just the page content.
|
||||
|
||||
**Override:** Use `hide_list = true` in `metadata.ini` to force page view even with subdirectories.
|
||||
|
||||
## Custom URL Slugs
|
||||
|
||||
Don't like your folder name as the URL? Override it with metadata:
|
||||
|
||||
```ini
|
||||
slug = "custom-url-path"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
content/blog/2024-12-15-very-long-title-that-i-regret/
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
```ini
|
||||
slug = "short-title"
|
||||
```
|
||||
|
||||
URL becomes `/blog/short-title/` instead of `/blog/very-long-title-that-i-regret/`.
|
||||
|
||||
## Navigation Menus
|
||||
|
||||
Add pages to the navigation menu with metadata:
|
||||
|
||||
```ini
|
||||
title = "About Us"
|
||||
menu = 1 # Show in menu
|
||||
menu_order = 20 # Position (lower numbers first)
|
||||
```
|
||||
|
||||
Menu items are sorted by `menu_order`, then alphabetically by title.
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Simple Blog Post
|
||||
|
||||
```
|
||||
content/blog/2024-12-15-my-first-post/
|
||||
├── index.md
|
||||
├── cover.jpg
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
**index.md:**
|
||||
```markdown
|
||||
# My First Post
|
||||
|
||||
This is my blog post content.
|
||||
|
||||

|
||||
```
|
||||
|
||||
**metadata.ini:**
|
||||
```ini
|
||||
title = "My First Post"
|
||||
summary = "An introduction to my blog"
|
||||
```
|
||||
|
||||
### Multi-Section Page
|
||||
|
||||
```
|
||||
content/services/
|
||||
├── 00-hero.php
|
||||
├── 01-intro.md
|
||||
├── 02-pricing.html
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
All files render together as one page at `/services/`.
|
||||
|
||||
### Documentation Site
|
||||
|
||||
```
|
||||
content/docs/
|
||||
├── index.md
|
||||
├── getting-started/
|
||||
│ └── index.md
|
||||
├── tutorial/
|
||||
│ ├── index.md
|
||||
│ ├── basics.md
|
||||
│ └── advanced.md
|
||||
└── reference/
|
||||
└── index.md
|
||||
```
|
||||
|
||||
Creates a hierarchical documentation structure with automatic list views.
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you know how to add content, learn how to:
|
||||
- **[Customize styling](#)** — Make it look like your own
|
||||
- **[Create templates](#)** — Control how content is presented
|
||||
- **[Add multilingual support](#)** — Reach a global audience
|
||||
|
||||
Or jump to the [Reference](#) for detailed documentation on metadata, templates, and configuration.
|
||||
432
docs/02-tutorial/02-styling.md
Normal file
432
docs/02-tutorial/02-styling.md
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
# Styling Your Site
|
||||
|
||||
FolderWeb embraces modern CSS—no preprocessors, no build steps, just the good stuff browsers support today. Let's make your site look exactly how you want.
|
||||
|
||||
## Where Styles Live
|
||||
|
||||
FolderWeb has a simple style hierarchy:
|
||||
|
||||
```
|
||||
custom/
|
||||
├── styles/
|
||||
│ ├── base.css # Your main stylesheet
|
||||
│ └── custom-theme.css # Additional stylesheets (optional)
|
||||
└── content/
|
||||
└── my-page/
|
||||
└── styles.css # Page-specific styles (optional)
|
||||
```
|
||||
|
||||
**Loading order:**
|
||||
1. `custom/styles/base.css` — Your main styles (always loaded)
|
||||
2. Page-specific `styles.css` — If it exists in the content directory
|
||||
|
||||
## Editing the Main Stylesheet
|
||||
|
||||
Start by editing `custom/styles/base.css`. This is where your site-wide styles live.
|
||||
|
||||
**Default structure:**
|
||||
|
||||
```css
|
||||
/* CSS Variables (Design Tokens) */
|
||||
:root {
|
||||
--color-primary: oklch(60% 0.15 250);
|
||||
--color-text: oklch(20% 0 0);
|
||||
--color-bg: oklch(98% 0 0);
|
||||
--font-base: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, monospace;
|
||||
--spacing-unit: 1rem;
|
||||
}
|
||||
|
||||
/* Base Typography */
|
||||
body {
|
||||
font-family: var(--font-base);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* More styles... */
|
||||
```
|
||||
|
||||
Edit these values to match your brand. No build step, no compilation—just save and refresh.
|
||||
|
||||
## Modern CSS Features
|
||||
|
||||
FolderWeb encourages using modern CSS features. Here's what you should know:
|
||||
|
||||
### CSS Variables (Custom Properties)
|
||||
|
||||
Define once, use everywhere:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: oklch(65% 0.2 150);
|
||||
--radius-sm: 0.25rem;
|
||||
--shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--color-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
```
|
||||
|
||||
### OKLCH Colors
|
||||
|
||||
Use modern color spaces for better color manipulation:
|
||||
|
||||
```css
|
||||
/* Traditional RGB/HSL */
|
||||
--old: rgb(100, 150, 200);
|
||||
--old-hsl: hsl(210, 50%, 60%);
|
||||
|
||||
/* Modern OKLCH (better perceptual uniformity) */
|
||||
--new: oklch(65% 0.1 250);
|
||||
```
|
||||
|
||||
**Format:** `oklch(lightness chroma hue / alpha)`
|
||||
- **Lightness:** 0% (black) to 100% (white)
|
||||
- **Chroma:** 0 (gray) to ~0.4 (vivid)
|
||||
- **Hue:** 0-360 degrees (color wheel)
|
||||
|
||||
### CSS Nesting
|
||||
|
||||
Nest related styles without preprocessors:
|
||||
|
||||
```css
|
||||
.card {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
& h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fluid Typography with `clamp()`
|
||||
|
||||
Responsive sizing without media queries:
|
||||
|
||||
```css
|
||||
h1 {
|
||||
/* Min 2rem, ideal 5vw, max 4rem */
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
p {
|
||||
/* Min 1rem, ideal 1.125rem, max 1.25rem */
|
||||
font-size: clamp(1rem, 1.125rem, 1.25rem);
|
||||
}
|
||||
```
|
||||
|
||||
### Logical Properties
|
||||
|
||||
Use logical properties for better internationalization:
|
||||
|
||||
```css
|
||||
/* Old way (assumes left-to-right) */
|
||||
.old {
|
||||
margin-left: 1rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
/* New way (respects text direction) */
|
||||
.new {
|
||||
margin-inline-start: 1rem;
|
||||
padding-inline-end: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Common logical properties:**
|
||||
- `margin-inline-start` / `margin-inline-end` (left/right in LTR)
|
||||
- `margin-block-start` / `margin-block-end` (top/bottom)
|
||||
- `padding-inline` / `padding-block`
|
||||
- `inline-size` (width)
|
||||
- `block-size` (height)
|
||||
|
||||
### Grid Layout
|
||||
|
||||
Use CSS Grid for layout (not flexbox for everything):
|
||||
|
||||
```css
|
||||
.page-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(65ch, 100%) 1fr;
|
||||
gap: 2rem;
|
||||
|
||||
& > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
& > .full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Classless CSS Philosophy
|
||||
|
||||
FolderWeb defaults to **classless CSS**—styling HTML elements directly instead of adding classes everywhere.
|
||||
|
||||
**Good (classless):**
|
||||
```html
|
||||
<article>
|
||||
<h1>Page Title</h1>
|
||||
<p>Content here.</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
```css
|
||||
article {
|
||||
max-width: 65ch;
|
||||
margin: 0 auto;
|
||||
|
||||
& h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Less good (class-heavy):**
|
||||
```html
|
||||
<article class="article-container">
|
||||
<h1 class="article-title">Page Title</h1>
|
||||
<p class="article-text">Content here.</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
**When to use classes:**
|
||||
- Component variants (`.button-primary`, `.button-secondary`)
|
||||
- JavaScript hooks (`.js-toggle`)
|
||||
- Utility overrides (`.visually-hidden`)
|
||||
|
||||
## Page-Specific Styles
|
||||
|
||||
Add `styles.css` to a content directory for page-specific styling:
|
||||
|
||||
```
|
||||
content/portfolio/
|
||||
├── index.md
|
||||
└── styles.css
|
||||
```
|
||||
|
||||
**styles.css:**
|
||||
```css
|
||||
/* Scoped to this page only */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
FolderWeb automatically loads and includes page-specific styles with cache-busting:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/portfolio/styles.css?v=abc123def">
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Add dark mode with CSS variables and `prefers-color-scheme`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-bg: oklch(98% 0 0);
|
||||
--color-text: oklch(20% 0 0);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: oklch(15% 0 0);
|
||||
--color-text: oklch(95% 0 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All colors using the variables automatically adapt.
|
||||
|
||||
## Responsive Design
|
||||
|
||||
Use fluid layouts and relative units:
|
||||
|
||||
```css
|
||||
/* Bad: fixed breakpoints */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Good: fluid and flexible */
|
||||
.container {
|
||||
width: min(90%, 1200px);
|
||||
padding: clamp(1rem, 3vw, 3rem);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: clamp(1rem, 3vw, 2rem);
|
||||
}
|
||||
```
|
||||
|
||||
**Use media queries sparingly:**
|
||||
- Layout changes (sidebar position)
|
||||
- Font size adjustments
|
||||
- Complex interactions
|
||||
|
||||
**Prefer fluid CSS:**
|
||||
- Spacing (`clamp()`)
|
||||
- Typography (`clamp()`)
|
||||
- Grids (`auto-fit`, `minmax()`)
|
||||
|
||||
## Cache Busting
|
||||
|
||||
FolderWeb automatically versions CSS files with MD5 hashes:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/custom/styles/base.css?v=a1b2c3d4">
|
||||
```
|
||||
|
||||
When you edit your CSS, the hash changes and browsers fetch the new version. No manual cache clearing needed.
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Simple Blog Theme
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: oklch(55% 0.15 220);
|
||||
--color-text: oklch(25% 0 0);
|
||||
--color-bg: oklch(99% 0 0);
|
||||
--max-width: 65ch;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Georgia, serif;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
article {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
|
||||
& h1 {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Portfolio Grid
|
||||
|
||||
```css
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: 2rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Documentation Site
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
}
|
||||
|
||||
.docs-layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
gap: 3rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
|
||||
& ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& a {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
text-decoration: none;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start simple:** Edit colors and fonts first, then tackle layout
|
||||
- **Use the inspector:** Browser dev tools show computed values and help debug
|
||||
- **Test in different browsers:** Modern CSS has excellent support, but always verify
|
||||
- **Keep it readable:** Future you will thank present you for clear, organized styles
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you can style your site, learn how to:
|
||||
- **[Create custom templates](#)** — Control HTML structure and layout
|
||||
- **[Add multilingual support](#)** — Style for different languages and text directions
|
||||
|
||||
Or explore the [Reference](#) for detailed documentation on all available template variables and hooks.
|
||||
461
docs/02-tutorial/03-templates.md
Normal file
461
docs/02-tutorial/03-templates.md
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
# Working with Templates
|
||||
|
||||
Templates control how your content is presented. FolderWeb uses a simple PHP-based template system—no complex templating languages, just HTML with a sprinkle of PHP.
|
||||
|
||||
## Template Types
|
||||
|
||||
FolderWeb has three template levels:
|
||||
|
||||
### 1. Base Template (`base.php`)
|
||||
|
||||
The HTML scaffold wrapping every page:
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Page Title</title>
|
||||
<link rel="stylesheet" href="...">
|
||||
</head>
|
||||
<body>
|
||||
<header><!-- Navigation --></header>
|
||||
<main><!-- Page content here --></main>
|
||||
<footer><!-- Footer --></footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**You typically customize this once** to set up your site structure.
|
||||
|
||||
### 2. Page Template (`page.php`)
|
||||
|
||||
Wraps single-page content:
|
||||
|
||||
```php
|
||||
<article>
|
||||
<?= $content ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
**Customize this** to control how individual pages look.
|
||||
|
||||
### 3. List Template (`list.php`, `list-grid.php`, `list-compact.php`)
|
||||
|
||||
Displays multiple items from subdirectories:
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="item-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<h2><a href="<?= $item['url'] ?>"><?= $item['title'] ?></a></h2>
|
||||
<p><?= $item['summary'] ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Customize this** to control how lists of content (blogs, portfolios, etc.) appear.
|
||||
|
||||
## Template Location
|
||||
|
||||
Templates live in your `custom/` directory:
|
||||
|
||||
```
|
||||
custom/
|
||||
└── templates/
|
||||
├── base.php # HTML scaffold
|
||||
├── page.php # Single page wrapper
|
||||
├── list.php # Default list layout
|
||||
├── list-grid.php # Grid card layout
|
||||
└── list-compact.php # Compact list layout
|
||||
```
|
||||
|
||||
**FolderWeb falls back** to `app/default/templates/` if a custom template doesn't exist.
|
||||
|
||||
## Customizing the Base Template
|
||||
|
||||
Let's modify `base.php` to add your site name and custom navigation:
|
||||
|
||||
**custom/templates/base.php:**
|
||||
|
||||
```php
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= $currentLang ?? 'en' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
|
||||
<?php if (isset($metaDescription)): ?>
|
||||
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($socialImageUrl)): ?>
|
||||
<meta property="og:image" content="<?= $socialImageUrl ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<link rel="stylesheet" href="/custom/styles/base.css?v=<?= $cssHash ?? '' ?>">
|
||||
|
||||
<?php if (isset($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/" class="logo">My Site</a>
|
||||
<ul>
|
||||
<?php foreach ($navigation as $item): ?>
|
||||
<li>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© <?= date('Y') ?> My Site</p>
|
||||
<p>
|
||||
<small>Generated in <?= number_format($pageLoadTime, 4) ?>s</small>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Always escape user content: `htmlspecialchars($var)`
|
||||
- Use short echo tags: `<?= $var ?>`
|
||||
- Check if variables exist: `isset($var)`
|
||||
- The `$content` variable contains the rendered page/list content
|
||||
|
||||
## Customizing Page Templates
|
||||
|
||||
The page template wraps your single-page content. Let's add a reading time estimate:
|
||||
|
||||
**custom/templates/page.php:**
|
||||
|
||||
```php
|
||||
<article>
|
||||
<?php if (isset($metadata['title'])): ?>
|
||||
<header>
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
|
||||
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||||
<time datetime="<?= $metadata['date'] ?>">
|
||||
<?= $metadata['formatted_date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Estimate reading time (avg 200 words/min)
|
||||
$wordCount = str_word_count(strip_tags($content));
|
||||
$readingTime = max(1, round($wordCount / 200));
|
||||
?>
|
||||
<p class="reading-time"><?= $readingTime ?> min read</p>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Customizing List Templates
|
||||
|
||||
List templates display collections of content. Let's create a custom blog list:
|
||||
|
||||
**custom/templates/list.php:**
|
||||
|
||||
```php
|
||||
<?php if ($pageContent): ?>
|
||||
<div class="page-intro">
|
||||
<?= $pageContent ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="blog-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="blog-item">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<img
|
||||
src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<header>
|
||||
<h2>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?= $item['url'] ?>">Read more →</a>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Choosing List Templates
|
||||
|
||||
You can create multiple list templates and select them per directory:
|
||||
|
||||
**Available by default:**
|
||||
- `list.php` — Simple vertical list
|
||||
- `list-grid.php` — Card grid layout
|
||||
- `list-compact.php` — Minimal compact list
|
||||
|
||||
**Select in metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "Projects"
|
||||
|
||||
[settings]
|
||||
page_template = "list-grid"
|
||||
```
|
||||
|
||||
Now the `projects/` directory uses the grid layout.
|
||||
|
||||
## Creating Custom List Templates
|
||||
|
||||
Let's create a timeline template for a blog:
|
||||
|
||||
**custom/templates/list-timeline.php:**
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="timeline">
|
||||
<?php
|
||||
$currentYear = null;
|
||||
foreach ($items as $item):
|
||||
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
|
||||
|
||||
// Print year marker if it changed
|
||||
if ($year && $year !== $currentYear):
|
||||
$currentYear = $year;
|
||||
?>
|
||||
<div class="year-marker">
|
||||
<h3><?= $year ?></h3>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<article class="timeline-item">
|
||||
<time><?= $item['formatted_date'] ?? '' ?></time>
|
||||
<div class="timeline-content">
|
||||
<h4><a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a></h4>
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use in metadata.ini:**
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
page_template = "list-timeline"
|
||||
```
|
||||
|
||||
## Available Template Variables
|
||||
|
||||
Templates have access to these variables (see [Reference: Template Variables](#) for complete list):
|
||||
|
||||
**Base template:**
|
||||
```php
|
||||
$content // Rendered page/list HTML
|
||||
$pageTitle // Page title for <title> tag
|
||||
$metaDescription // SEO description
|
||||
$navigation // Array of menu items
|
||||
$homeLabel // "Home" link text (translated)
|
||||
$currentLang // Current language code
|
||||
$languageUrls // Links to other language versions
|
||||
$translations // Translated UI strings
|
||||
$cssHash // Cache-busting hash for CSS
|
||||
$pageCssUrl // Page-specific CSS URL (if exists)
|
||||
$pageLoadTime // Page generation time
|
||||
```
|
||||
|
||||
**Page template:**
|
||||
```php
|
||||
$content // Rendered HTML
|
||||
$metadata // Metadata array (title, date, etc.)
|
||||
```
|
||||
|
||||
**List template:**
|
||||
```php
|
||||
$items // Array of items to display
|
||||
$pageContent // Optional intro content from page
|
||||
$metadata // Directory metadata
|
||||
|
||||
// Each $item has:
|
||||
$item['url'] // Full URL to item
|
||||
$item['title'] // Item title
|
||||
$item['summary'] // Short description
|
||||
$item['date'] // ISO date (YYYY-MM-DD)
|
||||
$item['formatted_date'] // Localized date string
|
||||
$item['cover_image'] // Cover image URL (if exists)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Escape Output
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<h1><?= $title ?></h1>
|
||||
|
||||
<!-- Good -->
|
||||
<h1><?= htmlspecialchars($title) ?></h1>
|
||||
```
|
||||
|
||||
**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown).
|
||||
|
||||
### 2. Check Variables Exist
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<p><?= $metadata['summary'] ?></p>
|
||||
|
||||
<!-- Good -->
|
||||
<?php if (isset($metadata['summary'])): ?>
|
||||
<p><?= htmlspecialchars($metadata['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### 3. Use Short Echo Tags
|
||||
|
||||
```php
|
||||
<!-- Verbose -->
|
||||
<?php echo htmlspecialchars($title); ?>
|
||||
|
||||
<!-- Concise -->
|
||||
<?= htmlspecialchars($title) ?>
|
||||
```
|
||||
|
||||
### 4. Keep Logic Minimal
|
||||
|
||||
Templates should display data, not process it. Complex logic belongs in plugins.
|
||||
|
||||
```php
|
||||
<!-- Bad: complex logic in template -->
|
||||
<?php
|
||||
$posts = array_filter($items, function($item) {
|
||||
return strtotime($item['date']) > strtotime('-30 days');
|
||||
});
|
||||
usort($posts, function($a, $b) {
|
||||
return strcmp($b['date'], $a['date']);
|
||||
});
|
||||
?>
|
||||
|
||||
<!-- Good: prepare data in a plugin, display in template -->
|
||||
<?php foreach ($recentPosts as $post): ?>
|
||||
...
|
||||
<?php endforeach; ?>
|
||||
```
|
||||
|
||||
### 5. Use Semantic HTML
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<div class="title">Title</div>
|
||||
<div class="content">Content</div>
|
||||
|
||||
<!-- Good -->
|
||||
<article>
|
||||
<h1>Title</h1>
|
||||
<div class="content">Content</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Simple Portfolio Page Template
|
||||
|
||||
```php
|
||||
<article class="portfolio-item">
|
||||
<header>
|
||||
<?php if (isset($metadata['cover_image'])): ?>
|
||||
<img src="<?= $metadata['cover_image'] ?>" alt="">
|
||||
<?php endif; ?>
|
||||
|
||||
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
|
||||
<?php if (isset($metadata['project_url'])): ?>
|
||||
<footer>
|
||||
<a href="<?= htmlspecialchars($metadata['project_url']) ?>"
|
||||
class="button">View Project →</a>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Card Grid List Template
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="card-grid">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="card">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<img src="<?= $item['cover_image'] ?>" alt="" loading="lazy">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card-content">
|
||||
<h3>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
## What's Next?
|
||||
|
||||
You now know how to customize templates. Next, learn about:
|
||||
- **[Template Variables Reference](#)** — Complete list of available variables
|
||||
- **[Creating Plugins](#)** — Extend functionality and add custom data to templates
|
||||
- **[Internationalization](#)** — Build multilingual sites
|
||||
|
||||
Or explore the examples in `app/default/content/examples/templates-demo/` to see templates in action.
|
||||
304
docs/03-reference/01-configuration.md
Normal file
304
docs/03-reference/01-configuration.md
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
# Configuration Reference
|
||||
|
||||
FolderWeb uses INI files for configuration. The configuration system follows a simple hierarchy with sensible defaults.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
```
|
||||
app/default/config.ini # Framework defaults (don't modify)
|
||||
custom/config.ini # Your overrides (create this)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
1. FolderWeb loads `app/default/config.ini`
|
||||
2. If `custom/config.ini` exists, its values override the defaults
|
||||
3. Only override what you need—missing values fall back to defaults
|
||||
|
||||
## Creating Your Configuration
|
||||
|
||||
**custom/config.ini:**
|
||||
|
||||
```ini
|
||||
[languages]
|
||||
default = "en"
|
||||
available = "en,no,de"
|
||||
|
||||
[plugins]
|
||||
enabled = "languages,my-custom-plugin"
|
||||
|
||||
[site]
|
||||
title = "My Website"
|
||||
```
|
||||
|
||||
That's it. Only add what you need to change.
|
||||
|
||||
## Available Configuration Options
|
||||
|
||||
### `[languages]`
|
||||
|
||||
Controls multilingual support (requires the `languages` plugin).
|
||||
|
||||
```ini
|
||||
[languages]
|
||||
default = "en" # Default language code
|
||||
available = "en,no,de" # Comma-separated list of available languages
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `default` — Language code used when no language is specified in URL
|
||||
- `available` — Comma-separated list of language codes (ISO 639-1)
|
||||
|
||||
**Example:**
|
||||
```ini
|
||||
[languages]
|
||||
default = "no"
|
||||
available = "no,en"
|
||||
```
|
||||
|
||||
### `[plugins]`
|
||||
|
||||
Controls which plugins are loaded.
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
enabled = "languages,analytics,custom-plugin"
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `enabled` — Comma-separated list of plugin names (without `.php` extension)
|
||||
|
||||
**Plugin loading order:**
|
||||
1. `app/plugins/global/` — Built-in global plugins
|
||||
2. `custom/plugins/global/` — Your global plugins
|
||||
3. `app/plugins/page/` — Built-in page plugins (not yet used)
|
||||
4. `custom/plugins/page/` — Your page plugins (not yet used)
|
||||
|
||||
**Example:**
|
||||
```ini
|
||||
[plugins]
|
||||
enabled = "languages"
|
||||
```
|
||||
|
||||
To disable all plugins, leave the value empty:
|
||||
```ini
|
||||
[plugins]
|
||||
enabled = ""
|
||||
```
|
||||
|
||||
### Custom Sections
|
||||
|
||||
Add your own configuration sections for custom plugins:
|
||||
|
||||
```ini
|
||||
[analytics]
|
||||
tracking_id = "UA-12345678-1"
|
||||
enabled = true
|
||||
|
||||
[social]
|
||||
twitter = "@myhandle"
|
||||
github = "myusername"
|
||||
|
||||
[api]
|
||||
endpoint = "https://api.example.com"
|
||||
key = "secret-key-here"
|
||||
```
|
||||
|
||||
Access in plugins via the `$config` parameter:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
|
||||
$trackingId = $config['analytics']['tracking_id'] ?? null;
|
||||
// Use the config value...
|
||||
return $ctx;
|
||||
});
|
||||
```
|
||||
|
||||
## Default Configuration
|
||||
|
||||
Here's what's included in `app/default/config.ini`:
|
||||
|
||||
```ini
|
||||
[languages]
|
||||
default = "en"
|
||||
available = "en,no"
|
||||
|
||||
[plugins]
|
||||
enabled = "languages"
|
||||
```
|
||||
|
||||
These values are active unless you override them in `custom/config.ini`.
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### 1. Only Override What Changes
|
||||
|
||||
**Bad:**
|
||||
```ini
|
||||
[languages]
|
||||
default = "en"
|
||||
available = "en,no"
|
||||
|
||||
[plugins]
|
||||
enabled = "languages"
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```ini
|
||||
# Only change the default language
|
||||
[languages]
|
||||
default = "no"
|
||||
```
|
||||
|
||||
### 2. Use Comments
|
||||
|
||||
```ini
|
||||
[languages]
|
||||
default = "no" # Norwegian site
|
||||
available = "no,en,de" # Also support English and German
|
||||
|
||||
[plugins]
|
||||
enabled = "languages,analytics" # Google Analytics plugin
|
||||
```
|
||||
|
||||
### 3. Keep Secrets Separate
|
||||
|
||||
Don't commit API keys and secrets to version control. Use environment-specific config or `.gitignore`:
|
||||
|
||||
```ini
|
||||
[api]
|
||||
key = "dev-key-here" # Override in production
|
||||
```
|
||||
|
||||
### 4. Organize by Purpose
|
||||
|
||||
```ini
|
||||
# Multilingual settings
|
||||
[languages]
|
||||
default = "en"
|
||||
available = "en,no"
|
||||
|
||||
# Third-party services
|
||||
[analytics]
|
||||
enabled = true
|
||||
tracking_id = "UA-12345678-1"
|
||||
|
||||
# Custom features
|
||||
[reading_time]
|
||||
words_per_minute = 200
|
||||
```
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
FolderWeb doesn't have built-in environment detection, but you can handle it manually:
|
||||
|
||||
**Option 1: Different files**
|
||||
|
||||
```bash
|
||||
# Development
|
||||
ln -s custom/config.dev.ini custom/config.ini
|
||||
|
||||
# Production
|
||||
ln -s custom/config.prod.ini custom/config.ini
|
||||
```
|
||||
|
||||
**Option 2: Server-side includes**
|
||||
|
||||
**custom/config.ini:**
|
||||
```ini
|
||||
[languages]
|
||||
default = "en"
|
||||
```
|
||||
|
||||
**custom/config.prod.ini:**
|
||||
```ini
|
||||
[api]
|
||||
key = "production-key"
|
||||
```
|
||||
|
||||
Load production config in your deployment script:
|
||||
```bash
|
||||
cat custom/config.prod.ini >> custom/config.ini
|
||||
```
|
||||
|
||||
**Option 3: Environment variables**
|
||||
|
||||
Read from environment variables in a custom plugin:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
|
||||
// Override config with environment variables
|
||||
$apiKey = getenv('API_KEY') ?: ($config['api']['key'] ?? null);
|
||||
$ctx->set('api_key', $apiKey);
|
||||
return $ctx;
|
||||
});
|
||||
```
|
||||
|
||||
## Accessing Configuration in Code
|
||||
|
||||
Configuration is passed to plugin hooks:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
|
||||
// Access configuration
|
||||
$defaultLang = $config['languages']['default'] ?? 'en';
|
||||
$plugins = $config['plugins']['enabled'] ?? '';
|
||||
|
||||
// Use it
|
||||
$ctx->set('site_lang', $defaultLang);
|
||||
|
||||
return $ctx;
|
||||
});
|
||||
```
|
||||
|
||||
Configuration is **not** directly available in templates. If you need config values in templates, set them via a plugin hook:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
global $config;
|
||||
|
||||
$vars['siteTitle'] = $config['site']['title'] ?? 'My Site';
|
||||
$vars['socialLinks'] = [
|
||||
'twitter' => $config['social']['twitter'] ?? null,
|
||||
'github' => $config['social']['github'] ?? null,
|
||||
];
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
FolderWeb doesn't enforce a schema—you can add any sections and keys you need. However, these are the recognized built-in options:
|
||||
|
||||
| Section | Key | Type | Default | Description |
|
||||
|---------|-----|------|---------|-------------|
|
||||
| `languages` | `default` | string | `"en"` | Default language code |
|
||||
| `languages` | `available` | string | `"en,no"` | Comma-separated language codes |
|
||||
| `plugins` | `enabled` | string | `"languages"` | Comma-separated plugin names |
|
||||
|
||||
All other sections are custom and plugin-specific.
|
||||
|
||||
## Debugging Configuration
|
||||
|
||||
To see the active configuration, create a debug page:
|
||||
|
||||
**content/debug.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
global $config;
|
||||
echo '<pre>';
|
||||
print_r($config);
|
||||
echo '</pre>';
|
||||
?>
|
||||
```
|
||||
|
||||
Visit `/debug/` to see the merged configuration array.
|
||||
|
||||
**Remember to delete this page** before deploying to production.
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Metadata Reference](#)** — Configure individual pages with `metadata.ini`
|
||||
- **[Template Variables](#)** — Access configuration in templates
|
||||
- **[Creating Plugins](#)** — Use configuration in custom plugins
|
||||
447
docs/03-reference/02-metadata.md
Normal file
447
docs/03-reference/02-metadata.md
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
# Metadata Reference
|
||||
|
||||
Metadata files (`metadata.ini`) configure individual pages and directories. They control titles, URLs, templates, navigation, and more.
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```ini
|
||||
title = "Page Title"
|
||||
summary = "Short description"
|
||||
date = "2024-12-15"
|
||||
search_description = "SEO-friendly description"
|
||||
```
|
||||
|
||||
Place `metadata.ini` in any content directory:
|
||||
|
||||
```
|
||||
content/blog/my-post/
|
||||
├── index.md
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
## Core Fields
|
||||
|
||||
### `title`
|
||||
|
||||
The page or item title.
|
||||
|
||||
```ini
|
||||
title = "My Blog Post"
|
||||
```
|
||||
|
||||
**Default:** Extracted from first `# Heading` in Markdown, or folder name
|
||||
**Used in:** Page `<title>`, list items, navigation menu
|
||||
**Type:** String
|
||||
|
||||
### `summary`
|
||||
|
||||
Short description shown in list views.
|
||||
|
||||
```ini
|
||||
summary = "A brief introduction to this topic"
|
||||
```
|
||||
|
||||
**Default:** None (empty)
|
||||
**Used in:** List item previews, RSS feeds, social media cards
|
||||
**Type:** String
|
||||
**Recommended length:** 150-200 characters
|
||||
|
||||
### `date`
|
||||
|
||||
Publication or modification date.
|
||||
|
||||
```ini
|
||||
date = "2024-12-15"
|
||||
```
|
||||
|
||||
**Default:** Extracted from folder name (`YYYY-MM-DD-title`)
|
||||
**Format:** `YYYY-MM-DD` (ISO 8601)
|
||||
**Used in:** List sorting, date displays, `<time>` elements
|
||||
**Type:** String (date)
|
||||
|
||||
### `search_description`
|
||||
|
||||
SEO meta description for search engines.
|
||||
|
||||
```ini
|
||||
search_description = "Learn how to build fast, maintainable websites with FolderWeb"
|
||||
```
|
||||
|
||||
**Default:** Uses `summary` if not set
|
||||
**Used in:** `<meta name="description">` tag
|
||||
**Type:** String
|
||||
**Recommended length:** 150-160 characters
|
||||
|
||||
### `slug`
|
||||
|
||||
Custom URL slug (overrides folder name).
|
||||
|
||||
```ini
|
||||
slug = "custom-url"
|
||||
```
|
||||
|
||||
**Default:** Folder name (with date prefix removed)
|
||||
**Used in:** URL generation
|
||||
**Type:** String (alphanumeric, hyphens, underscores)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Folder: content/blog/2024-12-15-very-long-title/
|
||||
Slug: short-title
|
||||
URL: /blog/short-title/
|
||||
```
|
||||
|
||||
### `menu`
|
||||
|
||||
Show this page in the navigation menu.
|
||||
|
||||
```ini
|
||||
menu = 1
|
||||
```
|
||||
|
||||
**Default:** `0` (not in menu)
|
||||
**Values:** `1` (show) or `0` (hide)
|
||||
**Type:** Integer
|
||||
|
||||
### `menu_order`
|
||||
|
||||
Position in navigation menu (lower numbers first).
|
||||
|
||||
```ini
|
||||
menu = 1
|
||||
menu_order = 10
|
||||
```
|
||||
|
||||
**Default:** `999` (last)
|
||||
**Used in:** Navigation sorting
|
||||
**Type:** Integer
|
||||
|
||||
**Example:**
|
||||
```ini
|
||||
# Home
|
||||
menu = 1
|
||||
menu_order = 1
|
||||
|
||||
# About
|
||||
menu = 1
|
||||
menu_order = 10
|
||||
|
||||
# Blog
|
||||
menu = 1
|
||||
menu_order = 20
|
||||
|
||||
# Contact
|
||||
menu = 1
|
||||
menu_order = 30
|
||||
```
|
||||
|
||||
## Settings Section
|
||||
|
||||
Advanced settings go in a `[settings]` section:
|
||||
|
||||
```ini
|
||||
title = "My Page"
|
||||
|
||||
[settings]
|
||||
page_template = "list-grid"
|
||||
show_date = true
|
||||
hide_list = false
|
||||
```
|
||||
|
||||
### `page_template`
|
||||
|
||||
Which template to use for list views.
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
page_template = "list-grid"
|
||||
```
|
||||
|
||||
**Default:** `list` (uses `list.php`)
|
||||
**Available templates:**
|
||||
- `list` — Simple vertical list
|
||||
- `list-grid` — Card grid layout
|
||||
- `list-compact` — Minimal compact list
|
||||
- Custom templates you create
|
||||
|
||||
**Used in:** List view rendering
|
||||
**Type:** String (template name without `.php`)
|
||||
|
||||
### `show_date`
|
||||
|
||||
Display the date on the page.
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
show_date = true
|
||||
```
|
||||
|
||||
**Default:** `true`
|
||||
**Values:** `true` or `false`
|
||||
**Type:** Boolean
|
||||
|
||||
### `hide_list`
|
||||
|
||||
Don't show list view even if directory has subdirectories.
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
hide_list = true
|
||||
```
|
||||
|
||||
**Default:** `false`
|
||||
**Values:** `true` or `false`
|
||||
**Type:** Boolean
|
||||
**Use case:** Section landing pages that should show content instead of list
|
||||
|
||||
## Language-Specific Overrides
|
||||
|
||||
Add language-specific sections to override fields:
|
||||
|
||||
```ini
|
||||
title = "About Us"
|
||||
summary = "Learn about our company"
|
||||
slug = "about"
|
||||
|
||||
[no]
|
||||
title = "Om oss"
|
||||
summary = "Les om bedriften vår"
|
||||
slug = "om"
|
||||
|
||||
[de]
|
||||
title = "Über uns"
|
||||
summary = "Erfahren Sie mehr über unser Unternehmen"
|
||||
slug = "uber-uns"
|
||||
```
|
||||
|
||||
**Supported fields in language sections:**
|
||||
- `title`
|
||||
- `summary`
|
||||
- `search_description`
|
||||
- `slug`
|
||||
|
||||
**Language codes:** Must match your configured languages (`config.ini`).
|
||||
|
||||
**URLs with language-specific slugs:**
|
||||
- English: `/about/`
|
||||
- Norwegian: `/no/om/`
|
||||
- German: `/de/uber-uns/`
|
||||
|
||||
## Custom Fields
|
||||
|
||||
Add any custom fields you need:
|
||||
|
||||
```ini
|
||||
title = "Project X"
|
||||
author = "Jane Doe"
|
||||
client = "ACME Corp"
|
||||
project_url = "https://example.com"
|
||||
featured = true
|
||||
tags = "web,design,portfolio"
|
||||
```
|
||||
|
||||
Access custom fields in templates:
|
||||
|
||||
```php
|
||||
<?php if (isset($metadata['author'])): ?>
|
||||
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($metadata['project_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($metadata['project_url']) ?>">
|
||||
View Project
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
Or in plugins:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$metadata = $ctx->get('metadata', []);
|
||||
|
||||
if (isset($metadata['tags'])) {
|
||||
$vars['tags'] = explode(',', $metadata['tags']);
|
||||
}
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
**content/blog/2024-12-15-building-fast-sites/metadata.ini:**
|
||||
|
||||
```ini
|
||||
# Core fields
|
||||
title = "Building Fast Websites"
|
||||
summary = "Learn how to optimize your site for speed and performance"
|
||||
date = "2024-12-15"
|
||||
search_description = "A comprehensive guide to building fast, performant websites in 2024"
|
||||
|
||||
# Navigation
|
||||
menu = 0 # Don't show in main menu
|
||||
menu_order = 999
|
||||
|
||||
# Custom slug
|
||||
slug = "fast-sites"
|
||||
|
||||
# Settings
|
||||
[settings]
|
||||
show_date = true
|
||||
|
||||
# Custom fields
|
||||
author = "Jane Doe"
|
||||
category = "Web Performance"
|
||||
tags = "performance,optimization,web"
|
||||
estimated_reading_time = 8
|
||||
|
||||
# Norwegian translation
|
||||
[no]
|
||||
title = "Bygge raske nettsider"
|
||||
summary = "Lær hvordan du optimaliserer nettstedet ditt for hastighet og ytelse"
|
||||
search_description = "En omfattende guide til å bygge raske, effektive nettsteder i 2024"
|
||||
slug = "raske-sider"
|
||||
```
|
||||
|
||||
## Metadata Priority
|
||||
|
||||
When determining values, FolderWeb follows this priority:
|
||||
|
||||
1. **Language-specific metadata** (e.g., `[no]` section)
|
||||
2. **Root metadata** (e.g., `title = "..."`)
|
||||
3. **Auto-extracted values** (e.g., first heading, folder date)
|
||||
4. **Defaults** (e.g., folder name)
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
Folder: content/blog/2024-12-15-my-post/
|
||||
```
|
||||
|
||||
**Title resolution:**
|
||||
1. Check `metadata.ini` for `[en] title = "..."`
|
||||
2. Check `metadata.ini` for `title = "..."`
|
||||
3. Extract from first `# Heading` in Markdown
|
||||
4. Use folder name: "my-post"
|
||||
|
||||
## Metadata in List Items
|
||||
|
||||
When rendering list views, each item receives these metadata fields:
|
||||
|
||||
```php
|
||||
$item = [
|
||||
'url' => '/blog/my-post/',
|
||||
'path' => '/content/blog/2024-12-15-my-post',
|
||||
'title' => 'My Post',
|
||||
'summary' => 'Short description',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024', // Language-specific
|
||||
'cover_image' => '/blog/my-post/cover.jpg', // If exists
|
||||
// All custom metadata fields...
|
||||
'author' => 'Jane Doe',
|
||||
'tags' => 'web,design',
|
||||
];
|
||||
```
|
||||
|
||||
Access in list templates:
|
||||
|
||||
```php
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<h2><?= htmlspecialchars($item['title']) ?></h2>
|
||||
|
||||
<?php if (isset($item['author'])): ?>
|
||||
<p>By <?= htmlspecialchars($item['author']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Consistent Field Names
|
||||
|
||||
```ini
|
||||
# Good: consistent naming
|
||||
author = "Jane Doe"
|
||||
published_date = "2024-12-15"
|
||||
featured = true
|
||||
|
||||
# Bad: inconsistent naming
|
||||
Author = "Jane Doe"
|
||||
PublishDate = "2024-12-15"
|
||||
is_featured = true
|
||||
```
|
||||
|
||||
### 2. Keep Summaries Concise
|
||||
|
||||
```ini
|
||||
# Good
|
||||
summary = "Learn to optimize website performance in 5 steps"
|
||||
|
||||
# Too long
|
||||
summary = "This comprehensive article will teach you everything you need to know about optimizing website performance, including lazy loading, code splitting, image optimization, and much more in detailed steps with examples"
|
||||
```
|
||||
|
||||
### 3. Use Semantic Custom Fields
|
||||
|
||||
```ini
|
||||
# Good: clear purpose
|
||||
author = "Jane Doe"
|
||||
category = "Tutorial"
|
||||
difficulty = "Beginner"
|
||||
|
||||
# Bad: unclear purpose
|
||||
field1 = "Jane Doe"
|
||||
field2 = "Tutorial"
|
||||
field3 = "Beginner"
|
||||
```
|
||||
|
||||
### 4. Add Comments
|
||||
|
||||
```ini
|
||||
# SEO and social media
|
||||
title = "Building Fast Websites"
|
||||
search_description = "A guide to web performance optimization"
|
||||
|
||||
# Author and categorization
|
||||
author = "Jane Doe"
|
||||
category = "Performance"
|
||||
|
||||
# Custom display options
|
||||
featured = true # Show in featured section
|
||||
priority = 10 # Higher = more prominent
|
||||
```
|
||||
|
||||
## Debugging Metadata
|
||||
|
||||
To see parsed metadata, create a debug template:
|
||||
|
||||
**custom/templates/page.php:**
|
||||
|
||||
```php
|
||||
<pre><?php print_r($metadata); ?></pre>
|
||||
<hr>
|
||||
<?= $content ?>
|
||||
```
|
||||
|
||||
Or in list templates:
|
||||
|
||||
```php
|
||||
<pre><?php print_r($items); ?></pre>
|
||||
<hr>
|
||||
<?= $pageContent ?>
|
||||
```
|
||||
|
||||
**Remember to revert** before deploying to production.
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Template Variables](#)** — See how metadata is used in templates
|
||||
- **[Internationalization](#)** — Use language-specific metadata
|
||||
- **[Creating Plugins](#)** — Process metadata in custom plugins
|
||||
474
docs/03-reference/03-template-variables.md
Normal file
474
docs/03-reference/03-template-variables.md
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# Template Variables Reference
|
||||
|
||||
Templates have access to a set of variables provided by FolderWeb and its plugins. This reference documents all available variables and their types.
|
||||
|
||||
## Base Template Variables
|
||||
|
||||
Available in `base.php`:
|
||||
|
||||
### `$content`
|
||||
|
||||
The fully rendered HTML content from the page or list template.
|
||||
|
||||
**Type:** String (HTML)
|
||||
**Example:**
|
||||
```php
|
||||
<main>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
```
|
||||
|
||||
### `$pageTitle`
|
||||
|
||||
The page title for the `<title>` tag.
|
||||
|
||||
**Type:** String
|
||||
**Default:** `"FolderWeb"`
|
||||
**Example:**
|
||||
```php
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'FolderWeb') ?></title>
|
||||
```
|
||||
|
||||
**Source:**
|
||||
1. Language-specific metadata `[lang] title`
|
||||
2. Root metadata `title`
|
||||
3. First heading in content
|
||||
4. Folder name
|
||||
|
||||
### `$metaDescription`
|
||||
|
||||
SEO description for the `<meta name="description">` tag.
|
||||
|
||||
**Type:** String
|
||||
**Optional:** May be empty
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($metaDescription)): ?>
|
||||
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Source:**
|
||||
1. Metadata `search_description`
|
||||
2. Metadata `summary`
|
||||
3. Empty if not set
|
||||
|
||||
### `$socialImageUrl`
|
||||
|
||||
URL to cover image for social media meta tags.
|
||||
|
||||
**Type:** String (URL)
|
||||
**Optional:** May be empty
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($socialImageUrl)): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Source:** First `cover.*` image found in content directory
|
||||
|
||||
### `$navigation`
|
||||
|
||||
Array of navigation menu items.
|
||||
|
||||
**Type:** Array of associative arrays
|
||||
**Structure:**
|
||||
```php
|
||||
[
|
||||
['url' => '/about/', 'title' => 'About'],
|
||||
['url' => '/blog/', 'title' => 'Blog'],
|
||||
['url' => '/contact/', 'title' => 'Contact'],
|
||||
]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($navigation)): ?>
|
||||
<nav>
|
||||
<?php foreach ($navigation as $item): ?>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Source:** Pages with `menu = 1` in metadata, sorted by `menu_order`
|
||||
|
||||
### `$homeLabel`
|
||||
|
||||
Text for the home link.
|
||||
|
||||
**Type:** String
|
||||
**Default:** `"Home"`
|
||||
**Example:**
|
||||
```php
|
||||
<a href="/"><?= htmlspecialchars($homeLabel ?? 'Home') ?></a>
|
||||
```
|
||||
|
||||
**Source:** Translation string `translations['home']` or fallback "Home"
|
||||
|
||||
### `$currentLang`
|
||||
|
||||
Current language code.
|
||||
|
||||
**Type:** String
|
||||
**Default:** From `config.ini` `languages.default`
|
||||
**Example:**
|
||||
```php
|
||||
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
|
||||
```
|
||||
|
||||
**Values:** ISO 639-1 language codes (`en`, `no`, `de`, etc.)
|
||||
|
||||
### `$langPrefix`
|
||||
|
||||
URL prefix for the current language.
|
||||
|
||||
**Type:** String
|
||||
**Default:** Empty string for default language
|
||||
**Example:**
|
||||
```php
|
||||
<a href="<?= htmlspecialchars($langPrefix ?? '') ?>/">Home</a>
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `""` (empty) for default language
|
||||
- `"/no"` for Norwegian
|
||||
- `"/de"` for German
|
||||
- etc.
|
||||
|
||||
### `$languageUrls`
|
||||
|
||||
URLs to switch between available languages.
|
||||
|
||||
**Type:** Associative array
|
||||
**Structure:**
|
||||
```php
|
||||
[
|
||||
'en' => '/page/',
|
||||
'no' => '/no/side/',
|
||||
'de' => '/de/seite/',
|
||||
]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
|
||||
<nav class="language-switcher">
|
||||
<?php foreach ($languageUrls as $lang => $url): ?>
|
||||
<a href="<?= htmlspecialchars($url) ?>"
|
||||
<?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>>
|
||||
<?= htmlspecialchars(strtoupper($lang)) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### `$translations`
|
||||
|
||||
Translated UI strings for the current language.
|
||||
|
||||
**Type:** Associative array
|
||||
**Structure:**
|
||||
```php
|
||||
[
|
||||
'home' => 'Home',
|
||||
'footer_handcoded' => 'Generated in',
|
||||
'footer_page_time' => 'ms',
|
||||
'months' => 'January,February,March,...',
|
||||
]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
<p><?= htmlspecialchars($translations['home'] ?? 'Home') ?></p>
|
||||
```
|
||||
|
||||
**Source:** Language files in `custom/languages/[lang].ini` or `app/default/languages/[lang].ini`
|
||||
|
||||
### `$pageCssUrl`
|
||||
|
||||
URL to page-specific CSS file.
|
||||
|
||||
**Type:** String (URL)
|
||||
**Optional:** Only set if `styles.css` exists in content directory
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### `$pageCssHash`
|
||||
|
||||
MD5 hash of page-specific CSS for cache busting.
|
||||
|
||||
**Type:** String (MD5 hash)
|
||||
**Optional:** Only set if `$pageCssUrl` exists
|
||||
**Example:** See `$pageCssUrl` above
|
||||
|
||||
## Page Template Variables
|
||||
|
||||
Available in `page.php`:
|
||||
|
||||
### `$content`
|
||||
|
||||
The fully rendered HTML content from the page.
|
||||
|
||||
**Type:** String (HTML)
|
||||
**Example:**
|
||||
```php
|
||||
<article>
|
||||
<?= $content ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### `$metadata`
|
||||
|
||||
All metadata for the current page.
|
||||
|
||||
**Type:** Associative array
|
||||
**Structure:**
|
||||
```php
|
||||
[
|
||||
'title' => 'Page Title',
|
||||
'summary' => 'Short description',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024',
|
||||
'show_date' => true,
|
||||
'author' => 'Jane Doe', // Custom fields
|
||||
'tags' => 'web,design',
|
||||
// ... all other metadata fields
|
||||
]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (isset($metadata['title'])): ?>
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||||
<time datetime="<?= $metadata['date'] ?>">
|
||||
<?= $metadata['formatted_date'] ?? $metadata['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
## List Template Variables
|
||||
|
||||
Available in `list.php`, `list-grid.php`, `list-compact.php`, etc.:
|
||||
|
||||
### `$pageContent`
|
||||
|
||||
Optional intro content from the directory's own files.
|
||||
|
||||
**Type:** String (HTML)
|
||||
**Optional:** May be empty
|
||||
**Example:**
|
||||
```php
|
||||
<?php if ($pageContent): ?>
|
||||
<div class="intro">
|
||||
<?= $pageContent ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Source:** Content files in the list directory itself (not subdirectories)
|
||||
|
||||
### `$items`
|
||||
|
||||
Array of items to display in the list.
|
||||
|
||||
**Type:** Array of associative arrays
|
||||
**Structure:**
|
||||
```php
|
||||
[
|
||||
[
|
||||
'url' => '/blog/my-post/',
|
||||
'path' => '/content/blog/2024-12-15-my-post',
|
||||
'title' => 'My Post',
|
||||
'summary' => 'Short description',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024',
|
||||
'cover_image' => '/blog/my-post/cover.jpg',
|
||||
// All custom metadata fields...
|
||||
'author' => 'Jane Doe',
|
||||
'category' => 'Tutorial',
|
||||
],
|
||||
// ... more items
|
||||
]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<img src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<h2>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
```
|
||||
|
||||
### `$metadata`
|
||||
|
||||
Metadata for the list directory itself.
|
||||
|
||||
**Type:** Associative array
|
||||
**Structure:** Same as page metadata
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (isset($metadata['title'])): ?>
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
## Item Properties
|
||||
|
||||
Each item in `$items` has these properties:
|
||||
|
||||
| Property | Type | Description | Optional |
|
||||
|----------|------|-------------|----------|
|
||||
| `url` | String | Full URL to the item | No |
|
||||
| `path` | String | Filesystem path to item | No |
|
||||
| `title` | String | Item title | No |
|
||||
| `summary` | String | Short description | Yes |
|
||||
| `date` | String | ISO date (YYYY-MM-DD) | Yes |
|
||||
| `formatted_date` | String | Localized date string | Yes |
|
||||
| `cover_image` | String | URL to cover image | Yes |
|
||||
| Custom fields | Mixed | Any metadata fields | Yes |
|
||||
|
||||
## Adding Custom Variables
|
||||
|
||||
Use the `Hook::TEMPLATE_VARS` hook to add custom variables:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
// Add a custom variable
|
||||
$vars['siteName'] = 'My Website';
|
||||
|
||||
// Add computed values
|
||||
$vars['currentYear'] = date('Y');
|
||||
|
||||
// Add from context
|
||||
$vars['userName'] = $ctx->get('user_name', 'Guest');
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
Then use in templates:
|
||||
|
||||
```php
|
||||
<p>© <?= $currentYear ?> <?= htmlspecialchars($siteName) ?></p>
|
||||
```
|
||||
|
||||
## Variable Availability by Template
|
||||
|
||||
| Variable | `base.php` | `page.php` | `list.php` |
|
||||
|----------|------------|------------|------------|
|
||||
| `$content` | ✓ | ✓ | — |
|
||||
| `$pageTitle` | ✓ | — | — |
|
||||
| `$metaDescription` | ✓ | — | — |
|
||||
| `$socialImageUrl` | ✓ | — | — |
|
||||
| `$navigation` | ✓ | — | — |
|
||||
| `$homeLabel` | ✓ | — | — |
|
||||
| `$currentLang` | ✓ | — | — |
|
||||
| `$langPrefix` | ✓ | — | — |
|
||||
| `$languageUrls` | ✓ | — | — |
|
||||
| `$translations` | ✓ | — | — |
|
||||
| `$pageCssUrl` | ✓ | — | — |
|
||||
| `$pageCssHash` | ✓ | — | — |
|
||||
| `$metadata` | — | ✓ | ✓ |
|
||||
| `$pageContent` | — | — | ✓ |
|
||||
| `$items` | — | — | ✓ |
|
||||
|
||||
**Note:** All variables are technically available everywhere via plugin hooks, but this table shows the default availability.
|
||||
|
||||
## Escaping Output
|
||||
|
||||
**Always escape user content** to prevent XSS attacks:
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
|
||||
<!-- Bad -->
|
||||
<h1><?= $metadata['title'] ?></h1>
|
||||
<p><?= $item['summary'] ?></p>
|
||||
```
|
||||
|
||||
**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown):
|
||||
|
||||
```php
|
||||
<!-- Good (content is already HTML) -->
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Checking Variable Existence
|
||||
|
||||
Always check if optional variables exist:
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<?php if (isset($metadata['author'])): ?>
|
||||
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Bad (may cause warnings) -->
|
||||
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||||
```
|
||||
|
||||
Use null coalescing for defaults:
|
||||
|
||||
```php
|
||||
<p><?= htmlspecialchars($metadata['author'] ?? 'Anonymous') ?></p>
|
||||
```
|
||||
|
||||
## Debugging Variables
|
||||
|
||||
To see all available variables in a template:
|
||||
|
||||
```php
|
||||
<pre><?php var_dump(get_defined_vars()); ?></pre>
|
||||
```
|
||||
|
||||
Or specific variables:
|
||||
|
||||
```php
|
||||
<pre><?php print_r($metadata); ?></pre>
|
||||
<pre><?php print_r($items); ?></pre>
|
||||
```
|
||||
|
||||
**Remember to remove debug code** before deploying to production.
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Internationalization](#)** — Use language-specific variables
|
||||
- **[Creating Plugins](#)** — Add custom template variables
|
||||
- **[Template Tutorial](#)** — See variables in action
|
||||
526
docs/03-reference/04-internationalization.md
Normal file
526
docs/03-reference/04-internationalization.md
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
# Internationalization (i18n)
|
||||
|
||||
FolderWeb supports multilingual websites through the built-in `languages` plugin. This guide covers everything you need to build sites in multiple languages.
|
||||
|
||||
## How It Works
|
||||
|
||||
The language plugin provides URL-based language selection:
|
||||
|
||||
- **Default language:** `/about/` (no language prefix)
|
||||
- **Other languages:** `/no/om/`, `/de/uber-uns/`
|
||||
|
||||
Language is determined from the URL, and content files, metadata, and translations adapt automatically.
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable and configure languages in `custom/config.ini`:
|
||||
|
||||
```ini
|
||||
[languages]
|
||||
default = "en" # Default language (no URL prefix)
|
||||
available = "en,no,de" # Comma-separated language codes
|
||||
|
||||
[plugins]
|
||||
enabled = "languages" # Enable the language plugin
|
||||
```
|
||||
|
||||
**Language codes:** Use ISO 639-1 two-letter codes (`en`, `no`, `de`, `fr`, `es`, etc.).
|
||||
|
||||
## Language-Specific Content Files
|
||||
|
||||
Create language variants of content files using the naming pattern `name.lang.ext`:
|
||||
|
||||
```
|
||||
content/about/
|
||||
├── index.md # Default language (English)
|
||||
├── index.no.md # Norwegian version
|
||||
└── index.de.md # German version
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- URL `/about/` → Shows `index.md`
|
||||
- URL `/no/om/` → Shows `index.no.md`
|
||||
- URL `/de/uber-uns/` → Shows `index.de.md`
|
||||
|
||||
**Fallback behavior:** If no language-specific file exists, the default file is shown.
|
||||
|
||||
### Multiple Files Per Page
|
||||
|
||||
Language variants work with multiple content files:
|
||||
|
||||
```
|
||||
content/portfolio/
|
||||
├── 00-hero.php
|
||||
├── 00-hero.no.php
|
||||
├── 01-intro.md
|
||||
├── 01-intro.no.md
|
||||
├── 02-projects.html
|
||||
└── 02-projects.no.html
|
||||
```
|
||||
|
||||
- URL `/portfolio/` → Shows `00-hero.php` + `01-intro.md` + `02-projects.html`
|
||||
- URL `/no/portfolio/` → Shows `00-hero.no.php` + `01-intro.no.md` + `02-projects.no.html`
|
||||
|
||||
## Language-Specific Metadata
|
||||
|
||||
Override metadata fields for each language using sections in `metadata.ini`:
|
||||
|
||||
```ini
|
||||
# Default (English)
|
||||
title = "About Us"
|
||||
summary = "Learn about our company"
|
||||
slug = "about"
|
||||
|
||||
# Norwegian
|
||||
[no]
|
||||
title = "Om oss"
|
||||
summary = "Les om bedriften vår"
|
||||
slug = "om"
|
||||
|
||||
# German
|
||||
[de]
|
||||
title = "Über uns"
|
||||
summary = "Erfahren Sie mehr über unser Unternehmen"
|
||||
slug = "uber-uns"
|
||||
```
|
||||
|
||||
**Supported fields:**
|
||||
- `title` — Page/item title
|
||||
- `summary` — Short description
|
||||
- `search_description` — SEO description
|
||||
- `slug` — Custom URL slug
|
||||
|
||||
**Result:**
|
||||
- `/about/` — Title: "About Us"
|
||||
- `/no/om/` — Title: "Om oss"
|
||||
- `/de/uber-uns/` — Title: "Über uns"
|
||||
|
||||
## Translation Files
|
||||
|
||||
UI strings (home link, footer text, month names) are translated using language files:
|
||||
|
||||
```
|
||||
custom/languages/
|
||||
├── en.ini
|
||||
├── no.ini
|
||||
└── de.ini
|
||||
```
|
||||
|
||||
### Creating Translation Files
|
||||
|
||||
**custom/languages/en.ini:**
|
||||
|
||||
```ini
|
||||
home = "Home"
|
||||
footer_handcoded = "Generated in"
|
||||
footer_page_time = "ms"
|
||||
months = "January,February,March,April,May,June,July,August,September,October,November,December"
|
||||
```
|
||||
|
||||
**custom/languages/no.ini:**
|
||||
|
||||
```ini
|
||||
home = "Hjem"
|
||||
footer_handcoded = "Generert på"
|
||||
footer_page_time = "ms"
|
||||
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"
|
||||
```
|
||||
|
||||
**custom/languages/de.ini:**
|
||||
|
||||
```ini
|
||||
home = "Startseite"
|
||||
footer_handcoded = "Generiert in"
|
||||
footer_page_time = "ms"
|
||||
months = "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember"
|
||||
```
|
||||
|
||||
### Using Translations in Templates
|
||||
|
||||
Access translations via the `$translations` variable:
|
||||
|
||||
```php
|
||||
<a href="/">
|
||||
<?= htmlspecialchars($translations['home'] ?? 'Home') ?>
|
||||
</a>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<?= htmlspecialchars($translations['footer_handcoded'] ?? 'Generated in') ?>
|
||||
<?= number_format($pageLoadTime, 4) ?>
|
||||
<?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
|
||||
</p>
|
||||
</footer>
|
||||
```
|
||||
|
||||
### Adding Custom Translation Strings
|
||||
|
||||
Add any strings you need:
|
||||
|
||||
**custom/languages/en.ini:**
|
||||
|
||||
```ini
|
||||
read_more = "Read more"
|
||||
posted_on = "Posted on"
|
||||
by_author = "by"
|
||||
categories = "Categories"
|
||||
tags = "Tags"
|
||||
```
|
||||
|
||||
**custom/languages/no.ini:**
|
||||
|
||||
```ini
|
||||
read_more = "Les mer"
|
||||
posted_on = "Publisert"
|
||||
by_author = "av"
|
||||
categories = "Kategorier"
|
||||
tags = "Tagger"
|
||||
```
|
||||
|
||||
Use in templates:
|
||||
|
||||
```php
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?> →
|
||||
</a>
|
||||
|
||||
<p>
|
||||
<?= htmlspecialchars($translations['posted_on'] ?? 'Posted on') ?>
|
||||
<?= $item['formatted_date'] ?>
|
||||
</p>
|
||||
```
|
||||
|
||||
## Language Switcher
|
||||
|
||||
The language plugin automatically provides language switcher URLs in the `$languageUrls` variable.
|
||||
|
||||
**In base.php:**
|
||||
|
||||
```php
|
||||
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
|
||||
<nav class="language-switcher" aria-label="Language">
|
||||
<?php foreach ($languageUrls as $lang => $url): ?>
|
||||
<a href="<?= htmlspecialchars($url) ?>"
|
||||
<?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>>
|
||||
<?= htmlspecialchars(strtoupper($lang)) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- The switcher links to the **same page** in different languages
|
||||
- Language-specific slugs are automatically resolved
|
||||
- Current language is marked with `aria-current="true"`
|
||||
|
||||
**Example URLs:**
|
||||
- On `/about/`: EN → `/about/`, NO → `/no/om/`, DE → `/de/uber-uns/`
|
||||
- On `/no/om/`: EN → `/about/`, NO → `/no/om/`, DE → `/de/uber-uns/`
|
||||
|
||||
## Date Formatting
|
||||
|
||||
Dates are automatically formatted using translated month names.
|
||||
|
||||
**With `months` in language files:**
|
||||
|
||||
```ini
|
||||
# en.ini
|
||||
months = "January,February,March,April,May,June,July,August,September,October,November,December"
|
||||
|
||||
# no.ini
|
||||
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- English: "15. December 2024"
|
||||
- Norwegian: "15. desember 2024"
|
||||
|
||||
**Date format:** `[day]. [month] [year]` (e.g., "15. December 2024")
|
||||
|
||||
## Complete Multilingual Example
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
content/
|
||||
├── metadata.ini
|
||||
├── index.md
|
||||
├── index.no.md
|
||||
└── blog/
|
||||
├── metadata.ini
|
||||
├── 2024-12-15-first-post/
|
||||
│ ├── metadata.ini
|
||||
│ ├── index.md
|
||||
│ ├── index.no.md
|
||||
│ └── cover.jpg
|
||||
└── 2024-12-20-second-post/
|
||||
├── metadata.ini
|
||||
├── index.md
|
||||
└── index.no.md
|
||||
```
|
||||
|
||||
### Root Metadata
|
||||
|
||||
**content/metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "My Site"
|
||||
|
||||
[no]
|
||||
title = "Min Side"
|
||||
```
|
||||
|
||||
### Blog Metadata
|
||||
|
||||
**content/blog/metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "Blog"
|
||||
summary = "Latest articles and updates"
|
||||
|
||||
[no]
|
||||
title = "Blogg"
|
||||
summary = "Siste artikler og oppdateringer"
|
||||
```
|
||||
|
||||
### Post Metadata
|
||||
|
||||
**content/blog/2024-12-15-first-post/metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "My First Post"
|
||||
summary = "An introduction to my blog"
|
||||
slug = "first-post"
|
||||
|
||||
[no]
|
||||
title = "Mitt første innlegg"
|
||||
summary = "En introduksjon til bloggen min"
|
||||
slug = "forste-innlegg"
|
||||
```
|
||||
|
||||
### URLs Generated
|
||||
|
||||
**English (default):**
|
||||
- Home: `/`
|
||||
- Blog: `/blog/`
|
||||
- Post: `/blog/first-post/`
|
||||
|
||||
**Norwegian:**
|
||||
- Home: `/no/`
|
||||
- Blog: `/no/blogg/`
|
||||
- Post: `/no/blogg/forste-innlegg/`
|
||||
|
||||
## Language-Aware Navigation
|
||||
|
||||
Navigation menus automatically use language-specific titles:
|
||||
|
||||
**content/about/metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "About"
|
||||
menu = 1
|
||||
menu_order = 10
|
||||
|
||||
[no]
|
||||
title = "Om"
|
||||
```
|
||||
|
||||
**Result in navigation:**
|
||||
- English site: "About"
|
||||
- Norwegian site: "Om"
|
||||
|
||||
## Template Variables for i18n
|
||||
|
||||
The language plugin provides these template variables:
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `$currentLang` | String | Current language code (e.g., `"en"`, `"no"`) |
|
||||
| `$defaultLang` | String | Default language from config |
|
||||
| `$langPrefix` | String | URL prefix (e.g., `""`, `"/no"`) |
|
||||
| `$languageUrls` | Array | URLs to switch languages |
|
||||
| `$translations` | Array | Translated UI strings |
|
||||
| `$availableLangs` | Array | All available language codes |
|
||||
|
||||
**Example usage:**
|
||||
|
||||
```php
|
||||
<html lang="<?= htmlspecialchars($currentLang) ?>">
|
||||
|
||||
<a href="<?= htmlspecialchars($langPrefix) ?>/">
|
||||
<?= htmlspecialchars($translations['home'] ?? 'Home') ?>
|
||||
</a>
|
||||
|
||||
<nav>
|
||||
<?php foreach ($navigation as $item): ?>
|
||||
<a href="<?= htmlspecialchars($langPrefix . $item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
```
|
||||
|
||||
## Right-to-Left (RTL) Languages
|
||||
|
||||
For RTL languages (Arabic, Hebrew, etc.), set the `dir` attribute:
|
||||
|
||||
**custom/templates/base.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
$rtlLangs = ['ar', 'he', 'fa', 'ur'];
|
||||
$dir = in_array($currentLang, $rtlLangs) ? 'rtl' : 'ltr';
|
||||
?>
|
||||
<html lang="<?= htmlspecialchars($currentLang) ?>" dir="<?= $dir ?>">
|
||||
```
|
||||
|
||||
Use logical CSS properties for proper RTL support:
|
||||
|
||||
```css
|
||||
/* Good: logical properties */
|
||||
.card {
|
||||
margin-inline-start: 1rem;
|
||||
padding-inline-end: 2rem;
|
||||
}
|
||||
|
||||
/* Bad: directional properties */
|
||||
.card {
|
||||
margin-left: 1rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Provide Fallbacks
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?>
|
||||
|
||||
<!-- Bad -->
|
||||
<?= htmlspecialchars($translations['read_more']) ?>
|
||||
```
|
||||
|
||||
### 2. Use Language Codes Consistently
|
||||
|
||||
```ini
|
||||
# Good
|
||||
[languages]
|
||||
available = "en,no,de" # Lowercase, ISO 639-1
|
||||
|
||||
# Bad
|
||||
available = "EN,nb-NO,de-DE" # Mixed case, non-standard
|
||||
```
|
||||
|
||||
### 3. Translate Everything
|
||||
|
||||
Don't mix languages on the same page:
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<p><?= htmlspecialchars($translations['posted_on']) ?> <?= $item['formatted_date'] ?></p>
|
||||
|
||||
<!-- Bad -->
|
||||
<p>Posted on <?= $item['formatted_date'] ?></p> <!-- English hardcoded -->
|
||||
```
|
||||
|
||||
### 4. Test All Languages
|
||||
|
||||
Verify:
|
||||
- Content files load correctly
|
||||
- Metadata overrides work
|
||||
- Language switcher links are correct
|
||||
- Navigation uses translated titles
|
||||
- Dates format properly
|
||||
|
||||
### 5. Handle Missing Translations Gracefully
|
||||
|
||||
```php
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php else: ?>
|
||||
<p><?= htmlspecialchars($translations['no_summary'] ?? 'No description available') ?></p>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
### No Automatic Translation
|
||||
|
||||
FolderWeb doesn't translate content automatically. You must:
|
||||
- Create separate content files for each language
|
||||
- Manually translate all metadata
|
||||
- Provide all translation strings
|
||||
|
||||
### No Language Detection
|
||||
|
||||
FolderWeb doesn't detect browser language. Users must:
|
||||
- Click the language switcher
|
||||
- Visit a language-specific URL directly
|
||||
|
||||
You can add browser detection with a custom plugin if needed.
|
||||
|
||||
### Fixed URL Structure
|
||||
|
||||
All languages share the same folder structure. You cannot have:
|
||||
- Content in `/en/blog/` and `/no/nyheter/` (different folder names)
|
||||
|
||||
You must use:
|
||||
- Content in `/blog/` with language-specific slugs and content files
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Language Switcher Shows Wrong URLs
|
||||
|
||||
**Problem:** Language switcher links to incorrect pages.
|
||||
|
||||
**Solution:** Check that language-specific slugs are set in metadata:
|
||||
|
||||
```ini
|
||||
slug = "about"
|
||||
|
||||
[no]
|
||||
slug = "om" # Must be set
|
||||
```
|
||||
|
||||
### Content Not Changing Language
|
||||
|
||||
**Problem:** Same content appears in all languages.
|
||||
|
||||
**Solution:** Verify file naming:
|
||||
- ✓ `index.no.md` (correct)
|
||||
- ✗ `index-no.md` (wrong)
|
||||
- ✗ `index_no.md` (wrong)
|
||||
|
||||
### Dates Not Translating
|
||||
|
||||
**Problem:** Dates show in English for all languages.
|
||||
|
||||
**Solution:** Add `months` to language files:
|
||||
|
||||
```ini
|
||||
months = "January,February,March,April,May,June,July,August,September,October,November,December"
|
||||
```
|
||||
|
||||
### Navigation Shows English Titles
|
||||
|
||||
**Problem:** Menu items use English even in other languages.
|
||||
|
||||
**Solution:** Add language sections to metadata:
|
||||
|
||||
```ini
|
||||
title = "About"
|
||||
menu = 1
|
||||
|
||||
[no]
|
||||
title = "Om"
|
||||
```
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Configuration Reference](#)** — Configure available languages
|
||||
- **[Metadata Reference](#)** — Set language-specific metadata
|
||||
- **[Template Variables](#)** — Use i18n variables in templates
|
||||
- **[Creating Plugins](#)** — Extend i18n functionality
|
||||
648
docs/04-development/01-plugin-system.md
Normal file
648
docs/04-development/01-plugin-system.md
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
# Plugin System
|
||||
|
||||
FolderWeb uses a minimal hook-based plugin system for extensibility. Plugins let you modify content, add functionality, and inject custom variables into templates—all without touching the framework code.
|
||||
|
||||
## How Plugins Work
|
||||
|
||||
Plugins are PHP files that register callbacks with one or more **hooks**:
|
||||
|
||||
1. **`Hook::CONTEXT_READY`** — After context is created, before routing
|
||||
2. **`Hook::PROCESS_CONTENT`** — When loading/processing content
|
||||
3. **`Hook::TEMPLATE_VARS`** — Before rendering templates
|
||||
|
||||
Each hook receives data, allows modification, and returns the modified data.
|
||||
|
||||
## Plugin Locations
|
||||
|
||||
```
|
||||
app/plugins/
|
||||
├── global/ # Built-in global plugins (don't modify)
|
||||
│ └── languages.php
|
||||
└── page/ # Built-in page plugins (empty by default)
|
||||
|
||||
custom/plugins/
|
||||
├── global/ # Your global plugins
|
||||
│ ├── analytics.php
|
||||
│ └── reading-time.php
|
||||
└── page/ # Your page plugins (not yet used)
|
||||
```
|
||||
|
||||
**Global plugins:** Loaded on every request
|
||||
**Page plugins:** Reserved for future use
|
||||
|
||||
## Enabling Plugins
|
||||
|
||||
List enabled plugins in `custom/config.ini`:
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
enabled = "languages,analytics,reading-time"
|
||||
```
|
||||
|
||||
Plugin names correspond to filenames without `.php`:
|
||||
- `languages` → `languages.php`
|
||||
- `analytics` → `analytics.php`
|
||||
- `reading-time` → `reading-time.php`
|
||||
|
||||
FolderWeb loads plugins from:
|
||||
1. `app/plugins/global/` (built-in)
|
||||
2. `custom/plugins/global/` (yours)
|
||||
|
||||
## The Three Hooks
|
||||
|
||||
### `Hook::CONTEXT_READY`
|
||||
|
||||
Called after the context object is created, before routing begins.
|
||||
|
||||
**Use for:**
|
||||
- Setting global context values
|
||||
- Processing configuration
|
||||
- Adding cross-cutting concerns
|
||||
|
||||
**Signature:**
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) {
|
||||
// Modify context
|
||||
$ctx->set('key', 'value');
|
||||
|
||||
// Must return context
|
||||
return $ctx;
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `$ctx` — Context object (see [Context API](#context-api))
|
||||
- `$config` — Merged configuration array from `config.ini`
|
||||
|
||||
**Must return:** Modified `$ctx`
|
||||
|
||||
### `Hook::PROCESS_CONTENT`
|
||||
|
||||
Called when loading or processing content (files, metadata, dates).
|
||||
|
||||
**Use for:**
|
||||
- Filtering content files
|
||||
- Transforming metadata
|
||||
- Custom content processing
|
||||
|
||||
**Signature:**
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::PROCESS_CONTENT, function(mixed $data, string $dirOrType, string $extraContext = '') {
|
||||
// Process data based on type
|
||||
if ($extraContext === 'metadata') {
|
||||
// Modify metadata array
|
||||
$data['custom_field'] = 'value';
|
||||
}
|
||||
|
||||
// Must return data
|
||||
return $data;
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `$data` — The data being processed (type varies)
|
||||
- `$dirOrType` — Directory path or processing type
|
||||
- `$extraContext` — Additional context (e.g., `"metadata"`, `"date_format"`)
|
||||
|
||||
**Must return:** Modified `$data`
|
||||
|
||||
**Common `$extraContext` values:**
|
||||
- `"metadata"` — Processing metadata array
|
||||
- `"date_format"` — Formatting a date string
|
||||
|
||||
### `Hook::TEMPLATE_VARS`
|
||||
|
||||
Called before rendering templates, allowing you to add variables.
|
||||
|
||||
**Use for:**
|
||||
- Adding custom template variables
|
||||
- Computing values for display
|
||||
- Injecting data into templates
|
||||
|
||||
**Signature:**
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
// Add custom variables
|
||||
$vars['siteName'] = 'My Website';
|
||||
$vars['currentYear'] = date('Y');
|
||||
|
||||
// Must return vars
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `$vars` — Array of template variables
|
||||
- `$ctx` — Context object
|
||||
|
||||
**Must return:** Modified `$vars` array
|
||||
|
||||
## Context API
|
||||
|
||||
The `Context` object stores global state. Access it in hooks:
|
||||
|
||||
```php
|
||||
// Set a value
|
||||
$ctx->set('key', 'value');
|
||||
|
||||
// Get a value
|
||||
$value = $ctx->get('key');
|
||||
|
||||
// Get with default
|
||||
$value = $ctx->get('key', 'default');
|
||||
|
||||
// Check if exists
|
||||
if ($ctx->has('key')) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Built-in context values:**
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `requestPath` | String | URL path (e.g., `"blog/my-post"`) |
|
||||
| `contentDir` | String | Filesystem path to content |
|
||||
| `currentLang` | String | Current language (from languages plugin) |
|
||||
| `defaultLang` | String | Default language |
|
||||
| `translations` | Array | Translated strings |
|
||||
| `metadata` | Array | Current page metadata |
|
||||
|
||||
## Creating Your First Plugin
|
||||
|
||||
Let's create a plugin that adds a reading time estimate to posts.
|
||||
|
||||
### Step 1: Create the Plugin File
|
||||
|
||||
**custom/plugins/global/reading-time.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Add reading time to template variables
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
// Only calculate if we have content
|
||||
if (isset($vars['content'])) {
|
||||
$wordCount = str_word_count(strip_tags($vars['content']));
|
||||
$wordsPerMinute = 200;
|
||||
$readingTime = max(1, round($wordCount / $wordsPerMinute));
|
||||
|
||||
$vars['readingTime'] = $readingTime;
|
||||
$vars['wordCount'] = $wordCount;
|
||||
}
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
### Step 2: Enable the Plugin
|
||||
|
||||
**custom/config.ini:**
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
enabled = "languages,reading-time"
|
||||
```
|
||||
|
||||
### Step 3: Use in Template
|
||||
|
||||
**custom/templates/page.php:**
|
||||
|
||||
```php
|
||||
<article>
|
||||
<header>
|
||||
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||||
|
||||
<?php if (isset($readingTime)): ?>
|
||||
<p class="reading-time">
|
||||
<?= $readingTime ?> min read (<?= number_format($wordCount) ?> words)
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
Done! Every page now shows reading time.
|
||||
|
||||
## Plugin Examples
|
||||
|
||||
### Analytics Plugin
|
||||
|
||||
Add Google Analytics tracking ID to all pages.
|
||||
|
||||
**custom/plugins/global/analytics.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
global $config;
|
||||
|
||||
// Read tracking ID from config
|
||||
$trackingId = $config['analytics']['tracking_id'] ?? null;
|
||||
|
||||
if ($trackingId) {
|
||||
$vars['analyticsId'] = $trackingId;
|
||||
}
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
**custom/config.ini:**
|
||||
|
||||
```ini
|
||||
[analytics]
|
||||
tracking_id = "G-XXXXXXXXXX"
|
||||
|
||||
[plugins]
|
||||
enabled = "languages,analytics"
|
||||
```
|
||||
|
||||
**custom/templates/base.php:**
|
||||
|
||||
```php
|
||||
<head>
|
||||
<!-- ... -->
|
||||
|
||||
<?php if (isset($analyticsId)): ?>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<?= htmlspecialchars($analyticsId) ?>"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<?= htmlspecialchars($analyticsId) ?>');
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
```
|
||||
|
||||
### Table of Contents Plugin
|
||||
|
||||
Generate a table of contents from headings.
|
||||
|
||||
**custom/plugins/global/toc.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
if (!isset($vars['content'])) {
|
||||
return $vars;
|
||||
}
|
||||
|
||||
$content = $vars['content'];
|
||||
$toc = [];
|
||||
|
||||
// Extract headings
|
||||
if (preg_match_all('/<h([2-3])>(.*?)<\/h\1>/i', $content, $matches)) {
|
||||
foreach ($matches[0] as $i => $match) {
|
||||
$level = (int)$matches[1][$i];
|
||||
$text = strip_tags($matches[2][$i]);
|
||||
$id = slugify($text);
|
||||
|
||||
// Add ID to heading
|
||||
$newHeading = str_replace('<h' . $level . '>', '<h' . $level . ' id="' . $id . '">', $match);
|
||||
$content = str_replace($match, $newHeading, $content);
|
||||
|
||||
$toc[] = [
|
||||
'level' => $level,
|
||||
'text' => $text,
|
||||
'id' => $id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$vars['content'] = $content;
|
||||
$vars['tableOfContents'] = $toc;
|
||||
|
||||
return $vars;
|
||||
});
|
||||
|
||||
function slugify(string $text): string {
|
||||
$text = strtolower($text);
|
||||
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
|
||||
return trim($text, '-');
|
||||
}
|
||||
```
|
||||
|
||||
**Use in template:**
|
||||
|
||||
```php
|
||||
<?php if (!empty($tableOfContents)): ?>
|
||||
<nav class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<?php foreach ($tableOfContents as $item): ?>
|
||||
<li class="toc-level-<?= $item['level'] ?>">
|
||||
<a href="#<?= htmlspecialchars($item['id']) ?>">
|
||||
<?= htmlspecialchars($item['text']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<article>
|
||||
<?= $content ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Author Bio Plugin
|
||||
|
||||
Add author information from metadata.
|
||||
|
||||
**custom/plugins/global/author-bio.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$metadata = $ctx->get('metadata', []);
|
||||
|
||||
// Load author data if specified
|
||||
if (isset($metadata['author'])) {
|
||||
$authorSlug = slugify($metadata['author']);
|
||||
$authorFile = dirname(__DIR__, 2) . "/content/authors/$authorSlug.ini";
|
||||
|
||||
if (file_exists($authorFile)) {
|
||||
$authorData = parse_ini_file($authorFile);
|
||||
$vars['authorBio'] = $authorData;
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
});
|
||||
|
||||
function slugify(string $text): string {
|
||||
return strtolower(preg_replace('/[^a-z0-9]+/', '-', $text));
|
||||
}
|
||||
```
|
||||
|
||||
**content/authors/jane-doe.ini:**
|
||||
|
||||
```ini
|
||||
name = "Jane Doe"
|
||||
bio = "Web developer and writer"
|
||||
email = "jane@example.com"
|
||||
twitter = "@janedoe"
|
||||
website = "https://janedoe.com"
|
||||
```
|
||||
|
||||
**Use in template:**
|
||||
|
||||
```php
|
||||
<?php if (isset($authorBio)): ?>
|
||||
<aside class="author-bio">
|
||||
<h3><?= htmlspecialchars($authorBio['name'] ?? 'Unknown') ?></h3>
|
||||
<p><?= htmlspecialchars($authorBio['bio'] ?? '') ?></p>
|
||||
|
||||
<?php if (isset($authorBio['website'])): ?>
|
||||
<a href="<?= htmlspecialchars($authorBio['website']) ?>">Website</a>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Related Posts Plugin
|
||||
|
||||
Show related posts based on tags.
|
||||
|
||||
**custom/plugins/global/related-posts.php:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$metadata = $ctx->get('metadata', []);
|
||||
|
||||
// Only for pages with tags
|
||||
if (!isset($metadata['tags'])) {
|
||||
return $vars;
|
||||
}
|
||||
|
||||
$currentPath = $ctx->get('currentPath', '');
|
||||
$currentTags = array_map('trim', explode(',', $metadata['tags']));
|
||||
|
||||
// Find other posts with similar tags
|
||||
$contentDir = $ctx->contentDir;
|
||||
$relatedPosts = findRelatedPosts($contentDir, $currentPath, $currentTags);
|
||||
|
||||
if (!empty($relatedPosts)) {
|
||||
$vars['relatedPosts'] = $relatedPosts;
|
||||
}
|
||||
|
||||
return $vars;
|
||||
});
|
||||
|
||||
function findRelatedPosts(string $contentDir, string $currentPath, array $currentTags): array {
|
||||
$posts = [];
|
||||
|
||||
// Recursively scan content directory
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getFilename() === 'metadata.ini') {
|
||||
$dir = dirname($file->getPathname());
|
||||
|
||||
// Skip current page
|
||||
if ($dir === $currentPath) continue;
|
||||
|
||||
$metadata = parse_ini_file($file->getPathname());
|
||||
|
||||
if (isset($metadata['tags'])) {
|
||||
$tags = array_map('trim', explode(',', $metadata['tags']));
|
||||
$commonTags = array_intersect($currentTags, $tags);
|
||||
|
||||
if (!empty($commonTags)) {
|
||||
$posts[] = [
|
||||
'title' => $metadata['title'] ?? basename($dir),
|
||||
'url' => str_replace($contentDir, '', $dir) . '/',
|
||||
'tags' => $tags,
|
||||
'relevance' => count($commonTags),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
usort($posts, fn($a, $b) => $b['relevance'] <=> $a['relevance']);
|
||||
|
||||
// Return top 3
|
||||
return array_slice($posts, 0, 3);
|
||||
}
|
||||
```
|
||||
|
||||
**Use in template:**
|
||||
|
||||
```php
|
||||
<?php if (!empty($relatedPosts)): ?>
|
||||
<aside class="related-posts">
|
||||
<h3>Related Posts</h3>
|
||||
<ul>
|
||||
<?php foreach ($relatedPosts as $post): ?>
|
||||
<li>
|
||||
<a href="<?= htmlspecialchars($post['url']) ?>">
|
||||
<?= htmlspecialchars($post['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Return Modified Data
|
||||
|
||||
```php
|
||||
// Good
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$vars['custom'] = 'value';
|
||||
return $vars; // Always return
|
||||
});
|
||||
|
||||
// Bad
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$vars['custom'] = 'value';
|
||||
// Missing return - breaks other plugins!
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Use Configuration for Settings
|
||||
|
||||
```php
|
||||
// Good: configurable
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
global $config;
|
||||
|
||||
$wordsPerMinute = $config['reading_time']['words_per_minute'] ?? 200;
|
||||
// Use $wordsPerMinute...
|
||||
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
**custom/config.ini:**
|
||||
|
||||
```ini
|
||||
[reading_time]
|
||||
words_per_minute = 250
|
||||
```
|
||||
|
||||
### 3. Check Variable Existence
|
||||
|
||||
```php
|
||||
// Good: defensive
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
if (isset($vars['content'])) {
|
||||
// Process content
|
||||
}
|
||||
return $vars;
|
||||
});
|
||||
|
||||
// Bad: assumes content exists
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$wordCount = str_word_count($vars['content']); // May error
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Namespace Helper Functions
|
||||
|
||||
```php
|
||||
// Good: prefixed function name
|
||||
function readingTime_calculate(string $content): int {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Bad: generic name (may conflict)
|
||||
function calculate(string $content): int {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use Type Hints
|
||||
|
||||
```php
|
||||
// Good: type hints for clarity
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx): array {
|
||||
$vars['custom'] = 'value';
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Plugins
|
||||
|
||||
### Check Plugin Loading
|
||||
|
||||
Add debug output to verify your plugin loads:
|
||||
|
||||
```php
|
||||
<?php
|
||||
error_log("My plugin loaded!");
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
error_log("TEMPLATE_VARS hook called");
|
||||
error_log("Variables: " . print_r(array_keys($vars), true));
|
||||
|
||||
$vars['debug'] = 'Plugin is working';
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
Check the error log:
|
||||
```bash
|
||||
tail -f /var/log/apache2/error.log
|
||||
```
|
||||
|
||||
### Inspect Hook Order
|
||||
|
||||
Hooks execute in the order they're registered. Check order by logging:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
error_log("Hook 1: " . json_encode(array_keys($vars)));
|
||||
return $vars;
|
||||
});
|
||||
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
error_log("Hook 2: " . json_encode(array_keys($vars)));
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
### Dump Variables in Templates
|
||||
|
||||
Temporarily add to your template:
|
||||
|
||||
```php
|
||||
<pre><?php var_dump($customVariable); ?></pre>
|
||||
```
|
||||
|
||||
**Remove before deploying to production.**
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No inter-plugin communication:** Plugins can't directly call each other
|
||||
- **Single execution order:** Hooks execute in registration order (no priority system)
|
||||
- **Global scope:** Be careful with global variables and function names
|
||||
- **No automatic loading:** Plugins must be listed in `config.ini`
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Hook Reference](#)** — Detailed documentation of all hooks
|
||||
- **[Example Plugins](#)** — More real-world plugin examples
|
||||
- **[Contributing](#)** — Share your plugins with the community
|
||||
719
docs/04-development/02-creating-templates.md
Normal file
719
docs/04-development/02-creating-templates.md
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
# Creating Custom Templates
|
||||
|
||||
Templates control the HTML structure and presentation of your content. This guide covers advanced template creation, from simple page layouts to complex list views.
|
||||
|
||||
## Template Hierarchy
|
||||
|
||||
FolderWeb uses a three-level template system:
|
||||
|
||||
1. **Base template** (`base.php`) — The HTML scaffold wrapping everything
|
||||
2. **Content template** — Either `page.php` or a list template
|
||||
3. **Partials** (optional) — Reusable components you create
|
||||
|
||||
```
|
||||
base.php
|
||||
└── page.php or list.php
|
||||
└── Rendered content
|
||||
```
|
||||
|
||||
## Template Resolution
|
||||
|
||||
When rendering a page, FolderWeb looks for templates in this order:
|
||||
|
||||
**For page views:**
|
||||
1. `custom/templates/page.php`
|
||||
2. `app/default/templates/page.php` (fallback)
|
||||
|
||||
**For list views:**
|
||||
1. `custom/templates/{page_template}.php` (e.g., `list-grid.php`)
|
||||
2. `custom/templates/list.php`
|
||||
3. `app/default/templates/{page_template}.php`
|
||||
4. `app/default/templates/list.php` (fallback)
|
||||
|
||||
**For base:**
|
||||
1. `custom/templates/base.php`
|
||||
2. `app/default/templates/base.php` (fallback)
|
||||
|
||||
## Creating a Custom Base Template
|
||||
|
||||
The base template defines the HTML structure for every page.
|
||||
|
||||
### Step 1: Copy the Default
|
||||
|
||||
```bash
|
||||
cp app/default/templates/base.php custom/templates/base.php
|
||||
```
|
||||
|
||||
### Step 2: Customize
|
||||
|
||||
**custom/templates/base.php:**
|
||||
|
||||
```php
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'My Site') ?></title>
|
||||
|
||||
<?php if (!empty($metaDescription)): ?>
|
||||
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="<?= htmlspecialchars($pageTitle ?? 'My Site') ?>">
|
||||
<?php if (!empty($metaDescription)): ?>
|
||||
<meta property="og:description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($socialImageUrl)): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($socialImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/custom/styles/base.css">
|
||||
<?php if (!empty($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main" class="skip-link">Skip to main content</a>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<a href="<?= htmlspecialchars($langPrefix ?? '') ?>/" class="site-title">
|
||||
My Website
|
||||
</a>
|
||||
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
<ul>
|
||||
<?php foreach ($navigation ?? [] as $item): ?>
|
||||
<li>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
|
||||
<nav class="language-switcher" aria-label="Language">
|
||||
<?php foreach ($languageUrls as $lang => $url): ?>
|
||||
<a href="<?= htmlspecialchars($url) ?>"
|
||||
<?= ($lang === ($currentLang ?? 'en')) ? 'aria-current="true"' : '' ?>>
|
||||
<?= htmlspecialchars(strtoupper($lang)) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="site-main">
|
||||
<div class="container">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<nav aria-label="Footer navigation">
|
||||
<a href="/privacy/">Privacy</a>
|
||||
<a href="/terms/">Terms</a>
|
||||
<a href="/contact/">Contact</a>
|
||||
</nav>
|
||||
|
||||
<p class="copyright">
|
||||
© <?= date('Y') ?> My Website
|
||||
</p>
|
||||
|
||||
<p class="performance">
|
||||
<?= htmlspecialchars($translations['footer_handcoded'] ?? 'Generated in') ?>
|
||||
<?= number_format((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000, 2) ?>
|
||||
<?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Skip link** for accessibility
|
||||
- **Container divs** for layout control
|
||||
- **Semantic HTML** (header, nav, main, footer)
|
||||
- **ARIA labels** for screen readers
|
||||
- **Open Graph tags** for social media
|
||||
- **Performance metrics** in footer
|
||||
|
||||
## Creating Custom Page Templates
|
||||
|
||||
Page templates wrap single-page content.
|
||||
|
||||
### Blog Post Template
|
||||
|
||||
**custom/templates/page.php:**
|
||||
|
||||
```php
|
||||
<article class="blog-post">
|
||||
<header class="post-header">
|
||||
<?php if (isset($metadata['title'])): ?>
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="post-meta">
|
||||
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||||
<time datetime="<?= $metadata['date'] ?>">
|
||||
<?= $metadata['formatted_date'] ?? $metadata['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($metadata['author'])): ?>
|
||||
<span class="author">
|
||||
by <?= htmlspecialchars($metadata['author']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($readingTime)): ?>
|
||||
<span class="reading-time">
|
||||
<?= $readingTime ?> min read
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (isset($metadata['tags'])): ?>
|
||||
<div class="post-tags">
|
||||
<?php foreach (explode(',', $metadata['tags']) as $tag): ?>
|
||||
<span class="tag"><?= htmlspecialchars(trim($tag)) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($relatedPosts)): ?>
|
||||
<aside class="related-posts">
|
||||
<h2>Related Posts</h2>
|
||||
<ul>
|
||||
<?php foreach ($relatedPosts as $post): ?>
|
||||
<li>
|
||||
<a href="<?= htmlspecialchars($post['url']) ?>">
|
||||
<?= htmlspecialchars($post['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Portfolio Item Template
|
||||
|
||||
**custom/templates/page-portfolio.php:**
|
||||
|
||||
```php
|
||||
<article class="portfolio-item">
|
||||
<?php if (isset($metadata['cover_image'])): ?>
|
||||
<div class="project-hero">
|
||||
<img src="<?= $metadata['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($metadata['title'] ?? '') ?>">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<header class="project-header">
|
||||
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||||
|
||||
<dl class="project-details">
|
||||
<?php if (isset($metadata['client'])): ?>
|
||||
<dt>Client</dt>
|
||||
<dd><?= htmlspecialchars($metadata['client']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($metadata['year'])): ?>
|
||||
<dt>Year</dt>
|
||||
<dd><?= htmlspecialchars($metadata['year']) ?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($metadata['role'])): ?>
|
||||
<dt>Role</dt>
|
||||
<dd><?= htmlspecialchars($metadata['role']) ?></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<div class="project-content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
|
||||
<?php if (isset($metadata['project_url'])): ?>
|
||||
<footer class="project-footer">
|
||||
<a href="<?= htmlspecialchars($metadata['project_url']) ?>"
|
||||
class="button" target="_blank" rel="noopener">
|
||||
View Live Project →
|
||||
</a>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
**To use:** Set in metadata:
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
page_template = "page-portfolio"
|
||||
```
|
||||
|
||||
Wait, that won't work for page templates—only list templates use `page_template`. For page templates, you'd need to select via a plugin or use different template files per directory. Let's stick with one `page.php` that adapts based on metadata.
|
||||
|
||||
## Creating Custom List Templates
|
||||
|
||||
List templates display collections of items.
|
||||
|
||||
### Card Grid Layout
|
||||
|
||||
**custom/templates/list-cards.php:**
|
||||
|
||||
```php
|
||||
<?php if ($pageContent): ?>
|
||||
<div class="list-intro">
|
||||
<?= $pageContent ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card-grid">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="card">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<a href="<?= $item['url'] ?>" class="card-image">
|
||||
<img src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>"
|
||||
loading="lazy">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time class="card-date" datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p class="card-summary">
|
||||
<?= htmlspecialchars($item['summary']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?= $item['url'] ?>" class="card-link">
|
||||
Read more →
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Corresponding CSS:**
|
||||
|
||||
```css
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
|
||||
& a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-date {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
margin: 0 0 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timeline Layout
|
||||
|
||||
**custom/templates/list-timeline.php:**
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="timeline">
|
||||
<?php
|
||||
$currentYear = null;
|
||||
foreach ($items as $item):
|
||||
// Extract year from date
|
||||
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
|
||||
|
||||
// Show year marker when year changes
|
||||
if ($year && $year !== $currentYear):
|
||||
$currentYear = $year;
|
||||
?>
|
||||
<div class="timeline-year">
|
||||
<h2><?= $year ?></h2>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<article class="timeline-item">
|
||||
<time class="timeline-date">
|
||||
<?= $item['formatted_date'] ?? ($item['date'] ?? '') ?>
|
||||
</time>
|
||||
|
||||
<div class="timeline-content">
|
||||
<h3>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS:**
|
||||
|
||||
```css
|
||||
.timeline {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
margin: 2rem 0;
|
||||
padding-left: 2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-year {
|
||||
margin: 2rem 0 1rem;
|
||||
|
||||
& h2 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2.5rem;
|
||||
top: 0.5rem;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
border: 2px solid var(--color-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
& h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Magazine Layout
|
||||
|
||||
**custom/templates/list-magazine.php:**
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<?php if (!empty($items)): ?>
|
||||
<div class="magazine-layout">
|
||||
<!-- Featured post (first item) -->
|
||||
<?php $featured = array_shift($items); ?>
|
||||
<article class="magazine-featured">
|
||||
<?php if (isset($featured['cover_image'])): ?>
|
||||
<a href="<?= $featured['url'] ?>" class="featured-image">
|
||||
<img src="<?= $featured['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($featured['title']) ?>">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="featured-content">
|
||||
<h2>
|
||||
<a href="<?= $featured['url'] ?>">
|
||||
<?= htmlspecialchars($featured['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($featured['summary'])): ?>
|
||||
<p class="featured-summary">
|
||||
<?= htmlspecialchars($featured['summary']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?= $featured['url'] ?>" class="read-more">
|
||||
Read article →
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Remaining posts in grid -->
|
||||
<?php if (!empty($items)): ?>
|
||||
<div class="magazine-grid">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="magazine-item">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<img src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>"
|
||||
loading="lazy">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
## Using Partials (Template Includes)
|
||||
|
||||
Break complex templates into reusable components.
|
||||
|
||||
### Creating a Partial
|
||||
|
||||
**custom/templates/partials/post-card.php:**
|
||||
|
||||
```php
|
||||
<article class="post-card">
|
||||
<?php if (isset($post['cover_image'])): ?>
|
||||
<a href="<?= $post['url'] ?>">
|
||||
<img src="<?= $post['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($post['title']) ?>">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3>
|
||||
<a href="<?= $post['url'] ?>">
|
||||
<?= htmlspecialchars($post['title']) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (isset($post['summary'])): ?>
|
||||
<p><?= htmlspecialchars($post['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Using a Partial
|
||||
|
||||
**custom/templates/list.php:**
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="post-list">
|
||||
<?php foreach ($items as $post): ?>
|
||||
<?php include __DIR__ . '/partials/post-card.php'; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Note:** Set `$post` before including, as the partial expects it.
|
||||
|
||||
## Conditional Templates
|
||||
|
||||
Use metadata to vary presentation.
|
||||
|
||||
```php
|
||||
<?php if (isset($metadata['layout']) && $metadata['layout'] === 'wide'): ?>
|
||||
<article class="wide-layout">
|
||||
<?= $content ?>
|
||||
</article>
|
||||
<?php else: ?>
|
||||
<article class="standard-layout">
|
||||
<div class="container">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Set in metadata:**
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
layout = "wide"
|
||||
```
|
||||
|
||||
## Template Best Practices
|
||||
|
||||
### 1. Always Escape Output
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<h1><?= htmlspecialchars($title) ?></h1>
|
||||
|
||||
<!-- Bad -->
|
||||
<h1><?= $title ?></h1>
|
||||
```
|
||||
|
||||
### 2. Check Before Using
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<?php if (isset($metadata['author'])): ?>
|
||||
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Bad -->
|
||||
<p>By <?= htmlspecialchars($metadata['author']) ?></p>
|
||||
```
|
||||
|
||||
### 3. Use Semantic HTML
|
||||
|
||||
```php
|
||||
<!-- Good -->
|
||||
<article>
|
||||
<header><h1>Title</h1></header>
|
||||
<div class="content">Content</div>
|
||||
<footer>Meta</footer>
|
||||
</article>
|
||||
|
||||
<!-- Bad -->
|
||||
<div class="post">
|
||||
<div class="title">Title</div>
|
||||
<div class="content">Content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 4. Add ARIA Labels
|
||||
|
||||
```php
|
||||
<nav aria-label="Main navigation">
|
||||
<!-- navigation items -->
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Language">
|
||||
<!-- language switcher -->
|
||||
</nav>
|
||||
```
|
||||
|
||||
### 5. Keep Logic Minimal
|
||||
|
||||
```php
|
||||
<!-- Good: simple check -->
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time><?= $item['formatted_date'] ?></time>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Bad: complex logic (move to plugin) -->
|
||||
<?php
|
||||
$recentPosts = array_filter($items, fn($item) =>
|
||||
strtotime($item['date']) > strtotime('-30 days')
|
||||
);
|
||||
usort($recentPosts, fn($a, $b) => strcmp($b['date'], $a['date']));
|
||||
?>
|
||||
```
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Template Variables Reference](#)** — See all available variables
|
||||
- **[Plugin System](#)** — Add custom variables to templates
|
||||
- **[Styling Guide](#)** — Style your custom templates
|
||||
240
docs/index.md
Normal file
240
docs/index.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# FolderWeb Documentation
|
||||
|
||||
Welcome to FolderWeb—a minimal PHP framework that turns your folder structure into a website. No build steps, no databases, no complexity. Just files, folders, and modern web standards.
|
||||
|
||||
## What is FolderWeb?
|
||||
|
||||
FolderWeb is a file-based content management system that:
|
||||
|
||||
- **Uses your folder structure as URL structure** — Drop files in folders, get pages
|
||||
- **Works without JavaScript** — Pure PHP and modern CSS
|
||||
- **Requires no build process** — Edit, save, refresh. Done.
|
||||
- **Supports multiple languages** — Built-in i18n with URL-based language switching
|
||||
- **Stays maintainable** — Code designed to last years or decades
|
||||
|
||||
Perfect for blogs, documentation sites, portfolios, and small business websites where simplicity and longevity matter.
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
### New to FolderWeb?
|
||||
|
||||
Start here to get up and running:
|
||||
|
||||
1. **[Getting Started](01-getting-started/index.md)** — Installation and quick start guide
|
||||
2. **[Adding Content](02-tutorial/01-adding-content.md)** — Learn how to create pages
|
||||
3. **[Styling Your Site](02-tutorial/02-styling.md)** — Customize the design
|
||||
4. **[Working with Templates](02-tutorial/03-templates.md)** — Control presentation
|
||||
|
||||
### Building a Site?
|
||||
|
||||
Practical guides for common tasks:
|
||||
|
||||
- **[Tutorial: Adding Content](02-tutorial/01-adding-content.md)** — Files, folders, metadata
|
||||
- **[Tutorial: Styling](02-tutorial/02-styling.md)** — Modern CSS techniques
|
||||
- **[Tutorial: Templates](02-tutorial/03-templates.md)** — Page and list layouts
|
||||
|
||||
### Need a Reference?
|
||||
|
||||
Look up specific features and options:
|
||||
|
||||
- **[Configuration](03-reference/01-configuration.md)** — Config file options
|
||||
- **[Metadata](03-reference/02-metadata.md)** — Page and directory metadata
|
||||
- **[Template Variables](03-reference/03-template-variables.md)** — Available template variables
|
||||
- **[Internationalization](03-reference/04-internationalization.md)** — Multilingual setup
|
||||
|
||||
### Extending FolderWeb?
|
||||
|
||||
Advanced guides for developers:
|
||||
|
||||
- **[Plugin System](04-development/01-plugin-system.md)** — Create plugins with hooks
|
||||
- **[Creating Templates](04-development/02-creating-templates.md)** — Advanced template creation
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### File-Based Routing
|
||||
|
||||
Your folder structure **is** your URL structure:
|
||||
|
||||
```
|
||||
content/
|
||||
├── about.md → /about/
|
||||
├── blog/
|
||||
│ ├── index.md → /blog/
|
||||
│ └── first-post/
|
||||
│ └── index.md → /blog/first-post/
|
||||
└── contact.html → /contact/
|
||||
```
|
||||
|
||||
No configuration, no route definitions—just create folders and files.
|
||||
|
||||
### Content Types
|
||||
|
||||
FolderWeb supports three content types:
|
||||
|
||||
- **Markdown (`.md`)** — Write in Markdown, get HTML
|
||||
- **HTML (`.html`)** — Static HTML content
|
||||
- **PHP (`.php`)** — Dynamic content with PHP
|
||||
|
||||
Mix and match as needed. All three can coexist in the same directory.
|
||||
|
||||
### Template System
|
||||
|
||||
Templates control how content is presented:
|
||||
|
||||
- **Base template** — HTML scaffold (header, nav, footer)
|
||||
- **Page template** — Wraps single-page content
|
||||
- **List template** — Displays collections (blogs, portfolios)
|
||||
|
||||
Override defaults by creating `custom/templates/`.
|
||||
|
||||
### Metadata Files
|
||||
|
||||
Configure pages with `metadata.ini` files:
|
||||
|
||||
```ini
|
||||
title = "My Page"
|
||||
summary = "Short description"
|
||||
date = "2024-12-15"
|
||||
slug = "custom-url"
|
||||
menu = 1
|
||||
```
|
||||
|
||||
Control titles, URLs, navigation, templates, and more.
|
||||
|
||||
### Plugin System
|
||||
|
||||
Extend functionality with a simple hook system:
|
||||
|
||||
```php
|
||||
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
|
||||
$vars['customVariable'] = 'value';
|
||||
return $vars;
|
||||
});
|
||||
```
|
||||
|
||||
Three hooks provide extensibility without complexity.
|
||||
|
||||
## Philosophy
|
||||
|
||||
FolderWeb follows these principles:
|
||||
|
||||
### Minimalism
|
||||
|
||||
Use only what's strictly necessary. No frameworks, no build tools, no package managers for frontend code. If it doesn't provide clear, lasting value, it doesn't belong.
|
||||
|
||||
### Longevity
|
||||
|
||||
Code should be readable and maintainable for years or decades. Avoid rapidly changing components and dependencies. Favor web standards over abstractions.
|
||||
|
||||
### Simplicity
|
||||
|
||||
HTML, PHP, and CSS. That's it. No preprocessing, no transpiling, no complex build pipelines. Edit a file, refresh the browser, see the result.
|
||||
|
||||
### Performance
|
||||
|
||||
Fast page loads, minimal HTTP requests, no JavaScript parsing delay. Performance metrics displayed in the footer—transparency and pride.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── 01-getting-started/
|
||||
│ └── index.md # Installation and quick start
|
||||
├── 02-tutorial/
|
||||
│ ├── 01-adding-content.md # How to create content
|
||||
│ ├── 02-styling.md # Customizing styles
|
||||
│ └── 03-templates.md # Working with templates
|
||||
├── 03-reference/
|
||||
│ ├── 01-configuration.md # Config options
|
||||
│ ├── 02-metadata.md # Metadata fields
|
||||
│ ├── 03-template-variables.md # Available variables
|
||||
│ └── 04-internationalization.md # Multilingual setup
|
||||
└── 04-development/
|
||||
├── 01-plugin-system.md # Creating plugins
|
||||
└── 02-creating-templates.md # Advanced templates
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### I want to...
|
||||
|
||||
**...add a new page**
|
||||
→ Create a `.md`, `.html`, or `.php` file in `content/`
|
||||
→ See [Adding Content](02-tutorial/01-adding-content.md)
|
||||
|
||||
**...change the design**
|
||||
→ Edit `custom/styles/base.css`
|
||||
→ See [Styling Your Site](02-tutorial/02-styling.md)
|
||||
|
||||
**...customize the layout**
|
||||
→ Copy and edit templates in `custom/templates/`
|
||||
→ See [Working with Templates](02-tutorial/03-templates.md)
|
||||
|
||||
**...add a blog**
|
||||
→ Create `content/blog/` with dated subfolders
|
||||
→ See [Adding Content](02-tutorial/01-adding-content.md)
|
||||
|
||||
**...translate my site**
|
||||
→ Enable the languages plugin and create language files
|
||||
→ See [Internationalization](03-reference/04-internationalization.md)
|
||||
|
||||
**...add custom functionality**
|
||||
→ Create a plugin in `custom/plugins/global/`
|
||||
→ See [Plugin System](04-development/01-plugin-system.md)
|
||||
|
||||
**...add pages to the menu**
|
||||
→ Set `menu = 1` in `metadata.ini`
|
||||
→ See [Metadata Reference](03-reference/02-metadata.md)
|
||||
|
||||
**...use a different list layout**
|
||||
→ Set `page_template = "list-grid"` in `metadata.ini`
|
||||
→ See [Template Variables](03-reference/03-template-variables.md)
|
||||
|
||||
## Examples
|
||||
|
||||
FolderWeb includes extensive examples in `app/default/content/examples/`:
|
||||
|
||||
- **Markdown demo** — Full Markdown feature showcase
|
||||
- **Cover images** — How cover images work
|
||||
- **Metadata examples** — All metadata options demonstrated
|
||||
- **Template demos** — Grid and compact list layouts
|
||||
- **Multilingual** — Language support examples
|
||||
- **Mixed formats** — Combining Markdown, HTML, and PHP
|
||||
|
||||
Browse the examples to see features in action.
|
||||
|
||||
## Getting Help
|
||||
|
||||
**Read the docs:**
|
||||
Start with [Getting Started](01-getting-started/index.md) and work through the tutorial.
|
||||
|
||||
**Check examples:**
|
||||
Look in `app/default/content/examples/` for working code.
|
||||
|
||||
**Review the code:**
|
||||
FolderWeb is intentionally simple. Read the source in `app/` to understand how it works.
|
||||
|
||||
**File an issue:**
|
||||
Found a bug or have a question? Open an issue on GitHub.
|
||||
|
||||
## Contributing
|
||||
|
||||
FolderWeb is open source and welcomes contributions:
|
||||
|
||||
- **Report bugs** — Open an issue with steps to reproduce
|
||||
- **Suggest features** — Propose improvements (but remember: minimalism is key)
|
||||
- **Share plugins** — Created a useful plugin? Share it with the community
|
||||
- **Improve docs** — Found something unclear? Submit a pull request
|
||||
|
||||
## What's Next?
|
||||
|
||||
Ready to build? Start with [Getting Started](01-getting-started/index.md) to install FolderWeb and create your first page.
|
||||
|
||||
Or jump straight to:
|
||||
- **[Tutorial: Adding Content](02-tutorial/01-adding-content.md)** — Learn the basics
|
||||
- **[Configuration Reference](03-reference/01-configuration.md)** — Dive into details
|
||||
- **[Plugin System](04-development/01-plugin-system.md)** — Extend functionality
|
||||
|
||||
---
|
||||
|
||||
**FolderWeb** — Just enough web, nothing more.
|
||||
Loading…
Add table
Reference in a new issue