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:
Ruben 2025-11-27 23:01:02 +01:00
parent 0e19040473
commit 76697e4656
11 changed files with 4724 additions and 0 deletions

View 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.

View 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
![Alt text](diagram.png)
```
**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.
![Photo](cover.jpg)
```
**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.

View 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.

View 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>&copy; <?= 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.

View 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

View 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

View 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>&copy; <?= $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

View 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

View 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

View 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">
&copy; <?= 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
View 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.