35 KiB
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
audit20/20 +critique33/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 naarclamp(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 zijnhero-after/before/gevel.webpongebruikt;hero-after/gevel.webpworden wel in de dienst-galerijen + over-ons-hero gebruikt,hero-before.webpnergens (alleen via het ongebruikteBeforeAfter.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 zelfhidePrefooteren geeft zijn formulier-sectieid="contact"; de sticky-CTA wordt verborgen alshidePrefooter(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-motionoveral 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) — zietokens.css.- Beeld: WebP + srcset via
src/lib/img.ts(srcsetFor+IMG_WIDTHS, 768/1280/1920). Hero-galerij gebruikt kleinewerk-N-480/768.webp-varianten; alle hero-cardsloading=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:
Remove-Item -Recurse -Force node_modules/.vite, .astro$env:NODE_OPTIONS="--use-system-ca"; npx astro dev --port 4321→ wacht, doe 1 GET op/(crasht = warmt cache), Ctrl+C- opnieuw
npx astro dev --port 4321(zonder cache wissen) → stabiel. Daarna werktnpm run devdirect 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(enshoot-*.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/postsverwijderd.site_settingsstuurt nu de site-brede SEO (Base.astroleestmeta_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 (singletonhomepage: 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: versionf576e989.
★ 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): mailboxinfo@helvoirtgevelreiniging.nlop srv8.dewebsmid.nl (DirectAdmin; ontdekt via/webmail→ 301 naarsrv8.dewebsmid.nl/roundcube).CONTACT_SMTP_PASSsecret gezet (bash printf).CONTACT_SMTP_HOST=srv8.dewebsmid.nlinwrangler.jsoncvars — bewust NIETmail.helvoirtgevelreiniging.nlwant 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 — Amail→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 ophelvoirtgevelreiniging.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 inauth_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_PASSer 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 viaINSERT INTO users(email_verified=1). Magic-link login live geverifieerd (POST /_emdash/api/auth/magic-link/send→ 200 + token inauth_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"inastro.config.mjs(canonical/sitemap-base). Sitemap live geverifieerd incl./maastrichtuit CMS. Base.astro: JSON-LDHomeAndConstructionBusiness(NAW/areaServed/4,9-rating uitsite_settings), GTM-renderblok (head + body-noscript, leestsite_settings.gtm, leeg=niets), theme-color + twitter-cards + og:image-default (hero-after-1280.webp).- Footer-pagina's:
privacy.astro+voorwaarden.astro(NAW uitsite_settingsmet fallback; voorwaarden = algemene basistekst, klant laten controleren) + footer-links. - Mail + login (code af, secret/gebruiker open):
src/lib/smtp.ts(auth-SMTP overcloudflare:sockets),api/contact.tsslaat op in D1contact_submissions(migratie0001, toegepast op remote D1, live POST→200+rij getest+opgeruimd) + 2 mails;src/plugins/smtp-email.ts(magic-link-provider) geregistreerd inastro.config;src/middleware.ts(http→https + www→apex);src/cloudflare-env.d.ts. Mailtrap geschrapt → eigen mailbox-SMTP. Mail/magic-link = no-op totCONTACT_SMTP_PASSgezet is. - OG/JSON-LD/icon-pass (deploy
50886467): gebrande 1200×630 OG-image (public/images/og-default.webpviascripts/make-og.py— gevel + petrol-gradient + wordmark + 4,9-Google) + og:image-dims;Service+BreadcrumbListJSON-LD op de 5 dienstpagina's (3 ld+json-blokken/pagina);apple-touch-icon.png(180×180 H-monogram, full-bleed).FAQPagebewust 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+Expandedlevert 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): collectieshero_cards(3) +galerij(6);Hero.astro(viacards-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.mjsis nu importeerbaar (generate(), CLI achterisMain-guard). Patroon media-populatie: R2object put <key> --remote+media-rij (storage_key==key) + veld-JSON{id,provider:"local",…,meta:{storageKey}}; CMS = één formaat (geen srcset),srcsetForgeeftundefinedvoor 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): collectiediensten(5 entries, 36 velden) stuurt tegelijk de 5 detailpagina's (/diensten/[slug]), het/diensten-overzicht, de homepage-accordion én het nav-dropdownmenu aan (sortering opnav_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:
- 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 insite_settings); domein-DNS (zone+nameservers+routes(rebuild)+mail-DNS+HTTPS). - 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 = viamigrations/*.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 <script>). 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. Viawrangler tailbleek de echte fout: elke content-mutatie-route in Emdash 0.1.1 crasht ná de geslaagde DB-actie opif (cache.enabled) await cache.invalidate(...)—cachewordt 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: allecache.enabled→cache?.enabledinnode_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 elkenpm installhandmatignpx 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].astrolocatie-tak): naast hero + (optionele)hero_imagenu 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 oudecontent-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'scontentwas teruggevallen op één leeg block) en is overbodig naast de 4 blokken. Alleen de velddefinitie weg (admin); de kolom blijft (data-behoud).paginashoudt zijn inhoud-veld wél. Deploy7ef99e24. Ook de CTA-balk verwijderd (klantverzoek, dubbelop: het offerteformulier staat er al onder) —loc-band-sectie uit[...slug].astro+ veldencta_heading/cta_textvanlocaties(migrations/0018_locaties_remove_cta.sql), deploy63882d87. 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)
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 inCMS-SEO-handleiding.md.- 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 vianode node_modules/<tool>/bin/.... - 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. - Nieuwe collectie/veld toevoegen =
scripts/gen-cms-sql.mjsmodel →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. - Volgorde go-live: zie
newsite.md§12 (zone/nameservers, custom domain viaroutes+ rebuild, mail-DNS, HTTPS, SEO-basics, checklist, handover). Design-werk indien nodig:/impeccable(register = brand, conversiegericht).