Project

General

Profile

Actions

Feature #3915

open
MR MR

Build Arabic book-a-tour pages (Redwood, Odyssey)

Feature #3915: Build Arabic book-a-tour pages (Redwood, Odyssey)

Added by Manoj R 16 days ago. Updated 16 days ago.

Status:
Resolved
Priority:
Normal
Assignee:
Category:
-
Target version:
-
Start date:
10/05/2026
Due date:
10/06/2026 (24 days late)
% Done:

100%

Estimated time:
16:00 h
Source:

Description

Goal: Deliver an Arabic (RTL) version of the "book a tour" landing page for Redwood and Odyssey, visually matching the Lovable design, while still submitting leads through the existing WordPress CF7 → Salesforce pipeline. Rebuilding the page natively in the Timber/Twig theme was attempted first and rejected — it couldn't reproduce the Lovable design's interactions and icon set closely enough.

Build pipeline:

  • Source is a Lovable-synced React app (TanStack Start + React 19 + Tailwind v4), pulled via git pull only — Lovable owns main.
  • static-export.mjs performs an SSR render of the app and downloads any Lovable-hosted (R2) images locally.
  • build-wp-template.py converts that export into a Timber template (templates/page-book-a-tour-ar.twig) plus a versioned, hashed asset bundle uploaded to wp-content/uploads/ar-book-a-tour/assets/ — uploads live outside the theme folder so they survive theme updates.
  • A test page (-test slug, its own asset folder) is always stood up before touching the live page; this caught the router crash below before it reached production.

Implementation:

  1. Hydration crash at the WordPress sub-path. Nitro (the SSR server) only ever serves /, so the match IDs baked into the exported HTML are for /. WordPress serves the real page from /book-a-tour-ar/, so TanStack Router's hydration check fails with Invariant failed and the page goes blank. Fixed by switching the router to createMemoryHistory({ initialEntries: ["/"] }), which keeps the router's internal notion of the URL at / regardless of what the browser address bar shows, and disabling scrollRestoration (which otherwise sets history.scrollRestoration = "manual" and breaks hash-based scrolling). A tempting alternative — rewriting window.history to / and restoring it after hydration — was tried and rejected: it stops the crash but leaves TanStack's internal history out of sync with the real DOM history, which then causes its own click handler to swallow every in-page anchor click.
  2. Shortcodes don't expand in Twig. A bare [contact-form-7 id="..."] string renders as literal visible text unless piped through Timber's filter: {{ ('[contact-form-7 id="..."]')|shortcodes }}.
  3. CF7 form loses binding to its JS. wpcf7.submit() reads form.wpcf7.id, a property CF7's own init script attaches to the DOM node. React's reconciliation can replace that node, silently detaching the property — the submit call then throws, falls back to a native POST, and the page reloads with no lead ever sent and no visible error. Fixed by checking if (!form.wpcf7 && wpcf7.init) wpcf7.init(form) immediately before calling wpcf7.submit(form, {submitter}) (CF7 6.1+ requires the second argument).
  4. Two CF7 forms on one page. The page renders the booking form twice — once in the hero sidebar, once in a dedicated section lower down. Any code using a bare document.querySelector for a form field (both the form's own inline script and the theme's reCAPTCHA token tracker) always resolves to the first instance, so a visitor filling in the second form has their submission routed using data read from the empty first one. Fixed by stripping the original inline script and shipping a replacement scoped to .fullwidth_form wrapper divs, resolving each event's target form via .closest() rather than a page-wide selector.
  5. No #fragment in the URL. CF7 bakes a #wpcf7-f-o fragment into its form action attribute by default. Combined with the in-page section links also writing a hash during scroll, this broke mobile form submission and could even point the test page's form submissions at the live page's URL. Fixed by filtering wpcf7_form_action_url to strip the query/hash, and by implementing section navigation as a plain JS scrollIntoView() call with preventDefault() rather than an anchor-driven hash change.

Arabic translation rollout

  • Applied the client's approved translation CSV to a LOCATION_LABELS_AR map in each theme's functions.php — this translates the visible label of each branch dropdown option only.
  • Deliberately left dropdown option values in English: LOCATION_OWNERS (which sets the hidden your-recipient/your-owner-id fields client-side) and CF7's own branch→Salesforce-ID pipe mapping both match against those exact English strings. Translating a value breaks both silently — the form still reports a successful submission, but the lead arrives in Salesforce with no owner assigned.
  • CF7-side strings that no code depends on (age-range options, field placeholders, submit button text) were left as a manual WP-admin edit rather than themed, since getting the routing-critical vs. cosmetic distinction wrong is the main risk here.
  • Verified post-translation that every branch (20 on Redwood, 12 on Odyssey) still correctly populates its recipient/owner hidden fields when selected.
Actions

Also available in: PDF Atom