# ~jisifu Website — Agents Guide Architecture, operations, and reference for AI agents and developers maintaining this site. --- ## Table of Contents - [Architecture: Aki JSON Pattern](#architecture-aki-json-pattern) - [System Flow](#system-flow) - [JSON DOM Enhancement Pattern](#json-dom-enhancement-pattern) - [Directory Structure](#directory-structure) - [German Section Architecture](#german-section-architecture) - [Component Reference](#component-reference) - [Build Pipeline](#build-pipeline) - [Operations](#operations) - [CI Pipeline](#ci-pipeline) - [Optimizations](#optimizations) - [Troubleshooting](#troubleshooting) - [Changelog Summary](#changelog-summary) --- ## Architecture: Aki JSON Pattern This website follows the **Aki architecture** (inspired by [Aki](https://github.com/vilgacx/aki)), which uses a "JSON-as-data" approach to create a fast, Single-Page Application (SPA) experience without a heavy framework. ### Core Principle Instead of full page reloads, the frontend fetches JSON objects from the server. These objects contain the HTML content, which is dynamically injected into the DOM using simple JavaScript selectors. ### System Flow ``` Browser Request → nginx → route file exists? (e.g., german/index.php) YES → serve route file → includes ../index.php → HTML shell + JS NO → 404 (should not happen for existing sections) JS on page load parses window.location.pathname: /german → fetch public/german/index.json /blog/post → fetch public/blog/post.json / → fetch public/home/index.json ↓ ┌──────────────────────────────────────┐ │ generate_json.php (build step) │ │ Reads content/*.md │ │ Renders to HTML via comrak │ │ Wraps in JSON → public/*/index.json │ └──────────────────────────────────────┘ ↑ content/*/index.md (source of truth) ↓ ┌──────────────────────────────────────┐ │ generate_routes.php (build step) │ │ Creates route files for nginx: │ │ german/index.php → requires ../index.php │ │ blog/post/index.php → requires ../../index.php │ └──────────────────────────────────────┘ ``` ### JSON Structure Generated JSON files in `public/` follow this format: ```json { "html": {"scrollTop": 0}, "body": {"scrollTop": 0}, "#panel": {"tabIndex": -1}, "#grammar": {"open": true}, "meta": { "title": "Page Title", "description": "Optional meta description" }, "section": { "innerHTML": "

Page Title

Content...

