Skip to content

Known patterns

What we've learned about Wix → static replatform that must be folded into the pipeline so we don't relearn it on every site.

Each pattern: how it manifests, where it bites, and where the fix lives (or needs to live) in the codebase. When lib/structure-gap.js flags a mystery section, check this list before debugging from scratch.

When you spot a new pattern: add an entry here in the same session you fix it. A shipped fix without a catalogued pattern is half the value.


Hero / above-the-fold

Wix slideshow with no visible "next" button (auto-rotates)

Bites: capture-layout.js's slide enumerator clicked a next-button selector and returned only the active slide. Hero rendered as a single static frame.

Fix: lib/capture-layout.js#captureSlides — three-pass: 1. Query every <img> / [style*="background-image"] / <video> inside the slideshow root. Wix often pre-renders all slides as siblings, just toggling visibility — picks them up in one shot. 2. Try the next-button click flow (works on older Wix templates). 3. Auto-rotation watcher: poll the active slide for ~20s, collect new URLs as the slideshow advances itself.

Status: shipped 2026-05-09.

Hero heading wrapped in <h3> / <h4> instead of <h1> / <h2>

Bites: Wix editors choose heading element by visual style, not semantics. Hero matcher checked only h1/h2 → empty <h1></h1> slot on 4/5 garvanbay service pages. Silent regression — the build emitted broken pages until Gemini caught it.

Fix: lib/matchers/Hero.js#extract — heading lookup is `h1 > h2 > h3 > h4

