commit 548aa12e7828b07182b4f6edfae64e85a433f3ad Author: Brummel Date: Wed May 20 22:21:29 2026 +0200 chore: bootstrap repo with design spec and implementation plan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ee3f88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Zola build output +public/ +processed_images/ + +# Project-local artifacts not for upstream +orig/ +.superpowers/ + +# Editor / OS noise +*.swp +*.swo +*~ +.DS_Store + +# Logs / caches that might leak in +*.log +.zola/ diff --git a/docs/plans/2026-05-20-praxis-zola-rebuild.md b/docs/plans/2026-05-20-praxis-zola-rebuild.md new file mode 100644 index 0000000..88c3773 --- /dev/null +++ b/docs/plans/2026-05-20-praxis-zola-rebuild.md @@ -0,0 +1,1637 @@ +# Praxis am Lienhardplatz — Zola Rebuild Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Zola static site that reproduces the current Grav-based praxis-am-lienhardplatz.de 1:1, deployed to the staging subdomain `preview.praxis-am-lienhardplatz.de`, without touching the live site. + +**Architecture:** Zola SSG. Content as Markdown with TOML frontmatter under `content/`. Tera templates under `templates/`. Bootstrap 3 CSS/JS lifted verbatim from the Grav theme. Custom image shortcode wrapping Zola's `resize_image()`. Deploy via `rsync` to `~/public/praxis/` on `mycelium`. + +**Tech Stack:** Zola v0.22.1, Tera templates, Markdown, TOML, Bootstrap 3.3.6, jQuery 2.1.4, Font Awesome 4.x (verbatim from Grav theme), Google Maps via ` + +``` + +- [ ] **Step 3: Uncomment `{{ map() }}` in `templates/index.html`** (was a comment in Task 13). + +- [ ] **Step 4: Verify** + +```fish +zola build +grep -A2 "map-section" public/index.html +``` + +Expected: iframe markup appears in the rendered home page. + +- [ ] **Step 5: Commit** + +```fish +git add templates/shortcodes/map.html templates/index.html content/contact.md +git commit -m "feat: map shortcode with iframe embed" +``` + +--- + +## Task 21: robots.txt + +**Files:** +- Create: `static/robots.txt` + +- [ ] **Step 1: Write `static/robots.txt`** + +``` +User-agent: * +Disallow: / +``` + +- [ ] **Step 2: Verify after build** + +```fish +zola build +cat public/robots.txt +``` + +Expected: file contents exactly as above. + +- [ ] **Step 3: Commit** + +```fish +git add static/robots.txt +git commit -m "feat: robots.txt blocks all crawlers for staging" +``` + +--- + +## Task 22: First Deploy Test (Routing Sanity Check) — GATED + +This task verifies the subdomain routes to `~/public/praxis/` before any real content lands there. It is a separate gated step: ask the user for confirmation before running the rsync. + +**Files:** +- Create: `static/_hello.html` (temporary, removed in Task 24) + +- [ ] **Step 1: Write `static/_hello.html`** + +```html + +Routing OK +

Routing OK

+

If you see this, preview.praxis-am-lienhardplatz.de routes to ~/public/praxis/.

