Files
helvoirtgevelreiniging.nl/architecture.md
T

189 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Helvoirt Gevelreiniging — Architecture
Status: **marketing-pagina's LIVE + CMS-migratie loopt (16-06-2026) — morgen verder met live zetten.** Astro 6 + Emdash + Cloudflare, gemodelleerd naar `../exterios`. Live op **`https://helvoirt-gevelreiniging.mike-197.workers.dev`** (preview; echt domein nog niet gekoppeld). **Laatste deploy: version `f576e989`.** Live: homepage + `/diensten`-overzicht + 5 dienst-detailpagina's + `/over-ons` + `/contact` + **`/maastricht`** (locatiepagina) (onbekende slug → branded 404). `astro check` 0, axe 0 (kwaliteitsmeting van vóór de CMS-migratie). Wijzig uitsluitend in `C:\projects\dewebsmid\helvoirt`.
**CMS (emdash, geseed op remote D1):** SEO-partij beheert `paginas` (gewone pagina's, root-URL) + `locaties` (locatiepagina's, root-URL) + `site_settings` (NAW + site-brede SEO + GTM). Ingebouwde `pages`/`posts` verwijderd. Zie `CMS-SEO-handleiding.md`. **Bestaande pagina's worden "volledig naar CMS" gemigreerd** (klantopdracht), elk met de huidige tekst als fallback (site breekt nooit) + live sentinel-verificatie: **Over ons** (`over_ons` + `waarden`-repeater + foto's in mediabibliotheek), **Homepage-tekst** (`homepage` + werkwijze/reviews/FAQ-repeaters), **Contact** (`contact_page`) = KLAAR.
**Go-live code-laag AF + LIVE (17-06-2026, deploy `15e076ff`):** `robots.txt`, `sitemap.xml.ts` (vaste routes + live CMS-`paginas`/`locaties`; overschrijft emdash's eigen sitemap), `site:` in config, JSON-LD `HomeAndConstructionBusiness` + GTM-render + twitter/theme-color in `Base.astro`, `privacy`/`voorwaarden`-pagina's + footer-links, `lib/smtp.ts` + `api/contact.ts` (D1 `contact_submissions` + 2 mails), `plugins/smtp-email.ts` (magic-link) geregistreerd, `middleware.ts` (http→https/www→apex), `cloudflare-env.d.ts`. **Resteert:** externe input (secret `CONTACT_SMTP_PASS`, SEO-partij-`users`-rij, klantcontent, domein-DNS) + optioneel CMS-migratie Wave 3/2b, self-hosted fonts, echte OG-image. Zie `todo.md` §9/§12 + `newsite.md` §12.
---
## Wat is dit project
**Helvoirt Gevelreiniging** — lokaal gevelreinigings-/renovatiebedrijf. Diensten: gevelreiniging + renovatie, voegwerk, dak reinigen + coating, schoorsteen-voegwerk, algemeen reinigen. Domein `helvoirtgevelreiniging.nl`.
**Register:** conversiegerichte marketing-site (doel: gratis-offerte-aanvragen). Volgt de blauw/petrol-palette van het zusterbedrijf `onderhoudsbedrijfvanhelvoirt.nl`. **Niet** de quiet-luxury van `../exterios`: hier zijn before/after, reviews en een dominante CTA juist gewenst. We hergebruiken wel de **technische** architectuur van exterios.
---
## Repo layout (huidige staat)
```
C:\projects\dewebsmid\helvoirt\
├── architecture.md ← dit document
├── CLAUDE.md ← AI-assistent context (stack, IDs, regels, Vite-fix, status)
├── todo.md ← bouwplan + status
├── package.json ← Astro 6 + Emdash 0.1.0 + Cloudflare adapter
├── tsconfig.json ← extends astro/tsconfigs/strict
├── astro.config.mjs ← output:server, cloudflare adapter, react, emdash (d1/r2), Vite noDiscovery
├── wrangler.jsonc ← worker "helvoirt-gevelreiniging", D1 (PLACEHOLDER id) + R2 bindings, nodejs_compat
├── emdash-env.d.ts ← auto-gen types (door dev server)
├── .env ← GEMINI_API_KEY (gitignored)
├── input/ ← klant-assets (NIET in build)
│ ├── Content.docx ← dienstenlijst + referentie-instructie
│ └── foto's/ ← 6 echte werkfoto's (schoorsteen+lood, tuinmuur, voegwerk, bemost dak, dakreiniging, trap)
├── public/
│ ├── favicon.svg ← cyaan tegel + witte waterdruppel (merkmark)
│ └── images/ ← WebP's + 768/1280/1920-srcset-varianten
│ ├── hero-after.webp / hero-before.webp ← schone + (PIL-vergroende) gevel (before/after-paar; hero-before alleen via het ongebruikte BeforeAfter.astro; hero-after ook in dienst-galerijen)
│ ├── hero-gevel.webp ← schone gevel (over-ons-hero + voegwerk-galerij)
│ ├── dienst-{gevelreiniging,voegwerk,dakreiniging,schoorsteen,algemeen}.webp
│ └── werk-1..6.webp ← echte input-foto's, geconverteerd
├── scripts/
│ ├── nano-banana-with-truststore.py ← Gemini beeldgeneratie (Windows cert-wrapper)
│ ├── img-variants.py ← srcset-varianten (PIL)
│ ├── degrade-before.py ← maakt hero-before.webp uit hero-after.webp
│ ├── convert-input.py ← input-foto's → WebP
│ └── screenshot-home.py / shoot-*.py / axe.py / contrast.py / touch.py ← QA/verificatie
└── src/
├── worker.ts ← re-export PluginBridge + Cloudflare handler
├── live.config.ts ← emdashLoader() voor live content collections
├── lib/img.ts ← buildSrcset + srcsetFor + IMG_WIDTHS (hero + dienst)
├── lib/images.ts ← resolveCmsImage (Emdash image-veld → src/alt, met fallback)
├── styles/tokens.css ← palette, type-scale, .btn-cta/.btn-outline/.btn-text, section-helpers, .reveal
├── layouts/Base.astro ← header, drawer, prefooter-form, footer, sticky-CTA, reveal-IO, SEO-head
├── components/
│ ├── Hero.astro ← homepage-hero (tekst + werkfoto-collage)
│ ├── BeforeAfter.astro ← herbruikbare voor/na-sleepvergelijker (range + :has-focus); ONGEBRUIKT (projecten geschrapt), bewaard als herbruikbaar component
│ └── ContactForm.astro ← herbruikbaar offerte-formulier
└── pages/
├── index.astro ← homepage (alle secties inline)
├── over-ons.astro ← over ons (hero + verhaal + 4 waarden + werkgebied)
├── contact.astro ← contact (hidePrefooter + NAW + formulier, id="contact")
├── diensten/index.astro ← diensten-overzicht (5 kaarten → detail)
├── diensten/[slug].astro ← SSR dienst-detailsjabloon (5 slugs gevuld; onbekend → 404)
├── werkgebied/index.astro ← werkgebied-hub (lijst gepubliceerde locaties → /<plaats>, anders pills)
├── [...slug].astro ← root-router: locatiepagina (`locaties`) of CMS-pagina (`paginas`), beide op root, + branded 404
└── api/contact.ts ← POST /api/contact (honeypot + time-trap + validatie)
```
**Gebouwd deze ronde:** `seed/seed.json` (CMS-model), `lib/images.ts`, `pages/[...slug].astro` (root-router: locatie- én gewone CMS-pagina's op de root + branded 404), `pages/werkgebied/index.astro` (hub), `CMS-SEO-handleiding.md`. **Nog te bouwen:** `pages/{privacy,voorwaarden}.astro` (of als CMS-pagina), `pages/sitemap.xml.ts` (incl. CMS-`paginas`/`locaties`), `robots.txt`, JSON-LD, `DESIGN.md`. (Projecten geschrapt op klantverzoek.) Homepage-secties zijn nog inline in `index.astro`; extraheren naar herbruikbare componenten zodra de subpagina's ze delen.
---
## Stack
| Laag | Technologie | Detail |
|---|---|---|
| Framework | Astro `^6.1.2` | `output: "server"` (SSR) |
| Adapter | `@astrojs/cloudflare` `^13.1.6` | Workers SSR; KV (`SESSION`) + Images auto-provisioned |
| React | `^19.2.4` | voor Emdash admin-UI |
| CMS | Emdash `^0.1.0` + `@emdash-cms/cloudflare` | admin `/_emdash/admin` (nog niet geseed) |
| Database | Cloudflare D1 | `helvoirt-db` (WEUR) — `database_id` `0fb83f17-f91d-4910-ac8c-80f00569ce35`, aangemaakt |
| Storage | Cloudflare R2 | `helvoirt-media` — aangemaakt |
| Styling | scoped CSS + CSS vars | `tokens.css`, geen Tailwind |
| Fonts | Archivo Expanded + Open Sans | Google Fonts |
| Hosting | Cloudflare Workers | account `41972f4905f2c83f51111b80283e030b` |
| Mail | Mailtrap | nog te configureren (todo §12) |
`astro.config.mjs`: `react()` + `emdash({ database: d1({binding:"DB",session:"auto"}), storage: r2({binding:"MEDIA"}) })`, `vite.ssr.optimizeDeps.noDiscovery`.
---
## Design-systeem (`src/styles/tokens.css`)
- **Strategie:** *committed* blauw/petrol. Eén familie draagt het ontwerp; cyaan is de accent-spaarzaamheid.
- **Kleuren:** actie-blauw `#0069ff` / hover `#0055ce` (CTA, links), cyaan `#4ebfd9` + `#8fd9e8` (accent/labels), petrol `#1d4852` / diep `#15363d` (donkere secties, header, footer), off-white `#f6fafb` (page) / `#eef4f7` (soft) / `#fdfeff` (card), tint `#e3f6fd`, ink `#1f2a2e` / soft `#5b6a70` / line `#dce6eb`. Status/ster: `--c-on-error`, `--c-on-success`, `--c-star`. Neutralen getint (geen puur wit/zwart in surfaces).
- **Typografie:** `--font-display` Archivo Expanded (700/800), `--font-body` Open Sans. Fluid `clamp()`-schaal.
- **Knoppen:** `.btn-cta` (actie-blauw fill, wit, radius 8px), `.btn-outline`, `.btn-text` (pijl-link, action-deep). **Tapdoelen ≥44px.**
- **Sectie-helpers:** `.section--paper / --soft / --tint / --petrol / --petrol-deep`.
- **Motion:** `.reveal` IO-systeem (`.revealed` + `data-delay`) + varianten `.reveal--draw` (lijn tekent zich in vanaf links — tijdlijn) en `.reveal--stagger` (directe kinderen na elkaar omhoog, `--i`-index — werkgebied-pills + FAQ), ease-out-expo. Verder: sitebrede scroll-reveals op elke sectie, werkwijze-tijdlijn `.flow__line` die zich intekent terwijl de cijfers inploppen (`flow-num`), `:active`-indruk op `.btn-cta`/`.btn-outline`, offerteformulier-spinner (`aria-busy` → `cform-spin`) + geanimeerde succesmelding (`cform-success`), trage ambient gloed in de hero (`hero-glow`). `prefers-reduced-motion` overal gerespecteerd (transform/opacity-only).
- **Contrast:** alle gemeten paren ruim WCAG AA; axe-core 0 violations.
> Een formele `DESIGN.md` is nog niet geschreven (de design-context staat in dit document + `CLAUDE.md`).
---
## Componenten & homepage-secties
**`Base.astro`** — solide **donkere petrol headerbalk** (altijd zichtbaar): merkmark (cyaan tegel + witte druppel-SVG) + "Helvoirt / Gevelreiniging", desktop-nav met cyaan-underline + Diensten-dropdown, klikbaar telefoonnummer (44px hit-area) en "Gratis offerte"-CTA; mobiele **drawer** (`inert` focus-trap + ESC); **prefooter** (`id="contact"`, petrol) met `ContactForm`; **footer** (petrol-deep); **sticky-mobile-CTA** die verbergt bij de prefooter; `.reveal`-IO + formulier-AJAX-JS; SEO-head (canonical/OG, `lang="nl-NL"`). `site_settings` via `getEmDashEntry` in try/catch → werkt vóór seed met fallbacks (telefoon/e-mail placeholder).
**`Hero.astro`** — **tekst links + foto-collage rechts** (Tailark hero-6 nagebouwd in Astro/scoped-CSS; `audit` 20/20 + `critique`-fixes; donker petrol, geen before/after-slider, **geen kicker**). Links (`.hero__inner`, z-3): Archivo-kop "Je gevel weer als nieuw." (**800**, wit `--c-on-dark`), sub, `.btn-cta` → `#contact` + onderstreepte tekstlink (taphit ≥44px), `.hero__points` (3 vinkjes) en een prominente `.hero__trust` (grote sterren + display-score "4,9 op Google"). Rechts (`.hero__gallery`, absoluut, z-1, `width: min(72%,1120px)`): `.hero__rail` met **3** staande `.hero__card`'s (`werk-1/3/5`: Voegwerk/Schoorsteen/Dakreiniging; breedte `clamp(200px,19vw,280px)`, verspringend via nth-child translateY, `--shadow-card-dark`, diepe `.hero__cap` bijschrift-overlay) die links **overlopen in het tekstblok**; `.hero__fade` (z-2, sterk links) laat ze wegvloeien in `--c-petrol-deep` (klantvoorkeur boven een rechts-uitgelijnde collage). Was 4 cards (incl. Metselwerk `werk-6`); op klantverzoek terug naar 3 — 4 was te druk — en de cards verbreed zodat ze de rechterzone blijven vullen. Cards laden kleine srcset-varianten (`werk-N-480/768.webp`); hover = subtiele image-zoom. Gestaggerde entrance + trage ambient gloed achter `prefers-reduced-motion`. Props: `subtitle`. **Mobiel** (≤900px): flex-kolom, tekst boven (`order:0`), rail als **swipebare scroll-snap-rail** eronder (`order:1`). Donkere overlays/scrims/schaduw via tokens (`--c-scrim-rgb`, `--c-petrol-deep-rgb`, `--c-brand-rgb`, `--shadow-card-dark`). NB: `hero-after/before/gevel.webp` nu ongebruikt op de homepage.
**`ContactForm.astro`** — offerte-formulier: naam (placeholder "Naam"), e-mail, telefoon, bericht + honeypot + time-trap. Eén instantie per pagina (prefooter OF /contact). Styling (`.cform*`) + AJAX-submit-JS globaal in `Base.astro`. Tijdens verzenden zet de JS `aria-busy` op de submitknop → pijl wisselt voor een spinner (`cform-spin`); de succesmelding komt geanimeerd binnen (`cform-success`).
**`index.astro`** secties (boven→onder):
1. **Hero** (donker, tekst links + werkfoto-galerij rechts die links wegvloeit in de achtergrond; mobiel swipebaar. Geen before/after-slider).
2. **Intro** (paper) — groot Archivo-statement + alinea + één inline meta-regel.
3. **Diensten** (petrol-deep) — **uitklappende beeld-accordion**: 5 hoge foto-panelen, het paneel onder cursor/focus klapt open (grote foto + titel + tekst + "Bekijk"), de rest smalle stroken met verticale titel + "+"-indicator; blijft open om te lezen; mobiel = 5 gestapelde kaarten.
4. **Werkwijze** (soft) — verbonden tijdlijn, 4 stappen, blauwe genummerde cirkels.
5. **Bewijs** (paper) — asymmetrische galerij van de **echte** werkfoto's.
6. **Reviews** (tint) — één uitgelichte quote + Google-score + 2 kleine (placeholder).
7. **Werkgebied** (soft) — plaats-pills.
8. **FAQ "Goed om te weten"** (paper) — 4 vragen vóór het formulier.
9. **Prefooter** (globaal, petrol) — offerte-formulier.
**`api/contact.ts`** — POST: honeypot + time-trap (2s–6u) + per-veld validatie; honeypot/time-trap → silent 200; fouten → 422 `{errors}`. Nu `console.log` (Mailtrap-send = todo §12). `prerender = false`.
---
## Emdash datamodel (emdash — GESEED op remote D1; todo §3)
**Bewust SEO-vriendelijk + simpel** ingericht zodat de externe SEO-partij zelf pagina's kan aanmaken/optimaliseren. Drie collecties (i.p.v. het volledige sectie-model):
- **`site_settings`** (singleton, slug `main`): NAW (`phone`, `email`, `address`, `address_street`, `address_city`), `kvk`, `btw`, `whatsapp`, `werkgebied`, `openingstijden`, social-URL's, `google_reviews_url`, `overall_rating`, `logo`, `favicon`, `og_image`, **`meta_title`/`meta_description`** (site-brede SEO-fallback) en **`gtm`** (Tag Manager-ID).
- **`paginas`** (gewone pagina's, `drafts/revisions/search`): `title` (H1), `intro`, `content` (portableText) + **SEO-groep** `meta_title`/`meta_description`/`canonical`/`og_image`/`noindex` + **`embed_code`** (rauwe HTML/script-embed, bv. cookieverklaring; `migrations/0013_paginas_embed.sql`). `[...slug].astro` rendert `embed_code` via `set:html` ZONDER sanitatie (PortableText/htmlBlock filtert `<script>` eruit; dit veld is de bewuste escape-hatch voor vertrouwde admins). Het script draait omdat het in de server-gerenderde HTML staat.
- **`locaties`** (locatiepagina's per plaats, idem supports): `plaats`, `title` (H1, leeg = "Gevelreiniging in {plaats}"), `intro`, `content` (portableText) + dezelfde SEO-groep.
SEO-velden zijn **expliciet** gedefinieerd (geen impliciete emdash-`seo`-support) met nette NL-labels, zodat ze voorspelbaar zijn én door de routes worden uitgelezen. Image-velden via `resolveCmsImage` (`src/lib/images.ts`). PortableText render via `<PortableText value={…} />` uit `emdash/ui`. Geen `getStaticPaths()` (alles SSR).
### Pagina-migratie ("volledig naar CMS", klantopdracht) — collecties per pagina
Elke bestaande pagina leest uit het CMS **met de huidige tekst als fallback** (de site breekt nooit, ook niet als een collectie leeg/onbereikbaar is). Toegevoegd via `migrations/*.sql` (gegenereerd door `scripts/gen-cms-sql.mjs` uit `scripts/cms-models/*`, die emdash's eigen tabelstructuur spiegelt; **geen search/FTS** op deze collecties). Repeaters = inline rijen (`validation.subFields`); image-rijen → aparte collectie (repeater kan geen image-subveld).
- **`over_ons`** (singleton): hero-velden, verhaal (portableText), waarden-koppen, SEO + **beeldvelden** `hero_image`/`story_image` (foto's staan in de **mediabibliotheek**: R2 + `media`-rij). "Waarden" = inline **repeater**. ✅ KLAAR.
- **`homepage`** (singleton): hero (titel/sub) + repeaters `hero_points`/`werkwijze`/`reviews_small`/`faq` + losse review-/sectie-velden. Gelezen door `index.astro` + `Hero.astro` (props). ✅ KLAAR (tekst). Beeld (hero-cards + galerij) = Wave 2b (TODO).
- **`contact_page`** (singleton): kicker/H1/intro + SEO. ✅ KLAAR. NAW blijft uit `site_settings`.
- **`diensten`** (collectie, 5 entries, 36 velden) — ✅ KLAAR (Wave 3, `0009_diensten.sql`). Deelt content tussen de **homepage-accordion** (`nav_*`), het **`/diensten`-overzicht**, de **`/diensten/[slug]`-detailpagina's** (alle velden: hero/points/intro/included/resultaat-galerij/aanpak/faq/seo/noindex) en het **nav-dropdownmenu** (`Base.astro`), gesorteerd op `nav_order`. Alles met code-fallback; beeldvelden leeg = statische foto. Resultaat-galerij = 3 vaste image-slots (repeater kan geen image-subveld).
Velden verplicht maken (om "(optional)" te onderdrukken) en labels wijzigen = `UPDATE _emdash_fields` op de remote D1. **`pages`/`posts` (emdash-defaults) zijn verwijderd** (`scripts/cleanup-default-collections.sql`); blijven weg zolang de setup-wizard niet opnieuw draait.
---
## Routes & data flow
```
/ ✓ index.astro (homepage; site_settings via Base, rest hardcoded/placeholder)
/api/contact ✓ POST honeypot+time-trap+validatie → console.log (Mailtrap TODO)
/_emdash/admin ✓ Emdash admin (na seed/setup)
/contact ✓ contact.astro (hidePrefooter + NAW + formulier, sectie id="contact")
/diensten ✓ index.astro (overzicht) + /diensten/[slug] (5 diensten) — uit CMS-collectie `diensten` (fallback in code); onbekende slug → 404
/over-ons ✓ over-ons.astro (hero + verhaal + waarden + werkgebied)
/werkgebied ✓ werkgebied/index.astro (hub: lijst gepubliceerde locaties → /<plaats>, anders pills)
/<plaats> ✓ locatiepagina uit collectie `locaties` (CMS), op de root (bv. /maastricht)
/<slug> ✓ gewone CMS-pagina uit collectie `paginas`, op de root (bv. /onderhoudstips)
/[...slug] ✓ root-router (catch-all, laagste prioriteit): zoekt locaties → paginas; onbekend → branded 404
/projecten ❌ GESCHRAPT (klantverzoek) — uit nav + homepage-link; BeforeAfter.astro ongebruikt bewaard
/privacy /voorwaarden ✓ code-pagina's (NAW uit site_settings); footer-links
/sitemap.xml ✓ sitemap.xml.ts (vaste routes + live CMS-paginas/locaties; overschrijft emdash's eigen sitemap)
/robots.txt ✓ public/robots.txt
```
---
## Build, dev & deploy (Windows)
- **Install:** `npm install --ignore-scripts`.
- **Dev (twee-fase bij koude cache):** wis `node_modules/.vite` + `.astro`; `$env:NODE_OPTIONS="--use-system-ca"; npx astro dev --port 4321`; 1 GET op `/` (crasht = warmt cache); Ctrl+C; opnieuw starten → stabiel.
- **Check/build:** `$env:NODE_OPTIONS="--use-system-ca"; npx astro check` (0 errors), `npx astro build`.
- **Deploy:** `$env:CLOUDFLARE_ACCOUNT_ID="…"; npx wrangler deploy` — pas ná D1/R2 + echte `database_id`.
---
## Bekende issues (geërfd, gelden hier)
1. **Vite SSR chunk-error (Windows)** → twee-fase opstart (zie boven). Niet `optimizeDeps.exclude` voor emdash zetten.
2. **`NODE_OPTIONS=--use-system-ca`** vereist voor elke astro/wrangler-call (corporate-cert), anders TLS-fout.
3. **better-sqlite3** compileert niet op Windows → `--ignore-scripts`.
4. **`database_id`** staat nu op de echte id (`0fb83f17-…`) van `helvoirt-db` in `wrangler.jsonc` (was placeholder-UUID; lege id breekt lokale miniflare).
5. **Emdash seed CLI** werkt niet lokaal → setup-wizard of `wrangler d1 execute`.
6. **nano-banana** via de truststore-wrapper + system python.org 3.12 (zie CLAUDE.md).
---
## Kwaliteit (laatste meting)
`astro check` 0 errors · productie-build OK · **axe-core 0 violations op alle 9 pagina's** · geen horizontale overflow (1440/768/390) · contrast ruim AA · tapdoelen ≥44px (telefoon/e-mail/logo/skip-link gefixt in de audit) · impeccable-detector schoon (`[]`) op alle bronbestanden · **hero-audit 20/20 · hero-critique 33/40 · dienst-critique 33/40 · site-audit 17/20** (alle fixes door) · **alle 9 routes live 200, onbekend 404** (deploy version `ff725970`).