large-font-p`. Subtext also looks at h4.

Status: shipped 2026-05-08 after Gemini verify-design pass surfaced it.

Page-aware hero overlay (single image vs slideshow / video)

Bites: subpage heroes use a single static image — needs a strong dark overlay so white heading text reads. Homepage heroes use slideshow / video which have their own visual dimming — strong overlay over those reads as a murky black band.

Fix: lib/assembler-fulldev/translate.js#translateHero infers scrimIntensity: 'light' when video or slideshow >1, 'strong' for single image, 'medium' as fallback. lib/components-v3/blocks/hero-4.astro swaps three opacity tiers.

Status: shipped 2026-05-09.

Background video extraction without component render

Bites: Hero matcher captured backgroundVideo correctly — hero-4 component only ever rendered <Image>. Video silently dropped.

Fix: lib/components-v3/blocks/hero-4.astro branches on slides / backgroundVideo / image priority. Video uses autoplay muted loop playsinline with image.src as poster.

Status: shipped 2026-05-09.


Sections / matchers

TeamGrid false-positives on services

Bites: TeamGrid matcher fires at priority 75 on any section with 2+ content images and matching paragraph count. wfpainters services (farm painting, commercial cleaning, roof painting, tarmac restoration) hit this signal and rendered as 4 team-member cards instead of a service grid.

Fix needed: TeamGrid match() should require team-name patterns (Firstname Lastname[, CREDS]) in card-text segments, not just "image-with-text repetition". Falls back to ServiceGrid when no name pattern.

Status: open. Affects every FCR service page that uses image+text repetition for service offerings.

Checklist sections (heading + ✔-prefixed bullets)

Bites: USPBar/InfoCards both partially-matched and rendered nearly empty content (heading-only, or paired bullets as title+description cards).

Fix: lib/matchers/Checklist.js (priority 75 — beats both). Detects 50%+ of paragraphs starting with [✔✓☑], strips prefix, emits one item per bullet. Renders via dedicated lib/components-v3/blocks/checklist.astro (borderless inline list).

Status: shipped 2026-05-08.

TeamGrid heading regex eats credentials

Bites: First version matched Tom Holmes but stopped before BBS, FCCA because [A-Z][a-z]+ doesn't allow an internal capital (McNamara) and the credentials clause wanted \s+ not ,.

Fix: lib/matchers/TeamGrid.js#FIRST_NAME_RE accepts \p{L}'’- chars within a name word and trailing [\s,]+[A-Z]{2,} groups for credential strings (2+ caps rules out "S" from "Senior").

Status: shipped 2026-05-08.

TeamGrid DOM-order ≠ visual order (seniority)

Bites: Wix uses absolute positioning so DOM-order doesn't track visual order. Garvanbay's about page rendered Tom → Cathy → John → Orla, live shows Tom → John → Cathy → Orla.

Fix: Sort items by numeric prefix in image alt (001.jpg, 002.jpg …) — FCR upload-order convention tracks seniority. Items without a numeric prefix go last.

Better fix queued: sort by image y-position from per-page capture-layout.js output. General solution; needs assembler-fulldev to load <slug>.layout.json (currently only homepage is captured).

Status: alt-numeric shipped 2026-05-08; layout-y queued.

Multi-line address in Contact section

Bites: Live FCR contact pages render the address as a single <p> with <br>-separated lines. Default cleanText flattens to one string, losing the line breaks; the contact slot showed "Garvanbay Accounting 26A Parnell Street Dungarvan Waterford Ireland" run-on.

Fix: lib/matchers/Contact.js#extract walks the address <p>'s child nodes preserving <br> separators, yields addressLines: string[]. Translator emits structured <div class="contact-info-name"> + lines.

Status: shipped 2026-05-09.

Contact form recaptcha + Wix-internal hidden fields

Bites: Contact matcher surfaced every <input> including g-recaptcha-response, hidden honeypots, and Wix-mangled names like textarea_comp-l6.... The form rendered with junk fields.

Fix: lib/assembler-fulldev/translate.js#translateContact filters by name regex (drop recaptcha/honeypot/empty-name/hidden/checkbox), maps common field names → human labels (First name / Last name / Email / Message). Falls back to a sane 4-field shape if extraction is too sparse.

Status: shipped 2026-05-09.

Phone/email as plain text instead of tel: / mailto:

Bites: Some FCR contact pages render contact info as styled rich-text ("Phone: 058 89555") not anchors. Contact matcher only pulled <a href="tel:"> so phone went missing.

Fix: lib/matchers/Contact.js#extract — fallback regex scan for IE-style phone patterns and email addresses in paragraph text when the anchor pass yields zero.

Status: shipped 2026-05-09.

Bites: LocationMap matcher pulls address from the section's first short <p> — flaky because the address often isn't in the section's prose at all. Wix mapSrc is a parastorage URL that won't load outside Wix.

Fix: lib/assembler-fulldev/emit.js#applyCrossEntryBackfills looks up the footer's "Address" column and joins the non-link entries to backfill the LocationMap's address prop. Map-wcp embeds Google Maps via maps.google.com/maps?q=...&output=embed (no API key).

Status: shipped 2026-05-09.


Components / pipeline

Floating CTA + Floating Social — components missing

Bites: Both translators emit a deferred passthrough referencing component files that don't exist (floating-cta-wcp.astro, floating-social-wcp.astro). Build fails with rollup import errors as soon as the matchers fire.

Fix: Translators currently return component: null to skip. Real fix: build minimal pinned-corner CTA component (FAB-style with phone icon) and a multi-icon social rail.

Status: dropped at translate-time as a stopgap (2026-05-09). Real components TODO.

Map mapSrc points at parastorage

Bites: Wix's mapSrc extracted from iframes is a Wix-internal URL that won't resolve outside their CDN.

Fix: Ignore extracted mapSrc; embed Google Maps via the no-key maps.google.com/maps?q=ADDRESS&output=embed URL using the address text.

Status: shipped 2026-05-09.

Phase 1 captures desktop viewport only — mobile-only widgets miss

Bites: Wix mobile-only QuickActionBar (sticky bottom phone+email strip) renders only at mobile widths. Phase 1 captures at 1440x900 → QuickActionBar never enters our DOM extract.

Fix: Synthesise the bar from the TopBar entry's contact data. lib/assembler-fulldev/emit.js injects <StickyMobileCta phone email /> at </body> whenever phone or email exists. lib/components-v3/blocks/sticky-mobile-cta.astro hidden above 640px, reserves 3.25rem body padding-bottom on mobile.

Status: shipped 2026-05-08.

Multi-tone heading colour spans

Bites: Wix lets editors recolour individual words inside a heading via inline <span style="color: #...">. Plain-text cleanText loses that; "GARVANBAY ACCOUNTING" rendered as a single colour instead of the live yellow-green + cyan two-tone.

Fix: lib/matchers/About.js#extractHeadingHtml walks heading children preserving color / font-weight / font-style inline styles, sanitises URL/parens/angle-brackets to block XSS, drops unstyled wrappers. Translator prefers headingHtml over escaped plain text.

Status: shipped 2026-05-08.

Service-section accent leaking onto Hero / welcome / CTA-strip

Bites: [data-slot="section-prose"] :is(h1,h2,h3)::before/::after rule applied to every section-prose, so once garvanbay's --service-accent-above-display flipped to block, a yellow line appeared above Hero, content-1, and CTA-strip headings too.

Fix: lib/assembler-fulldev/theme.js scopes accent rules to .content-2 only. Service-heading-color rule too.

Status: shipped 2026-05-08.

Subpage routing — emit per-slug page + content files

Bites: First subpage build clobbered the homepage's index.astro and content.ts.

Fix: lib/assembler-fulldev.js threads slug through meta. lib/assembler-fulldev/emit.js writes src/pages/<slug>.astro and src/data/content-<slug>.ts (homepage + slug='index'/'homepage' stay at index.astro + content.ts).

Status: shipped 2026-05-08.

Cross-entry backfills must run before content.ts is serialized

Bites: Map address backfill mutated layoutMap inside emitIndexAstro, which runs after emitContentFile. The mutation never reached the runtime data.

Fix: lib/assembler-fulldev/emit.js#applyCrossEntryBackfills lifted out and called at the top of emitProject, before any file writes. Any future cross-entry prop derivation should go in there.

Status: shipped 2026-05-09.


Out-of-scope / open questions

Why Choose Us section absent from captured DOM

Bites: wfpainters live shows "Why Choose Us" content; nothing in our captured DOM matches the wording. Either Wix lazy-renders it client-side after Phase 1's load event, or it's ascii-encoded inside a sub-tree we're not visiting.

Status: open. Need to: 1. Confirm presence on live (manual eyeball) 2. If present in source, find what tag wraps it and why our enumeration misses it 3. If JS-rendered, decide whether to wait longer in Phase 1 or add a second enumeration pass

Booking — Cal.com Teams require paid plan

Bites: chosen direction was Cal.com self-host for portfolio. Cal.com free tier doesn't support Teams (the multi-tenant primitive).

Status: deferred. Open alternatives — self-host Cal.com (AGPLv3 free on our infra), schedule-x + custom backend, lightweight request form. See memory/project_booking_solution.md.

Bottom CTA bar styling — bg/text per-site

Bites: Live garvanbay's mobile QuickActionBar is black/white. Other FCR sites may use brand colours. We hardcode black/white as portfolio default.

Status: open. Per-site override via theme.json field would generalise.

verify-design --selector body only sees above-the-fold

Bites: Gemini run only checks the hero region; below-the-fold issues slip through. placement-check.js does full-page but is heavier.

Status: open. Either run multi-region per page (header / hero / each content section / FAQ / footer) or extend verify-design to scroll-capture.