Add docs
This commit is contained in:
parent
b97b2f5503
commit
ad516600bb
14 changed files with 6093 additions and 0 deletions
739
docs/explanation/architecture.md
Normal file
739
docs/explanation/architecture.md
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
# Architecture
|
||||
|
||||
Understanding how FolderWeb works under the hood.
|
||||
|
||||
## High-Level Overview
|
||||
|
||||
FolderWeb follows a simple request-response flow:
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
↓
|
||||
router.php (entry point)
|
||||
↓
|
||||
Parse request path
|
||||
↓
|
||||
Find content files
|
||||
↓
|
||||
Determine content type (page/list/file)
|
||||
↓
|
||||
Render content
|
||||
↓
|
||||
Wrap in templates
|
||||
↓
|
||||
HTTP Response
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Router (`app/router.php`)
|
||||
|
||||
**Purpose**: Entry point for all requests, determines what to serve
|
||||
|
||||
**Responsibilities**:
|
||||
- Receive HTTP requests
|
||||
- Check for root-level assets (`/custom/assets/`)
|
||||
- Parse request path
|
||||
- Dispatch to appropriate renderer
|
||||
- Handle redirects (trailing slashes)
|
||||
- Serve 404 for missing content
|
||||
|
||||
**Key Flow**:
|
||||
```php
|
||||
// 1. Check for root-level assets
|
||||
if (file_exists("/custom/assets/$path")) {
|
||||
serve_static_file();
|
||||
}
|
||||
|
||||
// 2. Empty path = home page (render all root content files)
|
||||
if (empty($path)) {
|
||||
render_all_files_in_root();
|
||||
}
|
||||
|
||||
// 3. Parse request path
|
||||
$result = parseRequestPath($ctx);
|
||||
|
||||
// 4. Handle based on type
|
||||
match($result['type']) {
|
||||
'page' => renderMultipleFiles(...),
|
||||
'file' => renderFile(...),
|
||||
'directory' => renderListView(...),
|
||||
'not_found' => show_404()
|
||||
};
|
||||
```
|
||||
|
||||
**Location**: `/app/router.php` (lines 1-100+)
|
||||
|
||||
### 2. Content Discovery (`app/content.php`)
|
||||
|
||||
**Purpose**: Find and parse content files and directories
|
||||
|
||||
**Key Functions**:
|
||||
|
||||
#### `parseRequestPath($ctx)`
|
||||
Analyzes request path and determines content type.
|
||||
|
||||
**Returns**:
|
||||
```php
|
||||
[
|
||||
'type' => 'page' | 'file' | 'directory' | 'not_found',
|
||||
'path' => '/full/system/path',
|
||||
'files' => [...], // For page type
|
||||
// ... other data
|
||||
]
|
||||
```
|
||||
|
||||
**Logic**:
|
||||
1. Resolve translated slugs to real paths
|
||||
2. Check if path exists
|
||||
3. If directory:
|
||||
- Has subdirectories? → `type: 'directory'` (list view)
|
||||
- Has content files only? → `type: 'page'` (multi-file)
|
||||
4. If matches file? → `type: 'file'`
|
||||
5. Otherwise → `type: 'not_found'`
|
||||
|
||||
#### `findAllContentFiles($dir, $lang, $defaultLang, $availableLangs)`
|
||||
Scans directory for content files.
|
||||
|
||||
**Process**:
|
||||
1. Read directory contents
|
||||
2. Filter for valid extensions (`.md`, `.html`, `.php`)
|
||||
3. Parse filenames for language suffix
|
||||
4. Filter by current language:
|
||||
- Show `.{lang}.ext` files for that language
|
||||
- Show default files (no suffix) only if no language variant
|
||||
5. Sort alphanumerically
|
||||
6. Return array of file paths
|
||||
|
||||
**Example**:
|
||||
```php
|
||||
// Directory contains:
|
||||
// - index.md
|
||||
// - index.no.md
|
||||
// - about.md
|
||||
|
||||
// English request (lang=en, default=en):
|
||||
findAllContentFiles() → ['index.md', 'about.md']
|
||||
|
||||
// Norwegian request (lang=no):
|
||||
findAllContentFiles() → ['index.no.md', 'about.md']
|
||||
```
|
||||
|
||||
#### `loadMetadata($dirPath, $lang, $defaultLang)`
|
||||
Loads and merges metadata for a directory.
|
||||
|
||||
**Process**:
|
||||
1. Check for `metadata.ini` in directory
|
||||
2. Parse INI file with sections
|
||||
3. Start with base values
|
||||
4. Override with language-specific section if exists
|
||||
5. Return merged array
|
||||
|
||||
**Example**:
|
||||
```ini
|
||||
title = "About"
|
||||
summary = "Learn about us"
|
||||
|
||||
[no]
|
||||
title = "Om"
|
||||
summary = "Lær om oss"
|
||||
```
|
||||
|
||||
For Norwegian request:
|
||||
```php
|
||||
loadMetadata(..., 'no', 'en') → [
|
||||
'title' => 'Om', // Overridden
|
||||
'summary' => 'Lær om oss' // Overridden
|
||||
]
|
||||
```
|
||||
|
||||
#### `resolveTranslatedPath($ctx, $requestPath)`
|
||||
Maps translated slugs back to real directory names.
|
||||
|
||||
**Example**:
|
||||
```ini
|
||||
; In content/about/metadata.ini:
|
||||
[no]
|
||||
slug = "om-oss"
|
||||
```
|
||||
|
||||
Request to `/no/om-oss/` resolves to `content/about/`.
|
||||
|
||||
**Process**:
|
||||
1. Split path into segments
|
||||
2. For each segment:
|
||||
- Load metadata of parent directory
|
||||
- Check if any subdirectory has matching translated slug
|
||||
- Replace segment with real directory name
|
||||
3. Return resolved path
|
||||
|
||||
### 3. Rendering Engine (`app/rendering.php`)
|
||||
|
||||
**Purpose**: Convert content to HTML and wrap in templates
|
||||
|
||||
**Key Functions**:
|
||||
|
||||
#### `renderContentFile($filePath)`
|
||||
Converts a single content file to HTML.
|
||||
|
||||
**Process**:
|
||||
```php
|
||||
switch (extension) {
|
||||
case 'md':
|
||||
return Parsedown->text(file_contents);
|
||||
case 'html':
|
||||
return file_contents;
|
||||
case 'php':
|
||||
ob_start();
|
||||
include $filePath; // $ctx available
|
||||
return ob_get_clean();
|
||||
}
|
||||
```
|
||||
|
||||
#### `renderFile($ctx, $filePath)`
|
||||
Renders single file wrapped in templates.
|
||||
|
||||
**Process**:
|
||||
1. Convert file to HTML
|
||||
2. Load metadata
|
||||
3. Wrap in page template
|
||||
4. Wrap in base template
|
||||
5. Return HTML
|
||||
|
||||
#### `renderMultipleFiles($ctx, $filePaths, $pageDir)`
|
||||
Renders multiple files as single page.
|
||||
|
||||
**Process**:
|
||||
1. Convert each file to HTML
|
||||
2. Concatenate HTML (in order)
|
||||
3. Load metadata
|
||||
4. Wrap in page template
|
||||
5. Wrap in base template
|
||||
6. Return HTML
|
||||
|
||||
**Used for**: Multi-file pages (documentation, long articles)
|
||||
|
||||
#### `renderTemplate($ctx, $content, $statusCode = 200)`
|
||||
Wraps content in base template.
|
||||
|
||||
**Process**:
|
||||
1. Extract variables for template
|
||||
2. Set HTTP status code
|
||||
3. Include base template
|
||||
4. Return HTML
|
||||
|
||||
**Variables provided**:
|
||||
- `$content` - Rendered HTML
|
||||
- `$ctx` - Context object
|
||||
- `$currentLang`, `$navigation`, `$homeLabel`, etc.
|
||||
|
||||
### 4. Context Object (`app/context.php`)
|
||||
|
||||
**Purpose**: Immutable request context with computed properties
|
||||
|
||||
**Implementation**:
|
||||
```php
|
||||
readonly class Context {
|
||||
public function __construct(
|
||||
public private(set) string $contentDir,
|
||||
public private(set) string $currentLang,
|
||||
// ... other properties
|
||||
) {}
|
||||
|
||||
// Computed property (PHP 8.4 hook)
|
||||
public string $langPrefix {
|
||||
get => $this->currentLang !== $this->defaultLang
|
||||
? "/{$this->currentLang}"
|
||||
: '';
|
||||
}
|
||||
|
||||
// Lazy-loaded computed property
|
||||
public array $navigation {
|
||||
get => buildNavigation($this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **Immutability**: Cannot be changed after creation
|
||||
- **Type safety**: All properties typed
|
||||
- **Computed values**: Calculated on-demand
|
||||
- **No globals**: Passed explicitly
|
||||
|
||||
**Creation**:
|
||||
```php
|
||||
$ctx = createContext();
|
||||
```
|
||||
|
||||
This function:
|
||||
1. Loads configuration
|
||||
2. Extracts language from URL
|
||||
3. Determines content directory
|
||||
4. Resolves template paths
|
||||
5. Returns readonly Context object
|
||||
|
||||
### 5. Configuration (`app/config.php`)
|
||||
|
||||
**Purpose**: Load and merge configuration
|
||||
|
||||
**Process**:
|
||||
1. Parse `/app/config.ini` (defaults)
|
||||
2. Parse `/custom/config.ini` if exists
|
||||
3. Merge arrays (custom overrides defaults)
|
||||
4. Extract language settings
|
||||
5. Validate configuration
|
||||
|
||||
**Configuration Used**:
|
||||
```ini
|
||||
[languages]
|
||||
default = "en"
|
||||
available = "en,no,fr"
|
||||
```
|
||||
|
||||
### 6. Helper Functions (`app/helpers.php`)
|
||||
|
||||
**Purpose**: Utility functions used throughout
|
||||
|
||||
**Key Helpers**:
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `resolveTemplate($name, $type)` | Find custom or default template |
|
||||
| `getSubdirectories($dir)` | List subdirectories only |
|
||||
| `extractTitle($filePath, $lang, $defaultLang)` | Extract H1 from content |
|
||||
| `formatNorwegianDate($date)` | Format date as "2. november 2025" |
|
||||
| `extractDateFromFolder($name)` | Parse date from folder name |
|
||||
| `findCoverImage($dir)` | Locate cover image |
|
||||
| `findPdfFile($dir)` | Find first PDF |
|
||||
|
||||
### 7. Static File Server (`app/static.php`)
|
||||
|
||||
**Purpose**: Serve CSS, fonts, and other static assets
|
||||
|
||||
**Process**:
|
||||
1. Validate path (prevent directory traversal)
|
||||
2. Resolve real path
|
||||
3. Check file exists and is readable
|
||||
4. Determine MIME type
|
||||
5. Set headers
|
||||
6. Output file contents
|
||||
|
||||
**Routes**:
|
||||
- `/app/styles/base.css` → Custom or default CSS
|
||||
- `/app/default-styles/base.css` → Default CSS
|
||||
- `/custom/fonts/*` → Custom fonts
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Request Flow Diagram
|
||||
|
||||
```
|
||||
1. HTTP Request: /blog/2025-11-02-post/
|
||||
↓
|
||||
2. router.php receives request
|
||||
↓
|
||||
3. createContext()
|
||||
├─ Load config
|
||||
├─ Extract language from URL
|
||||
├─ Determine content directory
|
||||
└─ Return Context object
|
||||
↓
|
||||
4. parseRequestPath($ctx)
|
||||
├─ resolveTranslatedPath() - map slug to real path
|
||||
├─ Check path exists
|
||||
├─ findAllContentFiles() - scan for content
|
||||
└─ Return ['type' => 'file', 'path' => '...']
|
||||
↓
|
||||
5. renderFile($ctx, $filePath)
|
||||
├─ renderContentFile() - convert to HTML
|
||||
├─ loadMetadata() - get metadata
|
||||
├─ Apply page template
|
||||
└─ Apply base template
|
||||
↓
|
||||
6. HTTP Response: HTML
|
||||
```
|
||||
|
||||
### List View Flow
|
||||
|
||||
```
|
||||
1. Request: /blog/
|
||||
↓
|
||||
2. parseRequestPath() → type: 'directory'
|
||||
↓
|
||||
3. Load directory metadata
|
||||
├─ Get page_template setting
|
||||
└─ Get other directory metadata
|
||||
↓
|
||||
4. getSubdirectories() - find all subdirs
|
||||
↓
|
||||
5. For each subdirectory:
|
||||
├─ loadMetadata() - get title, date, summary
|
||||
├─ findCoverImage() - locate cover
|
||||
├─ findPdfFile() - locate PDF
|
||||
└─ Build item array
|
||||
↓
|
||||
6. Render list template with $items
|
||||
↓
|
||||
7. Wrap in base template
|
||||
↓
|
||||
8. Return HTML
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Separation of Concerns
|
||||
|
||||
```
|
||||
app/
|
||||
├── router.php # Entry point, request handling
|
||||
├── content.php # Content discovery, parsing
|
||||
├── rendering.php # HTML generation, templates
|
||||
├── context.php # Request context
|
||||
├── config.php # Configuration loading
|
||||
├── helpers.php # Utility functions
|
||||
├── constants.php # Constants (extensions)
|
||||
└── static.php # Static file serving
|
||||
```
|
||||
|
||||
Each file has a single responsibility.
|
||||
|
||||
### Template Resolution
|
||||
|
||||
Templates use fallback chain:
|
||||
|
||||
```
|
||||
1. /custom/templates/{name}.php
|
||||
↓ (if not found)
|
||||
2. /app/default/templates/{name}.php
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
```php
|
||||
function resolveTemplate($name, $type = 'templates') {
|
||||
$custom = __DIR__ . "/../custom/$type/$name";
|
||||
$default = __DIR__ . "/default/$type/$name";
|
||||
return file_exists($custom) ? $custom : $default;
|
||||
}
|
||||
```
|
||||
|
||||
This pattern applies to:
|
||||
- Templates
|
||||
- Styles
|
||||
- Languages
|
||||
- Any overridable resource
|
||||
|
||||
## Language Handling
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
/ → Default language
|
||||
/no/ → Norwegian
|
||||
/fr/page/ → French page
|
||||
```
|
||||
|
||||
### Language Extraction
|
||||
|
||||
From URL path:
|
||||
```php
|
||||
// Request: /no/blog/post/
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
$firstSegment = $segments[0] ?? '';
|
||||
|
||||
if (in_array($firstSegment, $availableLangs)) {
|
||||
$currentLang = $firstSegment;
|
||||
$pathWithoutLang = implode('/', array_slice($segments, 1));
|
||||
} else {
|
||||
$currentLang = $defaultLang;
|
||||
$pathWithoutLang = $path;
|
||||
}
|
||||
```
|
||||
|
||||
### Content Filtering
|
||||
|
||||
Files with language suffixes (`.{lang}.ext`) are filtered:
|
||||
|
||||
```php
|
||||
// Parse filename
|
||||
$parts = explode('.', $filename);
|
||||
$lastPart = $parts[count($parts) - 2] ?? null;
|
||||
|
||||
// Check if second-to-last part is a language
|
||||
if (in_array($lastPart, $availableLangs)) {
|
||||
$fileLang = $lastPart;
|
||||
} else {
|
||||
$fileLang = $defaultLang;
|
||||
}
|
||||
|
||||
// Include file if:
|
||||
// - It matches current language, OR
|
||||
// - It's default language AND no specific variant exists
|
||||
```
|
||||
|
||||
## Navigation Building
|
||||
|
||||
### Process
|
||||
|
||||
```php
|
||||
function buildNavigation($ctx) {
|
||||
$items = [];
|
||||
|
||||
// 1. Scan content root for directories
|
||||
$dirs = getSubdirectories($ctx->contentDir);
|
||||
|
||||
// 2. For each directory
|
||||
foreach ($dirs as $dir) {
|
||||
// Load metadata
|
||||
$metadata = loadMetadata($dir, $ctx->currentLang, $ctx->defaultLang);
|
||||
|
||||
// Skip if menu = false
|
||||
if (!($metadata['menu'] ?? false)) continue;
|
||||
|
||||
// Build item
|
||||
$items[] = [
|
||||
'title' => $metadata['title'] ?? basename($dir),
|
||||
'url' => $ctx->langPrefix . '/' . basename($dir) . '/',
|
||||
'order' => $metadata['menu_order'] ?? 999
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Sort by menu_order
|
||||
usort($items, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||
|
||||
return $items;
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
Navigation is a computed property, calculated once per request:
|
||||
|
||||
```php
|
||||
public array $navigation {
|
||||
get => buildNavigation($this);
|
||||
}
|
||||
```
|
||||
|
||||
PHP memoizes the result automatically.
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Time Complexity
|
||||
|
||||
| Operation | Complexity | Notes |
|
||||
|-----------|------------|-------|
|
||||
| Route resolution | O(1) | Direct file checks |
|
||||
| Content file scan | O(n) | n = files in directory |
|
||||
| Metadata loading | O(1) | Single file read |
|
||||
| Template rendering | O(m) | m = content size |
|
||||
| Navigation build | O(d) | d = top-level directories |
|
||||
|
||||
### Space Complexity
|
||||
|
||||
- **Memory**: O(c) where c = content size
|
||||
- **No caching**: Each request independent
|
||||
- **Stateless**: No session storage
|
||||
|
||||
### Optimization Points
|
||||
|
||||
1. **OPcache**: PHP bytecode caching (biggest impact)
|
||||
2. **Web server cache**: Serve cached HTML
|
||||
3. **Reverse proxy**: Varnish, Cloudflare
|
||||
4. **Minimize file reads**: Context created once per request
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Path Validation
|
||||
|
||||
Multiple layers prevent directory traversal:
|
||||
|
||||
```php
|
||||
// 1. Remove .. segments
|
||||
$path = str_replace('..', '', $path);
|
||||
|
||||
// 2. Resolve to real path
|
||||
$realPath = realpath($path);
|
||||
|
||||
// 3. Ensure within content directory
|
||||
if (!str_starts_with($realPath, $contentDir)) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
// 4. Check readable
|
||||
if (!is_readable($realPath)) {
|
||||
return 404;
|
||||
}
|
||||
```
|
||||
|
||||
### Output Escaping
|
||||
|
||||
All user-generated content escaped:
|
||||
|
||||
```php
|
||||
<?= htmlspecialchars($metadata['title']) ?>
|
||||
```
|
||||
|
||||
This prevents XSS attacks.
|
||||
|
||||
### MIME Type Validation
|
||||
|
||||
Static files served with correct MIME types:
|
||||
|
||||
```php
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'woff2' => 'font/woff2',
|
||||
'jpg' => 'image/jpeg',
|
||||
// ...
|
||||
];
|
||||
|
||||
header('Content-Type: ' . $mimeTypes[$extension]);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
- **200 OK**: Successful content render
|
||||
- **301 Moved Permanently**: Missing trailing slash
|
||||
- **404 Not Found**: Content doesn't exist
|
||||
|
||||
### 404 Handling
|
||||
|
||||
When content not found:
|
||||
|
||||
```php
|
||||
renderTemplate($ctx, '<h1>404 Not Found</h1>', 404);
|
||||
```
|
||||
|
||||
Base template rendered with 404 status.
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Custom Templates
|
||||
|
||||
Override any template:
|
||||
|
||||
```php
|
||||
// Framework checks:
|
||||
$custom = '/custom/templates/list-my-custom.php';
|
||||
if (file_exists($custom)) {
|
||||
include $custom;
|
||||
} else {
|
||||
include '/app/default/templates/list.php';
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Functions
|
||||
|
||||
Add your own in `/custom/functions.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Custom helper functions
|
||||
|
||||
function myCustomFunction() {
|
||||
// Your code
|
||||
}
|
||||
```
|
||||
|
||||
Include in router:
|
||||
|
||||
```php
|
||||
if (file_exists(__DIR__ . '/../custom/functions.php')) {
|
||||
require_once __DIR__ . '/../custom/functions.php';
|
||||
}
|
||||
```
|
||||
|
||||
### Content Files as PHP
|
||||
|
||||
`.php` content files have full access:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// In content/dynamic/index.php
|
||||
$currentTime = date('Y-m-d H:i:s');
|
||||
?>
|
||||
|
||||
# Dynamic Content
|
||||
|
||||
Current time: <?= $currentTime ?>
|
||||
|
||||
The language is: <?= $ctx->currentLang ?>
|
||||
```
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. Create test content
|
||||
2. Start dev server: `php -S localhost:8000 -t . app/router.php`
|
||||
3. Visit URLs, verify output
|
||||
4. Check different languages
|
||||
5. Test edge cases (missing files, invalid paths)
|
||||
|
||||
### Automated Testing (Future)
|
||||
|
||||
Possible test structure:
|
||||
|
||||
```php
|
||||
// tests/RouterTest.php
|
||||
test('renders home page', function() {
|
||||
$response = request('/');
|
||||
expect($response->status)->toBe(200);
|
||||
expect($response->body)->toContain('<h1>');
|
||||
});
|
||||
|
||||
test('handles 404', function() {
|
||||
$response = request('/nonexistent/');
|
||||
expect($response->status)->toBe(404);
|
||||
});
|
||||
```
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Simple Deployment
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/you/your-site
|
||||
|
||||
# 2. Point web server to directory
|
||||
# Document root: /path/to/site
|
||||
# Rewrite all requests to: /app/router.php
|
||||
|
||||
# 3. Done
|
||||
```
|
||||
|
||||
### With Build Step (Optional)
|
||||
|
||||
```bash
|
||||
# 1. Clone and build
|
||||
git clone ...
|
||||
cd site
|
||||
|
||||
# 2. Process custom styles (optional)
|
||||
# E.g., PostCSS, autoprefixer
|
||||
|
||||
# 3. Deploy
|
||||
rsync -av . server:/var/www/site/
|
||||
```
|
||||
|
||||
### Zero-Downtime Deployment
|
||||
|
||||
```bash
|
||||
# 1. Deploy to new directory
|
||||
rsync -av . server:/var/www/site-new/
|
||||
|
||||
# 2. Symlink switch
|
||||
ln -sfn /var/www/site-new /var/www/site-current
|
||||
|
||||
# 3. Web server serves from /var/www/site-current
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Philosophy](philosophy.md)
|
||||
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
|
||||
- [File Structure Reference](../reference/file-structure.md)
|
||||
- [Template Reference](../reference/templates.md)
|
||||
496
docs/explanation/philosophy.md
Normal file
496
docs/explanation/philosophy.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# Philosophy
|
||||
|
||||
Understanding the principles and thinking behind FolderWeb.
|
||||
|
||||
## Core Idea
|
||||
|
||||
**Your file system is your content management system.**
|
||||
|
||||
FolderWeb embraces the simplest possible approach to web publishing: create a folder structure that mirrors your site hierarchy, drop files into folders, and they immediately become pages. No database, no admin panel, no build process.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Just Enough, Nothing More
|
||||
|
||||
FolderWeb applies minimal PHP to enable modern conveniences while remaining maintainable for years or decades. Every feature must justify its existence by solving a real problem without creating new complexity.
|
||||
|
||||
**What this means:**
|
||||
- No frameworks that might be abandoned
|
||||
- No build tools that need maintenance
|
||||
- No package managers introducing dependencies
|
||||
- No abstractions unless they provide lasting value
|
||||
|
||||
**Example**: Instead of using a routing library, FolderWeb uses PHP's native file functions to map folders to URLs. This will work identically in 2025 and 2035.
|
||||
|
||||
### 2. Longevity Over Novelty
|
||||
|
||||
Code should outlive trends. FolderWeb prioritizes stability and backward compatibility over cutting-edge features.
|
||||
|
||||
**What this means:**
|
||||
- Standard PHP (no exotic extensions)
|
||||
- Plain HTML and CSS (no JavaScript required)
|
||||
- Simple file formats (Markdown, INI)
|
||||
- Conventions over configuration
|
||||
|
||||
**Why it matters**: A site built today should still work in 10 years without updates. The web's foundational technologies (HTML, CSS, PHP) change slowly and maintain backward compatibility.
|
||||
|
||||
### 3. Transparent and Readable
|
||||
|
||||
You should be able to open any file and immediately understand what it does. No magic, no hidden behavior.
|
||||
|
||||
**What this means:**
|
||||
- Sparse, meaningful comments
|
||||
- Descriptive function names
|
||||
- Simple control flow
|
||||
- Minimal abstraction layers
|
||||
|
||||
**Example**: Want to know how templates work? Open `app/rendering.php` and read 100 lines of straightforward PHP. No framework documentation needed.
|
||||
|
||||
### 4. Files Are Content
|
||||
|
||||
Your content lives in plain text files you can edit with any text editor. You own your content completely.
|
||||
|
||||
**What this means:**
|
||||
- Content is portable (copy files, migrate easily)
|
||||
- Version control friendly (Git tracks changes)
|
||||
- No lock-in (files work without FolderWeb)
|
||||
- Backup-friendly (copy a folder)
|
||||
|
||||
**Example**: Your entire site is a folder structure. Zip it, move it to another server, extract it, and it works. No database export/import, no migration scripts.
|
||||
|
||||
## What FolderWeb Is
|
||||
|
||||
### A File-Based Router
|
||||
|
||||
FolderWeb maps your folder structure to URLs:
|
||||
```
|
||||
content/blog/2025-11-02-post/ → yoursite.com/blog/2025-11-02-post/
|
||||
```
|
||||
|
||||
That's it. No route definitions, no controllers, no configuration.
|
||||
|
||||
### A Content Renderer
|
||||
|
||||
FolderWeb converts Markdown to HTML, wraps it in templates, and serves it. Three steps:
|
||||
1. Find content files
|
||||
2. Convert to HTML
|
||||
3. Wrap in template
|
||||
|
||||
### A Minimal Template System
|
||||
|
||||
FolderWeb provides just enough templating to avoid repetition:
|
||||
- Base template for site structure
|
||||
- Page template for content wrapper
|
||||
- List templates for directory views
|
||||
|
||||
All using plain PHP includes. No template language to learn.
|
||||
|
||||
### A Convention Framework
|
||||
|
||||
FolderWeb establishes conventions that eliminate configuration:
|
||||
- `metadata.ini` for structured data
|
||||
- `cover.jpg` for images
|
||||
- `YYYY-MM-DD-slug` for dates
|
||||
- `filename.lang.ext` for translations
|
||||
|
||||
Learn the conventions once, apply them everywhere.
|
||||
|
||||
## What FolderWeb Is Not
|
||||
|
||||
### Not a CMS
|
||||
|
||||
No admin panel. Edit files directly with your text editor, commit to Git, deploy.
|
||||
|
||||
**Why**: Admin panels add complexity, require maintenance, create security risks, and limit what you can do. Text files are simpler and more powerful.
|
||||
|
||||
### Not a Static Site Generator
|
||||
|
||||
FolderWeb renders pages on request, not at build time.
|
||||
|
||||
**Why**: No build step means immediate feedback. Save a file, refresh your browser, see changes. No waiting for builds, no deployment pipelines required (though you can add them).
|
||||
|
||||
### Not a JavaScript Framework
|
||||
|
||||
Zero JavaScript in the framework. HTML and CSS only.
|
||||
|
||||
**Why**: JavaScript adds complexity, breaks without it, requires builds/transpilation, and changes rapidly. HTML and CSS are stable and sufficient for content sites.
|
||||
|
||||
### Not Opinionated About Design
|
||||
|
||||
FolderWeb provides minimal default styles. Your design is your own.
|
||||
|
||||
**Why**: Design trends change. FolderWeb gives you a clean foundation and gets out of the way. Override everything in `/custom/`.
|
||||
|
||||
## Design Decisions Explained
|
||||
|
||||
### Why PHP 8.4+?
|
||||
|
||||
**Modern features without complexity.**
|
||||
|
||||
PHP 8.4 provides:
|
||||
- Readonly classes (immutability)
|
||||
- Property hooks (computed properties)
|
||||
- Arrow functions (concise code)
|
||||
- Modern array functions
|
||||
- Asymmetric visibility (controlled access)
|
||||
|
||||
These features make code clearer without adding dependencies or build steps. PHP 8.4 will be supported for years.
|
||||
|
||||
**Tradeoff**: Requires newer PHP, but gains clarity and performance.
|
||||
|
||||
### Why No Database?
|
||||
|
||||
**Files are simpler.**
|
||||
|
||||
Databases add:
|
||||
- Setup complexity
|
||||
- Backup complexity
|
||||
- Migration complexity
|
||||
- Performance tuning
|
||||
- Additional failure points
|
||||
|
||||
For content sites, files provide:
|
||||
- Version control integration
|
||||
- Simple backups (copy folder)
|
||||
- Portability
|
||||
- Transparent storage
|
||||
- No setup required
|
||||
|
||||
**When you might need a database**: User-generated content, real-time updates, complex queries, thousands of pages. For those cases, use a different tool.
|
||||
|
||||
### Why INI Files for Metadata?
|
||||
|
||||
**Simple, readable, PHP-native.**
|
||||
|
||||
INI format:
|
||||
- No parsing library needed (built into PHP)
|
||||
- Human-readable and editable
|
||||
- Supports sections for languages
|
||||
- Familiar format
|
||||
|
||||
**Alternatives considered**:
|
||||
- **YAML**: Requires library, complex syntax
|
||||
- **JSON**: Not as human-friendly, no comments
|
||||
- **TOML**: Requires library, less familiar
|
||||
- **Frontmatter**: Mixes content and metadata
|
||||
|
||||
### Why Markdown?
|
||||
|
||||
**Readable as plain text, converts to HTML.**
|
||||
|
||||
Markdown is:
|
||||
- Easy to learn (15 minutes)
|
||||
- Readable without rendering
|
||||
- Widely supported
|
||||
- Future-proof (plain text)
|
||||
- Version control friendly
|
||||
|
||||
**Alternatives supported**: HTML (for complex layouts), PHP (for dynamic content).
|
||||
|
||||
### Why No Build Tools?
|
||||
|
||||
**Immediate feedback, zero setup.**
|
||||
|
||||
Build tools add:
|
||||
- Installation steps
|
||||
- Configuration files
|
||||
- Waiting for builds
|
||||
- Build failures to debug
|
||||
- Another thing that can break
|
||||
|
||||
Without builds:
|
||||
- Save file → refresh browser → see result
|
||||
- No setup (just PHP)
|
||||
- Nothing to configure
|
||||
- Nothing to break
|
||||
|
||||
**Tradeoff**: Can't use Sass, TypeScript, etc. But you can use modern CSS, which is very capable.
|
||||
|
||||
### Why Trailing Slashes?
|
||||
|
||||
**Consistency and clarity.**
|
||||
|
||||
```
|
||||
/blog/ # Directory (list view)
|
||||
/blog # Redirects to /blog/
|
||||
```
|
||||
|
||||
Trailing slashes clarify that URLs represent directories, not files. Consistent URLs prevent duplicate content and simplify routing.
|
||||
|
||||
### Why Language Prefixes?
|
||||
|
||||
**Clear, hackable URLs.**
|
||||
|
||||
```
|
||||
yoursite.com/en/about/ # English
|
||||
yoursite.com/no/about/ # Norwegian
|
||||
```
|
||||
|
||||
Language in URL:
|
||||
- User sees current language
|
||||
- Can manually change URL
|
||||
- Bookmarkable per language
|
||||
- SEO-friendly (clear language signal)
|
||||
|
||||
**Default language has no prefix** (shorter, cleaner URLs for primary audience).
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Immutable Context
|
||||
|
||||
The `Context` object is readonly (PHP 8.4):
|
||||
```php
|
||||
readonly class Context {
|
||||
public function __construct(
|
||||
public private(set) string $contentDir,
|
||||
public private(set) string $currentLang,
|
||||
// ...
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Prevents accidental mutation, makes code predictable, enables safe sharing of state.
|
||||
|
||||
### Computed Properties
|
||||
|
||||
Properties calculate values on-demand:
|
||||
```php
|
||||
public string $langPrefix {
|
||||
get => $this->currentLang !== $this->defaultLang
|
||||
? "/{$this->currentLang}"
|
||||
: '';
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Keeps related logic together, avoids storing derived data, updates automatically.
|
||||
|
||||
### Function-Based API
|
||||
|
||||
Core functionality exposed as functions, not classes:
|
||||
```php
|
||||
renderFile($ctx, $filePath);
|
||||
findAllContentFiles($dir, $lang, $defaultLang, $availableLangs);
|
||||
```
|
||||
|
||||
**Why**: Simple to understand, easy to test, no object lifecycle to manage.
|
||||
|
||||
### Template Fallback
|
||||
|
||||
Check custom, fall back to default:
|
||||
```php
|
||||
$custom = "/custom/templates/$name.php";
|
||||
$default = "/app/default/templates/$name.php";
|
||||
return file_exists($custom) ? $custom : $default;
|
||||
```
|
||||
|
||||
**Why**: Never modify defaults, always override. Clean separation between framework and customization.
|
||||
|
||||
## Performance Philosophy
|
||||
|
||||
### Performance Through Simplicity
|
||||
|
||||
FolderWeb is fast because it does less:
|
||||
- No database queries
|
||||
- No heavy frameworks
|
||||
- No JavaScript parsing
|
||||
- Minimal file reads
|
||||
- Direct PHP includes
|
||||
|
||||
**Measured performance**: Page load time displayed in footer. Pride in speed through simplicity.
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
FolderWeb doesn't implement caching. Instead:
|
||||
- Use OPcache (PHP bytecode cache)
|
||||
- Use web server caching (Apache/Nginx)
|
||||
- Use reverse proxy (Varnish, Cloudflare)
|
||||
- CSS versioned automatically (MD5 hash)
|
||||
|
||||
**Why**: Let specialized tools handle caching. FolderWeb focuses on core functionality.
|
||||
|
||||
### Optimization Priorities
|
||||
|
||||
1. **Avoid work**: Don't render what's not needed
|
||||
2. **Use native functions**: PHP's file functions are optimized
|
||||
3. **Minimal abstraction**: Fewer layers = less overhead
|
||||
|
||||
## Security Philosophy
|
||||
|
||||
### Defense in Depth
|
||||
|
||||
Multiple security layers:
|
||||
- Path validation (prevent directory traversal)
|
||||
- Realpath checks (resolve symlinks, verify paths)
|
||||
- Content root enforcement (files must be in document root)
|
||||
- Output escaping (prevent XSS)
|
||||
- MIME type validation (proper content types)
|
||||
|
||||
### Simplicity Is Security
|
||||
|
||||
Less code = smaller attack surface:
|
||||
- No database (no SQL injection)
|
||||
- No user input rendering (no XSS in content)
|
||||
- No file uploads (no upload vulnerabilities)
|
||||
- No authentication (no auth bypasses)
|
||||
|
||||
**For user-generated content**: Use a different tool. FolderWeb is for static content you control.
|
||||
|
||||
## Maintenance Philosophy
|
||||
|
||||
### Code Should Age Gracefully
|
||||
|
||||
FolderWeb aims to require zero maintenance:
|
||||
- Standard PHP (no exotic dependencies)
|
||||
- Minimal third-party code (one library: Parsedown)
|
||||
- Stable APIs (PHP doesn't break backward compatibility)
|
||||
- No framework upgrades needed
|
||||
|
||||
**Goal**: Deploy once, forget about it. Check in years later, it still works.
|
||||
|
||||
### Convention Over Configuration
|
||||
|
||||
Fewer configuration options = less to maintain:
|
||||
- File conventions replace config
|
||||
- Sensible defaults for everything
|
||||
- Only configure what's necessary (languages)
|
||||
|
||||
### Documentation Is Core
|
||||
|
||||
Documentation is part of the project:
|
||||
- Comprehensive reference
|
||||
- Clear examples
|
||||
- Explanation of decisions
|
||||
- How-to guides for common tasks
|
||||
|
||||
**Why**: Future you (or someone else) will thank present you.
|
||||
|
||||
## When to Use FolderWeb
|
||||
|
||||
### Ideal For
|
||||
|
||||
- **Blogs**: Write Markdown, publish immediately
|
||||
- **Documentation**: Multi-file pages, clear structure
|
||||
- **Portfolios**: Grid layouts, cover images
|
||||
- **Marketing sites**: Static content, fast loading
|
||||
- **Personal sites**: Simple, maintainable
|
||||
- **Long-term projects**: Will work for decades
|
||||
|
||||
### Not Ideal For
|
||||
|
||||
- **User-generated content**: No database, no auth
|
||||
- **E-commerce**: Needs dynamic inventory, checkout
|
||||
- **Social networks**: Real-time updates, complex data
|
||||
- **SPAs**: JavaScript-heavy, API-driven
|
||||
- **Large-scale sites**: Thousands of pages (consider static generation)
|
||||
|
||||
### Perfect Fit Scenario
|
||||
|
||||
You want a blog or content site that:
|
||||
- You control all content
|
||||
- Loads fast
|
||||
- Requires minimal maintenance
|
||||
- Will work for years without updates
|
||||
- Integrates with Git workflow
|
||||
- Gives you complete control
|
||||
|
||||
## Comparison to Alternatives
|
||||
|
||||
### vs WordPress
|
||||
|
||||
**WordPress**: Full-featured CMS, database-driven, plugin ecosystem, admin panel, requires regular updates
|
||||
|
||||
**FolderWeb**: File-based, no database, no plugins, no admin, zero maintenance
|
||||
|
||||
**Choose WordPress if**: You need plugins, non-technical editors, or a proven ecosystem
|
||||
|
||||
**Choose FolderWeb if**: You want simplicity, longevity, and complete control
|
||||
|
||||
### vs Jekyll/Hugo (Static Generators)
|
||||
|
||||
**Static Generators**: Build at deploy time, generate HTML files, fast serving, requires builds
|
||||
|
||||
**FolderWeb**: Renders on request, no build step, immediate feedback, simpler workflow
|
||||
|
||||
**Choose Static Generator if**: You want maximum performance, have build infrastructure
|
||||
|
||||
**Choose FolderWeb if**: You want immediate feedback, simpler deployment, dynamic capabilities
|
||||
|
||||
### vs Laravel/Symfony (PHP Frameworks)
|
||||
|
||||
**Frameworks**: Full-stack, MVC architecture, ORM, routing, complex features
|
||||
|
||||
**FolderWeb**: Minimal, file-based routing, no ORM, single purpose
|
||||
|
||||
**Choose Framework if**: You're building a complex web application
|
||||
|
||||
**Choose FolderWeb if**: You're publishing content and want simplicity
|
||||
|
||||
## Future Direction
|
||||
|
||||
### Stability Over Features
|
||||
|
||||
FolderWeb aims to reach "done" status:
|
||||
- Core functionality complete
|
||||
- No major features needed
|
||||
- Focus on documentation and examples
|
||||
- Bug fixes and security updates only
|
||||
|
||||
### Possible Additions
|
||||
|
||||
Only if they maintain simplicity:
|
||||
- More template examples
|
||||
- Additional default styles (opt-in)
|
||||
- Performance optimizations
|
||||
- Better error messages
|
||||
|
||||
### Will Never Add
|
||||
|
||||
Features that contradict philosophy:
|
||||
- JavaScript requirement
|
||||
- Database integration
|
||||
- Build process
|
||||
- Admin panel
|
||||
- User authentication
|
||||
- Complex plugin system
|
||||
|
||||
## Contributing to FolderWeb
|
||||
|
||||
### Align With Philosophy
|
||||
|
||||
Proposed changes should:
|
||||
- Maintain simplicity
|
||||
- Avoid new dependencies
|
||||
- Work with PHP 8.4+
|
||||
- Be maintainable long-term
|
||||
- Solve real problems
|
||||
|
||||
### Ideal Contributions
|
||||
|
||||
- Bug fixes
|
||||
- Performance improvements
|
||||
- Better documentation
|
||||
- Example templates
|
||||
- Test cases
|
||||
- Clarification of existing code
|
||||
|
||||
### Before Adding Features
|
||||
|
||||
Ask:
|
||||
1. Can this be solved in userland (custom templates/code)?
|
||||
2. Does this add complexity for all users?
|
||||
3. Will this need maintenance in 5 years?
|
||||
4. Is this truly necessary?
|
||||
|
||||
## Conclusion
|
||||
|
||||
FolderWeb is deliberately simple. It does one thing—publishes content from files—and does it well. It resists feature creep, embraces constraints, and prioritizes longevity.
|
||||
|
||||
This isn't the right tool for every project. But for content sites that value simplicity, maintainability, and longevity, it might be perfect.
|
||||
|
||||
The code you write today should work in 2035. FolderWeb is built on that principle.
|
||||
|
||||
## Related
|
||||
|
||||
- [Getting Started Tutorial](../tutorial/00-getting-started.md)
|
||||
- [Architecture Overview](architecture.md)
|
||||
- [File Structure Reference](../reference/file-structure.md)
|
||||
Loading…
Add table
Add a link
Reference in a new issue