# 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 → /, 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 `