Architecture¶
How the Landing (swapps-campaigns) site is structured: Vike file-system routing,
the config-driven landing model, the data layer, components, and the lead flow.
Project structure¶
swapps-landing/
data/
landings.ts # EN registry: array of LandingConfig (add a landing here)
landings.es.ts # ES registry: same slugs, translated display text
landingData.ts # buildLandingData(): lookup + 404 + SEO/hreflang/selector
testimonials.ts # shared testimonial data
types.ts # LandingConfig + section types
pages/ # Vike file-system routing
+config.ts # ssr + prerender, no client-side routing
+Layout.tsx # loads the global stylesheet
+onRenderHtml.tsx # <head>: SEO meta, GTM, fonts, critical CSS
+onRenderClient.tsx
@slug/ # dynamic EN landing route -> /{slug}
+Page.tsx
+data.ts # slug -> LandingConfig + SEO; 404 if unknown
+onBeforePrerenderStart.ts # enumerates EN slugs to prerender
es/@slug/ # dynamic ES landing route -> /es/{slug}/
+Page.tsx
+data.ts
+onBeforePrerenderStart.ts # only slugs with an ES entry (hasLocale)
index/ # go.swapps.com/ (branded placeholder, noindex)
_error/ # 404 / error page
src/
components/ # section + form + layout components (see below)
lib/
analytics.ts # trackEvent() + event names -> window.dataLayer
utm.ts # getUtmParams() (first-touch, persisted per session)
leads.ts # submitLead() — decoupled lead submission
styles/ # design tokens (copied) + landing component styles
assets/ # logos + icons + testimonials imported in components
public/
images/ # hero, OG, company logos (referenced by URL)
assets/fonts/ # self-hosted Ofelia fonts (woff2) + styles.css
robots.txt
404.html
wrangler.toml # Cloudflare Pages project (swapps-landing)
Routing (Vike, prerendered)¶
The site uses Vike file-system routing. pages/+config.ts enables SSR and
prerendering and disables client-side routing, so each route is built to a static
HTML file.
| Route | Source | Public URL |
|---|---|---|
| Home placeholder | pages/index/ |
go.swapps.com/ (branded, noindex) |
| EN landing | pages/@slug/ |
go.swapps.com/{slug} |
| ES landing | pages/es/@slug/ |
go.swapps.com/es/{slug}/ |
| 404 / error | pages/_error/ |
served via public/404.html for unknown paths |
The @slug segment is Vike's dynamic route parameter. At build time,
+onBeforePrerenderStart.ts enumerates the slugs to prerender:
- the EN tree prerenders every slug in
landings; - the ES tree prerenders only slugs that have a Spanish entry (
hasLocale).
At request/render time, +data.ts resolves the slug to a LandingConfig via the
shared buildLandingData() helper and aborts with a 404 for unknown slugs.
Config-driven landings¶
Every landing is one entry in the registry — no new files are needed for a
standard landing. A LandingConfig (defined in data/types.ts) carries the
slug, theme, hero, an ordered list of sections, the lead form, and seo.
The slug becomes the public URL automatically.
Adding a landing¶
- Push a new
LandingConfigonto thelandingsarray indata/landings.ts. Required parts areslug,hero,sections,formandseo. - Add any imagery to
public/images/and reference it by URL. npm run devand openhttp://localhost:3100/{slug}.- Optionally add a Spanish version — see i18n and Content.
Worked example
The digital-operations entry in data/landings.ts is a full acquisition
landing (go.swapps.com/digital-operations) that exercises every section type,
a multi-field lead form with selects, a header link, pageType, and
noIndex: true. Copy it as a starting point. Current EN slugs include
website-support, digital-operations, construction-app-development, and
legacy-system-modernization.
Section types¶
Sections are an ordered array; src/components/LandingSections.tsx maps each
type to its component. The hero and header/footer are always rendered. CTAs that
link to #lead-form scroll to the form section. Section types defined in
data/types.ts:
| Type | Renders |
|---|---|
benefits |
Icon cards with title + description. |
list |
Short statements; variant: "cards" \| "tags" \| "check" with optional note. |
comparison |
Descriptive cards, one with highlight: true. |
differentiators |
Table comparison with check/cross cells. |
process |
Numbered steps; optional id for anchoring. |
plans |
Operating levels (Build / Launch / Scale) with optional CTA. |
socialProof |
Logos + testimonials. |
trustSignals |
Trust badges / signals. |
qualification |
Qualification block. |
situations |
Situation cards. |
coverage |
Coverage block. |
stats |
Stats / metrics band. |
caseStudies |
Case study cards. |
faq |
Zero-JS <details> accordion. |
leadForm |
The lead form, placed inline (omit to auto-append it last). |
diagnosticForm |
Multi-step diagnostic lead form (e.g. "request a technical review"). |
cta |
Final call-to-action band. |
To add a new section type: add it to data/types.ts, create the component,
and wire it into src/components/LandingSections.tsx.
Custom (bespoke) landings¶
Two landings have hand-built layouts rather than the generic section renderer:
digital-operations (DigitalOperationsLanding.tsx) and
construction-app-development (AecConstructionLanding.tsx). Their copy is kept
as { en, es } maps in digitalOperationsCopy.ts and aecConstructionCopy.ts.
Data layer¶
| File | Responsibility |
|---|---|
data/types.ts |
LandingConfig, Locale, section and field types. |
data/landings.ts |
English landings registry. |
data/landings.es.ts |
Spanish landingsEs registry (same slugs). |
data/landingData.ts |
buildLandingData(slug, locale) — lookup, 404, SEO <head>, canonical, hreflang alternates, locale selector paths. |
data/testimonials.ts |
Shared testimonial content. |
buildLandingData() is the single source for both route trees, so the EN and ES
+data.ts hooks stay one-liners. It computes the canonical URL, the absolute
hreflang alternates, and root-relative selector paths (which work on any host:
dev, preview, prod).
Components and libraries¶
src/components/ holds the section components listed above plus layout pieces
(LandingHeader, LandingHero, LandingFooter, LandingPage), the forms
(LeadForm, MultiStepLeadForm, ReCaptcha), the LanguageSwitcher,
StickyCta, and shared context (LandingContext).
src/lib/ holds three decoupled helpers:
| File | Export | Purpose |
|---|---|---|
analytics.ts |
trackEvent() |
Pushes named events to window.dataLayer (safe before GTM loads). |
utm.ts |
getUtmParams() |
Captures UTMs + ad click IDs from the URL; first-touch, persisted per session. |
leads.ts |
submitLead() |
Decoupled lead submission; resolves to the right backend (see below). |
See i18n and Content → Analytics events for the event catalog.
Leads (Pipedrive via swapps-worker)¶
Lead submission is decoupled in src/lib/leads.ts. submitLead() resolves in this
order:
LEADS_ENDPOINTset → POSTs the raw JSON payload there (custom CRM / n8n / Zapier / own API).API_URL+RECAPTCHA_SITE_KEYset (default) → POSTs toswapps-worker/api/support/send. The worker requires a reCAPTCHA token, so the form renders a reCAPTCHA v2 widget. All collected fields plus UTMs and the landing slug are composed into the lead'sproject_description.- Neither configured →
submitLead()logs the payload and resolves OK, so the form flow works locally without creating real leads.
CORS
swapps-worker sets CORS_ALLOWED_ORIGINS = "*" in production, so
go.swapps.com can call it from the browser with no worker change. The
Pipedrive, email and reCAPTCHA secrets all live on swapps-worker.
Keeping in sync with swapps-client¶
To stay deployable on its own, this project copied the design tokens and fonts
it uses from swapps-client:
src/styles/abstracts/*andsrc/styles/base/*— tokens, mixins, reset, typography, base elements/buttons/cards.public/assets/fonts/*— the Ofelia weights actually used.src/assets/logos,src/assets/icons,public/images/*— logos, icons, OG and company imagery.
If the brand tokens change in swapps-client, re-copy src/styles/abstracts and
src/styles/base to pick up the update.