Add getting started documentation
Add tutorial on adding content Add tutorial on styling Add tutorial on templates Add configuration reference Add metadata reference Add template variables reference Add internationalization reference Add plugin system documentation Add creating templates documentation Add index page
This commit is contained in:
parent
0e19040473
commit
76697e4656
11 changed files with 4724 additions and 0 deletions
308
docs/02-tutorial/01-adding-content.md
Normal file
308
docs/02-tutorial/01-adding-content.md
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
# Adding Content
|
||||
|
||||
FolderWeb turns your folder structure into a website. That's not marketing speak—it's literally how it works. Let's explore what that means in practice.
|
||||
|
||||
## The Basic Idea
|
||||
|
||||
```
|
||||
content/
|
||||
├── about.md → yoursite.com/about/
|
||||
├── blog/
|
||||
│ └── index.md → yoursite.com/blog/
|
||||
└── contact.html → yoursite.com/contact/
|
||||
```
|
||||
|
||||
Every file becomes a page. Every folder becomes a URL path. No configuration required.
|
||||
|
||||
## File Types
|
||||
|
||||
FolderWeb recognizes three content types:
|
||||
|
||||
### Markdown (`.md`)
|
||||
|
||||
The bread and butter of content creation. Write in Markdown, get HTML.
|
||||
|
||||
```markdown
|
||||
# My Page Title
|
||||
|
||||
This is a paragraph with **bold** and *italic* text.
|
||||
|
||||
- Lists work
|
||||
- And so do [links](https://example.com)
|
||||
|
||||
## Subheading
|
||||
|
||||
More content here.
|
||||
```
|
||||
|
||||
FolderWeb uses [Parsedown](https://parsedown.org/) to convert Markdown to HTML, with caching for performance.
|
||||
|
||||
### HTML (`.html`)
|
||||
|
||||
When you need more control or already have HTML:
|
||||
|
||||
```html
|
||||
<h1>My Page</h1>
|
||||
<p>This is plain HTML.</p>
|
||||
<div class="custom-widget">
|
||||
Whatever you want.
|
||||
</div>
|
||||
```
|
||||
|
||||
### PHP (`.php`)
|
||||
|
||||
For dynamic content:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$currentYear = date('Y');
|
||||
echo "<p>Copyright {$currentYear}</p>";
|
||||
?>
|
||||
|
||||
<h2>Latest Posts</h2>
|
||||
<?php
|
||||
$posts = ['Post 1', 'Post 2', 'Post 3'];
|
||||
foreach ($posts as $post) {
|
||||
echo "<li>{$post}</li>";
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
**Important:** PHP files are executed, so be careful with user input and security.
|
||||
|
||||
## Folder Structure as URL Structure
|
||||
|
||||
Your folder hierarchy determines your URLs:
|
||||
|
||||
```
|
||||
content/
|
||||
├── index.md → /
|
||||
├── about.md → /about/
|
||||
├── blog/
|
||||
│ ├── index.md → /blog/
|
||||
│ ├── 2024-12-15-first-post/
|
||||
│ │ └── index.md → /blog/first-post/
|
||||
│ └── 2024-12-20-second-post/
|
||||
│ └── index.md → /blog/second-post/
|
||||
└── projects/
|
||||
├── index.md → /projects/
|
||||
└── my-project/
|
||||
└── index.md → /projects/my-project/
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Folder names become URL slugs
|
||||
- Date prefixes (`YYYY-MM-DD-`) are stripped from URLs but preserved for sorting
|
||||
- Trailing slashes are enforced (FolderWeb redirects `/about` → `/about/`)
|
||||
|
||||
## Multiple Files in One Page
|
||||
|
||||
You can combine multiple content files in a single directory. They render in **alphabetical order**:
|
||||
|
||||
```
|
||||
content/portfolio/
|
||||
├── 00-hero.php # Renders first
|
||||
├── 01-intro.md # Renders second
|
||||
├── 02-gallery.html # Renders third
|
||||
└── 03-contact.md # Renders last
|
||||
```
|
||||
|
||||
All four files render as one page at `/portfolio/`.
|
||||
|
||||
**Use case:** Build pages from modular components—header, content, footer, etc.
|
||||
|
||||
## Dates in Folder Names
|
||||
|
||||
FolderWeb automatically extracts dates from folder names:
|
||||
|
||||
```
|
||||
content/blog/
|
||||
├── 2024-12-15-my-first-post/ # Date: December 15, 2024
|
||||
├── 2024-12-20-another-post/ # Date: December 20, 2024
|
||||
└── 2025-01-01-new-year-post/ # Date: January 1, 2025
|
||||
```
|
||||
|
||||
The date format is `YYYY-MM-DD-` and it's **stripped from the URL**:
|
||||
|
||||
- Folder: `2024-12-15-my-first-post`
|
||||
- URL: `/blog/my-first-post/`
|
||||
- Date: Extracted and available in templates
|
||||
|
||||
Dates are automatically formatted based on your language (e.g., "15. desember 2024" in Norwegian).
|
||||
|
||||
## Adding Assets (Images, Files)
|
||||
|
||||
Drop assets directly in your content folders:
|
||||
|
||||
```
|
||||
content/blog/my-post/
|
||||
├── index.md
|
||||
├── cover.jpg # Cover image (automatic)
|
||||
├── diagram.png # Referenced in content
|
||||
└── styles.css # Page-specific styles
|
||||
```
|
||||
|
||||
**Reference in Markdown:**
|
||||
|
||||
```markdown
|
||||

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

|
||||
```
|
||||
|
||||
**metadata.ini:**
|
||||
```ini
|
||||
title = "My First Post"
|
||||
summary = "An introduction to my blog"
|
||||
```
|
||||
|
||||
### Multi-Section Page
|
||||
|
||||
```
|
||||
content/services/
|
||||
├── 00-hero.php
|
||||
├── 01-intro.md
|
||||
├── 02-pricing.html
|
||||
└── metadata.ini
|
||||
```
|
||||
|
||||
All files render together as one page at `/services/`.
|
||||
|
||||
### Documentation Site
|
||||
|
||||
```
|
||||
content/docs/
|
||||
├── index.md
|
||||
├── getting-started/
|
||||
│ └── index.md
|
||||
├── tutorial/
|
||||
│ ├── index.md
|
||||
│ ├── basics.md
|
||||
│ └── advanced.md
|
||||
└── reference/
|
||||
└── index.md
|
||||
```
|
||||
|
||||
Creates a hierarchical documentation structure with automatic list views.
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you know how to add content, learn how to:
|
||||
- **[Customize styling](#)** — Make it look like your own
|
||||
- **[Create templates](#)** — Control how content is presented
|
||||
- **[Add multilingual support](#)** — Reach a global audience
|
||||
|
||||
Or jump to the [Reference](#) for detailed documentation on metadata, templates, and configuration.
|
||||
432
docs/02-tutorial/02-styling.md
Normal file
432
docs/02-tutorial/02-styling.md
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
# Styling Your Site
|
||||
|
||||
FolderWeb embraces modern CSS—no preprocessors, no build steps, just the good stuff browsers support today. Let's make your site look exactly how you want.
|
||||
|
||||
## Where Styles Live
|
||||
|
||||
FolderWeb has a simple style hierarchy:
|
||||
|
||||
```
|
||||
custom/
|
||||
├── styles/
|
||||
│ ├── base.css # Your main stylesheet
|
||||
│ └── custom-theme.css # Additional stylesheets (optional)
|
||||
└── content/
|
||||
└── my-page/
|
||||
└── styles.css # Page-specific styles (optional)
|
||||
```
|
||||
|
||||
**Loading order:**
|
||||
1. `custom/styles/base.css` — Your main styles (always loaded)
|
||||
2. Page-specific `styles.css` — If it exists in the content directory
|
||||
|
||||
## Editing the Main Stylesheet
|
||||
|
||||
Start by editing `custom/styles/base.css`. This is where your site-wide styles live.
|
||||
|
||||
**Default structure:**
|
||||
|
||||
```css
|
||||
/* CSS Variables (Design Tokens) */
|
||||
:root {
|
||||
--color-primary: oklch(60% 0.15 250);
|
||||
--color-text: oklch(20% 0 0);
|
||||
--color-bg: oklch(98% 0 0);
|
||||
--font-base: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, monospace;
|
||||
--spacing-unit: 1rem;
|
||||
}
|
||||
|
||||
/* Base Typography */
|
||||
body {
|
||||
font-family: var(--font-base);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* More styles... */
|
||||
```
|
||||
|
||||
Edit these values to match your brand. No build step, no compilation—just save and refresh.
|
||||
|
||||
## Modern CSS Features
|
||||
|
||||
FolderWeb encourages using modern CSS features. Here's what you should know:
|
||||
|
||||
### CSS Variables (Custom Properties)
|
||||
|
||||
Define once, use everywhere:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: oklch(65% 0.2 150);
|
||||
--radius-sm: 0.25rem;
|
||||
--shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--color-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
```
|
||||
|
||||
### OKLCH Colors
|
||||
|
||||
Use modern color spaces for better color manipulation:
|
||||
|
||||
```css
|
||||
/* Traditional RGB/HSL */
|
||||
--old: rgb(100, 150, 200);
|
||||
--old-hsl: hsl(210, 50%, 60%);
|
||||
|
||||
/* Modern OKLCH (better perceptual uniformity) */
|
||||
--new: oklch(65% 0.1 250);
|
||||
```
|
||||
|
||||
**Format:** `oklch(lightness chroma hue / alpha)`
|
||||
- **Lightness:** 0% (black) to 100% (white)
|
||||
- **Chroma:** 0 (gray) to ~0.4 (vivid)
|
||||
- **Hue:** 0-360 degrees (color wheel)
|
||||
|
||||
### CSS Nesting
|
||||
|
||||
Nest related styles without preprocessors:
|
||||
|
||||
```css
|
||||
.card {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
& h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fluid Typography with `clamp()`
|
||||
|
||||
Responsive sizing without media queries:
|
||||
|
||||
```css
|
||||
h1 {
|
||||
/* Min 2rem, ideal 5vw, max 4rem */
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
p {
|
||||
/* Min 1rem, ideal 1.125rem, max 1.25rem */
|
||||
font-size: clamp(1rem, 1.125rem, 1.25rem);
|
||||
}
|
||||
```
|
||||
|
||||
### Logical Properties
|
||||
|
||||
Use logical properties for better internationalization:
|
||||
|
||||
```css
|
||||
/* Old way (assumes left-to-right) */
|
||||
.old {
|
||||
margin-left: 1rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
/* New way (respects text direction) */
|
||||
.new {
|
||||
margin-inline-start: 1rem;
|
||||
padding-inline-end: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Common logical properties:**
|
||||
- `margin-inline-start` / `margin-inline-end` (left/right in LTR)
|
||||
- `margin-block-start` / `margin-block-end` (top/bottom)
|
||||
- `padding-inline` / `padding-block`
|
||||
- `inline-size` (width)
|
||||
- `block-size` (height)
|
||||
|
||||
### Grid Layout
|
||||
|
||||
Use CSS Grid for layout (not flexbox for everything):
|
||||
|
||||
```css
|
||||
.page-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(65ch, 100%) 1fr;
|
||||
gap: 2rem;
|
||||
|
||||
& > * {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
& > .full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Classless CSS Philosophy
|
||||
|
||||
FolderWeb defaults to **classless CSS**—styling HTML elements directly instead of adding classes everywhere.
|
||||
|
||||
**Good (classless):**
|
||||
```html
|
||||
<article>
|
||||
<h1>Page Title</h1>
|
||||
<p>Content here.</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
```css
|
||||
article {
|
||||
max-width: 65ch;
|
||||
margin: 0 auto;
|
||||
|
||||
& h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Less good (class-heavy):**
|
||||
```html
|
||||
<article class="article-container">
|
||||
<h1 class="article-title">Page Title</h1>
|
||||
<p class="article-text">Content here.</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
**When to use classes:**
|
||||
- Component variants (`.button-primary`, `.button-secondary`)
|
||||
- JavaScript hooks (`.js-toggle`)
|
||||
- Utility overrides (`.visually-hidden`)
|
||||
|
||||
## Page-Specific Styles
|
||||
|
||||
Add `styles.css` to a content directory for page-specific styling:
|
||||
|
||||
```
|
||||
content/portfolio/
|
||||
├── index.md
|
||||
└── styles.css
|
||||
```
|
||||
|
||||
**styles.css:**
|
||||
```css
|
||||
/* Scoped to this page only */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
FolderWeb automatically loads and includes page-specific styles with cache-busting:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/portfolio/styles.css?v=abc123def">
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Add dark mode with CSS variables and `prefers-color-scheme`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-bg: oklch(98% 0 0);
|
||||
--color-text: oklch(20% 0 0);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: oklch(15% 0 0);
|
||||
--color-text: oklch(95% 0 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All colors using the variables automatically adapt.
|
||||
|
||||
## Responsive Design
|
||||
|
||||
Use fluid layouts and relative units:
|
||||
|
||||
```css
|
||||
/* Bad: fixed breakpoints */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Good: fluid and flexible */
|
||||
.container {
|
||||
width: min(90%, 1200px);
|
||||
padding: clamp(1rem, 3vw, 3rem);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: clamp(1rem, 3vw, 2rem);
|
||||
}
|
||||
```
|
||||
|
||||
**Use media queries sparingly:**
|
||||
- Layout changes (sidebar position)
|
||||
- Font size adjustments
|
||||
- Complex interactions
|
||||
|
||||
**Prefer fluid CSS:**
|
||||
- Spacing (`clamp()`)
|
||||
- Typography (`clamp()`)
|
||||
- Grids (`auto-fit`, `minmax()`)
|
||||
|
||||
## Cache Busting
|
||||
|
||||
FolderWeb automatically versions CSS files with MD5 hashes:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/custom/styles/base.css?v=a1b2c3d4">
|
||||
```
|
||||
|
||||
When you edit your CSS, the hash changes and browsers fetch the new version. No manual cache clearing needed.
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Simple Blog Theme
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-accent: oklch(55% 0.15 220);
|
||||
--color-text: oklch(25% 0 0);
|
||||
--color-bg: oklch(99% 0 0);
|
||||
--max-width: 65ch;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Georgia, serif;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
article {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
|
||||
& h1 {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Portfolio Grid
|
||||
|
||||
```css
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
||||
gap: 2rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.1);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Documentation Site
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
}
|
||||
|
||||
.docs-layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
gap: 3rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
|
||||
& ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& a {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
text-decoration: none;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start simple:** Edit colors and fonts first, then tackle layout
|
||||
- **Use the inspector:** Browser dev tools show computed values and help debug
|
||||
- **Test in different browsers:** Modern CSS has excellent support, but always verify
|
||||
- **Keep it readable:** Future you will thank present you for clear, organized styles
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you can style your site, learn how to:
|
||||
- **[Create custom templates](#)** — Control HTML structure and layout
|
||||
- **[Add multilingual support](#)** — Style for different languages and text directions
|
||||
|
||||
Or explore the [Reference](#) for detailed documentation on all available template variables and hooks.
|
||||
461
docs/02-tutorial/03-templates.md
Normal file
461
docs/02-tutorial/03-templates.md
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
# Working with Templates
|
||||
|
||||
Templates control how your content is presented. FolderWeb uses a simple PHP-based template system—no complex templating languages, just HTML with a sprinkle of PHP.
|
||||
|
||||
## Template Types
|
||||
|
||||
FolderWeb has three template levels:
|
||||
|
||||
### 1. Base Template (`base.php`)
|
||||
|
||||
The HTML scaffold wrapping every page:
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Page Title</title>
|
||||
<link rel="stylesheet" href="...">
|
||||
</head>
|
||||
<body>
|
||||
<header><!-- Navigation --></header>
|
||||
<main><!-- Page content here --></main>
|
||||
<footer><!-- Footer --></footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**You typically customize this once** to set up your site structure.
|
||||
|
||||
### 2. Page Template (`page.php`)
|
||||
|
||||
Wraps single-page content:
|
||||
|
||||
```php
|
||||
<article>
|
||||
<?= $content ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
**Customize this** to control how individual pages look.
|
||||
|
||||
### 3. List Template (`list.php`, `list-grid.php`, `list-compact.php`)
|
||||
|
||||
Displays multiple items from subdirectories:
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="item-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<h2><a href="<?= $item['url'] ?>"><?= $item['title'] ?></a></h2>
|
||||
<p><?= $item['summary'] ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Customize this** to control how lists of content (blogs, portfolios, etc.) appear.
|
||||
|
||||
## Template Location
|
||||
|
||||
Templates live in your `custom/` directory:
|
||||
|
||||
```
|
||||
custom/
|
||||
└── templates/
|
||||
├── base.php # HTML scaffold
|
||||
├── page.php # Single page wrapper
|
||||
├── list.php # Default list layout
|
||||
├── list-grid.php # Grid card layout
|
||||
└── list-compact.php # Compact list layout
|
||||
```
|
||||
|
||||
**FolderWeb falls back** to `app/default/templates/` if a custom template doesn't exist.
|
||||
|
||||
## Customizing the Base Template
|
||||
|
||||
Let's modify `base.php` to add your site name and custom navigation:
|
||||
|
||||
**custom/templates/base.php:**
|
||||
|
||||
```php
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= $currentLang ?? 'en' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
|
||||
<?php if (isset($metaDescription)): ?>
|
||||
<meta name="description" content="<?= htmlspecialchars($metaDescription) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($socialImageUrl)): ?>
|
||||
<meta property="og:image" content="<?= $socialImageUrl ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<link rel="stylesheet" href="/custom/styles/base.css?v=<?= $cssHash ?? '' ?>">
|
||||
|
||||
<?php if (isset($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= $pageCssUrl ?>?v=<?= $pageCssHash ?? '' ?>">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/" class="logo">My Site</a>
|
||||
<ul>
|
||||
<?php foreach ($navigation as $item): ?>
|
||||
<li>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© <?= date('Y') ?> My Site</p>
|
||||
<p>
|
||||
<small>Generated in <?= number_format($pageLoadTime, 4) ?>s</small>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Always escape user content: `htmlspecialchars($var)`
|
||||
- Use short echo tags: `<?= $var ?>`
|
||||
- Check if variables exist: `isset($var)`
|
||||
- The `$content` variable contains the rendered page/list content
|
||||
|
||||
## Customizing Page Templates
|
||||
|
||||
The page template wraps your single-page content. Let's add a reading time estimate:
|
||||
|
||||
**custom/templates/page.php:**
|
||||
|
||||
```php
|
||||
<article>
|
||||
<?php if (isset($metadata['title'])): ?>
|
||||
<header>
|
||||
<h1><?= htmlspecialchars($metadata['title']) ?></h1>
|
||||
|
||||
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||||
<time datetime="<?= $metadata['date'] ?>">
|
||||
<?= $metadata['formatted_date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// Estimate reading time (avg 200 words/min)
|
||||
$wordCount = str_word_count(strip_tags($content));
|
||||
$readingTime = max(1, round($wordCount / 200));
|
||||
?>
|
||||
<p class="reading-time"><?= $readingTime ?> min read</p>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Customizing List Templates
|
||||
|
||||
List templates display collections of content. Let's create a custom blog list:
|
||||
|
||||
**custom/templates/list.php:**
|
||||
|
||||
```php
|
||||
<?php if ($pageContent): ?>
|
||||
<div class="page-intro">
|
||||
<?= $pageContent ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="blog-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="blog-item">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<img
|
||||
src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>"
|
||||
loading="lazy"
|
||||
>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<header>
|
||||
<h2>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?= $item['url'] ?>">Read more →</a>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Choosing List Templates
|
||||
|
||||
You can create multiple list templates and select them per directory:
|
||||
|
||||
**Available by default:**
|
||||
- `list.php` — Simple vertical list
|
||||
- `list-grid.php` — Card grid layout
|
||||
- `list-compact.php` — Minimal compact list
|
||||
|
||||
**Select in metadata.ini:**
|
||||
|
||||
```ini
|
||||
title = "Projects"
|
||||
|
||||
[settings]
|
||||
page_template = "list-grid"
|
||||
```
|
||||
|
||||
Now the `projects/` directory uses the grid layout.
|
||||
|
||||
## Creating Custom List Templates
|
||||
|
||||
Let's create a timeline template for a blog:
|
||||
|
||||
**custom/templates/list-timeline.php:**
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="timeline">
|
||||
<?php
|
||||
$currentYear = null;
|
||||
foreach ($items as $item):
|
||||
$year = isset($item['date']) ? date('Y', strtotime($item['date'])) : null;
|
||||
|
||||
// Print year marker if it changed
|
||||
if ($year && $year !== $currentYear):
|
||||
$currentYear = $year;
|
||||
?>
|
||||
<div class="year-marker">
|
||||
<h3><?= $year ?></h3>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<article class="timeline-item">
|
||||
<time><?= $item['formatted_date'] ?? '' ?></time>
|
||||
<div class="timeline-content">
|
||||
<h4><a href="<?= $item['url'] ?>"><?= htmlspecialchars($item['title']) ?></a></h4>
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use in metadata.ini:**
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
page_template = "list-timeline"
|
||||
```
|
||||
|
||||
## Available Template Variables
|
||||
|
||||
Templates have access to these variables (see [Reference: Template Variables](#) for complete list):
|
||||
|
||||
**Base template:**
|
||||
```php
|
||||
$content // Rendered page/list HTML
|
||||
$pageTitle // Page title for <title> tag
|
||||
$metaDescription // SEO description
|
||||
$navigation // Array of menu items
|
||||
$homeLabel // "Home" link text (translated)
|
||||
$currentLang // Current language code
|
||||
$languageUrls // Links to other language versions
|
||||
$translations // Translated UI strings
|
||||
$cssHash // Cache-busting hash for CSS
|
||||
$pageCssUrl // Page-specific CSS URL (if exists)
|
||||
$pageLoadTime // Page generation time
|
||||
```
|
||||
|
||||
**Page template:**
|
||||
```php
|
||||
$content // Rendered HTML
|
||||
$metadata // Metadata array (title, date, etc.)
|
||||
```
|
||||
|
||||
**List template:**
|
||||
```php
|
||||
$items // Array of items to display
|
||||
$pageContent // Optional intro content from page
|
||||
$metadata // Directory metadata
|
||||
|
||||
// Each $item has:
|
||||
$item['url'] // Full URL to item
|
||||
$item['title'] // Item title
|
||||
$item['summary'] // Short description
|
||||
$item['date'] // ISO date (YYYY-MM-DD)
|
||||
$item['formatted_date'] // Localized date string
|
||||
$item['cover_image'] // Cover image URL (if exists)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Escape Output
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<h1><?= $title ?></h1>
|
||||
|
||||
<!-- Good -->
|
||||
<h1><?= htmlspecialchars($title) ?></h1>
|
||||
```
|
||||
|
||||
**Exception:** Already-sanitized HTML like `$content` (rendered from Markdown).
|
||||
|
||||
### 2. Check Variables Exist
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<p><?= $metadata['summary'] ?></p>
|
||||
|
||||
<!-- Good -->
|
||||
<?php if (isset($metadata['summary'])): ?>
|
||||
<p><?= htmlspecialchars($metadata['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### 3. Use Short Echo Tags
|
||||
|
||||
```php
|
||||
<!-- Verbose -->
|
||||
<?php echo htmlspecialchars($title); ?>
|
||||
|
||||
<!-- Concise -->
|
||||
<?= htmlspecialchars($title) ?>
|
||||
```
|
||||
|
||||
### 4. Keep Logic Minimal
|
||||
|
||||
Templates should display data, not process it. Complex logic belongs in plugins.
|
||||
|
||||
```php
|
||||
<!-- Bad: complex logic in template -->
|
||||
<?php
|
||||
$posts = array_filter($items, function($item) {
|
||||
return strtotime($item['date']) > strtotime('-30 days');
|
||||
});
|
||||
usort($posts, function($a, $b) {
|
||||
return strcmp($b['date'], $a['date']);
|
||||
});
|
||||
?>
|
||||
|
||||
<!-- Good: prepare data in a plugin, display in template -->
|
||||
<?php foreach ($recentPosts as $post): ?>
|
||||
...
|
||||
<?php endforeach; ?>
|
||||
```
|
||||
|
||||
### 5. Use Semantic HTML
|
||||
|
||||
```php
|
||||
<!-- Bad -->
|
||||
<div class="title">Title</div>
|
||||
<div class="content">Content</div>
|
||||
|
||||
<!-- Good -->
|
||||
<article>
|
||||
<h1>Title</h1>
|
||||
<div class="content">Content</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Simple Portfolio Page Template
|
||||
|
||||
```php
|
||||
<article class="portfolio-item">
|
||||
<header>
|
||||
<?php if (isset($metadata['cover_image'])): ?>
|
||||
<img src="<?= $metadata['cover_image'] ?>" alt="">
|
||||
<?php endif; ?>
|
||||
|
||||
<h1><?= htmlspecialchars($metadata['title'] ?? 'Untitled') ?></h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<?= $content ?>
|
||||
</div>
|
||||
|
||||
<?php if (isset($metadata['project_url'])): ?>
|
||||
<footer>
|
||||
<a href="<?= htmlspecialchars($metadata['project_url']) ?>"
|
||||
class="button">View Project →</a>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Card Grid List Template
|
||||
|
||||
```php
|
||||
<?= $pageContent ?>
|
||||
|
||||
<div class="card-grid">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article class="card">
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<img src="<?= $item['cover_image'] ?>" alt="" loading="lazy">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card-content">
|
||||
<h3>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
## What's Next?
|
||||
|
||||
You now know how to customize templates. Next, learn about:
|
||||
- **[Template Variables Reference](#)** — Complete list of available variables
|
||||
- **[Creating Plugins](#)** — Extend functionality and add custom data to templates
|
||||
- **[Internationalization](#)** — Build multilingual sites
|
||||
|
||||
Or explore the examples in `app/default/content/examples/templates-demo/` to see templates in action.
|
||||
Loading…
Add table
Add a link
Reference in a new issue