46 KiB
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 <iframe> embed.
Reference: Design spec at docs/specs/2026-05-20-praxis-zola-rebuild-design.md. Read-only source snapshot at orig/.
Working directory: /home/brummel/dev/praxis/ is the repo root.
File Structure
Created in this plan:
/home/brummel/dev/praxis/ # repo root (git init here)
├── .gitignore # Task 1
├── config.toml # Task 3
├── README.md # Task 2
├── content/
│ ├── _index.md # Task 12
│ ├── news.md # Task 14
│ ├── services/
│ │ ├── _index.md # Task 15
│ │ ├── hv.md # Task 16
│ │ ├── corona.md # Task 16
│ │ ├── vorsorge.md # Task 16
│ │ ├── diagnostik.md # Task 16
│ │ ├── kardiologie.md # Task 16
│ │ └── diabetologie.md # Task 16
│ ├── team/
│ │ ├── _index.md # Task 18
│ │ ├── doctors.md # Task 18
│ │ ├── employees.md # Task 18
│ │ └── jobs.md # Task 18
│ ├── legal.md # Task 19
│ └── contact.md # Task 19
├── templates/
│ ├── base.html # Task 9
│ ├── index.html # Task 13
│ ├── section.html # Task 17
│ ├── page.html # Task 19
│ ├── partials/
│ │ ├── head.html # Task 10
│ │ └── navbar.html # Task 11
│ └── shortcodes/
│ ├── img.html # Task 8
│ └── map.html # Task 20
├── static/
│ ├── images/ # Task 5
│ ├── fonts/ # Task 6
│ ├── css/ # Task 6 (verbatim Bootstrap + theme CSS)
│ ├── js/ # Task 5 (jQuery, Bootstrap, flexSlider, etc.)
│ └── robots.txt # Task 21
├── sass/
│ ├── main.scss # Task 7 (entry point)
│ └── _praxis_theme.scss # Task 7 (lifted from theme)
└── docs/
├── specs/ # exists already
└── plans/ # exists already (this file lives here)
Not in the repo (gitignored):
orig/— read-only snapshot of the Grav site, used as reference.superpowers/— brainstorming session artifactspublic/— Zola build outputprocessed_images/— Zola image cache
Task 1: Repo Bootstrap
Files:
-
Create:
/home/brummel/dev/praxis/.gitignore -
Modify: git remote in
/home/brummel/dev/praxis/.git/config -
Step 1: Initialize git repo and configure remote
cd /home/brummel/dev/praxis
git init -b main
git remote add origin http://192.168.178.103:3000/Brummel/praxis-page.git
- Step 2: Write
.gitignore
# 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/
- Step 3: Verify git sees only intended files
Run: git status --short
Expected: lists docs/specs/2026-05-20-praxis-zola-rebuild-design.md and docs/plans/2026-05-20-praxis-zola-rebuild.md and .gitignore, nothing from orig/ or .superpowers/.
- Step 4: Stage and commit
git add .gitignore docs/specs/2026-05-20-praxis-zola-rebuild-design.md docs/plans/2026-05-20-praxis-zola-rebuild.md
git commit -m "chore: bootstrap repo with design spec and implementation plan"
- Step 5: Push to Gitea
git push -u origin main
Expected: push succeeds. If authentication prompts appear, the user provides credentials or has them cached.
Task 2: README
Files:
-
Create:
README.md -
Step 1: Write README
# Praxis am Lienhardplatz — Static Site
Static rebuild of the practice website, replacing the previous Grav 1.4.3 installation.
## Build
```fish
zola build # production build → ./public/
zola serve # dev server with auto-reload → http://127.0.0.1:1111
Deploy (staging)
zola build
rsync -av --delete public/ mycelium:public/praxis/
Staging URL: https://preview.praxis-am-lienhardplatz.de
Layout
content/— Markdown pagestemplates/— Tera templatesstatic/— verbatim assets (JS, CSS, fonts, images)sass/— SCSS sources (compiled by Zola)docs/specs/— design specsdocs/plans/— implementation plans
The directory orig/ (gitignored) is a read-only snapshot of the previous Grav installation, kept as a reference for content and visual parity.
- [ ] **Step 2: Commit**
```fish
git add README.md
git commit -m "docs: add README with build and deploy instructions"
Task 3: Zola Skeleton
Files:
-
Create:
config.toml -
Create:
content/_index.md(placeholder) -
Step 1: Write
config.toml
base_url = "https://preview.praxis-am-lienhardplatz.de"
title = "Praxis am Lienhardplatz"
description = "Dr. Oliver Riemann, Dr. Angela Krey und Christian Malyga begrüßen Sie in der Hausärztlichen Gemeinschaftspraxis am Lienhardplatz in Wuppertal-Vohwinkel"
default_language = "de"
compile_sass = true
build_search_index = false
generate_feeds = false
# Image processing
[imageprocessing]
# Defaults are fine; img shortcode controls per-image
[markdown]
highlight_code = false
external_links_target_blank = false
smart_punctuation = false
[extra]
# Site-wide values pulled into templates
practice_name = "Praxis am Lienhardplatz"
doctors = "Dr. Oliver Riemann, Dr. Angela Krey, Christian Malyga"
address = "Lienhardplatz 1, 42329 Wuppertal-Vohwinkel"
phone = "+49 202 123456" # TODO replace with actual number from contact page during Task 19
- Step 2: Write minimal
content/_index.md
+++
title = "Home"
template = "index.html"
+++
Skeleton placeholder. Real content lands in Task 12.
- Step 3: Create a minimal
templates/index.htmlso build doesn't fail
<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>{{ config.title }}</title></head>
<body>
<h1>{{ config.title }}</h1>
<p>{{ page.content | safe }}</p>
</body>
</html>
- Step 4: Verify
zola servestarts and serves the placeholder
zola serve
Expected: console says "Web server is available at http://127.0.0.1:1111". Open browser, see "Praxis am Lienhardplatz" heading. Stop with Ctrl-C.
- Step 5: Verify
zola build
zola build
ls public/
Expected: public/index.html and public/sitemap.xml exist. No errors.
- Step 6: Commit
git add config.toml content/_index.md templates/index.html
git commit -m "feat: zola skeleton with placeholder index"
Task 4: Site Title Placeholder Phone Number
Files:
-
Read:
orig/user/pages/10.contact/modular.md,orig/user/pages/10.contact/_contact/contact.md, plus any phone references inorig/user/pages/ -
Step 1: Find the practice's actual phone number in the orig snapshot
grep -rE '(0202|\+49)' orig/user/pages/ | head -20
Expected: locates a phone number string used on the contact or home page.
- Step 2: Update
config.tomlextra.phonewith the real number
Edit config.toml, replace the TODO placeholder.
- Step 3: Commit
git add config.toml
git commit -m "chore: set practice phone number in site config"
Task 5: Copy JS Assets
The Grav praxis theme inherits from x-corporation, so JS lives in both themes' directories. The live site loads (in order): jQuery → smoothscroll → mobile-menu → flexSlider → scripts.js → Bootstrap.
Files:
-
Create:
static/js/jquery-2.1.4.min.js(fromorig/user/themes/x-corporation/js/) -
Create:
static/js/smoothscroll.js -
Create:
static/js/mobile-menu.js -
Create:
static/js/scripts.js -
Create:
static/js/jquery.flexslider-min.js(fromorig/user/themes/x-corporation/js/flexSlider/) -
Create:
static/js/bootstrap.min.js(fromorig/user/themes/praxis/bootstrap-3.3.6/dist/js/) -
Step 1: Copy JS files
mkdir -p static/js
cp orig/user/themes/x-corporation/js/jquery-2.1.4.min.js static/js/
cp orig/user/themes/x-corporation/js/smoothscroll.js static/js/
cp orig/user/themes/x-corporation/js/mobile-menu.js static/js/
cp orig/user/themes/x-corporation/js/scripts.js static/js/
cp orig/user/themes/x-corporation/js/flexSlider/jquery.flexslider-min.js static/js/
cp orig/user/themes/praxis/bootstrap-3.3.6/dist/js/bootstrap.min.js static/js/
- Step 2: Verify
ls -la static/js/
Expected: six .js files, total a few hundred KB.
- Step 3: Commit
git add static/js/
git commit -m "feat: import JS assets from x-corporation and praxis themes"
Task 6: Copy CSS, Fonts, Images
Files:
-
Create:
static/css/bootstrap.min.css(fromorig/user/themes/praxis/bootstrap-3.3.6/dist/css/) -
Create:
static/css/bootstrap-theme.min.css(from same) -
Create:
static/css/mobile-menu.css(fromorig/user/themes/x-corporation/css/) -
Create:
static/css/flexslider.css(fromorig/user/themes/x-corporation/js/flexSlider/) -
Create:
static/fonts/populated with Font Awesome and any web fonts -
Create:
static/images/with all imagery fromorig/ -
Step 1: Copy CSS files
mkdir -p static/css
cp orig/user/themes/praxis/bootstrap-3.3.6/dist/css/bootstrap.min.css static/css/
cp orig/user/themes/praxis/bootstrap-3.3.6/dist/css/bootstrap-theme.min.css static/css/
cp orig/user/themes/x-corporation/css/mobile-menu.css static/css/
cp orig/user/themes/x-corporation/js/flexSlider/flexslider.css static/css/
- Step 2: Copy fonts
mkdir -p static/fonts
cp -r orig/user/themes/x-corporation/fonts/font-awesome static/fonts/
cp orig/user/themes/praxis/fonts/montserrat.css static/fonts/ # if present
cp orig/user/themes/praxis/fonts/roboto.css static/fonts/ # if present
# Copy any actual font files (woff, ttf, etc.) referenced by these CSS files:
find orig/user/themes -type d -name "fonts" -exec cp -rn {} static/ \;
Expected: static/fonts/font-awesome/ exists with css/ and fonts/ subdirectories.
- Step 3: Copy images, organized by section
mkdir -p static/images/carousel static/images/team static/images/services static/images/general
# Carousel
cp orig/user/pages/01.home/_carousel/*.jpg static/images/carousel/ 2>/dev/null || true
# Team
cp orig/user/pages/01.home/_team/*.jpg static/images/team/ 2>/dev/null || true
cp orig/user/pages/03.team/01._doctors/*.jpg static/images/team/ 2>/dev/null || true
cp orig/user/pages/03.team/02._employees/*.jpg static/images/team/ 2>/dev/null || true
# Services
cp orig/user/pages/01.home/_services/*.jpg static/images/services/ 2>/dev/null || true
find orig/user/pages/02.services -name "*.jpg" -exec cp {} static/images/services/ \;
# Anything else under user/pages
find orig/user/pages -name "*.jpg" -o -name "*.png" 2>/dev/null | while read f; do
base=$(basename "$f")
cp -n "$f" static/images/general/ # -n: don't overwrite
done
- Step 4: Verify
du -sh static/
find static -type f | wc -l
Expected: static/ is under 50 MB. File count is in the hundreds (mostly font glyphs and images).
- Step 5: Commit
git add static/
git commit -m "feat: import CSS, fonts, and images"
Task 7: SCSS Entry Point
The Grav theme's custom CSS (style.css, _style.css) gets lifted into the SCSS sources so Zola compiles it together with the rest.
Files:
-
Create:
sass/main.scss -
Create:
sass/_praxis_theme.scss -
Step 1: Copy theme CSS as SCSS partial
mkdir -p sass
cp orig/user/themes/praxis/css/style.css sass/_praxis_theme.scss
cp orig/user/themes/praxis/css/_style.css sass/_praxis_partial.scss 2>/dev/null || true
- Step 2: Write
sass/main.scss
// Praxis am Lienhardplatz — theme entry point
//
// Compiled by Zola into /main.css and loaded by base.html.
// Bootstrap, mobile-menu, flexslider, and Font Awesome are loaded as separate
// <link> tags from /css/ (Task 10), so we don't import them here.
@import "praxis_theme";
- Step 3: Verify Zola compiles SCSS
zola build
ls public/main.css
Expected: public/main.css exists and is non-empty.
- Step 4: Commit
git add sass/
git commit -m "feat: lift praxis theme CSS into sass entry point"
Task 8: Image Shortcode
Files:
-
Create:
templates/shortcodes/img.html -
Step 1: Write the shortcode
{# Image shortcode with width-based resize and lazy loading.
Usage in markdown:
{{ img(src="team/Olli.jpg", width=400, alt="Dr. Riemann") }}
{{ img(src="carousel/page1.jpg", width=1920, alt="Praxis") }}
Class is optional, e.g. class="float-right". #}
{%- set src = src | trim_start_matches(pat="/") -%}
{%- set source_path = "static/images/" ~ src -%}
{%- set resized = resize_image(path=source_path, width=width, op="fit_width", quality=85) -%}
<img
src="{{ resized.url }}"
width="{{ resized.width }}"
height="{{ resized.height }}"
alt="{{ alt | default(value="") }}"
{%- if class %} class="{{ class }}"{% endif %}
loading="lazy"
>
- Step 2: Sanity-test the shortcode with a sample image
Add a temporary call in content/_index.md body:
{{ img(src="team/Olli.jpg", width=400, alt="Dr. Riemann") }}
(Use any image actually present in static/images/team/ — verify with ls static/images/team/.)
- Step 3: Build and verify
zola build
grep -A1 '<img' public/index.html | head -5
ls processed_images/ | head -5
Expected: <img src="/processed_images/.../*.jpg" width=400 height=... loading="lazy"> appears in HTML. The processed_images/ directory is created with resized JPEG(s).
- Step 4: Remove the test call from
_index.md
Revert content/_index.md to its skeleton state from Task 3.
- Step 5: Commit
git add templates/shortcodes/img.html content/_index.md
git commit -m "feat: img shortcode with resize_image and lazy loading"
Task 9: Base Layout Template
Files:
-
Create:
templates/base.html -
Step 1: Write
templates/base.html(port ofpartials/base.html.twig)
<!DOCTYPE HTML>
<html lang="de">
<head>
{% include "partials/head.html" %}
</head>
<body>
<div id="main-wrapper">
<div id="preloader">
<div id="status">
<div class="status-mes"></div>
</div>
</div>
<div class="uc-mobile-menu-pusher">
<div class="content-wrapper">
{% include "partials/navbar.html" %}
{% block content %}{% endblock %}
</div>
</div>
<div class="uc-mobile-menu uc-mobile-menu-effect">
<button type="button" class="close" aria-hidden="true" data-toggle="offcanvas"
id="uc-mobile-menu-close-btn">×</button>
<div>
<div>
<ul id="menu">
{% set top = get_section(path="_index.md") %}
{% for p in top.subsections %}
{% set sub = get_section(path=p) %}
<li><a href="{{ sub.permalink }}">{{ sub.title }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
<script src="/js/jquery-2.1.4.min.js"></script>
<script src="/js/smoothscroll.js"></script>
<script src="/js/mobile-menu.js"></script>
<script src="/js/jquery.flexslider-min.js"></script>
<script src="/js/scripts.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
Note: script load order matches the Grav theme's head.html.twig comment ("REIHENFOLGE IST SUPERWICHTIG!!").
- Step 2: Commit
git add templates/base.html
git commit -m "feat: base layout template"
Task 10: Head Partial (with noindex for staging)
Files:
-
Create:
templates/partials/head.html -
Step 1: Write
templates/partials/head.html
<title>{% if page.title %}{{ page.title | safe }} | {% endif %}{{ config.title | safe }}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# STAGING: keep search engines out. Remove this line at production cutover. #}
<meta name="robots" content="noindex,nofollow">
<meta name="description" content="{{ config.description | safe }}">
<meta name="author" content="{{ config.extra.practice_name | safe }}">
<link rel="stylesheet" href="/fonts/roboto.css">
<link rel="stylesheet" href="/fonts/montserrat.css">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="/css/mobile-menu.css">
<link rel="stylesheet" href="/css/flexslider.css">
<link rel="stylesheet" href="/main.css">
- Step 2: Build and curl to verify noindex appears
zola build
grep -i 'robots' public/index.html
Expected: shows the noindex,nofollow meta tag.
- Step 3: Commit
git add templates/partials/head.html
git commit -m "feat: head partial with noindex meta for staging"
Task 11: Navbar Partial
This is the most complex template — recursive nested menu with dropdown logic. The HTML output must be byte-identical to Grav's so Bootstrap 3's mobile burger keeps working.
Files:
-
Create:
templates/partials/navbar.html -
Step 1: Write
templates/partials/navbar.html(port ofpartials/navbar.html.twig)
{%- macro nav_items(section) -%}
{%- for p in section.subsections -%}
{%- set sub = get_section(path=p) -%}
{%- set has_children = sub.subsections | length > 0 -%}
{%- if has_children -%}
<li class="dropdown m-menu-fw{% if current_url and current_url is starting_with(sub.permalink) %} active{% endif %}">
<a href="#" data-toggle="dropdown" class="dropdown-toggle">{{ sub.title }}
<span><i class="fa fa-angle-down"></i></span></a>
<ul class="dropdown-menu">
{%- for cp in sub.subsections -%}
{%- set child = get_section(path=cp) -%}
<li class="m-menu-content">
<ul class="col-sm-{{ (12 / (sub.subsections | length)) | round }}">
<li class="dropdown-header">{{ child.title }}</li>
{%- for cpage in child.pages -%}
<li><a href="{{ cpage.permalink }}">{{ cpage.title }}</a></li>
{%- endfor -%}
</ul>
</li>
{%- endfor -%}
</ul>
</li>
{%- else -%}
<li class="{% if current_url and current_url is starting_with(sub.permalink) %}active{% endif %}">
<a href="{{ sub.permalink }}">{{ sub.title }}</a>
</li>
{%- endif -%}
{%- endfor -%}
{%- endmacro -%}
<nav class="navbar m-menu navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">{{ config.title }}</a>
</div>
<div class="collapse navbar-collapse" id="navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
{% set top = get_section(path="_index.md") %}
{{ self::nav_items(section=top) }}
</ul>
</div>
</div>
</nav>
- Step 2: Build and inspect navbar output
zola build
sed -n '/<nav class="navbar/,/<\/nav>/p' public/index.html
Expected: nav structure exists. Items will only be populated once the section pages exist (Tasks 15-19). At this point the loop iterates over zero subsections — that's fine.
- Step 3: Diff against live navbar (for later reference, save baseline)
curl -s https://www.praxis-am-lienhardplatz.de/ | sed -n '/<nav class="navbar/,/<\/nav>/p' > /tmp/grav-navbar.html
This file is the reference for comparison in Task 25.
- Step 4: Commit
git add templates/partials/navbar.html
git commit -m "feat: navbar partial with recursive dropdown menu"
Task 12: Home Page Content
Files:
-
Modify:
content/_index.md -
Step 1: Read source data
cat orig/user/pages/01.home/_carousel/carousel.md
cat orig/user/pages/01.home/_about/about.md
cat orig/user/pages/01.home/_services/services.md
cat orig/user/pages/01.home/_team/*.md 2>/dev/null
cat orig/user/pages/01.home/_notfall/*.md 2>/dev/null
- Step 2: Write
content/_index.mdwith all home modular data
+++
title = "Willkommen"
template = "index.html"
[extra]
# Carousel slides (was orig/.../01.home/_carousel/carousel.md → slides:)
[[extra.slides]]
heading = "Willkommen in unserer Praxis am Lienhardplatz!"
image = "carousel/page1.jpg"
alt = "Willkommen in unserer Praxis"
[[extra.slides]]
heading = "Wir freuen uns auf Ihren Besuch!"
image = "carousel/page2.jpg"
alt = "Das Praxis-Team"
# About columns (was _about/about.md → columns:)
[[extra.about_columns]]
text = """
**Auf den folgenden Seiten** wollen wir Ihnen unsere hausärztliche Gemeinschaftspraxis vorstellen.
Sie finden wichtige Informationen, erfahren alles über das Leistungsspektrum der Praxis und lernen unser Team kennen.
Als moderne, kompetente hausärztliche Gemeinschaftspraxis möchten wir Sie in einem vertrauensvollen und persönlichen Umfeld in allen Bereichen Ihrer Gesundheit begleiten. Im Mittelpunkt stehen Sie als Patientin oder Patient mit Ihren individuellen Bedürfnissen, Beschwerden und ihrer speziellen Lebenssituation.
"""
[[extra.about_columns]]
text = """
**Die Basis unseren Handelns** ist die wissenschaftlich begründete Medizin. Neben dem kompletten Spektrum der hausärztlichen Versorgung liegen besondere Tätigkeitsschwerpunkte unserer Praxis im Bereich der Diabetologie, der Kardiologie und der Hypertensiologie (Diagnostik und Behandlung der Bluthochdruckerkrankungen).
Ein besonderes Anliegen ist uns die Prävention, sowie die Motivation unserer Patientinnen und Patienten, einen gesundheitsfördernden Lebensstil zu führen.
Wir freuen uns Sie im Herzen von Vohwinkel begrüßen zu dürfen!
"""
# Service grid cards (was _services/services.md → services:)
[[extra.service_cards]]
title = "Hausärztliche Versorgung"
description = "Wir sind Ihr erster Ansprechpartner in allen gesundheitlichen Fragen!"
url = "/services#HV"
image = "services/HV.jpg"
[[extra.service_cards]]
title = "Corona-Virus"
description = "Schnelltests, PCR-Tests und Impfungen."
url = "/services#Corona"
image = "services/Corona.jpg"
[[extra.service_cards]]
title = "Vorsorge"
description = "Gesundheits-Check, Krebsvorsorge und Impfungen."
url = "/services#Vorsorge"
image = "services/Vorsorge.jpg"
[[extra.service_cards]]
title = "Diagnostik"
description = "Lungenfunktionsprüfung, EKG-Diagnostik, Ultraschall- und Laboruntersuchungen."
url = "/services#Diagnostik"
image = "services/Diagnostik.jpg"
[[extra.service_cards]]
title = "Kardiologie"
description = "Diagnostik und Therapie des Herzkreislaufsystems."
url = "/services#Kardio"
image = "services/Kardio.jpg"
[[extra.service_cards]]
title = "Diabetologie"
description = "Schulung und intensive Betreuung von Diabetikern im Rahmen des DMP für Diabetes."
url = "/services#Diabetologie"
image = "services/Diabetologie.jpg"
# Team preview (small grid of doctor portraits on the home page)
# Fill exactly with what the orig/_team/ folder has — read its actual data structure first
# and translate each portrait into a [[extra.team_preview]] entry with name, role, image.
# Notfall block — short emergency-contact paragraph for the home page bottom.
# Read orig/.../01.home/_notfall/*.md for the verbatim text.
+++
{# The body below is the "Notfall" section text. Translate the markdown verbatim
from orig/user/pages/01.home/_notfall/*.md once you've read it. #}
## Notfall
(Body text from orig/user/pages/01.home/_notfall/ goes here verbatim.)
- Step 3: Fill in
[[extra.team_preview]]and Notfall body
Read the actual contents of orig/user/pages/01.home/_team/*.md and orig/user/pages/01.home/_notfall/*.md and add the corresponding TOML / markdown to _index.md.
- Step 4: Verify build
zola build
Expected: no errors. public/index.html exists with placeholder template output (templating the modulars comes in Task 13).
- Step 5: Commit
git add content/_index.md
git commit -m "feat: home page content with modular data in extra block"
Task 13: Home Page Template
Files:
-
Modify:
templates/index.html(replacing the skeleton from Task 3) -
Step 1: Write the full home template
{% extends "base.html" %}
{% block content %}
{# ───────── Carousel ───────── #}
<div id="x-corp-carousel" class="carousel slide hero-slide" data-ride="carousel">
<ol class="carousel-indicators">
{% for slide in page.extra.slides %}
<li data-target="#x-corp-carousel"
data-slide-to="{{ loop.index0 }}"
class="{% if loop.first %}active{% endif %}"></li>
{% endfor %}
</ol>
<div class="carousel-inner" role="listbox">
{% for slide in page.extra.slides %}
<div class="item{% if loop.first %} active{% endif %}">
{{ img(src=slide.image, width=1920, alt=slide.alt) }}
<div class="carousel-caption">
<p>{{ slide.heading }}</p>
</div>
</div>
{% endfor %}
</div>
<a class="left carousel-control" href="#x-corp-carousel" role="button" data-slide="prev">
<i class="fa fa-angle-left" aria-hidden="true"></i>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#x-corp-carousel" role="button" data-slide="next">
<i class="fa fa-angle-right" aria-hidden="true"></i>
<span class="sr-only">Next</span>
</a>
</div>
{# ───────── News (loaded from content/news.md) ───────── #}
{% set news_page = get_page(path="news.md") %}
<section class="ptb-100" id="aktuelles">
<div class="container">
{{ news_page.content | safe }}
</div>
</section>
{# ───────── About columns ───────── #}
<section class="ptb-100">
<div class="container">
<div class="row">
{% for col in page.extra.about_columns %}
<div class="col-md-6">{{ col.text | markdown | safe }}</div>
{% endfor %}
</div>
</div>
</section>
{# ───────── Service grid ───────── #}
<section class="ptb-100 bg-gray">
<div class="container">
<h2 class="text-center">Unsere Leistungen</h2>
<div class="row">
{% for card in page.extra.service_cards %}
<div class="col-md-4 col-sm-6">
<a href="{{ card.url }}" class="service-card">
{{ img(src=card.image, width=600, alt=card.title) }}
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</a>
</div>
{% endfor %}
</div>
</div>
</section>
{# ───────── Team preview ───────── #}
{% if page.extra.team_preview %}
<section class="ptb-100">
<div class="container">
<h2 class="text-center">Unser Team</h2>
<div class="row">
{% for member in page.extra.team_preview %}
<div class="col-md-4 col-sm-6 text-center">
{{ img(src=member.image, width=400, alt=member.name) }}
<h4>{{ member.name }}</h4>
<p>{{ member.role }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{# ───────── Notfall (body of _index.md) ───────── #}
<section class="ptb-100 bg-gray">
<div class="container">
{{ page.content | safe }}
</div>
</section>
{# ───────── Map ───────── #}
{{ map() }}
{% endblock %}
- Step 2: Verify build
zola build
Expected: build succeeds. Open public/index.html and visually scan — sections appear in order, image tags render, carousel markup is present.
Note: the {{ map() }} shortcode does not exist yet — replace with {# {{ map() }} #} (commented out) temporarily; uncomment in Task 20.
- Step 3: Local visual check
zola serve
Open http://127.0.0.1:1111 in browser. Carousel should be visible (Bootstrap JS isn't loaded yet because navbar pulls in scripts but flexSlider/etc might error — check browser console).
- Step 4: Commit
git add templates/index.html
git commit -m "feat: home page template with inline modular sections"
Task 14: News Page
Files:
-
Create:
content/news.md -
Step 1: Read source
cat orig/user/pages/01.home/_news/news.md
- Step 2: Write
content/news.mdwith verbatim translation
Frontmatter becomes:
+++
title = "Aktuelles"
template = "page.html"
weight = 5
+++
(Body markdown copied verbatim from orig/user/pages/01.home/_news/news.md,
including the date subscript, the headings, the embedded HTML table for
office hours, and the link to /services#Corona.)
- Step 3: Verify
zola build
grep -i "Aktuelles" public/news/index.html
Expected: news page renders standalone at /news/ AND the home page (Task 13) embeds the content via get_page("news.md").content.
- Step 4: Commit
git add content/news.md
git commit -m "feat: news page (Aktuelles)"
Task 15: Services Section Index
Files:
-
Create:
content/services/_index.md -
Step 1: Write services section index
+++
title = "Leistungen"
template = "section.html"
sort_by = "weight"
weight = 20
+++
(Optional intro text for the services overview page. If the original site has no
intro for /services/, leave the body empty — the section.html template renders
all child pages stacked anyway.)
- Step 2: Commit
git add content/services/_index.md
git commit -m "feat: services section index"
Task 16: Service Sub-Pages
Files:
-
Create:
content/services/hv.md -
Create:
content/services/corona.md -
Create:
content/services/vorsorge.md -
Create:
content/services/diagnostik.md -
Create:
content/services/kardiologie.md -
Create:
content/services/diabetologie.md -
Step 1: Migrate each service page
For each of HV, Corona, Vorsorge, Diagnostik, Kardiologie, Diabetologie:
- Read the source:
orig/user/pages/02.services/0N._<name>/<name>.md - Create
content/services/<name>.mdwith:
+++
title = "<Title from Grav frontmatter>"
weight = <10, 20, 30, 40, 50, 60>
[extra]
anchor = "<HV | Corona | Vorsorge | Diagnostik | Kardio | Diabetologie>"
+++
<body verbatim from Grav source, with image paths rewritten:
/home/_services/HV.jpg → use the img shortcode instead
example:
{{ img(src="services/HV.jpg", width=600, alt="Hausärztliche Versorgung", class="float-right") }}
>
The anchor values must match what the home page service-card URLs reference
(see content/_index.md [[extra.service_cards]].url from Task 12):
HV, Corona, Vorsorge, Diagnostik, Kardio, Diabetologie.
- Step 2: Commit
git add content/services/
git commit -m "feat: six service sub-pages with anchors matching home grid"
Task 17: Section Template
Files:
-
Create:
templates/section.html -
Step 1: Write
templates/section.html
{% extends "base.html" %}
{% block content %}
<section class="single-page-title">
<div class="container text-center hidden-xs">
<h2>{{ section.title }}</h2>
</div>
</section>
<section class="ptb-100">
<div class="container">
{% if section.content %}
<div class="row"><div class="col-md-12">{{ section.content | safe }}</div></div>
{% endif %}
{% for page in section.pages %}
<article id="{{ page.extra.anchor | default(value=page.slug) }}" class="anchor-section">
<h2 class="section-title">{{ page.title }}</h2>
{{ page.content | safe }}
</article>
{% endfor %}
</div>
</section>
{% endblock %}
- Step 2: Verify services renders all sub-pages with anchors
zola build
grep -E '<article id=' public/services/index.html
Expected: six <article> elements, IDs HV, Corona, Vorsorge, Diagnostik, Kardio, Diabetologie.
- Step 3: Verify anchor links from home work
In the built public/index.html, the service grid cards link to /services#HV etc. Combined with the services page having <article id="HV">, in-page navigation should work.
- Step 4: Commit
git add templates/section.html
git commit -m "feat: section template stacking subpages with anchors"
Task 18: Team Section and Pages
Files:
-
Create:
content/team/_index.md -
Create:
content/team/doctors.md -
Create:
content/team/employees.md -
Create:
content/team/jobs.md -
Step 1: Write team section index
+++
title = "Team"
template = "section.html"
sort_by = "weight"
weight = 30
+++
- Step 2: Migrate doctors page
Source: orig/user/pages/03.team/01._doctors/doctors.md
+++
title = "Ärzte"
weight = 10
[extra]
anchor = "doctors"
+++
(Body verbatim from source. Image references like /home/_team/Olli.jpg become
{{ img(src="team/Olli.jpg", width=400, alt="Dr. med. Oliver Riemann", class="float-right") }})
-
Step 3: Migrate employees and jobs pages (same pattern)
-
Step 4: Verify
zola build
ls public/team/
Expected: public/team/index.html lists all team members.
- Step 5: Commit
git add content/team/
git commit -m "feat: team section with doctors, employees, jobs"
Task 19: Legal, Contact, and Page Template
Files:
-
Create:
content/legal.md -
Create:
content/contact.md -
Create:
templates/page.html -
Step 1: Write
templates/page.html(port ofdefault.html.twig)
{% extends "base.html" %}
{% block content %}
<section class="single-page-title">
<div class="container text-center hidden-xs">
<h2>{{ page.title }}</h2>
</div>
</section>
<section class="ptb-100">
<div class="container">
<div class="row">
<div class="col-md-12">
{{ page.content | safe }}
</div>
</div>
</div>
</section>
{% endblock %}
- Step 2: Migrate legal page
Source: orig/user/pages/05.legal/default.md
+++
title = "Impressum"
template = "page.html"
weight = 50
+++
(Body verbatim from source.)
- Step 3: Migrate contact page
Source: orig/user/pages/10.contact/modular.md plus any actual contact info elsewhere (the form is intentionally omitted per spec).
+++
title = "Kontakt"
template = "page.html"
weight = 60
+++
## Praxis am Lienhardplatz
(Address, phone, email, opening hours — translate from the orig contact page
or from the practice's actual contact info. No form. Optional: embed map via
{{ map() }} once that shortcode exists (Task 20).)
- Step 4: Verify
zola build
ls public/legal/ public/contact/
Expected: public/legal/index.html and public/contact/index.html render with their content inside the single-page-title and main container sections.
- Step 5: Commit
git add content/legal.md content/contact.md templates/page.html
git commit -m "feat: legal, contact, and generic page template"
Task 20: Map Shortcode
Files:
-
Create:
templates/shortcodes/map.html -
Modify:
templates/index.html(uncomment{{ map() }}) -
Modify:
content/contact.md(add{{ map() }}if appropriate) -
Step 1: Look up the Google Maps embed URL for the practice
The address is Lienhardplatz 1, 42329 Wuppertal-Vohwinkel. Go to Google Maps in the browser, search for the address, click "Share" → "Embed a map", copy the iframe src URL.
For an unattended migration without manual browsing, use a generated URL:
https://www.google.com/maps/embed/v1/place?key=<EMBED_API_KEY>&q=Lienhardplatz+1,+42329+Wuppertal-Vohwinkel
Without an API key, fall back to:
https://maps.google.com/maps?q=Lienhardplatz+1,+42329+Wuppertal-Vohwinkel&t=&z=15&ie=UTF8&iwloc=&output=embed
(legacy embed URL, no key required, less customizable but stable enough for a static address marker).
- Step 2: Write
templates/shortcodes/map.html
<section class="map-section">
<iframe
src="https://maps.google.com/maps?q=Lienhardplatz+1,+42329+Wuppertal-Vohwinkel&t=&z=15&ie=UTF8&iwloc=&output=embed"
width="100%"
height="450"
style="border:0"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
title="Karte: Praxis am Lienhardplatz">
</iframe>
</section>
-
Step 3: Uncomment
{{ map() }}intemplates/index.html(was a comment in Task 13). -
Step 4: Verify
zola build
grep -A2 "map-section" public/index.html
Expected: iframe markup appears in the rendered home page.
- Step 5: Commit
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
zola build
cat public/robots.txt
Expected: file contents exactly as above.
- Step 3: Commit
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
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Routing OK</title></head>
<body><h1>Routing OK</h1>
<p>If you see this, preview.praxis-am-lienhardplatz.de routes to ~/public/praxis/.</p>
</body></html>
- Step 2: Build
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
rsync -av public/_hello.html mycelium:public/praxis/
- Step 5: Verify routing
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
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
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/
rm static/_hello.html
- Step 2: Build
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
rsync -av --delete public/ mycelium:public/praxis/
- Step 5: Verify root URL
curl -sI https://preview.praxis-am-lienhardplatz.de/
curl -s https://preview.praxis-am-lienhardplatz.de/ | grep -i '<title>'
Expected:
-
HTTP 200
-
Title contains "Praxis am Lienhardplatz"
-
Step 6: Verify noindex is live
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.txtreturnsDisallow: / -
Step 7: Commit
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)
- Window 1:
-
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
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 -shofpublic/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
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/onmycelium. Production deploys are explicitly out of scope. Everyrsyncin 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 ofpage.children, no automatic media object on pages. - Image paths in Markdown: never reference
/static/images/...— use theimgshortcode. Otherwise images bypassresize_image()and the site bloats. - If
zola buildfails on a Tera error, the line number in the error is in the template file. Most issues are mismatched{% endfor %}/{% endif %}or missing{% endblock %}.