Files
helvoirtgevelreiniging.nl/CLAUDE.md
T

35 KiB
Raw Blame History

Helvoirt Gevelreiniging — helvoirtgevelreiniging.nl

AI-assistent context. Lees ook architecture.md (technisch overzicht) en todo.md (bouwplan + status). Belangrijk: wijzig uitsluitend bestanden in C:\projects\dewebsmid\helvoirt.

Project

  • Type: Multipage marketing-site met Emdash CMS voor een gevelreinigings- en renovatiebedrijf.
  • Diensten: gevelreiniging + renovatie, voegwerkzaamheden, dak reinigen + coating, schoorsteen-voegwerk, algemeen reinigen.
  • Domein: helvoirtgevelreiniging.nl (nog te koppelen).
  • Zusterbedrijf / referentie: https://onderhoudsbedrijfvanhelvoirt.nl (dakwerken). Klant: "dezelfde kleuren en indeling, alleen niet qua grootte." → we volgen die palette/familie.
  • Register: conversiegericht (doel: gratis-offerte-aanvragen via het formulier). Marketing-homepage waar het ontwerp het product IS, maar niet de quiet-luxury van het zusterproject ../exterios. Before/after, Google-reviews en een prominente CTA zijn hier juist gewenst.

Tech stack

  • Framework: Astro 6 + TypeScript, output: "server" (SSR).
  • CMS: Emdash v0.1.0 (beta) — admin op /_emdash/admin. Nog niet geseed (zie todo §3).
  • Hosting: Cloudflare Workers. D1 + R2 + KV + Images (KV/Images auto-provisioned door de adapter).
  • Styling: scoped CSS + CSS custom properties in src/styles/tokens.css. Géén Tailwind.
  • Fonts: Archivo Expanded (koppen) + Open Sans (body) via Google Fonts.
  • Account: Mike@dewebsmid.nl (41972f4905f2c83f51111b80283e030b) — zelfde als exterios.

Cloudflare resources — AANGEMAAKT + GEDEPLOYED (preview)

Resource Naam Status
Worker helvoirt-gevelreiniging live op https://helvoirt-gevelreiniging.mike-197.workers.dev (preview; echt domein nog niet gekoppeld)
D1 helvoirt-db (WEUR) aangemaakt — database_id 0fb83f17-f91d-4910-ac8c-80f00569ce35 (staat in wrangler.jsonc)
R2 helvoirt-media aangemaakt
KV helvoirt-gevelreiniging-session (SESSION) auto-provisioned bij eerste deploy

Preview-deploy (gedaan): alle marketing-pagina's renderen live op fallbacks (200): homepage, /diensten (+ 5 dienstpagina's), /over-ons, /contact; onbekende route → 404. Emdash is nog niet geseed → /_emdash/admin redirect naar /_emdash/admin/setup (setup-wizard nog te doen, §3). Domein helvoirtgevelreiniging.nl staat bij een andere registrar → nameservers naar Cloudflare verzetten vóór koppeling (todo §12).

