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.

flowchart TD B["vike build"] --> EN["pages/@slug enumerates EN slugs"] B --> ES["pages/es/@slug enumerates ES slugs (hasLocale)"] EN --> D["buildLandingData(slug, 'en')"] ES --> D2["buildLandingData(slug, 'es')"] D --> H["dist/client/{slug}/index.html"] D2 --> H2["dist/client/es/{slug}/index.html"]

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

  1. Push a new LandingConfig onto the landings array in data/landings.ts. Required parts are slug, hero, sections, form and seo.
  2. Add any imagery to public/images/ and reference it by URL.
  3. npm run dev and open http://localhost:3100/{slug}.
  4. 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:

  1. LEADS_ENDPOINT set → POSTs the raw JSON payload there (custom CRM / n8n / Zapier / own API).
  2. API_URL + RECAPTCHA_SITE_KEY set (default) → POSTs to swapps-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's project_description.
  3. Neither configuredsubmitLead() logs the payload and resolves OK, so the form flow works locally without creating real leads.
sequenceDiagram participant U as Visitor participant F as "LeadForm" participant W as "swapps-worker" participant P as "Pipedrive + email" U->>F: Fill + submit (reCAPTCHA v2) F->>F: getUtmParams() + landing_slug F->>W: POST /api/support/send (token + fields) W->>P: Create lead, notify sales W-->>F: 200 OK F-->>U: Success state

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/* and src/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.