# Helvoirt Gevelreiniging — Bouwplan (todo) Stack: Astro 6 + Emdash 0.1.0 + Cloudflare Workers (D1 + R2 + KV). Register: **conversiegerichte lokale dienstensite**, familie van `onderhoudsbedrijfvanhelvoirt.nl`. Zie `architecture.md` (technisch) en `CLAUDE.md` (regels/workarounds). **Alleen wijzigen in `dewebsmid\helvoirt`.** **Status (16-06-2026): marketing-pagina's LIVE + CMS-migratie loopt — morgen verder met LIVE ZETTEN.** Alle pagina's live op `https://helvoirt-gevelreiniging.mike-197.workers.dev` (deploy version **`f576e989`**). **CMS geseed + ingericht** (zie §3); de SEO-partij kan pagina's + locatiepagina's beheren/maken/SEO-optimaliseren (`CMS-SEO-handleiding.md`). **Werkgebied-pills** = Helvoirt + Limburgse steden. **Bestaande pagina's worden "volledig naar CMS" gemigreerd** (klantopdracht), met fallback-veiligheid + sentinel-verificatie: **Over ons, Homepage(-tekst), Contact KLAAR**; **Maastricht-locatiepagina live**. ← **VOLGENDE STAP:** De **code-side go-live-laag is af + live** (17-06-2026, deploy `ed137121`): robots, sitemap (incl. CMS), JSON-LD (LocalBusiness + Service/Breadcrumb), GTM-render, OG-image + apple-touch-icon, middleware, privacy/voorwaarden, **mail-code + D1-opslag** (`contact_submissions`) + **magic-link-plugin**. **CMS = 100% van de pagina's** (Wave 3 Diensten klaar → ook de 5 dienstpagina's + overzicht + homepage-accordion + nav-menu uit de `diensten`-collectie). Wat nu nog rest is **(1) externe input** — mailbox-wachtwoord/host → secret `CONTACT_SMTP_PASS` (bash printf), SEO-partij-e-mail → `users`-rij, klantcontent (NAW/reviews/FAQ/foto's/GTM-ID), domein-DNS → zone+nameservers+`routes`(rebuild)+mail-DNS+HTTPS (zie **`GO-LIVE.md`**) — en **(2) optioneel** self-hosted fonts, Lighthouse-meting, checklist `.xlsx`. **CMS-migratie is 100% klaar** (alle pagina's + alle beelden, tekst én media, in het CMS — deploy `57651519`). Schema op live D1 = via `migrations/*.sql`. Herdeploy: `astro build` + `wrangler deploy`. --- ## ⏳ Open punten (afstemmen met klant) - [x] **Telefoon + adres aangeleverd:** `085 060 4956`, `Stationsplein 1, 6131 AS Sittard` (in `Base.astro` + `contact.astro` fallbacks; staat live). - [x] **Werkgebied-copy blijft "Helvoirt en omgeving" (16-06-2026):** kop + alle SEO-copy ongewijzigd ("Helvoirt en omgeving"). **Alleen de pills vernieuwd:** Helvoirt blijft vooraan, daarna de nieuwe steden. Eerdere reverteerde poging om naar "Sittard en omgeving" te gaan is teruggedraaid op klantverzoek. - [ ] **NAW resterend:** e-mail (nu placeholder `info@helvoirtgevelreiniging.nl`), KvK, BTW, WhatsApp, openingstijden bevestigen, **Google-reviews-URL + score**. - [ ] **Echte reviews** (i.p.v. de 3 placeholders) — liefst met de Google-link. - [ ] **Echte foto's**: hero + per dienst (resultaat-galerijen) — nu AI-placeholders + de 6 echte `werk-*`-foto's. (Before/after-paren niet meer nodig: projecten geschrapt.) - [x] **Werkgebied-plaatsen (pills) vastgesteld:** Helvoirt, Maastricht, Echt, Roermond, Weert, Sittard, Heerlen, Brunssum, Kerkrade, Landgraaf. Bron: klant 16-06-2026. (Kop blijft "Helvoirt en omgeving".) - [ ] **FAQ-teksten** "Goed om te weten" bevestigen. - [ ] **Logo:** levert klant een eigen logo aan? (nu tekst-wordmark "Helvoirt / Gevelreiniging" + **H-monogram**-merkmark; druppel verwijderd.) - [x] ~~Kleuren samplen~~ → uit Elementor-kit zusterbedrijf. ~~Fonts~~ → Archivo Expanded + Open Sans. ~~Worker/D1/R2-naamgeving~~ → `helvoirt-gevelreiniging` / `helvoirt-db` / `helvoirt-media`. ~~Hero-fotografie~~ → nano-banana placeholders (klantkeuze). ~~Blog~~ → buiten launch-scope. --- ## §1 — Scaffold (mirror exterios) ✅ GEDAAN - [x] `package.json`/`tsconfig`/`astro.config.mjs`/`worker.ts`/`live.config.ts`/`wrangler.jsonc`/`.gitignore`/`.env` + scripts + `lib/img.ts`. - [x] `npm install --ignore-scripts` (721 packages) · `astro sync`/`check`/`build` slagen. - **Geleerd:** `NODE_OPTIONS=--use-system-ca` voor elke astro/wrangler-call; placeholder-`database_id` (miniflare eist niet-leeg). ## §2 — Design tokens ✅ GEDAAN (DESIGN.md = optioneel, nog niet geschreven) - [x] `tokens.css`: committed blauw/petrol palette (cyaan `#4ebfd9`, actie `#0069ff`/`#0055ce`, petrol `#1d4852`/`#15363d`, off-white `#f6fafb`), getinte neutralen, status/ster-tokens. - [x] Fonts **Archivo Expanded** (kop) + **Open Sans** (body). Géén Poppins/Fraunces. - [x] `.btn-cta`/`.btn-outline`/`.btn-text`, section-helpers, `.reveal`. - [ ] (optioneel) `DESIGN.md` schrijven via `/impeccable document`. ## §3 — Emdash schema (seed/seed.json) + CMS voor SEO-partij 🟢 GESEED + LIVE (volledige migratie loopt, §3b) **Doel (klant 16-06-2026):** de SEO-partij moet zelf **gewone pagina's én locatiepagina's** kunnen aanmaken en optimaliseren voor SEO. Bewust **SEO-vriendelijk + simpel** ingericht (3 onderdelen i.p.v. het volledige sectie-model). - [x] **`seed/seed.json` geschreven** — collecties: **`site_settings`** (NAW + `address_street`/`address_city` + KvK/BTW/WhatsApp + social + **SEO-standaard meta_title/description** + **GTM** + logo/favicon/og_image), **`paginas`** (gewone pagina's: title/intro/portableText-content + SEO-groep), **`locaties`** (locatiepagina's: plaats/H1/intro/portableText-content + SEO-groep). SEO-groep = expliciete velden met nette NL-labels: `meta_title`, `meta_description`, `canonical`, `og_image`, `noindex`. Demo-content: `site_settings/main` (echte NAW, werkgebied "Helvoirt en omgeving", score 4,9) + 1 concept-sjabloon per type (Voorbeeldpagina + locatie Maastricht). - [x] **Routes gebouwd** (lezen CMS met try/catch + fallback → nette 404 vóór seed): - `src/pages/[...slug].astro` — **root-router**: zoekt de slug op in `locaties` (lokale pagina, petrol-hero + content) én `paginas` (gewone pagina); beide op de **root** (`/maastricht`, `/onderhoudstips`). Locatie wint bij botsing. Laagste prioriteit, PortableText, eigen branded 404. (Klantkeuze 16-06: locatiepagina's op `/URL`, niet `/werkgebied/URL`.) - `src/pages/werkgebied/index.astro` — werkgebied-hub die gepubliceerde locaties oplijst (linkt naar `/`); anders statische pills. - [x] **`src/lib/images.ts`** = `resolveCmsImage` (uit behangerbrabant) voor image-velden; **`Base.astro`** uitgebreid met `noindex`-prop + absolute OG-URL; gedeelde **`.prose`** in `tokens.css`. **`astro check` 0/0/0 + build OK.** - [x] **`CMS-SEO-handleiding.md`** geschreven voor de SEO-partij (inloggen, pagina/locatie aanmaken, SEO-velden, do's/don'ts, wat nog in code zit). - [x] **Geseed (16-06-2026):** setup-wizard gedraaid met "Include sample content" → `site_settings`/`paginas`/`locaties` + demo in remote D1. - [x] **Ingebouwde `pages`/`posts` verwijderd** (emdash-defaults, leeg/ongebruikt) via `scripts/cleanup-default-collections.sql`. Blijven weg (defaults worden alléén bij setup aangemaakt). CMS toont nu alleen relevante onderdelen. - [x] **`Base.astro` leest nu `site_settings.meta_title/meta_description/og_image`** (per-pagina prop > Instellingen > code-default) → SEO-partij kan site-brede SEO in Instellingen zetten. - [ ] **CMS-login**: e-mail-provider-plugin (magic-link, newsite.md §6b) + SEO-partij-gebruiker(s) in `users`. (nog te doen) ### §3b — Volledige CMS-migratie bestaande pagina's (klant: "Volledig naar CMS") ✅ KLAAR (17-06-2026) — alle pagina's + álle beelden in het CMS (tekst + media) **Aanpak (bewezen, risico-arm):** per pagina (a) model in `scripts/cms-models/.mjs`, (b) `scripts/gen-cms-sql.mjs` genereert D1-SQL die emdash's eigen tabelstructuur spiegelt (overgenomen uit de echte `ec_paginas`-DDL; **géén search/FTS** op nieuwe collecties), (c) toepassen op remote D1 via `wrangler d1 execute --file migrations/*.sql`, (d) `.astro` herschrijven om uit CMS te lezen **met de huidige tekst als fallback** (site blijft identiek), (e) deploy + verifiëren. - [x] **Machinerie gebouwd:** `scripts/gen-cms-sql.mjs` (generator: type→column_type, systeemkolommen, ULID via `ulidx`) + `scripts/cms-models/`. UTF-8 (·, —) blijft intact door de round-trip (geverifieerd). - [x] **Wave 1 — Over ons KLAAR + LIVE-GEVERIFIEERD** (version `3ce0ce32`). Singleton **`over_ons`** (hero/verhaal/waarden-koppen + SEO; verhaal = portableText) + **`waarden` als inline repeater-veld** op over_ons. `over-ons.astro` leest uit CMS met fallback. Sentinel-test bewees CMS-aansturing; 4/4 repeater-rijen + icoon-SVG renderen live. Visueel identiek. - [x] **Klantfeedback Over ons verwerkt (`0003_*.sql`):** "Waarden" van losse collectie → **inline repeater** (rijen toevoegen/verwijderen/**verslepen** binnen de pagina, WP-Bakery-achtig); kern-inhoudvelden **verplicht** zodat "(optional)" daar weg is. **Native emdash:** pagina's **dupliceren** (kopieer-icoon in lijst, `POST .../duplicate`), collectie-rijen add/delete. **"(optional)"** is niet globaal uitschakelbaar (geen branding/CSS-hook); alleen via velden verplicht maken. Repeater-velddef = `validation.subFields` (zie `scripts/gen-over-ons-repeater.mjs`). - [x] **Beeldvelden toegevoegd aan Over ons (`0004_*.sql`):** `hero_image` (banner) + `story_image` (verhaalfoto), via `resolveCmsImage` met de huidige statische foto's als fallback (leeg veld = standaardfoto). Sentinel-test bewees dat het veld de pagina aanstuurt; leeg → fallback. **Les voor volgende waves: beeldvelden meenemen** (hero/galerij/dienst-foto's) — niet alleen tekst. - [x] **Labels opgeschoond (`0005_*.sql`):** "(~N tekens)" uit alle SEO-labels (site_settings/paginas/locaties/over_ons) — ook in `seed.json` + model. **NB:** "(optional)" blijft (emdash-standaard, niet uitzetbaar). - [x] **Foto's in de mediabibliotheek gezet (`0006_*.sql` + R2-upload, `scripts/gen-over-ons-media.mjs`):** `hero-after-1280.webp` + `werk-3.webp` naar R2 (`helvoirt-media`, key=`.webp`) + `media`-rijen + veld-JSON (local provider). De Over ons-beeldvelden tonen nu de echte foto in het CMS (niet leeg). Geverifieerd: media serveert via `/_emdash/api/media/file/` (200, image/webp) en de pagina gebruikt de CMS-afbeeldingen. **Patroon voor volgende waves:** zo ook de echte foto's voor homepage/diensten in de library zetten. - [x] **Maastricht-locatiepagina LIVE** (`/maastricht`, gepubliceerd, 200, H1 correct). - [x] **Wave 2a — Homepage TEKST KLAAR + LIVE-GEVERIFIEERD** (version `1223ddda`). Singleton **`homepage`** (`0007_*.sql`): hero (titel/sub/**vinkjes-repeater**), intro, diensten-koppen, **werkwijze-repeater** (4), bewijs-kop, reviews (uitgelicht + **kleine-reviews-repeater**), werkgebied-koppen, **FAQ-repeater** (4). `Hero.astro` (props) + `index.astro` lezen uit CMS met fallback. Sentinel-test bewees CMS-aansturing; alle repeaters renderen live. Generator uitgebreid met repeater-`validation`/subFields. - [x] **Wave 2b — Homepage BEELD KLAAR + LIVE-GEVERIFIEERD (17-06-2026, `0011_homepage_media.sql`).** Collecties **`hero_cards`** (3) + **`galerij`** (6) als image-rijen (image+tekst); `Hero.astro` (via `cards`-prop uit `index.astro`) + de bewijs-sectie lezen eruit met fallback. Foto's in de mediabibliotheek (galerij hergebruikt de werk-foto's uit Wave-3-media; hero gebruikt kleine -768-varianten voor perf → 2 nieuwe media). Live: 3/3 hero + 6/6 galerij = CMS-media; screenshot bevestigt visueel identiek. - [x] **Diensten-BEELD in de mediabibliotheek (`0010_diensten_media.sql`, `gen-diensten-media.mjs`).** 13 unieke foto's naar R2 + `media`-rijen, gekoppeld aan alle **25 beeldvelden** (nav/hero + 3 resultaat-slots × 5 diensten). Live: detailpagina 4/4, overzicht 5/5, accordion 5/5 = CMS-media. CMS serveert één formaat → -1280 voor hero/cards, werk-basis voor galerij; `srcsetFor` geeft `undefined` voor CMS-paden (geen kapotte srcset). - [x] **Wave 3 — Diensten KLAAR + LIVE-GEVERIFIEERD (17-06-2026, `0009_diensten.sql`).** Collectie **`diensten`** (5 entries, 36 velden) gedeeld door: **homepage-accordion** (`nav_*`), **`/diensten`-overzicht** (`title` + `nav_*`), **`/diensten/[slug]`-detail** (alle velden: hero/points/intro/included/resultaat-galerij/aanpak/faq/seo/noindex) én het **nav-dropdownmenu** (`Base.astro`, op `nav_order`). Alles leest uit CMS met de hardcoded data als fallback. Beeldvelden leeg = code-fallback naar de huidige foto's (`nav_image`/`hero_image`/`result_N_image` per dienst; media-library-populatie = optionele vervolgstap zoals over-ons). Resultaat-galerij = **3 vaste image-slots** (repeater kan geen image-subveld). Sentinel-test bewees dat de CMS-waarden zowel de detailpagina als de homepage-accordion aansturen. Onbekende slug → 404. `astro check` 0. - [x] **Wave 4 — Contact KLAAR + LIVE-GEVERIFIEERD** (version `f576e989`, `0008_*.sql`). Singleton **`contact_page`** (kicker/H1/intro + SEO). `contact.astro` leest uit CMS met fallback (NAW blijft uit `site_settings`). Sentinel-test OK. Generator is nu argument-gedreven: `node scripts/gen-cms-sql.mjs `. ## §4 — Base layout + kerncomponenten ✅ GEDAAN - [x] `Base.astro` (donkere petrol header met telefoon + CTA + dropdown, drawer, prefooter-form `#contact`, footer, sticky-CTA, reveal-IO, SEO-head, site_settings try/catch). - [x] `ContactForm.astro` (naam [placeholder "Naam"], e-mail, telefoon, bericht + honeypot + time-trap). - [x] `api/contact.ts` (spam-bescherming + validatie + **D1-opslag `contact_submissions` + 2 mails via `src/lib/smtp.ts`**; secret = §12). - [ ] `CtaBand.astro` extraheren wanneer subpagina's het nodig hebben. ## §5 — Homepage (index.astro) ✅ GEDAAN + GEPOLISHED - [x] **Hero** = **tekst links + werkfoto-collage rechts** (Tailark hero-6 nagebouwd): Archivo-kop "Je gevel weer als nieuw." (800) + dominante blauwe CTA + vinkjes + prominente 4,9-Google-regel; rechts **3** staande werkfoto-cards die links overlopen in het tekstblok (`.hero__fade`), mobiel swipebaar. Doorlopen: craft → bolder (verworpen) → quieter → split (verworpen) → collage → audit 20/20 → critique 33/40 + fixes. **Géén before/after-slider** in hero (→ planning `/projecten`, §7). `hero-after/before/gevel.webp` + `degrade-before.py` nu ongebruikt op de homepage (de 3 hero-cards gebruiken `werk-1/3/5`; was 4 incl. `werk-6`/Metselwerk, op klantverzoek terug naar 3 + cards verbreed). - [x] **Intro** (statement + inline meta), **Diensten = uitklappende beeld-accordion** (5 panelen, "+"-indicator, blijft open om te lezen, mobiel gestapeld), **Werkwijze**-tijdlijn (licht), **Bewijs**-galerij (echte foto's), **Reviews** (uitgelicht + 2 klein), **Werkgebied**-pills, **FAQ** "Goed om te weten". - [x] Impeccable: craft ("het verschil zien") → bolder (donkere header) → overdrive (accordion) → **audit 19/20** (tapdoelen ≥44px, fetchpriority, tokens) → **critique 32/40** (accordion klikbaar, FAQ, scaffold-cleanup, hero→`#contact` + adresveld). - [x] **`animate`-pass:** samenhangende motion-laag — consistente scroll-reveals over álle secties, werkwijze-tijdlijn tekent zich in (`.flow__line` + `.reveal--draw`), gestaggerde werkgebied-pills + FAQ (`.reveal--stagger`/`--i`), `:active`-indruk op `.btn-cta`/`.btn-outline`, offerteformulier-spinner (`aria-busy`) + geanimeerde succesmelding, trage hero-gloed. Nieuwe token-primitives `.reveal--draw`/`.reveal--stagger` in `tokens.css`. Alles achter `prefers-reduced-motion`. - **Eerdere diensten-iteraties (verworpen):** zig-zag-banden → index+preview → foto-mozaïek → typografisch menu → **accordion** (definitief). - [ ] Inline secties later extraheren naar herbruikbare componenten. ## §6 — Diensten ✅ KLAAR (overzicht + 5 detailpagina's) - [x] **`pages/diensten/[slug].astro`** = herbruikbaar SSR-sjabloon met **alle 5 slugs gevuld** (zie onder); per pagina dienst-hero (+4,9-regel) + intro/checklist + **resultaat-galerij** + mid-page CTA-band + aanpak-stappen + dienst-FAQ → CTA's `#contact`. Onbekende slug → **404** (getest). `astro check` 0 errors, axe-core **0 violations**, geen overflow (1440/768/390), mobiel stapelt netjes. - [x] **Resultaat-galerij** i.p.v. een voor/na-slider op de dienstpagina (klantkeuze): 3 losse resultaatfoto's met bijschrift per stap (aanslag weg, voegwerk hersteld, strak/verzorgd). Reden: een echte voor/na-slider eist twee exact uitgelijnde foto's van dezelfde gevel, wat de klant zelden heeft. Werkt met losse werkfoto's. - [x] **`components/BeforeAfter.astro`** = herbruikbare voor/na-sleepvergelijker (range-input → sleep + toetsenbord, `:has()`-focusring, "voor"/"na"-labels). **Nergens geïmporteerd** (projecten geschrapt, §7); bewaard als herbruikbaar component. - [x] **Alle 5 slugs ingevuld** in `DIENSTEN`: `gevelreiniging`, `voegwerk`, `dakreiniging-coating`, `schoorsteen-voegwerk`, `algemeen-reinigen`. Elk: dienst-hero (+4,9-regel) + intro/checklist + resultaat-galerij + mid-page CTA-band + aanpak-stappen + dienst-FAQ. **Dak** gebruikt de 3 echte dakfoto's als narratief (mos → reinigen op hoogte → schoon, kop "Van mos tot een schoon dak"). Galerijen mixen de echte `werk-*`-foto's met dienst/hero-placeholders. - [x] **Smoke-test:** alle 5 slugs → **200** (juiste H1), onbekend → **404**. Per nieuwe pagina: axe **clean**, geen overflow (1440/768/390). `astro check` 0 errors, build OK. - [x] **`pages/diensten/index.astro`** (overzicht, 5 dienstkaarten met "Bekijk" → detail) gebouwd; nav-link "Diensten" werkt nu, breadcrumb-crumb "Diensten" hersteld op de detailpagina's. 200, axe clean, geen overflow. - [ ] Image-fallback per slug bestaat al (`/images/dienst-.webp`). Echte projectfoto's vervangen de placeholders zodra de klant ze levert. - [x] **`/impeccable critique` op `/diensten/gevelreiniging`** → **33/40** (sterk), detector `[]` (0 slop-patronen), cognitive load laag. Fixes doorgevoerd: **P0** alle CTA-knoppen (header/sticky/drawer) → `#contact` i.p.v. `/contact`-404 (globaal in `Base.astro`); **P1** 4,9-Google-trust-regel in de dienst-hero + **mid-page CTA-band** ("Klaar om je gevel weer als nieuw te zien?") na de galerij als desktop-snelkoppeling naar het formulier; **P2** dode breadcrumb-crumb "Diensten" verwijderd tot `/diensten` bestaat; **contrast-fix** stap-nummers cyaan → diep blauw (`--c-action-deep`, cyaan op `--soft` zakte onder WCAG). Galerij bewust gelaten (klantkeuze, foto's later). FAQ bewust plat gehouden (homepage-FAQ is óók plat; accordion zou juist inconsistent zijn). Na de fixes: axe 0, geen overflow (1440/768/390), build OK. ## §7 — Projecten (before/after) ❌ GESCHRAPT (klantverzoek) - [x] Projecten-pagina geschrapt op klantverzoek: uit de nav (`Base.astro`) en de homepage-link ("Alle projecten") verwijderd. `BeforeAfter.astro` blijft als **ongebruikt** herbruikbaar component staan (nergens geimporteerd, dus niet in de build). ## §8 — Contact / Over-ons / Werkgebied 🟡 DEELS (contact + over-ons af) - [x] **`pages/contact.astro`** (`hidePrefooter` + NAW + formulier, sectie `id="contact"`). Contactblok = **Bel ons / Mail ons / Adres** (Werkgebied + Bereikbaar op klantverzoek verwijderd) + 4,9-regel links, formulier-kaart rechts; e-mail op één regel (1-koloms). 200, axe clean, geen overflow. - [x] **`pages/over-ons.astro`** (hero + 4,9-regel + verhaal met werkfoto + 4 waarden + werkgebied-pills). 200, axe clean, geen overflow. - [x] Hoofd-CTA's (header/sticky/drawer) → `/contact` (was interim `#contact`); sticky-CTA verborgen wanneer `hidePrefooter` (dus op /contact). Naam-veld-placeholder in alle formulieren: "Naam" (was "Voor- en achternaam"). - [ ] Form end-to-end testen (valid / honeypot / time-trap / 422) — na Mailtrap (§12). - [ ] `pages/werkgebied.astro` (optioneel, lokale SEO) — nog te bepalen. ## §9 — Footer-pagina's + SEO 🟢 GROTENDEELS KLAAR (17-06-2026, deploy `15e076ff`) - [x] **Branded 404** bestaat nu via de catch-all `[...slug].astro` (onbekende route → nette 404-pagina met CTA i.p.v. blanco). Losse `404.astro` daardoor optioneel. - [x] **`public/robots.txt`** (Allow /, Disallow /_emdash/ + /api/, Sitemap-regel). Live geverifieerd. - [x] **`src/pages/sitemap.xml.ts`** = vaste code-routes + gepubliceerde `paginas` + `locaties` (live query, noindex uitgesloten; CMS-fout → vaste routes blijven). **Overschrijft emdash's eigen `/sitemap.xml`** (die gebruikt `/collectie/slug`, fout voor ons root-URL-model) — user-route in `src/pages/` wint (build-warning is verwacht/onschadelijk). Live geverifieerd: bevat `/maastricht` uit CMS + `/privacy`/`/voorwaarden`. - [x] **`site: "https://helvoirtgevelreiniging.nl"`** in `astro.config.mjs` → canonical + sitemap-base wijzen naar het echte domein (ook vóór koppeling). - [x] **`voorwaarden.astro`** = Algemene voorwaarden (17-06-2026: herschreven o.b.v. zusterbedrijf onderhoudsbedrijfvanhelvoirt.nl, 10 artikelen; **klant juridisch laten controleren**) + **in het CMS** (singleton `voorwaarden`, artikelen-repeater, `0012_voorwaarden.sql`, sentinel-geverifieerd). **Privacyverklaring op klantverzoek VERWIJDERD** (pagina + footer-link + sitemap; /privacy → 404). NB: voor de AVG is een privacyverklaring bij een contactformulier doorgaans wél aan te raden. - [x] **JSON-LD `HomeAndConstructionBusiness`** (LocalBusiness-subtype) site-breed in `Base.astro`: naam/desc/url/image/telefoon/e-mail/adres (gesplitst) + areaServed + aggregateRating (4,9) + optioneel kvk/btw/sameAs uit `site_settings`. Live geverifieerd (rendert met CMS-data). - [x] **GTM-renderblok** in `Base.astro` (head-script + body-noscript), leest `site_settings.gtm`; leeg = niets geïnjecteerd (geverifieerd). + theme-color + twitter-cards + og:image-default. - [x] **Gebrande 1200×630 OG-image** (`public/images/og-default.webp` via `scripts/make-og.py`: schone gevel + petrol-gradient + wordmark + 4,9-Google) + `og:image:width/height/type` in `Base.astro`. Live (200, image/webp). + **`apple-touch-icon.png`** (180×180 H-monogram) + ``. - [x] **`Service` + `BreadcrumbList` JSON-LD** op de 5 dienst-detailpagina's (`diensten/[slug].astro`, matcht het zichtbare kruimelpad). 3 ld+json-blokken per dienstpagina (LocalBusiness + Service + Breadcrumb). Live geverifieerd. - [ ] **Resterend (optioneel):** `FAQPage` JSON-LD (wacht op echte FAQ-teksten — placeholder nu, schema zou dun zijn), self-hosted fonts (Archivo Expanded niet via css2, zie §10/notitie), Lighthouse/CWV-meting (keyless PSI = 429, **eigen PSI-key nodig**). ## §10 — Beeld-pipeline 🟡 DEELS - [x] nano-banana placeholders: `hero-after/before.webp`, `hero-gevel.webp`, 5× `dienst-*.webp`; echte input → `werk-1..6.webp`; 768/1280/1920-varianten + `IMG_WIDTHS`/`srcsetFor` bedraad. In gebruik: dienst-hero's (`dienst-*`), homepage-hero + galerijen (`werk-*`), dienst-resultaat-galerijen + over-ons (`hero-after/gevel`). Ongebruikt: `hero-before.webp` (alleen via het geparkeerde `BeforeAfter.astro`). - [ ] Echte foto's van klant verwerken → placeholders vervangen; `IMG_WIDTHS` bijwerken. ## §11 — Cloudflare provisioning + deploy ✅ GEDAAN (preview) - [x] `wrangler d1 create helvoirt-db` → `database_id` `0fb83f17-f91d-4910-ac8c-80f00569ce35` in `wrangler.jsonc`; `r2 bucket create helvoirt-media`; KV `SESSION` auto-provisioned. - [x] `astro build` + `wrangler deploy` → **`https://helvoirt-gevelreiniging.mike-197.workers.dev`** (alle marketing-pagina's 200 op fallbacks; onbekend → 404; `/_emdash/admin` → setup). Laatste deploy: version `ff725970`. - [ ] Remote D1 seeden (dump/restore per tabel, NIET hele DB) + setup-wizard remote — hangt af van §3 (`seed.json`). ## §12 — Domein + Mail + CMS-login 🟡 CODE KLAAR, EXTERNE STAPPEN OPEN (17-06-2026) - [x] **Mail-code af** (patroon uit exterios/behangerbrabant): `src/lib/smtp.ts` (authenticated SMTP over `cloudflare:sockets`, poort 465) + `api/contact.ts` slaat nu op in **D1 `contact_submissions`** (migratie `0001`, **toegepast op remote D1**) + stuurt **2 mails** (melding-klant + autoreply) via `Promise.allSettled`. **Mailtrap geschrapt** → eigen mailbox-SMTP (besluit conform newsite.md §6). Live geverifieerd: POST → 200, D1-rij opgeslagen (sentinel getest + opgeruimd). **Mail is no-op tot secret gezet is.** - [x] **CMS-login-code af:** `src/plugins/smtp-email.ts` (email:deliver-provider voor magic-link) + geregistreerd in `astro.config.mjs` (`plugins:[…]`, absoluut pad/forward-slashes). + `src/cloudflare-env.d.ts` (env/sockets types) + `src/middleware.ts` (http→https + www→apex 301, no-op op *.workers.dev). - [x] **MAIL WERKT (17-06-2026, deploy `cb425dc4`).** `CONTACT_SMTP_PASS` secret gezet (bash printf). **`CONTACT_SMTP_HOST=srv8.dewebsmid.nl`** in `wrangler.jsonc` vars — mailbox draait op srv8 (ontdekt via `/webmail`→roundcube-redirect); `mail.helvoirtgevelreiniging.nl` is NXDOMAIN in Cloudflare-DNS. Live getest: beide mails verstuurd, alle paden (valid/422/honeypot/time-trap) OK, testrijen opgeruimd. Magic-link werkt nu ook (zelfde SMTP). - [ ] **Mail-DNS in Cloudflare (collega; mijn token mag geen DNS, 403):** A `mail`→srv8-IP (185.84.143.157, DNS-only), MX `@`→mail.helvoirtgevelreiniging.nl, SPF, DKIM (key uit DirectAdmin), DMARC. Nodig voor INKOMENDE mail naar info@ van externen + afleverbaarheid autoreply. Outbound form-mail werkt al zonder. - [ ] **CMS-gebruiker(s) SEO-partij** in `users` aanmaken (SQL, §6b newsite.md) — e-mailadres(sen) opvragen. - [x] **DOMEIN LIVE (17-06-2026, deploy `7ed8f33b`).** Zone `helvoirtgevelreiniging.nl` actief op Cloudflare (NS `cesar`/`priscilla.ns.cloudflare.com`). Custom domain (apex + `www`) gekoppeld via `wrangler.jsonc` `routes` + rebuild. **Site live op `https://helvoirtgevelreiniging.nl`** (alle pagina's 200, canonical/sitemap = echte domein, 404 werkt). www→apex + http→https = 301 (middleware). `*.workers.dev` is **uitgeschakeld** (custom-domain-only; geen duplicate content). NS-blokkade was: registrar wees nog naar hosted.nl i.p.v. Cloudflare → collega heeft NS omgezet. - [ ] **Mail-DNS** (MX/SPF/DKIM/DMARC) — wacht op mailbox (collega) + DNS-edit-token/dashboard (OAuth-token mag geen zone-settings/DNS, gaf 403). **HTTPS-hardening** (Always Use HTTPS + HSTS) = optioneel via dashboard; redirects werken al via middleware, SSL-cert actief (Workers custom domain → SSL-modus n.v.t.). - [ ] Echte NAW/reviews/werkgebied/FAQ + GTM-ID in `site_settings`; echte foto's. ## §13 — QA & oplevering 🟡 DEELS - [x] **`/impeccable audit` (hele site) = 17/20 (Good).** Fixes doorgevoerd + live (version `ff725970`): tap-doelen ≥44px (telefoon/e-mail prefooter+contact, logo, skip-link), scrim-kleuren → tokens, dienst-kaarttitels → `

`, eerste-fold-beelden eager+`fetchpriority`, accordion-reflow `contain: layout`. axe 0 op alle 9 pagina's, geen overflow (1440/768/390), `astro check` 0, detector `[]`. - [ ] Lighthouse/CWV (mobile+desktop) meten, formulier end-to-end (na Mailtrap §12), git-remote + eerste commit/push. --- ## Gedaan (referentie) - [x] Input gelezen (Content.docx + 6 werkfoto's), referentiesite geanalyseerd, `architecture.md`/`todo.md`/`CLAUDE.md` opgesteld en bijgehouden.