diff --git a/docs/01-getting-started/index.md b/docs/01-getting-started/index.md new file mode 100644 index 0000000..cf604db --- /dev/null +++ b/docs/01-getting-started/index.md @@ -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 + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)$ /app/router.php [L,QSA] + +``` + +### 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. diff --git a/docs/02-tutorial/01-adding-content.md b/docs/02-tutorial/01-adding-content.md new file mode 100644 index 0000000..05a2fde --- /dev/null +++ b/docs/02-tutorial/01-adding-content.md @@ -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 +

My Page

+

This is plain HTML.

+
+ Whatever you want. +
+``` + +### PHP (`.php`) + +For dynamic content: + +```php +Copyright {$currentYear}

"; +?> + +

Latest Posts

+{$post}"; +} +?> +``` + +**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. diff --git a/docs/02-tutorial/02-styling.md b/docs/02-tutorial/02-styling.md new file mode 100644 index 0000000..0aa78ba --- /dev/null +++ b/docs/02-tutorial/02-styling.md @@ -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 +
+

Page Title

+

Content here.

+
+``` + +```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 +
+

Page Title

+

Content here.

+
+``` + +**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 + +``` + +## 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 + +``` + +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. diff --git a/docs/02-tutorial/03-templates.md b/docs/02-tutorial/03-templates.md new file mode 100644 index 0000000..5e14654 --- /dev/null +++ b/docs/02-tutorial/03-templates.md @@ -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: + +``` + + + + Page Title + + + +
+
+ + + +``` + +**You typically customize this once** to set up your site structure. + +### 2. Page Template (`page.php`) + +Wraps single-page content: + +```php +
+ +
+``` + +**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 + + +
+ +
+

+

+
+ +
+``` + +**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 + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + +``` + +**Key points:** +- Always escape user content: `htmlspecialchars($var)` +- Use short echo tags: `` +- 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 +
+ +
+

+ + + + + + +

min read

+
+ + +
+ +
+
+``` + +## Customizing List Templates + +List templates display collections of content. Let's create a custom blog list: + +**custom/templates/list.php:** + +```php + +
+ +
+ + +
+ +
+ + + <?= htmlspecialchars($item['title']) ?> + + + +
+

+ + + +

+ + + + +
+ + +

+ + + Read more → +
+ +
+``` + +## 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 + + +
+ +
+

+
+ + +
+ +
+

+ +

+ +
+
+ +
+``` + +**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 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. diff --git a/docs/03-reference/01-configuration.md b/docs/03-reference/01-configuration.md new file mode 100644 index 0000000..7c3a7fc --- /dev/null +++ b/docs/03-reference/01-configuration.md @@ -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 diff --git a/docs/03-reference/02-metadata.md b/docs/03-reference/02-metadata.md new file mode 100644 index 0000000..7f8d806 --- /dev/null +++ b/docs/03-reference/02-metadata.md @@ -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 diff --git a/docs/03-reference/03-template-variables.md b/docs/03-reference/03-template-variables.md new file mode 100644 index 0000000..cd715bc --- /dev/null +++ b/docs/03-reference/03-template-variables.md @@ -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') ?> +``` + +**Source:** +1. Language-specific metadata `[lang] title` +2. Root metadata `title` +3. First heading in content +4. Folder name + +### `$metaDescription` + +SEO description for the `` tag. + +**Type:** String +**Optional:** May be empty +**Example:** +```php + + + +``` + +**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 + + + +``` + +**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 + + + +``` + +**Source:** Pages with `menu = 1` in metadata, sorted by `menu_order` + +### `$homeLabel` + +Text for the home link. + +**Type:** String +**Default:** `"Home"` +**Example:** +```php + +``` + +**Source:** Translation string `translations['home']` or fallback "Home" + +### `$currentLang` + +Current language code. + +**Type:** String +**Default:** From `config.ini` `languages.default` +**Example:** +```php + +``` + +**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 +Home +``` + +**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 + 1): ?> + + +``` + +### `$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 +

+``` + +**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 + + + +``` + +### `$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 +
+ +
+``` + +### `$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 + +

+ + + + + +``` + +## 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 + +
+ +
+ +``` + +**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 + +
+ + <?= htmlspecialchars($item['title']) ?> + + +

+ + + +

+ + + + + + +

