Restores markup contracts the theme CSS expects: section-title macro, single-page-title banner, footer partial, navbar logo+Home link, plus config.extra-driven footer values shared with the contact page. Scope is markup, not modernization — Bootstrap 3 and theme CSS stay.
10 KiB
Praxis Visual Parity Pass — Design Spec
Status: draft Date: 2026-05-20 Phase: 1.5 (finish-up after Phase 1 sign-off)
Context
Phase 1 produced a Zola-based staging site at http://preview.praxis-am-lienhardplatz.de/ that builds, deploys, and renders all content from the live Grav site. The user signed off Phase 1 acknowledging "MENGE Unterschiede" against the live look — accepting them as follow-up work.
A visual diff against the live HTML (https://www.praxis-am-lienhardplatz.de/) revealed ten material gaps that prevent the theme CSS in sass/_praxis_theme.scss from applying. The theme CSS itself was lifted verbatim from the Grav theme, so making it work is mostly a question of restoring the markup contracts (class names, element nesting) it expects.
This spec covers that visual-parity pass: closing the markup gaps so the staging site looks identical to the live site, while keeping the HTML clean (no Grav-era quirks reproduced).
Goals
- The staging site, rendered in a browser, is visually indistinguishable from the live site on the four routes that exist on both sides:
/,/services,/team,/kontakt(live) ↔/contact(staging),/impressum. - The theme CSS in
sass/_praxis_theme.scssis not modified — markup conforms to it. - Recurring patterns (section-title, single-page-title, footer) are factored into Tera macros / partials so Phase-2 modernization has a single point of change.
- Footer content (hours, contact, address) lives in
config.toml[extra]as a single source of truth, shared by the footer partial and the/contactpage.
Non-Goals (deferred to Phase 2)
- Markup modernization: no semantic HTML5 rewrite, no CSS Grid, no replacement of Bootstrap 3 grid classes.
- URL escaping in templates (
/): cosmetic in view-source, functional in browser. Phase 2. - Content drift (e.g., hours displayed on home news section vs. footer): content is the user's responsibility, not markup parity.
- Accessibility audit, WCAG fixes, alt-text rewrite.
- Reproducing Grav HTML quirks (double
<p><p>...</p></p>wrappers,<td>...</th>tag mismatches): we keep clean markup.
Reference
Live HTML snapshots were captured to /tmp/praxis-diff/live/ (transient — do not check in). The authoritative reference during implementation is what the live site currently serves; if in doubt, re-fetch.
Architecture
New macros
templates/macros/section_title.html — section_title(title, anchor="")
Renders the bordered-icon section-title pattern used on home (4×), services (6×), team (2+ ×). When anchor is given, wraps the heading in <div class="anchor" id="{anchor}">.
{% macro section_title(title, anchor="") %}
<section class="section-title">
<div class="container text-center">
{% if anchor %}<div class="anchor" id="{{ anchor }}">{% endif %}
<h2>{{ title }}</h2>
{% if anchor %}</div>{% endif %}
<span class="bordered-icon"><i class="fa fa-circle-thin"></i></span>
</div>
</section>
{% endmacro %}
templates/macros/page_title.html — page_title(title)
Renders the <section class="single-page-title"> banner that sits between navbar and content on every subpage.
{% macro page_title(title) %}
<section class="single-page-title">
<div class="container text-center">
<h2>{{ title }}</h2>
</div>
</section>
{% endmacro %}
New partial
templates/partials/footer.html — three-column footer widget block + copyright bar, drawing all values from config.extra.
Structure (matching live):
<footer class="footer">
<div class="footer-widget-section">
<div class="container text-center">
<div class="row">
<div class="col-sm-4 footer-block"> {# Sprechzeiten #} </div>
<div class="col-sm-4 footer-block"> {# Kontakt: tel/fax/mail #} </div>
<div class="col-sm-4 footer-block"> {# Hausanschrift + Impressum/Datenschutz #} </div>
</div>
</div>
</div>
<div class="copyright-section">
<div class="container clearfix">
<span class="copytext">Copyright © {current_year} | Praxis am Lienhardplatz</span>
<ul class="list-inline pull-right">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</footer>
base.html includes it before </body>, after the content block.
Modified files
templates/partials/navbar.html
- Replace text brand with
<img src="/images/logo.png" alt="Praxis am Lienhardplatz">. - Add Home (
/) as first menu item. - Compute active state by comparing
current_pathto each link'spermalink.
templates/partials/head.html
- Title logic: home page emits
<title>Praxis am Lienhardplatz - Dr. Oliver Riemann, Dr. Angela Krey, Christian Malyga</title>(the brand line). Subpages emit<title>{Page Title} | Praxis am Lienhardplatz - Dr. Oliver Riemann, Dr. Angela Krey, Christian Malyga</title>. - Add
<meta name="keywords" content="{{ config.extra.keywords }}">.
templates/index.html
- Wrap Aktuelles section with
<section class="news-text ptb-text">and add{{ section_title::section_title(title="Aktuelles") }}heading. - Add
{{ section_title::section_title(title="Willkommen in unserer Praxis am Lienhardplatz") }}to the about block. - Leistungen section: change class from
bg-graytogray-bg. Addx-servicesto section classes. Change column class fromcol-md-4 col-sm-6tocol-md-6. Replace flat<a class="service-card">with<div class="thumbnail clearfix"><a><img/></a><div class="caption"><h3><a>title</a></h3><p>desc</p></div></div>. Add{{ section_title::section_title(title="Unsere Leistungen") }}. - Team-preview: change wrapper from
<div class="col-md-4 col-sm-6 text-center">to<div class="col-md-4">containing<figure class="thumbnail"><a><img/></a><figcaption class="caption text-center"><h3>{name} - <small>{position}</small></h3></figcaption></figure>. Change section class toteam ptb-100. Heading "Ihr Ärzteteam" via macro.
templates/section.html
- Insert
{{ page_title::page_title(title=section.title) }}after the navbar (before content). - Each child page rendered as
<section class="text ptb-text">{{ section_title::section_title(title=page.title, anchor=page.extra.anchor) }} <div class="container"><div class="row">{{ page.content | safe }}</div></div></section>.
templates/page.html
- Same treatment as section.html for the page title banner; otherwise content goes into a
<section class="ptb-100"><div class="container">{{ page.content | safe }}</div></section>block.
templates/base.html
- Add
{% include "partials/footer.html" %}before</body>.
Config additions
config.toml [extra]:
email = "info@praxis-am-lienhardplatz.de"
address_street = "Kaiserstraße 23"
address_zip_city = "42329 Wuppertal"
keywords = "Arzt, Hausarzt, Allgemeinmedizin, Innere Medizin, Internist, Kardiologie, Praxis, Gemeinschaftspraxis, Vohwinkel, Lienhardplatz, Krey, Riemann, Malyga, Corona, COVID-19, Corona-Test"
[[extra.opening_hours]]
label = "Montag-Freitag"
time = "08:00-12:00"
[[extra.opening_hours]]
label = "Montag, Dienstag, Donnerstag"
time = "15:00-18:00"
Phone, fax, doctors, practice_name, address are already present in [extra] from Phase 1.
Content changes
content/contact.md
- Remove the hardcoded address/phone/fax/email block from markdown — the contact template will render those from
config.extrafor single-source-of-truth with the footer. - Keep Sprechzeiten and Bereitschaftsdienst text.
For the contact page to render config values inline, the simplest approach: switch its template to a dedicated templates/contact.html (uses page_title macro + a structured layout pulling from config.extra + page.content for the Bereitschaftsdienst paragraph). The template = "page.html" line in frontmatter changes to template = "contact.html".
content/_index.md
- Fix the table tag mismatch:
<td>...</th>→<td>...</td>in the Aktuelles news block.
Assets
static/images/logo.png ← orig/user/themes/praxis/img/Logo_Praxis_white_H110.png (copy verbatim — PNG with alpha, no resize).
Per-Route Definition of Done
After implementation, each route's rendered HTML should match the live equivalent on these checks (visual, not byte-equal):
| Route | Live | Staging must show |
|---|---|---|
/ |
logo+nav, carousel, Aktuelles w/ heading, Willkommen w/ heading, Leistungen 2-col w/ thumbnails, Ärzteteam w/ figures, footer | same |
/services |
single-page-title "Leistungen", 6 sub-sections each with section_title + anchor + image + text, footer | same |
/team |
single-page-title "Team", doctor/employee sub-sections w/ section_title + anchors, footer | same |
/contact |
single-page-title "Kontakt", address/hours pulled from config, Bereitschaftsdienst block, footer (Sprechzeiten in footer ↔ Sprechzeiten on page driven from same config keys) | same |
/impressum |
single-page-title "Impressum", Impressum+Datenschutz sub-sections w/ section_title + anchors, footer | same |
A successful pass means: visually open both URLs side by side in the browser, no obvious differences in layout, typography, spacing, or color blocks.
Risks & Mitigations
-
Risk: Theme CSS may have selectors that depend on Grav-specific DOM structure we don't reproduce exactly (e.g., wrapper divs we collapsed). Mitigation: Compare rendered output, not source. If a visual element is broken, grep the SCSS for the relevant selector and adapt the markup minimally.
-
Risk: Footer values diverging between config and
/contactpage if we forget to switch contact to template-driven rendering. Mitigation: This spec explicitly converts contact.md to usetemplates/contact.html. Implementer must not leave hardcoded values in the markdown. -
Risk: Active-nav-link detection in Tera using
current_pathmay be off by trailing slashes. Mitigation: Normalize comparison (current_path == link.pathor both with trailing/); test all four nav routes after change.
Out of Scope
Anything not listed above. Specifically:
- No new content. No deletions of content.
- No new pages or routes.
- No SSL/deploy automation changes (still rsync manual).
- No git workflow changes.
Open Questions
None at design time. Implementation may surface CSS-selector mismatches that need a tiny markup tweak beyond the spec; record those as small commits during the implementation pass.