Files
praxis-page/docs/plans/2026-05-20-praxis-zola-rebuild.md
T

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 artifacts
  • public/ — Zola build output
  • processed_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 pages
  • templates/ — Tera templates
  • static/ — verbatim assets (JS, CSS, fonts, images)
  • sass/ — SCSS sources (compiled by Zola)
  • docs/specs/ — design specs
  • docs/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.html so 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 serve starts 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 in orig/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.toml extra.phone with 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 (from orig/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 (from orig/user/themes/x-corporation/js/flexSlider/)

  • Create: static/js/bootstrap.min.js (from orig/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 (from orig/user/themes/praxis/bootstrap-3.3.6/dist/css/)

  • Create: static/css/bootstrap-theme.min.css (from same)

  • Create: static/css/mobile-menu.css (from orig/user/themes/x-corporation/css/)

  • Create: static/css/flexslider.css (from orig/user/themes/x-corporation/js/flexSlider/)

  • Create: static/fonts/ populated with Font Awesome and any web fonts

  • Create: static/images/ with all imagery from orig/

  • 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 of partials/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">&times;</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 of partials/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.md with 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.md with 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>.md with:
+++
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"

Files:

  • Create: content/legal.md

  • Create: content/contact.md

  • Create: templates/page.html

  • Step 1: Write templates/page.html (port of default.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() }} in templates/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.txt returns Disallow: /

  • 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)
  • 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 -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
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 %}.