Files
helvoirtgevelreiniging.nl/CLAUDE.md
T

140 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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
```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 <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).