# 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, '