# 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) ```powershell $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 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 ```powershell $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 ` (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=` (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 --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` → `

`; 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 `