+ +``` + +- [ ] **Step 2: Build** + +```fish +zola build +ls public/_hello.html +``` + +- [ ] **Step 3: REQUEST USER CONFIRMATION** before deploy + +Pause and ask the user: +> "Ready to push the test file to mycelium:public/praxis/ via rsync. Confirm to proceed?" + +Wait for explicit "yes" / "go ahead". Do not proceed without confirmation. + +- [ ] **Step 4: Deploy only the hello file** + +```fish +rsync -av public/_hello.html mycelium:public/praxis/ +``` + +- [ ] **Step 5: Verify routing** + +```fish +curl -sI https://preview.praxis-am-lienhardplatz.de/_hello.html +curl -s https://preview.praxis-am-lienhardplatz.de/_hello.html | head -10 +``` + +Expected: + - HTTP 200 OK + - Body contains "Routing OK" + +If 404 or wrong content: subdomain DNS or Apache vhost config needs adjusting (user contacts Variomedia or fixes in customer panel). Do not proceed to Task 23 until this works. + +--- + +## Task 23: Visual Build Audit + +A sanity check before the full deploy: does the local build look like what we want? + +- [ ] **Step 1: Run `zola serve`** + +```fish +zola serve +``` + +- [ ] **Step 2: Visit every page locally** + +In browser at `http://127.0.0.1:1111`: + - `/` — carousel, news, services grid, team preview, notfall, map + - `/services/` — all 6 service sub-sections rendered with anchors + - `/services/#HV` — page scrolls to HV section + - `/team/` — all team subsections + - `/legal/` — Impressum content + - `/contact/` — contact info + map + - `/news/` — standalone news page + +- [ ] **Step 3: Test interactive elements** + + - Carousel: indicators clickable, slides advance + - Mobile burger menu: shrink browser to <768px width, click burger, menu opens/closes + - Navbar dropdowns (if any): click parent, menu opens + +- [ ] **Step 4: Check browser console for errors** + +DevTools console should have no red errors. Yellow warnings are acceptable. + +- [ ] **Step 5: Total image weight** + +```fish +du -sh public/processed_images/ +``` + +Expected: under 5 MB. + +If anything is broken: fix in a follow-up commit before deploying. + +--- + +## Task 24: Full Staging Deploy — GATED + +- [ ] **Step 1: Remove the hello file from `static/`** + +```fish +rm static/_hello.html +``` + +- [ ] **Step 2: Build** + +```fish +zola build +``` + +- [ ] **Step 3: REQUEST USER CONFIRMATION** before deploy + +Pause and ask: +> "Ready to deploy the full Zola site to mycelium:public/praxis/. This replaces everything currently in that directory (including the test hello file). Confirm to proceed?" + +Wait for explicit confirmation. + +- [ ] **Step 4: Deploy** + +```fish +rsync -av --delete public/ mycelium:public/praxis/ +``` + +- [ ] **Step 5: Verify root URL** + +```fish +curl -sI https://preview.praxis-am-lienhardplatz.de/ +curl -s https://preview.praxis-am-lienhardplatz.de/ | grep -i '' +``` + +Expected: + - HTTP 200 + - Title contains "Praxis am Lienhardplatz" + +- [ ] **Step 6: Verify noindex is live** + +```fish +curl -s https://preview.praxis-am-lienhardplatz.de/ | grep -i robots +curl -s https://preview.praxis-am-lienhardplatz.de/robots.txt +``` + +Expected: + - `<meta name="robots" content="noindex,nofollow">` present + - `robots.txt` returns `Disallow: /` + +- [ ] **Step 7: Commit** + +```fish +git add -A +git commit -m "chore: prepare for full staging deploy" +git push +``` + +--- + +## Task 25: Visual Parity Verification + +This task closes Phase 1's main quality gate. Side-by-side comparison of the live Grav site and the new Zola staging site. + +- [ ] **Step 1: Open both sites side-by-side in browser** + + - Window 1: `https://www.praxis-am-lienhardplatz.de/` (Grav, live) + - Window 2: `https://preview.praxis-am-lienhardplatz.de/` (Zola, staging) + +- [ ] **Step 2: Per-page visual check at 3 viewports** + +For each page (`/`, `/services/`, `/team/`, `/legal/`, `/contact/`, `/news/`), open at three viewport widths via browser DevTools: + + - **375 px** (mobile) + - **768 px** (tablet) + - **1280 px** (desktop) + +Look for: + - Identical layout sections in the same order + - Identical heading text and structure + - Images appear in the same positions + - Carousel/news/services-grid visible in same positions on home + +Note any deviations. + +- [ ] **Step 3: Navbar HTML diff** + +```fish +curl -s https://www.praxis-am-lienhardplatz.de/ | sed -n '/<nav class="navbar/,/<\/nav>/p' > /tmp/grav-nav.html +curl -s https://preview.praxis-am-lienhardplatz.de/ | sed -n '/<nav class="navbar/,/<\/nav>/p' > /tmp/zola-nav.html +diff /tmp/grav-nav.html /tmp/zola-nav.html +``` + +Expected: minimal diff (whitespace and href URLs may differ; structural Bootstrap classes must match). + +- [ ] **Step 4: Carousel interaction test** + +On staging mobile + desktop: + - Click left/right indicators → slides change + - Wait → auto-advance after a few seconds (Bootstrap default) + - Touch-swipe on mobile (if device available) + +- [ ] **Step 5: Mobile burger test** + +Set viewport to 375 px. Click burger icon. Menu drawer appears. Click items, they link to correct pages. + +- [ ] **Step 6: Lighthouse run** + +In browser DevTools → Lighthouse → run for staging page on Desktop and Mobile. Record Performance, Accessibility, SEO, Best Practices scores. + +Compare against scores from the same Lighthouse run on the live Grav site. + +Expected: Zola staging Performance score ≥ Grav score (typically substantially higher because no PHP runtime overhead and smaller images). + +- [ ] **Step 7: Capture results** + +Note in a follow-up message to the user: + - Any visual deviations found + - Lighthouse score comparison + - Image weight reduction (compare `du -sh` of `public/processed_images/` against the 22 MB original) + +--- + +## Task 26: Sign-Off + +- [ ] **Step 1: Report to user** + +Summarize for the user: + - Staging URL is live and reachable + - All checks from Task 25 passed (or list any open items) + - Image weight reduction achieved + - Lighthouse improvement + +- [ ] **Step 2: Await user sign-off** + +Ask: +> "Phase 1 is complete pending your sign-off. Open issues from verification: +> [list any, or 'none']. Approve as 'Phase 1 done'?" + +- [ ] **Step 3: Final commit and push** + +```fish +git add -A +git commit -m "docs: Phase 1 sign-off — staging matches live" +git push +``` + +Phase 1 ends here. Production cutover is a separate, future plan. + +--- + +## Notes for the Implementer + +- **Never touch `~/grav/praxis/` on `mycelium`.** Production deploys are explicitly out of scope. Every `rsync` in this plan targets `~/public/praxis/` and nowhere else. +- **Confirm before every deploy.** Even if the previous deploy was approved in the same session, ask again. The user's invoicing process depends on controlled production delivery. +- **Tera ≠ Twig.** Most syntax matches, but watch for: `loop.index0`, `loop.first`, `get_section()`/`get_page()` instead of `page.children`, no automatic media object on pages. +- **Image paths in Markdown:** never reference `/static/images/...` — use the `img` shortcode. Otherwise images bypass `resize_image()` and the site bloats. +- **If `zola build` fails on a Tera error**, the line number in the error is in the template file. Most issues are mismatched `{% endfor %}`/`{% endif %}` or missing `{% endblock %}`. + diff --git a/docs/specs/2026-05-20-praxis-zola-rebuild-design.md b/docs/specs/2026-05-20-praxis-zola-rebuild-design.md new file mode 100644 index 0000000..0cf55b9 --- /dev/null +++ b/docs/specs/2026-05-20-praxis-zola-rebuild-design.md @@ -0,0 +1,258 @@ +# Praxis am Lienhardplatz — Static Rebuild on Zola + +**Status:** Approved design, ready for implementation planning +**Date:** 2026-05-20 +**Author:** Michael Schimmel (with Claude) +**Repo:** `http://192.168.178.103:3000/Brummel/praxis-page.git` (Gitea, LAN-only) + +## Context + +The website for the Hausärztliche Gemeinschaftspraxis am Lienhardplatz in Wuppertal-Vohwinkel (`www.praxis-am-lienhardplatz.de`) currently runs on Grav 1.4.3, a flat-file PHP CMS from 2018. The Grav installation has known unpatched CVEs, uses Bootstrap 3 (EOL 2019), and the previous developer's maintenance costs have become prohibitive. + +The site is purely informational: ten content pages, regularly-updated news section, image carousel, embedded map. No forms (the contact form defined in Grav was never published). No user accounts, no comments, no search. Single editor (Michael Schimmel); the practice team may need to update certain things (news, schedules) later but does not today. + +A read-only snapshot of the current Grav installation lives at `./orig/` (timestamps preserved). It is the source of truth for what must be reproduced. + +## Goals + +1. Replace the Grav-based site with a static site that produces the same visible result in Phase 1. +2. Eliminate the PHP/CMS attack surface and the maintenance cost of a Grav installation. +3. Establish a stack that can be operated reliably by an LLM, since the user does not intend to write code themselves. +4. Set up the project to evolve incrementally (modernization, live-status widget, team-editable admin UI) in later phases without rewriting from scratch. + +## Non-Goals (Phase 1) + +- Content rewrites or copy-edits. +- Visual modernization (typography, spacing, mobile-first redesign). +- Accessibility audit (WCAG) — done in Phase 2. +- New features: live "Arzt anwesend" status widget, admin UI, search, newsletter, multi-language, contact form. +- Production cutover. Phase 1 ends with a staging site behind a private URL; the live `praxis-am-lienhardplatz.de` is not touched. + +## Stack Choice: Zola + +Zola is a single-binary Rust static site generator with Markdown content, Tera templates (Jinja2-like), and built-in image processing, SCSS, and search. + +The choice was driven by three criteria: + +1. **LLM operability.** Tight conventions and a small surface area reduce the model's mistake budget. Tera syntax is close to Twig (which the current Grav theme uses) and to Jinja2 (well-represented in training data). +2. **Maintenance footprint.** A single 15 MB binary with no Node.js/npm dependency chain ages gracefully. The site builds reproducibly years later. +3. **Output guarantees.** Zola emits plain static HTML/CSS/JS, which is what the Variomedia shared webspace serves natively. No runtime dependency. + +Known caveat: Zola has been pre-1.0 since 2017 and the most recent release (v0.22.1, January 2025) is 16 months old at design time, despite ongoing commit activity. The current version is stable for the project's use; the migration risk in a worst-case "Zola is abandoned" scenario is bounded — the output is plain HTML and can be regenerated by any other SSG in a focused weekend of work. + +Alternatives considered: Eleventy (better team-handover story, broader plugin ecosystem; rejected because the user is sole operator and prefers single-binary stability), Hugo (similar single-binary philosophy with more releases; rejected because Go templates are harder for an LLM to write correctly than Tera), Astro (modern but Phase-1 overkill). + +## Architecture + +### Project Layout + +``` +praxis-page/ # Gitea repo root +├── config.toml # Site config (replaces user/config/*.yaml) +├── content/ # Markdown content (replaces user/pages/) +│ ├── _index.md # Home page (modular composition in [extra]) +│ ├── news.md # Standalone news page, included by home template +│ ├── services/ +│ │ ├── _index.md # Services list page (section index) +│ │ ├── hv.md +│ │ ├── corona.md +│ │ ├── vorsorge.md +│ │ ├── diagnostik.md +│ │ ├── kardiologie.md +│ │ └── diabetologie.md +│ ├── team/ +│ │ ├── _index.md +│ │ ├── doctors.md +│ │ ├── employees.md +│ │ └── jobs.md +│ ├── legal.md +│ └── contact.md +├── templates/ # Tera templates (replaces user/themes/praxis/templates/) +│ ├── base.html # Layout skeleton (was partials/base.html.twig) +│ ├── index.html # Home page with inline modular sections +│ ├── section.html # Section list page (services, team) +│ ├── page.html # Single page (legal, contact) +│ ├── partials/ +│ │ ├── head.html +│ │ └── navbar.html +│ └── shortcodes/ +│ ├── img.html # Image with resize + lazy-load +│ └── map.html # Google Maps iframe embed +├── static/ # Verbatim assets (no processing) +│ ├── images/ +│ │ ├── carousel/ +│ │ ├── team/ +│ │ ├── services/ +│ │ └── ... +│ ├── fonts/ # Montserrat, Roboto, Font Awesome +│ └── js/ +│ ├── jquery.min.js +│ └── bootstrap.min.js +├── sass/ +│ ├── main.scss # Imports the existing theme CSS verbatim +│ └── _praxis_theme.scss # Lifted from orig/user/themes/praxis/css/ +├── docs/ +│ ├── specs/ # This file lives here +│ └── plans/ # Implementation plans (next step) +└── README.md +``` + +### Content Migration Rules + +- **Frontmatter:** YAML → TOML. Standard Zola keys (`title`, `weight`, `template`) at top level; everything else under `[extra]`. +- **Numbering prefixes:** Grav's `01.`, `02.` directory prefixes drop from filesystem names; ordering moves to `weight = N` in frontmatter. +- **Modular Grav directories** (`_carousel/`, `_about/`, `_services/`, `_team/`, `_notfall/` under `01.home/`): translate to entries in `content/_index.md`'s `[extra]` block. The home template renders these inline. Exception: `_news/` content lives in `content/news.md` as a standalone page so it can be edited (and later admin-editable) independently. +- **Sub-pages of `02.services/` and `03.team/`:** each becomes a standalone Markdown file in the corresponding section directory. The section template iterates the section's pages and renders them as stacked anchored sections (`<section id="HV">`, etc.) on a single long page, matching the current Grav behavior. +- **Image paths:** rewrite from Grav's page-relative references (`/home/_services/HV.jpg`) to flat paths under `/images/` (`/images/services/HV.jpg`). +- **Internal anchor links** (e.g., `/services#HV`): preserved unchanged. The `<section id="HV">` rendering in `section.html` keeps them functional. +- **Inline HTML and Markdown tables:** carried over verbatim. + +### Template Migration + +The Grav theme is 11 Twig templates, 373 lines total. Migration is largely a syntactic substitution from Twig to Tera: + +| Grav/Twig | Zola/Tera | +|---|---| +| `page.header.X` | `page.extra.X` | +| `page.children.visible` | `section.pages` (filtered for `draft = false`) | +| `page.media.images[name].html()` | `{{ img(src="...", width=...) }}` shortcode | +| `loop.index - 1` | `loop.index0` | +| `loop.index == 1` | `loop.first` | +| `{% include "X.html.twig" %}` | `{% include "X.html" %}` | +| `config.plugins.email.X` | removed (no email plugin) | + +The complex piece is `partials/navbar.html` (73 lines), which renders a recursive dropdown menu. Its HTML output must be byte-identical to Grav's for Bootstrap 3's mobile burger menu to keep working. Verification approach: HTML diff of the navbar fragment between Grav-served and Zola-served pages. + +Modular templates (carousel, about, employees, map, news, text) inline into `templates/index.html` rather than living as separately includable partials. They are only used on the home page and don't benefit from reuse. + +### Image Handling + +Total image weight in the source is 22 MB across 126 files, with carousel slides at 1-2 MB each (5000+ pixels wide) and team portraits at 4.7 MB. Grav was serving cached resized versions; the static rebuild must replicate this or worse-than-Grav latency results. + +Approach: + +- Source images in `static/images/<section>/`. +- A Tera shortcode `img` wraps Zola's built-in `resize_image()` function: + + ```tera + {{ img(src="team/Christian.jpg", width=400, alt="...") }} + ``` + + emits a processed image at the requested width plus `<img width height loading="lazy" alt="...">`. Processed output goes under `processed_images/` with hashed filenames (Zola's default). + +- Standard widths per context, hard-coded in the shortcode (no magic numbers in Markdown): + - Carousel hero: 1920w + - Team portrait: 400w + - Service inline image: 600w + - Default fallback: 800w +- JPEG quality 85. No WebP / AVIF / `srcset` in Phase 1. +- All `<img>` tags carry explicit `width` and `height` attributes to prevent layout shift. + +Expected reduction: ~22 MB → ~3-5 MB across the site. + +### Bootstrap-3 Preservation + +Bootstrap 3.3.6 CSS and JS plus jQuery are lifted verbatim from `orig/user/themes/praxis/bootstrap-3.3.6/dist/` into `static/css/` and `static/js/`. The viewport meta tag, navbar mobile-toggle markup, carousel markup, and Bootstrap responsive utility classes (`hidden-xs`, `col-md-*`, etc.) are reproduced identically in the Tera templates. This preserves mobile responsiveness, carousel behavior, dropdown menus, and the burger menu without any reimplementation. + +Modernizing away from Bootstrap 3 is a Phase-2 concern. + +### Map Embed + +The Grav `google-maps` plugin is replaced by a simple `<iframe>` Google Maps embed inside a `map` shortcode. No API key required, no plugin runtime, identical visual result. + +## Build & Deploy + +### Local Development + +Prerequisites: Zola installed (`pacman -S zola` on Arch/CachyOS — already verified present). + +Workflow: + +```fish +git clone http://192.168.178.103:3000/Brummel/praxis-page.git +cd praxis-page +zola serve # local preview at http://127.0.0.1:1111, auto-reload +zola build # generates ./public/ +``` + +### Staging Deploy + +The new site deploys to `~/public/praxis/` on the Variomedia webspace (`mycelium`), which the user has configured as the document root for the subdomain `preview.praxis-am-lienhardplatz.de`. The directory was empty at project start. + +Deploy command: + +```fish +zola build +rsync -av --delete public/ mycelium:public/praxis/ +``` + +The `--delete` flag removes orphaned files at the destination so the deployed directory exactly matches the local build output. + +**Deploy is a gated step.** Claude must obtain explicit confirmation in the current session before executing the rsync, every time, even after prior approvals in the same session. This is to align with the user's invoicing workflow, which depends on production delivery timing being controlled. + +### Privacy of the Staging URL + +Three layers of protection, none of them strong on their own but adequate combined for sharing the URL selectively: + +1. **`noindex` meta tag** on every page (rendered from `base.html`): `<meta name="robots" content="noindex,nofollow">`. Keeps Google and other search engines from listing the staging site. +2. **`robots.txt`** at the staging document root with `User-agent: *` / `Disallow: /`. +3. **Unguessable subdomain.** `preview.praxis-am-lienhardplatz.de` is harder to discover than a public-facing path. Anyone who has the URL can access it; anyone who doesn't, can't easily guess it. + +HTTP Basic Auth via `.htaccess` is intentionally not added in Phase 1 because it complicates link sharing for the practice team review. It can be layered on later if needed. + +### Production Cutover (Out of Scope for Phase 1) + +When the user explicitly authorizes it (as a separate, billed step), the path forward is: + +1. Move the current Grav installation from `~/grav/praxis/` to `~/grav/praxis_grav_archive_YYYY-MM-DD/`. +2. Copy or symlink the contents of `~/public/praxis/` (or rebuild directly) into `~/grav/praxis/`. (Apache vhost still points the live domain at the Grav directory, so the docroot path stays the same; only the contents change.) +3. Remove `noindex` meta and `Disallow: /` robots entry. + +This is not part of Phase 1 and not designed in detail here. + +## Definition of Done (Phase 1) + +1. All ten pages reachable under `preview.praxis-am-lienhardplatz.de/[…]`. +2. Visual parity verified at three viewport widths (375, 768, 1280 px) by side-by-side comparison against the live Grav site. No obvious deviations. +3. Interactive elements working: carousel autoplay and click navigation, mobile burger toggle, dropdown menus, Google Maps embed loads. +4. Lighthouse Performance score ≥ Grav baseline (expected to be substantially higher). +5. Total served image weight < 5 MB. +6. `noindex` verified via `curl -I` and `robots.txt` blocks crawlers. +7. Live site `praxis-am-lienhardplatz.de` unchanged from project start. +8. User sign-off. + +## Risks & Mitigations + +| Risk | Mitigation | +|---|---| +| Subdomain DNS not propagated or vhost misconfigured | First deploy is a single `hello.html` to confirm routing before content migration | +| Navbar HTML drift breaks Bootstrap 3 mobile burger | HTML diff of the navbar fragment between Grav and Zola output is a required check | +| Image resize regression (banding, blurring) | Spot-check 2-3 representative images side-by-side in browser at target widths | +| Tera vs. Twig edge cases (whitespace, filter behavior) | Side-by-side visual verification and HTML diffs catch these | +| "Looks 95% right but something's off" | HTML diff per page surfaces structural deltas quickly | +| Accidentally touching the live site | Deploy commands target `~/public/praxis/` only; production path requires explicit per-step confirmation | + +## Out of Scope / Phase 2+ + +The following are explicitly deferred. None of them block Phase 1 acceptance. + +- **Modernization:** typography, spacing, mobile-first redesign, accessibility (WCAG audit), drop Bootstrap 3 dependency. +- **Live "Arzt anwesend" widget:** a `status.json` updated by a separate process plus client-side `fetch` in the page. Stack-independent. +- **Admin UI for team-editable content:** likely Decap CMS or a custom lightweight editor for the news file and a handful of other fields. +- **Image format upgrades:** WebP, AVIF, responsive `srcset`. +- **Auto-deploy:** Gitea webhook triggers a build/deploy pipeline. +- **Search functionality.** +- **Newsletter / mailing list.** +- **Contact form** (was never published in Grav either). +- **Multi-language support.** +- **Content rewrites or copy improvements.** + +Each of these gets its own spec when prioritized. + +## Open Questions + +None blocking. The following are tactical decisions that can be made during implementation without changing the design: + +- Exact directory layout under `static/images/` (mirror Grav's `_carousel/`, `_team/` subdirs, or reorganize by semantic category — implementation detail). +- Whether to pull `content/news.md` into the home template via `load_data(format="markdown")` or via `get_page("/news/")` and rendering `page.content` (both work; pick the simpler one when writing the template). +- Exact `weight` numbering scheme (start at 10, 20, 30… or 1, 2, 3…).