Files
praxis-page/docs/specs/2026-05-20-praxis-visual-parity-design.md
Brummel 6b6d79ffc9 docs: spec for Visual Parity Pass (Phase 1.5)
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.
2026-05-20 23:43:53 +02:00

10 KiB
Raw Permalink Blame History

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.scss is 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 /contact page.

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.htmlsection_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.htmlpage_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_path to each link's permalink.

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-gray to gray-bg. Add x-services to section classes. Change column class from col-md-4 col-sm-6 to col-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 to team 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.extra for 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.pngorig/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 /contact page if we forget to switch contact to template-driven rendering. Mitigation: This spec explicitly converts contact.md to use templates/contact.html. Implementer must not leave hardcoded values in the markdown.

  • Risk: Active-nav-link detection in Tera using current_path may be off by trailing slashes. Mitigation: Normalize comparison (current_path == link.path or 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.