Eerste commit voor Helvoirt Gevelreiniging

This commit is contained in:
Joery Disselhof
2026-08-26 13:57:08 +02:00
commit 276dba07a2
225 changed files with 23121 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
# 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).