Design (zie tokens.css)

  • Palette (committed blauw/petrol): actie-blauw #0069ff / hover #0055ce (CTA, links), cyaan #4ebfd9 (accent/labels), diep petrol #1d4852 / #15363d (donkere secties, header, footer), off-white #f6fafb (page), licht cyaan #e3f6fd (accent-sectie), ink #1f2a2e / ink-soft #5b6a70. Status/ster via tokens (--c-on-error, --c-on-success, --c-star). Neutralen getint (geen puur wit/zwart in surfaces).
  • Type: Archivo Expanded (700/800, stoer/industrieel) + Open Sans (400-700).
  • Hero: tekst links + foto-collage rechts (Tailark hero-6 nagebouwd in Astro/scoped-CSS, door audit 20/20 + critique 33/40 met fixes). Donker petrol, geen kicker (klant wilde "Gevelreiniging in Helvoirt en omgeving" weg). Links: Archivo-kop "Je gevel weer als nieuw." (800, wit), sub, dominante blauwe CTA + tekstlink, drie vinkjes, en een prominente 4,9-Google-regel (grote display-score + sterren). Rechts: rij van 3 werkfoto-cards (Voegwerk/Schoorsteen/Dakreiniging, staand, verspringend, schaduw, diepe bijschrift-overlay) die links overlopen in het tekstblok en wegvloeien in de petrol via .hero__fade (klantvoorkeur boven de rechts-uitgelijnde collage); mobiel = swipebare scroll-snap-rail onder de tekst. (Was 4 cards incl. Metselwerk; op klantverzoek terug naar 3 — 4 was te druk — en de cards verbreed naar clamp(200px,19vw,280px) zodat ze de rechterzone blijven vullen; overloop/fade/schaduw behouden.) Hero-cards = werk-1/3/5, kleine srcset-varianten (werk-N-480/768.webp). Géén before/after-slider in de hero (projecten is geschrapt; de dienstpagina's gebruiken een resultaat-galerij i.p.v. een slider). NB: op de homepage zijn hero-after/before/gevel.webp ongebruikt; hero-after/gevel.webp worden wel in de dienst-galerijen + over-ons-hero gebruikt, hero-before.webp nergens (alleen via het ongebruikte BeforeAfter.astro).
  • Diensten-sectie = uitklappende beeld-accordion (donker). Bewust gekozen ná het afwijzen van: zig-zag-banden, interactieve index, foto-mozaïek en typografisch menu (te saai / te veel als de fotogalerij).

Persistente regels

  • Geen em-dashes (—) in zichtbare site-tekst. Komma/punt/dubbele punt. (Wel toegestaan in code-comments en deze docs.)
  • Dot-separated meta (particulier en zakelijk · eerlijke prijs vooraf · …) lowercase, behalve eigennamen.
  • Eén dominante CTA: "Gratis offerte" (actie-blauw, gevuld). Header + sticky-mobile + drawer → /contact (de contactpagina, nu gebouwd). Hero's + dienst-CTA's + mid-page-band → #contact (de in-page prefooter, lage drempel). De contactpagina zet zelf hidePrefooter en geeft zijn formulier-sectie id="contact"; de sticky-CTA wordt verborgen als hidePrefooter (dus op /contact). (Header-CTA-ghost is geprobeerd maar op klantverzoek teruggezet naar gevuld blauw.)
  • Tapdoelen ≥ 44px (header-telefoon, hamburger, knoppen) — gefixt in de audit; houd dit zo.
  • prefers-reduced-motion overal respecteren (accordion, scroll-reveals, tijdlijn-tekenanimatie, hero-gloed, form-spinner). Reveal-varianten: .reveal--draw (lijn tekent zich in) + .reveal--stagger (kinderen na elkaar omhoog, --i-index) — zie tokens.css.
  • Beeld: WebP + srcset via src/lib/img.ts (srcsetFor + IMG_WIDTHS, 768/1280/1920). Hero-galerij gebruikt kleine werk-N-480/768.webp-varianten; alle hero-cards loading=lazy (boven de vouw laadt toch direct).

Windows-werkwijze (geërfd van exterios/ks-elektra)

  • Elke astro/wrangler-call: prefix met $env:NODE_OPTIONS="--use-system-ca" (corporate-cert, anders TLS-fout).
  • Install: npm install --ignore-scripts (better-sqlite3 compileert niet op Windows; geen impact, dev draait op workerd).
  • Vite SSR chunk-issue (twee-fase opstart) bij koude cache:
    1. Remove-Item -Recurse -Force node_modules/.vite, .astro
    2. $env:NODE_OPTIONS="--use-system-ca"; npx astro dev --port 4321 → wacht, doe 1 GET op / (crasht = warmt cache), Ctrl+C
    3. opnieuw npx astro dev --port 4321 (zonder cache wissen) → stabiel. Daarna werkt npm run dev direct zolang de cache intact is.
  • Emdash seed CLI werkt niet lokaal → via setup-wizard of wrangler d1 execute.

Beeldgeneratie (nano-banana, Windows)

$env:GEMINI_API_KEY = ((Get-Content .env | Where-Object {$_ -match '^GEMINI_API_KEY='}) -replace '^GEMINI_API_KEY=','').Trim()
$py = "C:\Users\info\AppData\Local\Programs\Python\Python312\python.exe"
uv run --native-tls --python $py scripts/nano-banana-with-truststore.py --prompt "..." --output public/images/x.webp --aspect 16:9 --size 2K
  • --native-tls (uv's pypi-fetch) + system python.org 3.12 (truststore crasht op uv's standalone-Python).
  • Before/after-hero: de "voor"-foto is een PIL-degradatie van de schone "na"-foto (scripts/degrade-before.py) zodat de slider pixel-identiek uitgelijnd is.
  • Varianten: scripts/img-variants.py <input.webp> 768,1280,1920 82. Echte input-foto's → WebP: scripts/convert-input.py.
  • Screenshots (verificatie): uv run --native-tls --with playwright --python $py scripts/screenshot-home.py (en shoot-*.py).

Build & deploy

$env:NODE_OPTIONS="--use-system-ca"
npx astro check          # type-check (0 errors verwacht)
npx astro build          # → dist/
$env:CLOUDFLARE_ACCOUNT_ID="41972f4905f2c83f51111b80283e030b"
npx wrangler deploy      # pas ná D1/R2 aangemaakt + echte database_id

Status (laatste sessie)

Bouwhistorie (homepage-fase, ter referentie — huidige status staat hieronder bij "← HUIDIGE STATUS"). Gedaan: scaffold, tokens, Base.astro, Hero.astro, ContactForm.astro, index.astro (intro → diensten-accordion → werkwijze-tijdlijn → bewijs-galerij → reviews → werkgebied → FAQ), api/contact.ts. Logo = H-monogram (Base.astro + favicon.svg, druppel verwijderd). Hero volledig uitgewerkt via meerdere impeccable-passes: craft → bolder (verworpen: te druk/schreeuwerig) → quieter → tekst-links + werkfoto-collage-rechts (Tailark hero-6 nagebouwd) → audit 20/20 → critique 33/40 met fixes (kop 800, prominente 4,9-review, getokeniseerde scrims/schaduw, fade-afstelling). Kop = "Je gevel weer als nieuw.". astro check 0 errors, axe-core 0 violations. Animate-pass (/impeccable animate): samenhangende motion-laag toegevoegd — consistente scroll-reveals over álle secties, werkwijze-tijdlijn die zich vanaf links intekent (.flow__line + .reveal--draw), gestaggerde werkgebied-pills + FAQ (.reveal--stagger met --i), :active-indruk op .btn-cta/.btn-outline, offerteformulier-spinner (aria-busy) + geanimeerde succesmelding (cform-success), en een trage ambient gloed in de hero (hero-glow). Alles achter prefers-reduced-motion, transform/opacity-only. Tegelijk: hero 4 → 3 cards (klantverzoek). Cloudflare + preview-deploy (§11): D1/R2/KV aangemaakt, echte database_id in wrangler.jsonc, laatste wrangler deploy (version 22109fc1, incl. animate-pass + 3-card-hero) → https://helvoirt-gevelreiniging.mike-197.workers.dev (homepage 200 op fallbacks). Klantkeuze: preview nu, echt domein pas als de site af is.

Marketing-pagina's (rendered output) staan live: homepage, /diensten-overzicht + 5 dienst-detailpagina's (/diensten/[slug].astro: gevelreiniging, voegwerk, dakreiniging-coating, schoorsteen-voegwerk, algemeen-reinigen; onbekend → 404), /over-ons, /contact. Projecten geschrapt. CTA's header/sticky/drawer → /contact, in-page → #contact.

STATUS (16-06-2026 — CMS-migratie; zie het nieuwere "← GO-LIVE CODE-LAAG AF"-blok hieronder voor de huidige stand 17-06-2026).

  • Werkgebied-pills omgezet naar Helvoirt + Limburgse steden (kop/SEO-copy blijft "Helvoirt en omgeving"); zie regel hieronder.
  • CMS volledig ingericht + GESEED (emdash, remote D1). Ingebouwde pages/posts verwijderd. site_settings stuurt nu de site-brede SEO (Base.astro leest meta_title/meta_description/og_image) + GTM-veld klaar.
  • CMS gebruiksvriendelijk voor SEO-partij: paginas (gewone pagina's, root-URL) + locaties (locatiepagina's, root-URL bv. /maastricht — LIVE) via root-router [...slug].astro; nette branded 404. Dupliceren (native), inline repeater-rijen, opgeschoonde SEO-labels. Handleiding: CMS-SEO-handleiding.md.
  • Volledige migratie bestaande pagina's → CMS (klant: "volledig naar CMS"), met fallback-veiligheid + sentinel-geverifieerd: Over ons (singleton over_ons + waarden-repeater + hero/verhaal-foto in mediabibliotheek), Homepage (singleton homepage: tekst + werkwijze/reviews/FAQ-repeaters), Contact (contact_page). Maastricht locatiepagina gepubliceerd.
  • Migratie-machinerie: scripts/gen-cms-sql.mjs <model> <out.sql> (spiegelt emdash's tabelstructuur, repeater-validation, ULID) + scripts/cms-models/* + migrations/*.sql. Media: scripts/gen-over-ons-media.mjs + wrangler r2 object put (storage_key == R2-key). Laatste deploy: version f576e989.

★ DE SITE IS LIVE OP https://helvoirtgevelreiniging.nl (17-06-2026, deploy 7ed8f33b). Zone actief op Cloudflare (NS cesar/priscilla.ns.cloudflare.com), custom domain apex+www gekoppeld (wrangler.jsonc routes + rebuild). Alle pagina's 200, www→apex + http→https 301 (middleware), canonical/sitemap = echte domein, 404 werkt, SSL actief. *.workers.dev UITGESCHAKELD (de oude preview-URL werkt niet meer; testen nu op het echte domein, evt. curl -sk als corporate-DNS niet resolvet). Nog open (extern): mail (secret CONTACT_SMTP_PASS + mail-DNS MX/SPF/DKIM/DMARC — wacht op mailbox; OAuth-token mag geen DNS/settings, 403 → dashboard of DNS-edit-token), CMS-login-gebruiker (SEO-partij-e-mail), optioneel HTTPS-hardening (Always Use HTTPS + HSTS via dashboard).

  • MAIL WERKT (17-06-2026, deploy cb425dc4): mailbox info@helvoirtgevelreiniging.nl op srv8.dewebsmid.nl (DirectAdmin; ontdekt via /webmail → 301 naar srv8.dewebsmid.nl/roundcube). CONTACT_SMTP_PASS secret gezet (bash printf). CONTACT_SMTP_HOST=srv8.dewebsmid.nl in wrangler.jsonc vars — bewust NIET mail.helvoirtgevelreiniging.nl want dat is NXDOMAIN in Cloudflare-DNS (mail-records niet meegegaan bij de NS-verhuizing); srv8 resolvet wel (185.84.143.157) + geldig TLS-cert. Verbinden via puur IP kan niet (cert-mismatch) → daarom de srv8-hostname. User/From/To blijven info@. Live getest: mail-sent melding-klant + mail-sent bevestiging-aanvrager; alle paden (valid/422/honeypot/time-trap) OK; testrijen opgeruimd. Nog open (collega, mijn token mag geen DNS, 403): mail-DNS in Cloudflare — A mail→srv8-IP, MX, SPF, DKIM, DMARC — nodig voor INKOMENDE mail naar info@ van externen + afleverbaarheid (autoreply anders kans op spam). Outbound form-mail werkt al zonder.
  • LOGIN-GOTCHA (passkey gebonden aan origin): de admin-user mike@dewebsmid.nl (role 50) registreerde z'n passkey/Windows-Hello-pincode op de oude *.workers.dev-origin → werkt NIET op helvoirtgevelreiniging.nl (passkeys zijn origin-gebonden), en magic-link gaf 503 (mailbox/secret ontbreekt). Oplossing zonder mailbox: handmatig een magic-link-token minten — token = base64url(randomBytes(32)), hash = base64url(sha256(tokenbytes)); INSERT in auth_tokens (hash PK, user_id, email, type='magic_link', expires_at ISO, created_at) + email_verified=1; geef de gebruiker …/_emdash/api/auth/magic-link/verify?token=<token> (single-use, 15min default → ik zet 48u). Daarna moet de gebruiker een nieuwe passkey registreren op het echte domein. Zodra de mailbox + CONTACT_SMTP_PASS er zijn werkt de normale "Sign in with email link".
  • CMS-gebruikers (17-06-2026): 2 admins (role 50) — mike@dewebsmid.nl + ries@rsonlinemarketing.nl (SEO-partij). Aangemaakt via INSERT INTO users (email_verified=1). Magic-link login live geverifieerd (POST /_emdash/api/auth/magic-link/send → 200 + token in auth_tokens + e-mail via srv8). Ries logt in via "Sign in with email link".

← GO-LIVE CODE-LAAG AF + LIVE (17-06-2026, deploy 15e076ff). In één sessie gebouwd, gedeployed en live geverifieerd (patronen 1-op-1 uit ../exterios/../behangerbrabant, zie newsite.md):

  • SEO-infra: public/robots.txt, src/pages/sitemap.xml.ts (vaste routes + live CMS-paginas/locaties, noindex eruit; overschrijft emdash's eigen /sitemap.xml — user-route wint, build-warning is verwacht), site: "https://helvoirtgevelreiniging.nl" in astro.config.mjs (canonical/sitemap-base). Sitemap live geverifieerd incl. /maastricht uit CMS.
  • Base.astro: JSON-LD HomeAndConstructionBusiness (NAW/areaServed/4,9-rating uit site_settings), GTM-renderblok (head + body-noscript, leest site_settings.gtm, leeg=niets), theme-color + twitter-cards + og:image-default (hero-after-1280.webp).
  • Footer-pagina's: privacy.astro + voorwaarden.astro (NAW uit site_settings met fallback; voorwaarden = algemene basistekst, klant laten controleren) + footer-links.
  • Mail + login (code af, secret/gebruiker open): src/lib/smtp.ts (auth-SMTP over cloudflare:sockets), api/contact.ts slaat op in D1 contact_submissions (migratie 0001, toegepast op remote D1, live POST→200+rij getest+opgeruimd) + 2 mails; src/plugins/smtp-email.ts (magic-link-provider) geregistreerd in astro.config; src/middleware.ts (http→https + www→apex); src/cloudflare-env.d.ts. Mailtrap geschrapt → eigen mailbox-SMTP. Mail/magic-link = no-op tot CONTACT_SMTP_PASS gezet is.
  • OG/JSON-LD/icon-pass (deploy 50886467): gebrande 1200×630 OG-image (public/images/og-default.webp via scripts/make-og.py — gevel + petrol-gradient + wordmark + 4,9-Google) + og:image-dims; Service + BreadcrumbList JSON-LD op de 5 dienstpagina's (3 ld+json-blokken/pagina); apple-touch-icon.png (180×180 H-monogram, full-bleed). FAQPage bewust overgeslagen (placeholder-FAQ = dun schema).
  • Font-leerpunt: Archivo Expanded is niet betrouwbaar self-te-hosten via de css2-API (Google heeft de width-varianten in de variabele "Archivo"-familie samengevoegd; family=Archivo+Expanded levert geen @font-face). Self-hosting bewust uitgesteld (zou de display-typografie kunnen breken) → fonts blijven op Google Fonts (preconnect + display=swap).
  • Go-live-runbook: GO-LIVE.md = kant-en-klare stappen ná DNS+mailaccount (secret via bash printf, CMS-users-rij, routes-blok + rebuild, mail-DNS, HTTPS, verificatie). Lighthouse keyless = 429 → eigen PSI-key nodig.
  • ALLE BEELDEN in het CMS (deploy 57651519): Diensten-media (0010_diensten_media.sql + gen-diensten-media.mjs): 13 unieke foto's → R2 + media-rijen, gekoppeld aan alle 25 dienst-beeldvelden (live: detail 4/4, overzicht 5/5, accordion 5/5). Homepage-BEELD = Wave 2b KLAAR (0011_homepage_media.sql + gen-homepage-media.mjs): collecties hero_cards (3) + galerij (6); Hero.astro (via cards-prop) + bewijs-sectie lezen eruit met fallback (3/3 + 6/6 CMS-media, screenshot-geverifieerd). Galerij hergebruikt de werk-media; hero gebruikt kleine -768-varianten (perf). gen-cms-sql.mjs is nu importeerbaar (generate(), CLI achter isMain-guard). Patroon media-populatie: R2 object put <key> --remote + media-rij (storage_key==key) + veld-JSON {id,provider:"local",…,meta:{storageKey}}; CMS = één formaat (geen srcset), srcsetFor geeft undefined voor CMS-paden. Over ons-media al gedaan (0006). Dus tekst + beeld van álle pagina's staan in het CMS.
  • CMS = 100% van de pagina's (Wave 3 Diensten KLAAR, deploy ed137121, 0009_diensten.sql): collectie diensten (5 entries, 36 velden) stuurt tegelijk de 5 detailpagina's (/diensten/[slug]), het /diensten-overzicht, de homepage-accordion én het nav-dropdownmenu aan (sortering op nav_order). Alles leest uit CMS met de hardcoded data als fallback; beeldvelden leeg = code-fallback naar de huidige foto's. Resultaat-galerij = 3 vaste image-slots (repeater kan geen image-subveld). Sentinel-geverifieerd. Admin-sidebar toont nu Contactpagina · Diensten · Homepage · Locatiepagina's · Over ons · Pagina's · Instellingen · Media = alle pagina's. Rest CMS-migratie = alleen Wave 2b (homepage-BEELD: hero-cards + bewijs-galerij) + diensten-foto's in de mediabibliotheek (optioneel; nu code-fallback). CMS-SEO-handleiding.md §6b/§7 bijgewerkt.

CMS "Bekijk"-link gefixt (25-06-2026, migrations/0016_collection_url_patterns.sql, pure D1). Het oogje/"Bekijk" in de admin wees naar /locaties/maastricht (emdash-default /{collectie}/{slug}) i.p.v. de echte root-URL /maastricht. emdash heeft per collectie een url_pattern (kolom _emdash_collections.url_pattern, {slug}-placeholder; leeg = de foute fallback). Gezet: locaties/paginas → /{slug}, diensten → /diensten/{slug}, homepage → /, over_ons → /over-ons, contact_page → /contact, voorwaarden → /voorwaarden. Manifest-API leest dit live → werkt na admin-herlaad, geen redeploy. Fixt ook emdash's auto-redirect-generatie bij slug-wijziging.

"Foto verwijderen lukt niet, komt steeds terug" GEFIXT (25-06-2026, deploy 9f26388c) — client-side emdash-bug. Eerste poging (Maastricht's out-of-sync draft verwerpen) was een pleister; de échte oorzaak zit in de admin-UI. In @emdash-cms/admin/dist/index.js doet de remove-knop (X) van het beeldveld handleRemove = () => onChange(void 0) → de veldwaarde wordt undefined → JSON.stringify(input) (de PUT-body, updateContent) laat undefined vallen → hero_image zit NIET in body.data → ContentRepository.update schrijft alleen aanwezige keys → kolom wordt nooit op NULL gezet → de foto komt terug. (onChange(null) blijft wél in JSON staan → kolom wordt NULL.) Fix: onChange(void 0) → onChange(null) in @emdash-cms/admin/dist/index.js, persistent via patch-package (patches/@emdash-cms+admin+0.1.1.patch). Geverifieerd: bronpatch + minified client-bundle (()=>{r(null)} naast storageKey). ⚠️ Client-bundle → de klant moet hard refreshen (Ctrl+F5) om de nieuwe admin-JS te laden. Geldt voor álle beeldvelden + zusterprojecten (zie memory emdash-image-clear-bug). Maastricht's foto was al opgeruimd via de eerdere draft-discard.

Cursor springt naar einde tijdens typen GEFIXT (25-06-2026, deploy f0cf759e). In de content-editor (@emdash-cms/admin/dist/index.js) herlaadt een useEffect (deps o.a. item.updatedAt/itemDataString) setFormData(item.data) telkens als item verandert; na elke autosave ververst React Query de entry → formData wordt opnieuw gezet → caret naar het einde (+ tijdens-opslaan getypte tekens konden verdwijnen). Fix: loadedIdRef toegevoegd → formulier-setters draaien alleen bij entry-id-wissel; setLastSavedData blijft elke keer draaien (dirty-tracking intact, geen autosave-loop). In dezelfde patch (patches/@emdash-cms+admin+0.1.1.patch). Bundle is leesbaar (niet minified) → met Edit te patchen. Memory emdash-caret-jump-bug. Hard refresh (Ctrl+F5) nodig.

Nog te doen vóór live — alleen externe input + optioneel:

  1. Externe input: secret CONTACT_SMTP_PASS (collega: mailbox-wachtwoord/host, bash printf!) → mail+magic-link testen; SEO-partij-e-mail → users-rij (§6b); klantcontent (NAW/reviews/FAQ/foto's/GTM-ID in site_settings); domein-DNS (zone+nameservers+routes(rebuild)+mail-DNS+HTTPS).
  2. Optioneel: CMS-migratie afmaken (§3b Wave 3 = diensten-collectie + dienstpagina's + overzicht, Wave 2b = homepage-beeld); echte 1200×630 OG-image; Lighthouse/CWV-meting; checklist .xlsx. Herdeploy per iteratie: astro build + wrangler deploy. Schema-wijzigingen op de live D1 = via migrations/*.sql (wrangler d1 execute --remote --file), NIET de wizard opnieuw.

/impeccable audit (hele site) = 17/20 (Good), fixes doorgevoerd: tap-doelen telefoon/e-mail (prefooter + contact) + logo + skip-link → ≥44px; accordion-scrims → --c-scrim-rgb/--c-action-deep-rgb-tokens; dienst-kaarttitels op /diensten → <h3>; eerste-fold-beelden eager + fetchpriority (homepage-hero + diensten-overzicht); accordion-flex-grow gescoped met contain: layout (bewuste, begrensde uitzondering). Na fixes: axe 0 op alle 9 pagina's, geen overflow (1440/768/390), astro check 0. Laatste deploy: version ff725970.

Embedcode-veld op paginas (23-06-2026, deploy 1e7fcdfc): klantvraag — een <script> (cookieverklaring) in het Inhoud-veld werd als tekst getoond. Oorzaak: PortableText escapet HTML, het "Code"-blok toont code bewust, en het htmlBlock filtert <script> via sanitize-html. Oplossing (optie C): nieuw veld embed_code op de collectie paginas (migrations/0013_paginas_embed.sql — ALTER TABLE + _emdash_fields-rij, want paginas was al geseed) dat [...slug].astro rauw via set:html rendert (geen sanitatie; bewuste escape-hatch, alleen vertrouwde admins). Sentinel-geverifieerd op de live /cookieverklaring (raw <script> aanwezig, geen &lt;script&gt;). Uitleg voor de SEO-partij in CMS-SEO-handleiding.md §3b. Site-brede cookiebanner hoort nog steeds in GTM. De CookieScript-<script> STAAT LIVE op /cookieverklaring (een autosave schreef embed_code succesvol weg vóór de fout hieronder).

Cold-start CPU-limiet / Error 1102 op zware/koude routes (23-06-2026, deploy 5fe90377): klant meldde "Autosave failed" + Cloudflare Error 1102 "Worker exceeded resource limits". Uitgezocht (live wrangler tail): outcome:exceededCpu op koude isolates voor laag-verkeer-routes (/_emdash/admin/*, 404-catch-all, favicon); de constant door Uptime-Kuma warmgehouden homepage faalt nooit; routes herstellen zodra warm (favicon 503→404, admin 503→302). Niet WAF (PUT/PATCH met <script>-body bereikt de app), niet de DB (kolomwrite werkt), niet mijn veld (de falende routes draaien mijn code niet). Oorzaak = zware worker cold-start: bundle ~4,8 MB (o.a. een 1,48 MB adapt-sandbox-entry-chunk), overschrijdt soms de vaste, niet-instelbare 1s startup-CPU-limiet. limits.cpu_ms ophogen helpt NIET (per-request default is al 30s; usage_model=standard, limits=None). Verzachting doorgevoerd: public/favicon.ico (statische asset i.p.v. SSR) + extensie-guard in [...slug].astro (paden met bestandsextensie → kale 404, scheelt 2 D1-lookups + Base-render per botscan). De zware emdash-admin blijft op koude isolates af en toe 1102 geven → herstelt met refresh; structureel verzachten = admin-URL aan de uptime-monitor toevoegen (warmhouden) of de worker-bundle afslanken (groter, emdash-intern, niet reactief gedaan).

"Failed to publish" GEFIXT + rijkere locatiepagina-opmaak (25-06-2026, deploy 69899ec5). Klant kon geen enkele locatie-/gewone pagina publiceren ("Failed to publish"); saven/deleten gaven ook "Failed to…" maar lukten na refresh. Echte oorzaak (deterministisch, lokaal 1-op-1 met python sqlite3 gereproduceerd): de setup-wizard had voor locaties + paginas een FTS5 external-content zoekindex (content='ec_locaties') met een kapotte UPDATE-trigger (DELETE FROM _emdash_fts_* WHERE rowid=OLD.rowid herberekent oude termen uit de al-bijgewerkte brontabel → database disk image is malformed → UPDATE rolt terug). Publiceren doet intern UPDATE ec_* SET <kolommen> (syncDataColumns) → trigger vuurt → faalt altijd. (/maastricht stond alleen live omdat die als 'published' is GESEED via INSERT, nooit het UPDATE-pad raakte; live_revision_id was NULL.) Saven schrijft naar revisions (geen kolom-UPDATE) en soft-delete zet alleen deleted_at (geen searchable kolom) → die raken de bug niet, vandaar "lukt na refresh". Fix: migrations/0014_fix_publish_drop_fts.sql dropt de 6 triggers + 2 FTS-tabellen en zet searchable=0 → net als de zelf-gegenereerde collecties die al prima publiceren. Pure D1-wijziging, geen redeploy (publiceren werkt meteen). Frontend gebruikt geen FTS; alleen de admin-zoekbalk voor die 2 collecties vervalt. Memory: zie emdash-fts-publish-bug — geldt mogelijk ook voor ../exterios/../behangerbrabant (zelfde wizard-seed). NB: de "Failed to saven/deleten maar lukt-na-refresh"-meldingen zijn de aparte, mildere cold-start/1102-kwestie (niet blokkerend).

  • "Failed to saven/deleten/dupliceren/publiceren maar lukt-na-refresh" GEFIXT (deploy fecc2ea3) — was NIET cold-start. Via wrangler tail bleek de echte fout: elke content-mutatie-route in Emdash 0.1.1 crasht ná de geslaagde DB-actie op if (cache.enabled) await cache.invalidate(...) — cache wordt uit de Astro-context gedestructureerd maar is undefined op Cloudflare-SSR → TypeError: Cannot read properties of undefined (reading 'enabled') → 500 → "Failed to …"-toast, terwijl de write al gecommit was (vandaar "lukt na refresh"). Dupliceren crasht volledig (geen FTS), vandaar "Failed to duplicate content:" terwijl de kopie-rij wél verscheen. Fix: alle cache.enabled → cache?.enabled in node_modules/emdash/src/astro/routes/api/content/* (11 plekken, 9 bestanden), persistent via patch-package (patches/emdash+0.1.1.patch + "postinstall":"patch-package"). ⚠️ Installs draaien met --ignore-scripts → postinstall draait NIET automatisch; na elke npm install handmatig npx patch-package + rebuild/deploy. Memory: emdash-cache-enabled-bug (geldt ook voor ../exterios/../behangerbrabant). Hierna geeft saven/publiceren/dupliceren géén foutmelding meer.
  • Rijkere locatiepagina-opmaak (migrations/0015_locaties_layout.sql + src/pages/[...slug].astro locatie-tak): naast hero + (optionele) hero_image nu 4 vaste content-blokken (blokN_heading/blokN_text/blokN_image, om-en-om/zig-zag, foto optioneel, leeg blok = niet getoond) + aanpasbare CTA-balk (cta_heading/cta_text, met plaats-fallback) + automatische "Onze diensten in {plaats}"-strip (vaste links naar de 5 dienstpagina's). Het oude content-veld is op klantverzoek VERWIJDERD van locatiepagina's (migrations/0017_locaties_remove_content.sql + render eruit in [...slug].astro): het gaf onderaan een lege grijze balk (Maastricht's content was teruggevallen op één leeg block) en is overbodig naast de 4 blokken. Alleen de velddefinitie weg (admin); de kolom blijft (data-behoud). paginas houdt zijn inhoud-veld wél. Deploy 7ef99e24. Ook de CTA-balk verwijderd (klantverzoek, dubbelop: het offerteformulier staat er al onder) — loc-band-sectie uit [...slug].astro + velden cta_heading/cta_text van locaties (migrations/0018_locaties_remove_cta.sql), deploy 63882d87. Locatiepagina = nu hero → 4 blokken → "Onze diensten in {plaats}"-strip → prefooter-offerteformulier (hero-CTA scrollt naar #contact).

Blok-tekst → RICH TEXT (PortableText) + 2 koppen → <p> (25-06-2026, deploy d47253f9). Klantverzoek: opsommingen/lijsten + interne links waren onmogelijk in het platte blokN_text-veld (zelfs rauwe <a> werd als tekst getoond). Fix: blok1_text..blok4_text op locaties omgezet van text → portableText (migrations/0019_locaties_blocks_portabletext.sql): admin toont nu de PortableTextEditor met toolbar (links via setLink, toggleBulletList/toggleOrderedList, vet/cursief); frontend rendert via <PortableText> (astro-portabletext → <ul>/<ol>/<a>) in een .loc-block__body.prose. Bestaande platte tekst geconverteerd met een Node-script (scratchpad/convert.mjs): \n\n→paragrafen, - →bullets, N. →genummerde lijst (titel+omschrijving samengevoegd per item), <a href>→echte link-marks; 12 rijen (o.a. gevelrenovatie-limburg 4 blokken, maastricht + kopieën). Live geverifieerd: <ul>/<ol>//diensten/voegwerk-link renderen. Twee niet-SEO-koppen → <p> (klantverzoek, heading-hygiëne): "Onze diensten in {plaats}" (.loc-diensten__h in [...slug].astro) en de prefooter "Vraag een offerte aan" (.prefooter__title in Base.astro, globaal — alle pagina's met prefooter; /contact heeft hidePrefooter + eigen kop). Beide via class gestyled als kop (display-font) maar zonder H2-semantiek. Kleur-regressie gefixt (deploy b28a7f74): door de h2→p-wissel matchte de bestaande .prefooter__lead p-regel (grijs, on-dark-soft) ook de titel en won qua specificiteit van .prefooter__title (wit) → titel werd grijs. Fix: .prefooter__lead p → .prefooter__lead p:not(.prefooter__title), titel weer wit.

2 content-blokken op dienst-detailpagina's (25-06-2026, deploy 9325b946, migrations/0020_diensten_blocks.sql). Klantverzoek (extra content voor Google Ads-landingspagina's): onder de eerste alinea-sectie van /diensten/<slug> 2 optionele blokken (kop + PortableText rich-text + optionele foto, om-en-om/zig-zag), exact zoals blok 1/2 op locaties. Velden blok1_*/blok2_* op collectie diensten (sort_order 16-21, ná included, vóór resultaat; rest 6 opgeschoven). diensten/[slug].astro: svcBlocks uit CMS (geen fallback) → .svc-block-sectie (section--paper) tussen svc-intro en svc-result; leeg = niet getoond. Rendert via <PortableText components={{mark: PtMark}}> (links zelfde tab + opt-in nieuw tabblad). Live geverifieerd op dakreiniging-coating (2 blokken, zig-zag, foto's, vóór resultaat-galerij; leeg = weg). astro check 0. Handleiding §6b bijgewerkt. De 4 blok-koppen blijven <h2> (echte content). astro check 0. Veldtype-wijziging = admin herladen (Ctrl+F5).

Links in rich text: standaard zelfde tabblad + opt-in "nieuw tabblad" (25-06-2026, deploy 6c1bf250). Klantverzoek: links openden in nieuw tabblad; moet standaard zelfde tabblad, met een vinkje voor nieuw tabblad. Renderer: src/components/PtMark.astro (custom astro-portabletext mark-component, doorgegeven via components={{ mark: PtMark }} in [...slug].astro voor blok-body én paginas-content). Rendert <a> zonder target (zelfde tab) tenzij node.markDef.blank === true → dan target="_blank" rel="noopener noreferrer". LET OP: moet een ENKELE mark-component zijn — components={{ mark: { link: ... } }} (record) vervangt de hele Mark-component en wijzigt de prop-shape (vet/cursief breken + blank werkt niet). emdash's eigen PM↔PT-conversie mapt target==="_blank" ↔ markDef.blank al; geverifieerd via json_set($[0].markDefs[0].blank,true) → live target="_blank". Editor: "Openen in nieuw tabblad"-checkbox toegevoegd aan BEIDE link-UI's (EditorBubbleMenu + EditorToolbar) in @emdash-cms/admin/dist/index.js → setLink({href, target: newTab?"_blank":null}) + laden via getAttributes("link").target. Persistent in patches/@emdash-cms+admin+0.1.1.patch (bevat nu image-clear + caret + link-newtab). Client-bundle → Ctrl+F5. Standaard same-tab werkt zonder editor-actie (renderer). Alles CMS-bewerkbaar door de SEO-partij met veilige fallbacks; live geverifieerd (hero/blokken/zig-zag/alinea-split/diensten-strip/CTA-band renderen, getest op een tijdelijk un-soft-delete'te kopie). astro check 0. Handleiding §4 bijgewerkt. Testkopieën: …copy/…copy-2 zijn soft-deleted, …copy-1 is de WIP-draft van de klant (onaangeroerd).

Aangeleverd (staat live): telefoon 085 060 4956, adres Stationsplein 1, 6131 AS Sittard. Werkgebied (klant 16-06-2026): kop + alle SEO-copy blijven "Helvoirt en omgeving". Alleen de werkgebied-pills vernieuwd: Helvoirt blijft vooraan, daarna de nieuwe steden — Helvoirt, Maastricht, Echt, Roermond, Weert, Sittard, Heerlen, Brunssum, Kerkrade, Landgraaf (in index.astro + over-ons.astro). Merknaam "Helvoirt Gevelreiniging" (= achternaam) blijft. Placeholders (wachten op klantdata): e-mail info@helvoirtgevelreiniging.nl, Google-reviews (+ -URL), FAQ-teksten, alle AI-foto's.

Hervatten (live zetten)

  1. GO-LIVE.md = de kant-en-klare go-live-checklist (secret, CMS-user, routes+rebuild, mail-DNS, HTTPS, verificatie) — start daar zodra DNS+mailaccount er zijn. Verder: todo.md (← VOLGENDE STAP + §9/§12) + dit bestand (← GO-LIVE CODE-LAAG AF) + architecture.md. SEO-partij-uitleg in CMS-SEO-handleiding.md.
  2. Werken gaat vooral via deploy (dev-server is instabiel; testen het betrouwbaarst op de live worker). Env elke call: $env:NODE_OPTIONS="--use-system-ca" (+ $env:CLOUDFLARE_ACCOUNT_ID="41972f4905f2c83f51111b80283e030b" voor wrangler). Tools via node node_modules/<tool>/bin/....
  3. CMS-content op de live D1 lezen/wijzigen: node node_modules/wrangler/bin/wrangler.js d1 execute helvoirt-db --remote --command "..." (of --file migrations/x.sql). Verifieer een wijziging via een sentinel: zet een herkenbare waarde, fetch de live pagina (Invoke-WebRequest), zet terug.
  4. Nieuwe collectie/veld toevoegen = scripts/gen-cms-sql.mjs model → migrations/*.sql → --remote --file. Pagina daarna laten lezen uit CMS met de huidige tekst als fallback (zo breekt de site nooit). Beeld = resolveCmsImage + wrangler r2 object put + media-rij.
  5. Volgorde go-live: zie newsite.md §12 (zone/nameservers, custom domain via routes + rebuild, mail-DNS, HTTPS, SEO-basics, checklist, handover). Design-werk indien nodig: /impeccable (register = brand, conversiegericht).