20 KiB
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 |
| 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-displayArchivo Expanded (700/800),--font-bodyOpen Sans. Fluidclamp()-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:
.revealIO-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__linedie 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-motionoveral gerespecteerd (transform/opacity-only). - Contrast: alle gemeten paren ruim WCAG AA; axe-core 0 violations.
Een formele
DESIGN.mdis 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):
- Hero (donker, tekst links + werkfoto-galerij rechts die links wegvloeit in de achtergrond; mobiel swipebaar. Geen before/after-slider).
- Intro (paper) — groot Archivo-statement + alinea + één inline meta-regel.
- 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.
- Werkwijze (soft) — verbonden tijdlijn, 4 stappen, blauwe genummerde cirkels.
- Bewijs (paper) — asymmetrische galerij van de echte werkfoto's.
- Reviews (tint) — één uitgelichte quote + Google-score + 2 kleine (placeholder).
- Werkgebied (soft) — plaats-pills.
- FAQ "Goed om te weten" (paper) — 4 vragen vóór het formulier.
- 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, slugmain): 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) engtm(Tag Manager-ID).paginas(gewone pagina's,drafts/revisions/search):title(H1),intro,content(portableText) + SEO-groepmeta_title/meta_description/canonical/og_image/noindex+embed_code(rauwe HTML/script-embed, bv. cookieverklaring;migrations/0013_paginas_embed.sql).[...slug].astrorendertembed_codeviaset:htmlZONDER 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 + beeldveldenhero_image/story_image(foto's staan in de mediabibliotheek: R2 +media-rij). "Waarden" = inline repeater. ✅ KLAAR.homepage(singleton): hero (titel/sub) + repeatershero_points/werkwijze/reviews_small/faq+ losse review-/sectie-velden. Gelezen doorindex.astro+Hero.astro(props). ✅ KLAAR (tekst). Beeld (hero-cards + galerij) = Wave 2b (TODO).contact_page(singleton): kicker/H1/intro + SEO. ✅ KLAAR. NAW blijft uitsite_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 opnav_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 + echtedatabase_id.
Bekende issues (geërfd, gelden hier)
- Vite SSR chunk-error (Windows) → twee-fase opstart (zie boven). Niet
optimizeDeps.excludevoor emdash zetten. NODE_OPTIONS=--use-system-cavereist voor elke astro/wrangler-call (corporate-cert), anders TLS-fout.- better-sqlite3 compileert niet op Windows →
--ignore-scripts. database_idstaat nu op de echte id (0fb83f17-…) vanhelvoirt-dbinwrangler.jsonc(was placeholder-UUID; lege id breekt lokale miniflare).- Emdash seed CLI werkt niet lokaal → setup-wizard of
wrangler d1 execute. - 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).