Files

20 KiB
Raw Permalink Blame History

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).