+ +
+ +``` + +### `$metadata` + +Metadata for the list directory itself. + +**Type:** Associative array +**Structure:** Same as page metadata +**Example:** +```php + +

+ +``` + +## 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 +

©

+``` + +## 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 + +

+

+ + +

+

+``` + +**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown): + +```php + +
+ +
+``` + +## Checking Variable Existence + +Always check if optional variables exist: + +```php + + +

By

+ + + +

By

+``` + +Use null coalescing for defaults: + +```php +

+``` + +## Debugging Variables + +To see all available variables in a template: + +```php +
+``` + +Or specific variables: + +```php +
+
+``` + +**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 diff --git a/docs/03-reference/04-internationalization.md b/docs/03-reference/04-internationalization.md new file mode 100644 index 0000000..7b7d830 --- /dev/null +++ b/docs/03-reference/04-internationalization.md @@ -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 + + + + + +``` + +### 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 + + → + + +

+ + +

+``` + +## Language Switcher + +The language plugin automatically provides language switcher URLs in the `$languageUrls` variable. + +**In base.php:** + +```php + 1): ?> + + +``` + +**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 + + + + + + + +``` + +## Right-to-Left (RTL) Languages + +For RTL languages (Arabic, Hebrew, etc.), set the `dir` attribute: + +**custom/templates/base.php:** + +```php + + +``` + +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 + + + + + +``` + +### 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 + +

+ + +

Posted on

+``` + +### 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 + +

+ +

+ +``` + +## 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 diff --git a/docs/04-development/01-plugin-system.md b/docs/04-development/01-plugin-system.md new file mode 100644 index 0000000..b5020c8 --- /dev/null +++ b/docs/04-development/01-plugin-system.md @@ -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 + +
+

+ + +

+ min read ( words) +

+ +
+ +
+ +
+ +``` + +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 + + + + + + + + +``` + +### Table of Contents Plugin + +Generate a table of contents from headings. + +**custom/plugins/global/toc.php:** + +```php +(.*?)<\/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('', '', $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 + + + + +
+ +
+``` + +### Author Bio Plugin + +Add author information from metadata. + +**custom/plugins/global/author-bio.php:** + +```php +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 + + + +``` + +### Related Posts Plugin + +Show related posts based on tags. + +**custom/plugins/global/related-posts.php:** + +```php +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 + + + +``` + +## 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 + +``` + +**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 diff --git a/docs/04-development/02-creating-templates.md b/docs/04-development/02-creating-templates.md new file mode 100644 index 0000000..abd49b6 --- /dev/null +++ b/docs/04-development/02-creating-templates.md @@ -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 + + + + + + <?= htmlspecialchars($pageTitle ?? 'My Site') ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + + +``` + +### 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 +
+
+ +

+ + + + + + + +
+ +
+ +
+ + + + +
+``` + +### Portfolio Item Template + +**custom/templates/page-portfolio.php:** + +```php + +``` + +**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 + +
+ +
+ + +
+ + + +
+``` + +**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 + + +
+ +
+

+
+ + +
+ + +
+

+ + + +

+ + +

+ +
+
+ +
+``` + +**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 + + + +
+ + + + + + +
+ + + +
+ +
+ +``` + +## Using Partials (Template Includes) + +Break complex templates into reusable components. + +### Creating a Partial + +**custom/templates/partials/post-card.php:** + +```php + +``` + +### Using a Partial + +**custom/templates/list.php:** + +```php + + +
+ + + +
+``` + +**Note:** Set `$post` before including, as the partial expects it. + +## Conditional Templates + +Use metadata to vary presentation. + +```php + +
+ +
+ +
+
+ +
+
+ +``` + +**Set in metadata:** + +```ini +[settings] +layout = "wide" +``` + +## Template Best Practices + +### 1. Always Escape Output + +```php + +

+ + +

+``` + +### 2. Check Before Using + +```php + + +

By

+ + + +

By

+``` + +### 3. Use Semantic HTML + +```php + +
+

Title

+
Content
+
Meta
+
+ + +
+
Title
+
Content
+
+``` + +### 4. Add ARIA Labels + +```php + + + +``` + +### 5. Keep Logic Minimal + +```php + + + + + + + + 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 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4b3ed27 --- /dev/null +++ b/docs/index.md @@ -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.