" } } ``` The frontend JavaScript iterates through the keys, finds corresponding DOM elements via `document.querySelector(key)`, and updates their properties (e.g., `innerHTML`, `open`, `scrollTop`). The `meta` key is specially handled for `` and `<meta name="description">` updates. --- ## JSON DOM Enhancement Pattern The Aki JSON loader can set **any valid DOM property** on any element matched by a CSS selector — not just `innerHTML`. The build script's `enhanceWithDomProperties()` function in `generate_json.php` injects additional JSON keys that enhance the page on load. ### How It Works The frontend loader in `index.php` processes every JSON key as a CSS selector and every nested key as a DOM property: ```javascript Object.entries(data).forEach(e => { Object.entries(e[1]).forEach(r => { const target = document.querySelector(e[0]); if (target) target[r[0]] = r[1]; }) }); ``` This means `"#satzklammer": {"open": true}` becomes `document.querySelector("#satzklammer").open = true`. **Important limitation**: Only DOM **properties** can be set this way — not methods. `.focus()` is a method and must be called separately in the frontend JS. `.scrollIntoView()` is a method but `.scrollTop` is a property. ### Existing Enhancements All defined in `enhanceWithDomProperties()` in `generate_json.php`: | JSON Key | DOM Properties | Effect | |----------|---------------|--------| | `"html"` | `{"scrollTop": 0}` | Scroll page to top on SPA navigation | | `"body"` | `{"scrollTop": 0}` | Defensive fallback scroll | | `"#panel"` | `{"tabIndex": -1}` | Make content panel focusable (frontend calls `.focus()` after metadata update) | | `"#<id>"` | `{"open": true}` | Auto-open `<details>` elements with `data-auto-open` attribute | ### The `data-auto-open` Convention To make a `<details>` element open by default on page load, add the `data-auto-open` attribute in the Markdown source: ```html <details id="grammar" data-auto-open><summary>Konjunktiv II</summary> ... </details> ``` The build script scans rendered HTML for `<details>` tags with `data-auto-open` and generates `"#grammar": {"open": true}` in the JSON. Only opt-in details auto-open — nested sub-details, stubs, and footnotes stay collapsed. ### Adding New DOM Enhancements To add a new DOM property injection: 1. **Add the enhancement** in `enhanceWithDomProperties()` in `generate_json.php`: ```php function enhanceWithDomProperties($html) { $enhancements = []; // Static enhancements (apply to all pages) $enhancements['html'] = ['scrollTop' => 0]; // Content-aware enhancements (depend on rendered HTML) // ... scan $html for patterns and add keys ... return $enhancements; } ``` 2. **If the enhancement needs a method call** (like `.focus()`), add it in `index.php` after the DOM property loop: ```javascript // After DOM updates, call methods that can't be set as properties if (mainContentArea.tabIndex === -1) { mainContentArea.focus(); } ``` 3. **Rebuild** with `./rebuild.sh` to regenerate JSON files. ### Design Guidelines - **Prefer properties over methods**: Use `scrollTop`, not `scrollIntoView()`. Use `open`, not `show()`. - **Keep enhancements static when possible**: Scroll-to-top applies to every page — add it once, not per-file. - **Use data attributes for opt-in behavior**: The `data-auto-open` convention prevents auto-opening everything. - **Order matters**: DOM properties are set in JSON key order. `scrollTop` before `innerHTML` is fine since all JS runs synchronously before reflow. - **Don't overwrite existing classes**: `className` assignment replaces all classes. Use specific properties instead. --- ## Directory Structure ``` public_html/ ├── index.php ← Entry point (shell + JS loader) ├── Makefile ← CI pipeline (test + validate + rebuild) ├── rebuild.sh ← Convenience rebuild script ├── generate_json.php ← Markdown → JSON converter ├── generate_routes.php ← nginx SPA route generator ├── watch.php ← Auto-watcher (optional) ├── .htaccess ← Cache headers, rewrites ├── lib/ │ ├── ContentRenderer.php ← comrak markdown → HTML │ ├── ContentManager.php ← Content discovery & metadata │ ├── equations.json ← Equation definitions (24 calculators) │ └── equations.schema.json ← JSON Schema (Draft-07) contract ├── validate-equations.php ← Standalone schema validation (CI) ├── partials/ │ ├── sidebar.php ← Navigation menu │ ├── footer.php ← Footer content │ ├── equations_visualizer.html │ ├── equation-visualizer.html │ └── equation-visualizer2.html ├── js/ │ └── enhance.js ← Progressive enhancement (prefetch, transitions) ├── content/ ← SOURCE OF TRUTH — edit these │ ├── home/index.md │ ├── blog/ ← Each .md file = one post │ ├── equations/index.md ← Auto-generated equation reference page │ ├── projects/index.md │ ├── about/index.md │ ├── files/ │ │ ├── index.md │ │ ├── x/index.md │ │ └── cv/ │ │ ├── resume.json │ │ ├── Ji_Matt_CV.typ │ │ └── jobby.nu │ ├── german/ │ │ ├── index.md ← Hub page with compact summaries │ │ ├── leseverstehen.md ← Sub-page (3 reading parts) │ │ ├── horverstehen.md ← Sub-page (3 listening parts) │ │ ├── redemittel.md ← Sub-page (4 exam parts) │ │ ├── konnektoren.md ← Sub-page (7 connector categories) │ │ ├── wortschatz.md ← Sub-page (5 vocabulary themes) │ │ ├── grammatik.md ← Sub-page (Satzklammer + Konjunktiv II) │ │ ├── sprachbausteine.md ← Sub-page (Lückentext exercise) │ │ ├── pruefungssimulation.md ← Sub-page (Schreiben + Sprechen) │ │ ├── exercise.md ← 22. Januar: Satzbauübungen │ │ ├── 26-jan.md ← 26. Januar: Starke Verben │ │ ├── 29-april.md ← 29. April: Infinitivsätze │ │ ├── hw-ansatz.md ← Hausaufgaben: Genitiv │ │ ├── mai-4.md ← 4. Mai: Konsekutivsätze │ │ ├── juni-1-als-wenn.md ← 1. Juni: als/wenn │ │ ├── juni-1-greetings.md ← 1. Juni: Begrüßungsphrasen │ │ ├── juni-2.md ← 2. Juni: Verben mit Präpositionen │ │ └── juni-8.md ← 8. Juni: Hausaufgaben │ ├── pics/index.md │ └── x/index.md ├── public/ ← AUTO-GENERATED — don't edit │ └── */index.json ├── cache/ ← Optional render cache ├── test.php ← Test suite └── verify_working.php ← Manual verification script ``` --- ## German Section Architecture The `/german` section has been refactored into a **hub-and-spoke** structure: - **Hub page**: `content/german/index.md` — the main overview with compact summaries, quick-nav, study plan, and Prüfungstipps - **8 sub-pages** (study resources, each with top breadcrumb + bottom prev/next nav): - `content/german/leseverstehen.md` — telc reading: MC, R/F, matching (3 parts) - `content/german/horverstehen.md` — telc listening: Durchsage, Interview, Diskussion (3 parts) - `content/german/redemittel.md` — phrases for all 4 exam parts (Schreiben, Sprechen 1–3) - `content/german/konnektoren.md` — B2 connectors grouped by function (7 categories) - `content/german/wortschatz.md` — vocabulary: Umwelt, Schönheit, Gesundheit, Ausschlussverfahren, Kleine Prüfung - `content/german/grammatik.md` — Satzklammer + Konjunktiv II - `content/german/sprachbausteine.md` — telc Lückentext exercise (10 gaps) + solutions - `content/german/pruefungssimulation.md` — full exam simulation: Schreiben (30 Min) + Sprechen (3 parts) - **9 course date pages** (individual lesson notes, each with breadcrumb): - `content/german/exercise.md` — 22. Januar: Satzbauübungen - `content/german/26-jan.md` — 26. Januar: Starke Verben (17 verbs with Präteritum/Perfekt, SRS-tagged) - `content/german/29-april.md` — 29. April: Infinitivsätze, Vermutungen - `content/german/hw-ansatz.md` — Hausaufgaben: Genitiv & Gegensätze - `content/german/mai-4.md` — 4. Mai: Konsekutivsätze (so...dass / solch-) - `content/german/juni-1-als-wenn.md` — 1. Juni: als/wenn, Studieren im Ausland - `content/german/juni-1-greetings.md` — 1. Juni: Begrüßungsphrasen, Wechselpräpositionen - `content/german/juni-2.md` — 2. Juni: Verben mit Präpositionen (27 verbs, SRS-tagged) - `content/german/juni-8.md` — 8. Juni: Hausaufgaben, Beispielsätze ### Navigation Conventions All sub-pages follow consistent navigation: - **Top**: `[← Zurück zur Übersicht](/german)` breadcrumb after the intro - **Bottom**: `---` separator + prev/next nav in quick-nav order: Leseverstehen → Hörverstehen → Redemittel → Konnektoren → Wortschatz → Grammatik → Sprachbausteine → Prüfungssimulation Course date pages have a top breadcrumb only (no prev/next chain). ### Cross-reference Design The hub index.md uses a dual-link pattern in the Kursübersicht tables: - `[#anchor](#anchor)` for in-page jump to compact summary - `[→](/german/subpage)` for the full content on the sub-page This means anchor IDs (`#reading`, `#listening`, `#redemittel`, `#speaking`, `#writing`, `#29-april`) must remain on the compact summaries in index.md even after content is moved to sub-pages. --- ## Component Reference ### `index.php` — Entry Point The main HTML shell. Contains: - Base HTML structure (`<!DOCTYPE html>`, `<head>`, `<body>`) - **`chdir(__DIR__)`** at the top — ensures all relative includes (`partials/sidebar.php`, etc.) resolve from the root directory even when `index.php` is included from a route subdirectory like `german/index.php` - **`$base` computed from `__FILE__` + `$_SERVER['DOCUMENT_ROOT']`** — instead of `$_SERVER['SCRIPT_NAME']`, so the base URL is correct regardless of which route file triggered the include - JavaScript Aki JSON loader logic: - Intercepts clicks on links with `fn="onclick"` and `url="..."` attributes - Fetches the JSON, iterates keys, updates DOM via `querySelector` - Integrates History API for back/forward navigation - Dynamic `<title>` and `<meta name="description">` updates from JSON metadata - Cache-busting via `?t=TIMESTAMP` query parameter - Loading indicators & error display - Strips trailing slashes from URL path before constructing JSON fetch URL (handles nginx directory redirects) ### `generate_json.php` — Build Engine Converts Markdown to Aki-compatible JSON. Key behaviors: - Scans `content/` directories for `index.md` files and standalone blog posts - Uses `ContentRenderer` to convert Markdown → HTML via comrak - Extracts `title` and `description` from YAML front matter (falls back to H1 + paragraph snippet) - Wraps rendered HTML in JSON and writes to `public/` - Supports incremental builds (skips unchanged files by comparing mtime + md5 hash) - Handles `envs.net` subdirectory path prefixing (`/~username/`) - **Auto-generated equation reference**: When building the `/equations` page, calls `generateEquationReferenceHtml()` which reads `lib/equations.json`, groups equations by category (Finance, Physics, Math, Info Theory, Engineering), and generates `<details>` blocks with variable range tables and output descriptions. The generated HTML is inserted after the H1 in the rendered markdown. ### `lib/ContentRenderer.php` — Markdown Rendering Uses `comrak` (Rust markdown processor) via `proc_open`. Configuration: - **Flags**: `--gfm` (strikethrough, table, autolink, tasklist — *without* tagfilter) - `--unsafe`: Allows raw HTML (iframe, div, etc.) through unescaped - `--syntax-highlighting base16-ocean.dark`: Code block highlighting - `-e footnotes`: Markdown footnotes enabled - `--smart`: Smart punctuation (curly quotes, em-dashes) - `--gemoji`: GitHub emoji shortcodes → Unicode emojis - Optional file caching via configurable cache directory **Important**: `tagfilter` is explicitly **omitted** from the GFM extensions so that `<iframe>` tags (used for PDF/Gravatar embeds) pass through unescaped. ### `lib/ContentManager.php` — Content Discovery Handles: - Discovering sections: `glob(content/*/index.md)` - Discovering blog posts: `glob(content/blog/*.md)` with date extraction from filenames - Parsing metadata from Markdown (YAML front matter, first heading, first paragraph) - File content caching via internal `$file_cache` array (reduces I/O) - Returns structured arrays of sections, pages, and blog posts with metadata ### `js/enhance.js` — Progressive Enhancement Adds smooth page transitions and prefetching. Key behaviors: - Intercepts link clicks for AJAX navigation - Updates DOM and browser URL via `history.pushState` - Smooth transition effects - Prefetches pages on hover - **Site works perfectly without JavaScript** — enhancement only ### `partials/equation-visualizer.html` — Sidebar Equation Visualizer Inline HTML/JS partial included in `sidebar.php`. Contains the full `EquationVisualizer` class with: - Dropdown selector for 24 equations - Dynamic slider-based form generation for variables - Live formula calculation with `new Function()` evaluation - **Client-side validation**: `validateData()` method checks the fetched JSON against schema constraints before rendering — validates required fields, variable ranges (`default` ∈ [`min`, `max`]), `calculated`/`calculatedFrom` dependency, and output structure. Shows a clear error message in the container if validation fails. **Initialization**: Uses PHP `$base` for URL path resolution (handles envs.net subdirectory prefixing). ### `js/equation-visualizer.js` — Standalone Visualizer Standalone JS file version of the `EquationVisualizer` class. Functionally identical to the inline version with the same `validateData()` method. Initializes with a hardcoded `/lib/equations.json` path. Used by the equation demo on the `/projects` page. ### `lib/equations.json` — Equation Library JSON file defining **24 interactive equation calculators** consumed by the `EquationVisualizer` frontend. Each equation has slider-adjustable input variables and computed outputs with live JS evaluation. **Categories**: Finance (3), Physics (4), Math (4), Info Theory (3), Engineering (5), plus existing legacy equations **Editor tooling**: The file includes `"$schema": "lib/equations.schema.json"` at the top level — VS Code and compatible editors provide autocomplete, type checking, and inline validation when editing equations. ### `lib/equations.schema.json` — Equation JSON Schema Formal [JSON Schema (Draft-07)](https://json-schema.org/draft-07/json-schema-release-notes) defining the contract between `lib/equations.json` and the `EquationVisualizer` frontend. **Schema structure:** | Level | Required fields | Conventions | |-------|----------------|-------------| | **Equation** | `id`, `name`, `description`, `formula`, `variables`, `outputs` | `id`: snake_case (`ohms_law`) | | **Variable** | `id`, `name`, `symbol`, `description`, `min`, `max`, `step`, `default`, `unit` | `id`: alpha-start (`sourceVoltage`); `default` ∈ [`min`, `max`] | | **Output** | `id`, `name`, `formula`, `unit`, `description` | `id`: lowercase-start (`snr_db`, `capVoltage`) | | **Derived var** | `calculated: true` + `calculatedFrom` | Skips slider UI; computed from other variables | **Validation layers:** - **CI pipeline**: `make validate-schema` (standalone `validate-equations.php`) checks file existence, JSON validity, structure, naming, ranges, and category coverage - **Test suite**: `test.php` runs 6 comprehensive schema tests including formula variable resolution (every variable reference in a formula must exist) - **Client-side**: `EquationVisualizer.validateData()` in both `partials/equation-visualizer.html` and `js/equation-visualizer.js` validates the fetched JSON before rendering, showing a clear error message if the data is malformed ### `partials/footer.php` — Footer Navigation Expanded from a single credit line to a full Aki-style navigation row linking all 8 site sections. Included from `index.php` via `require 'partials/footer.php'`. **Navigation links**: All use the Aki SPA pattern (`fn='onclick'`, `url='public/section/index.json'`, `data-section`) with `$base` prefix for subdirectory compatibility: | Link | Section | |------|---------| | `~jisifu` | Home | | `projects` | Projects | | `equations` | Equation Reference | | `blog` | Blog | | `german` | German B2 | | `pics` | Pics | | `files` | Files | | `about` | About | **Credit line**: Preserved below the nav — `made with care by {user} on envs.net`. ### `watch.php` — Auto-Watcher (Optional) Polls `content/` every 2 seconds for Markdown file changes. Automatically regenerates JSON when changes are detected. Can be run in background via `nohup` or `screen`. --- ## Build Pipeline ### Standard Workflow ``` 1. Edit content/*.md ← source of truth 2. Run ./rebuild.sh ← triggers generate_json.php then generate_routes.php 3. generate_json.php: a. Scans content/ for files b. For each file, checks if output is newer than source (incremental) c. Reads file content (cached via ContentManager) d. Renders Markdown → HTML via ContentRenderer (comrak) e. Extracts metadata (title, description) f. Writes JSON to public/*/index.json 4. generate_routes.php: a. Reads sections and blog posts from ContentManager b. Creates route files for nginx: section/index.php includes ../index.php c. Blog posts get blog/post-slug/index.php including ../../index.php 5. Refresh browser ← loads updated JSON via index.php ``` ### Incremental Build `generate_json.php` compares `filemtime()` of source and output: - If output is newer than source → skip (echoes `⊘ section/index.json (unchanged)`) - If source is newer or output missing → regenerate To force a full rebuild: ```bash rm -rf public/ cache/ ./rebuild.sh ``` ### Asset Paths Assets (images, PDFs) go in `content/section/` alongside markdown files. Reference them with web paths: ```markdown ![Image](/content/section/image.png) [PDF](/content/section/document.pdf) ``` --- ## Operations ### Three Modes | Mode | Command | Use Case | |------|---------|----------| | **Manual** | `./rebuild.sh` | Infrequent edits, full control | | **Auto-Watcher** | `nohup php watch.php > watch.log 2>&1 &` | Active development, auto-rebuild on save | | **Git Hook** | `.git/hooks/post-commit` with `php generate_json.php` | Production, version-controlled | ### Key Commands ```bash ./rebuild.sh # Rebuild site php generate_json.php # Rebuild (PHP directly) php watch.php # Start auto-watcher (foreground) pkill -f "php watch.php" # Stop auto-watcher tail -f watch.log # View watch logs find content -name "*.md" # List all markdown find content -type f ! -name "*.md" # List all assets ``` ### Git Hook Setup ```bash cat > .git/hooks/post-commit << 'EOF' #!/bin/bash php generate_json.php EOF chmod +x .git/hooks/post-commit ``` ### Cache Busting Browser caching for JSON is controlled via: - `.htaccess` (Apache) or nginx config: JSON files cached for 1 hour - `index.php`: Adds `?t=TIMESTAMP` to fetch URLs for immediate refresh To see changes immediately: <kbd>Ctrl+F5</kbd> (hard refresh) ### Adding Content **New page section:** ```bash mkdir -p content/mysection echo "# My Section" > content/mysection/index.md ./rebuild.sh # Appears in sidebar automatically ``` **New blog post:** ```bash echo "# Post Title" > content/blog/my-post.md ./rebuild.sh # Available at /blog/my-post ``` ### Update Sidebar Order Edit `partials/sidebar.php` — change the `$priority_sections` array. --- ## CI Pipeline The project uses a `Makefile` for a CI-friendly pipeline. All targets are `.PHONY`. ### Targets | Command | Runs | Purpose | |---------|------|---------| | `make all` (default) | `test` + `validate-schema` + `validate` + `check-routes` + `check-route-syntax` | Full CI pass — run before commits | | `make test` | `php test.php` | 178-test suite covering ContentRenderer, ContentManager, DOM enhancements, tag validation, route gen, build loop, SRS export, equation schema, watch.php, content structure, sidebar/footer, index.php/verify, content rendering | | `make validate` | `php validate_tags.php` | Checks all 28 content files have balanced `<details>`/`<summary>` tags | | `make validate-schema` | `php validate-equations.php` | Validates `lib/equations.json` against `lib/equations.schema.json` | | `make rebuild` | `./rebuild.sh` | Regenerate all JSON files and SPA routes | | `make check-routes` | Shell script | Asserts exactly 26 route files (6 section + 20 sub-page) | | `make check-route-syntax` | Shell + `php -l` | Validates PHP syntax of all route files | ### CI Usage **Pre-commit check**: ```bash make all # Expected: 178/178 tests pass + 28/28 files balanced tags + schema validated + 26 routes + all PHP clean ``` **In CI config** (e.g., GitHub Actions, Gitea, Woodpecker): ```yaml steps: - name: Test run: make test - name: Validate run: make validate ``` **Full rebuild in CI** (e.g., on push to production): ```bash make rebuild && make all ``` ### Test Suite Coverage The `test.php` suite (178 tests across 16 sections) covers: | Section | Tests | Covers | |---------|-------|--------| | ContentRenderer | 19 | Markdown rendering, GFM features, caching, comrak flags | | ContentManager | 23 | Section discovery, blog posts, metadata extraction, file caching | | DOM Enhancement | 9 | `enhanceWithDomProperties()`, `data-auto-open`, scroll/panel | | Tag Validation | 7 | Details/summary regex, balanced/unbalanced detection | | Route Generation | 5 | Route file paths, PHP syntax, existence checks | | Integration | 16 | German section structure, breadcrumbs, prev/next, JSON output, content rendering validation, section-specific keywords | | Build Loop | 10 | Sub-page discovery, incremental mtime, JSON structure, blog index | | Route Gen Logic | 7 | Section/sub-page/blog route paths, PHP syntax, home exclusion | | Validate Tags Loop | 6 | RecursiveIteratorIterator, regex patterns, SKIP_DOTS, error format | | SRS Export | 20 | parseTableRow, cleanCell, parseTable, regex matching, JSON structure, CLI guard | | Watch.php | 7 | MarkdownWatcher class, scan_files, mtime detection, blog index gen | | Content Page Structure | 10 | Home/about/projects/files page content, section links, polish | | Sidebar & Footer | 12 | Priority sections, Aki links, `$base` prefix, htmlspecialchars, route cross-refs, sidebar content files | | Index.php Base URL & Verify | 7 | `chdir(__DIR__)`, `$base` computation, Aki loader logic, verify_working.php | | Site Health | 10 | PHP syntax checks, partials existence, rebuild.sh exec + run | | Equation Schema Validation | 6 | JSON schema structure, variable resolution, naming conventions, categories | ### Extending Tests Add tests in `test.php` following the existing pattern: ```php test('descriptive test name', function () { // Arrange, Act, Assert assert_true($condition, 'Failure message'); assert_eq($actual, $expected); assert_contains($haystack, $needle); }); ``` Available assertions: `assert_true`, `assert_eq`, `assert_contains`, `assert_not_contains`. Each test is a self-contained closure. Tests that depend on comrak availability should check `$comrak_available` and return early (not fail) if comrak is missing: ```php test('render() converts markdown', function () use ($comrak_available) { if (!$comrak_available) return; // skip gracefully // ... test logic ... }); ``` --- ## Optimizations ### 1. Incremental Builds Only regenerates JSON files when source Markdown has changed (mtime comparison). ### 2. File Content Caching `ContentManager` caches file contents in `$file_cache` array, reducing I/O by ~50% during builds. ### 3. Render Cache `ContentRenderer` can cache comrak output to disk (configurable cache directory). Enabled in build script: ```php $renderer = new ContentRenderer(true, 'cache/'); ``` ### 4. Array Key Fix `array_values()` applied after `array_filter()` to prevent undefined index warnings. ### 5. JSON_UNESCAPED_SLASHES Cleaner JSON output from `json_encode()`. ### 6. Cache Headers (`.htaccess`) ``` application/json: 1 hour text/css: 1 week application/javascript: 1 week image/png: 1 month ``` --- ## Environment Optimized for **envs.net** shared hosting. The live site (`jisifu.envs.net`) runs on **nginx**, not Apache. ### SPA Routing on Nginx Since nginx doesn't support `.htaccess` rewrites, SPA routing is handled by generating route directories: - `generate_routes.php` (run during `./rebuild.sh`) creates a directory for each content section and blog post - Each route directory contains `index.php` that simply includes the main `index.php` - Example: `german/index.php` = `<?php require __DIR__ . '/../index.php'; ?>` - nginx finds this real file, executes PHP, and the JS parses the URL to load the right JSON `index.php` uses `chdir(__DIR__)` at the top to ensure all relative includes (`partials/sidebar.php`, etc.) resolve from the root directory, even when included from a route subdirectory. ### Requirements - PHP 7.0+ - Web server with PHP support (nginx or Apache) - `comrak` (Rust markdown processor) — install via `eget kivikakk/comrak` --- ## Troubleshooting | Problem | Likely Cause | Fix | |---------|-------------|-----| | Changes not showing | Need to rebuild | `./rebuild.sh` | | Browser shows old content | Cache | <kbd>Ctrl+F5</kbd> hard refresh | | Assets not loading | Wrong path in markdown | Use `/content/section/file.ext` | | JSON looks wrong | Corrupted build | `rm -rf public/` then `./rebuild.sh` | | 404 errors on page refresh | Missing route directory | Run `./rebuild.sh` to regenerate route files | | Sidebar not showing links | Content dirs missing | Check `content/*/index.md` exists | | Headings inside `<details>` render literally | comrak doesn't process Markdown inside raw HTML blocks | Write raw HTML (`<h2>`) in source | | `iframe` tags not rendering | `tagfilter` extension stripping them | Ensure `tagfilter` is omitted from GFM flags | ### Headings Inside `<details>` — Known Issue **Symptom**: Headings inside `<details>` blocks (e.g., `## Satz-klammer`) render incorrectly on the live site as `<h2># Satz-klammer</h2>` or `<h1># Fasnacht Ferien</h1>`. **Root cause**: comrak passes Markdown inside raw HTML blocks (`<details>`, `<div>`) through *literally* without processing it. The generated JSON therefore contains raw Markdown text (e.g., `## Satz-klammer`). A **client-side JavaScript** script (not part of this site's core code) then re-processes that literal Markdown in the DOM and transforms it into the faulty HTML. **Diagnostic test**: Create a static HTML file with `## Satz-klammer` inside `<details>` — it renders correctly as plain text. The incorrect live rendering is caused by an external/interfering script running after page load. **Workarounds**: 1. Write raw HTML (`<h2>Satz-klammer</h2>`) instead of Markdown headings inside `<details>` 2. Use browser DevTools (Network → Sources → Console) to identify and disable the interfering client-side script 3. Implement a dedicated client-side Markdown parser (e.g., `marked.js`, `markdown-it`) if dynamic processing is needed --- ## Changelog Summary ### Original State - Simple `index.php` with inline JavaScript - onclick handlers fetched JSON files - Content source unclear ### The Incident (Failed Refactor) - Attempted complex server-side routing (`router.php`, `template.php`) - Result: site completely broken - Lesson: too complex, not tested before deployment ### The Fix 1. Created `content/` directory as source of truth 2. Restored simple `index.php` with direct JSON fetching 3. Created `generate_json.php` build system 4. Fixed caching with `.htaccess` headers 5. Added incremental builds and file caching optimizations ### Current Architecture ``` content/*.md → generate_json.php → public/*/index.json → index.php (loads via JS) → Content (edit here) (build step) (don't edit) (HTML shell + JS loader) ``` ### Files Added - `content/` directory (all markdown) - `generate_json.php` (converter) - `rebuild.sh` (rebuild script) - `watch.php` (auto-watcher) - `lib/ContentRenderer.php` (markdown → HTML) - `lib/ContentManager.php` (content discovery) ### Files Removed - `router.php`, `template.php`, `submit.php` — legacy from failed refactor (deleted) - `build.nu`, `log.php` — unused legacy scripts (deleted) --- ## File-by-File Reference | File | Role | Editable? | |------|------|-----------| | `index.php` | Entry point, HTML shell, JS loader | Rarely | | `generate_json.php` | Build engine — Markdown → JSON | Rarely | | `generate_routes.php` | Build engine — nginx SPA route dirs | Rarely | | `rebuild.sh` | Build convenience script | No | | `watch.php` | Auto-watcher | No | | `.htaccess` | Cache headers, rewrites | Rarely | | `lib/ContentRenderer.php` | Markdown → HTML via comrak | Rarely | | `lib/ContentManager.php` | Content discovery | Rarely | | `lib/equations.json` | Equation definitions (24 calculators) | Yes | | `lib/equations.schema.json` | JSON Schema (Draft-07) | Rarely | | `validate-equations.php` | Standalone schema validation script | Rarely | | `content/equations/index.md` | Equation reference page (auto-generated content) | Rarely | | `partials/sidebar.php` | Navigation | Yes (order) | | `partials/footer.php` | Footer — Aki-style nav row (8 sections) + envs.net credit | Yes | | `partials/equation-visualizer*.html` | Equation display | Rarely | | `js/enhance.js` | Progressive enhancement | Rarely | | `Makefile` | CI pipeline (test + validate + rebuild) | Rarely | | `test.php` | Test suite | Rarely | | `verify_working.php` | Manual verification script | Rarely | | `content/german/index.md` | German hub page | **Yes** | | `content/german/*.md` | German sub-pages & course notes | **Yes** | | `content/*/index.md` | Page content | **Yes** | | `content/blog/*.md` | Blog posts | **Yes** | | `public/*/index.json` | Generated output | **Never** | ## Typst Global Grid Spacing Convention In Typst decks (e.g. `content/files/cv/pruefungsstrategie.typ`), the convention is to set table-like spacing **globally on `#grid(...)`** rather than per-call, since `#grid` is the workhorse: ```typ #set grid( row-gutter: 14pt, column-gutter: 18pt, ) ``` - **Why global**: every `#grid(...)` call in the deck picks up uniform rhythm — no per-table drift, future tables inherit the look automatically. - **Why `#grid` over `#set table(...)`**: the codebase uses `#grid(...)` for tabular layouts; `#table(inset:, ...)` directives only apply to `#table(/* TARTABLE */)` calls (none in current Typst files). - **Reusable pattern**: when adding a new Typst deck, copy the 4-line `#set grid(...)` block and pick row/column gutter to taste (for tighter tables use `8pt`/`12pt`, for sparser tables use `14pt`/`18pt`). - **Caveat for many-row tables**: each row-gutter of `14pt` adds ~`14pt × (N-1)` to the table height. Verifying `pdfinfo` page-count after a global change catches overflow cascades early.