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.
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
# 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.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}">`.
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% 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.
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% 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):
|
||||||
|
```html
|
||||||
|
<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]`:
|
||||||
|
```toml
|
||||||
|
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.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 `/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.
|
||||||
Reference in New Issue
Block a user