profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialBlogPhilosophy
DocsTutorialBlogPhilosophy

Tutorials

Build & Ship an Airport DirectoryDeploymentsBuild & Ship a Stripe Storefront

Feature

Documentation Site TemplateFeature Template BuilderTranslation ServiceOrganizations & Website HeirarchyConnect Profound CMS to your AI clientSettings IntegrationsSettings API KeysSettings UsageSettings Websites
All Systems Operational
Powered Byprofound-logo
Theme

Build & Ship an Airport Directory

A hands-on walkthrough: build an internationalized airport directory on Profound CMS β€” one parametric route that renders a page for every airport, in 35 languages.

Build a real, internationalized airport directory on Profound CMS: one URL pattern that renders a page for every airport, in 35 languages. You write the Next.js code and build the CMS structure by hand; Claude Code (via the Profound MCP) handles three jobs β€” generating the airport data, wiring a design system, and generating the four React components. Three parts: Setup, Build, Production.

Watch the full video walkthrough.

What you'll build

  • ~50 airport records, each at its own URL.
  • One parametric route /{language}/{airport_code}.
  • Four UI elements β€” nav, headline, body, footer β€” rendered by cms-renderer.
  • Live CMS data through CEL bindings.
  • All 35 languages, translated in one click.
  • Deployed to Vercel with live preview and in-place editing.

Prerequisites

  • Bun β‰₯ 1.3 β€” curl -fsSL https://bun.sh/install | bash
  • Claude Code with the Profound MCP connected (Part 1).
  • A Profound CMS account.
  • For Part 3: GitHub CLI (gh) signed in, and a Vercel account linked to GitHub.

How it fits together

  • Components define content shape. A Route Slug field makes a component routable; a UI Element tag makes it placeable on a page; System components (like language) ship built-in.
  • Variants are the content (JFK, SFO, LAX…).
  • UI elements are page sections, filled with CEL β€” Profound's in-template scripting, evaluated at render.
  • A parametric route maps a URL to a variant plus a set of UI elements.
  • Your Next.js app renders it all through the cms-renderer SDK with a read-tier API key.

Part 1 β€” Setup

End state: ~50 airports in the CMS, the app wired to read them, and the design system in place.

1. Create the website

Sign up at Profound (WorkOS auth) and create a website named airports. Copy two things: the website ID (the UUID in the admin URL) and a read-tier API key from Deployments β†’ Create API key. The app only reads, so a read key is all you need.

2. Scaffold and connect the app

bunx create-profound-next airports
cd airports

Add your credentials to .env.local:

PROFOUND_API_KEY=<your read key>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<your website id>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net

Run bun dev and open localhost:3000 to confirm it connects.

3. Create the airport component

In the admin, go to Components β†’ Create new component and name it airport. Add six fields: code, name, city, country (Text) and latitude, longitude (Number). Mark code as the Route Slug field and set the status to Active. Leave every field optional; add no UI Element tag.

The&#32;airport&#32;component β€” six fields, with&#32;code&#32;set as the Route Slug.

4. Pull the component into types

bun run generate-schemas

This writes airportSchema (Zod) and Airport (type) to generated/cms-schemas.ts, and confirms your credentials work.

5. Seed the data via the MCP

Connect the MCP and authenticate:

claude mcp add --transport http Profound http://107.21.107.99:8081/mcp

The server authenticates over OAuth (WorkOS). In Claude Code, run /mcp, choose Profound, and complete the sign-in in your browser β€” Claude Code also prompts you automatically the first time a Profound tool is called. Once /mcp shows Profound as connected, prompt Claude:

Generate fifty real airports β€” proper IATA codes, names, cities, countries, and coordinates. Save them as JSON in a data folder, validate against our airport component, then create each as a published document via the Profound MCP, in parallel.

Open airport β†’ Variants to confirm ~50 published airports. The app reads with the API key; the MCP writes with its own WorkOS session.

Around 50 airports, seeded as published Variants.

6. Add a design system

The scaffold ships unstyled. Put a DESIGN.md (a Tailwind v4 @theme block plus tokens) at the project root β€” your own, or download one from refero.design. Then prompt Claude:

Read DESIGN.md. Set up Tailwind v4 and wire in the theme and fonts. Use next/font for fonts, not a runtime Google import. Styling only β€” no pages or components yet.

Confirm src/app/globals.css has @import "tailwindcss"; plus the @theme block, and that localhost:3000 shows the tokens.

Part 2 β€” Build

Build the rendering layer: the UI elements, the React components, the route, the CEL bindings, and translation.

1. Define the four UI element components

In the admin, Create new component four times (no Route Slug). For each, add the fields as Text unless noted, set the status to Active, and add the UI Element tag in Settings β†’ Tags:

  • nav β†’ brand
  • headline β†’ title, subtitle
  • body β†’ code, city, country, latitude (Number), longitude (Number)
  • footer β†’ text

body.latitude and body.longitude must be Number to match the airport component.

Each UI element is its own component β€” here&#32;body, set Active with the&#32;UI Element&#32;tag.

All four UI elements alongside the&#32;airport&#32;component.

2. Regenerate types

bun run generate-schemas

3. Generate the React components and register them

Prompt Claude:

Build four React components β€” nav, headline, body, footer β€” in components/. Each takes a single content prop typed as BlockComponentProps<T> from cms-renderer/lib/types, where T is the element's generated type, and reads its fields off content. Register all four in the catch-all route's registry by component name. Style them with our design system, but as our own components β€” don't copy the source site's layout. Nav: brand on the left. Headline: airport name with a code Β· city, country subtitle. Body: details panel with code, city, country, and coordinates. Footer: one static line. Layout and styling only.

Claude creates the four components and fills the registry in src/app/[...slug]/page.tsx:

const registry = { nav: Nav, headline: Headline, body: Body, footer: Footer };

Two rules: each component reads its fields off content (not as separate props), and registry keys must match the CMS component names exactly β€” a mismatch renders blank.

4. Create the parametric route

In the admin, go to Pages β†’ Create page and set the pattern /{airport_code}. Under Dynamic Segment Mappings, map airport_code β†’ the airport component, slug field code. Save β€” you land in the Page Builder. Verify /JFK resolves.

5. Add UI elements, wire CEL, and publish

Adding a UI element to the page from the Custom tab.

The four UI elements added to the JFK binding in the Page Builder.

  1. Pick the JFK binding β†’ Add UI Element β†’ Custom tab β†’ add nav, headline, body, footer in order. Profound propagates the set to every airport binding.

  2. Fill each field. Static values (nav brand, footer text): type them directly. Dynamic values: switch to dynamic and write CEL. The shared lookup is documents.get("airport", meta.params.airport_code).

    FieldCEL value
    headline.titledocuments.get("airport", meta.params.airport_code).name
    headline.subtitledocuments.get("airport", meta.params.airport_code).code + " Β· " + documents.get("airport", meta.params.airport_code).city + ", " + documents.get("airport", meta.params.airport_code).country
    body.code / body.city / body.countrydocuments.get("airport", meta.params.airport_code).<field>
    body.latitude / body.longitudedocuments.get("airport", meta.params.airport_code).latitude (and .longitude)
    nav.brand, footer.textstatic strings
  3. Publish the page (top-right).

6. Verify in English

Open localhost:3000/JFK, then /SFO, /LAX β€” same template, different airport.

The finished page rendering JFK's data, in English.

7. Internationalize

  1. Translate the components first. In the admin, open each component (any component β€” it doesn't have to be a UI element) and click Translate β†’ Submit. Profound translates its content into all 35 languages at once, static field values included. Do this before you edit the route or touch any CEL.

    Translate β†’ Submit sends a component to all 35 languages at once.

  2. Add the language dimension. In Pages, change the pattern to /{language}/{airport_code}. Add a Dynamic Segment Mapping for language β†’ the built-in language System component, slug field code. Save and verify /en/JFK resolves.

    Adding the&#32;{language}&#32;segment to the route.

  3. Point every translatable field at the translated fetch. Internationalizing the page means switching every language-dependent field β€” not just the headline β€” from documents.get(...) to documents.translated("airport", meta.params.airport_code, meta.params.language):

    FieldTranslated CEL
    headline.titledocuments.translated("airport", meta.params.airport_code, meta.params.language).name
    headline.subtitledocuments.get("airport", meta.params.airport_code).code + " Β· " + documents.translated("airport", meta.params.airport_code, meta.params.language).city + ", " + documents.translated("airport", meta.params.airport_code, meta.params.language).country
    body.city / body.countrydocuments.translated("airport", meta.params.airport_code, meta.params.language).city (and .country)

    Leave the IATA code and the coordinates on documents.get β€” they're identical in every language. nav.brand and footer.text are already covered by the component Translate in step 1.

When all three are done, /fr/SFO, /de/SFO, etc. resolve fully translated.

Part 3 β€” Production

1. Deploy: GitHub, then Vercel

Push to GitHub:

git init
git add -A
git commit -m "Airport directory"
gh repo create airports --public --source=. --push

Import into Vercel: Add New β†’ Project β†’ import the airports repo. Add the env vars β€” PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, and optionally NEXT_PUBLIC_BUNNY_CDN_URL β€” then Deploy. Visit /en/JFK and /fr/SFO. Every future git push redeploys.

2. Live preview and in-place editing

Both ship with the scaffold.

  • Live preview: the <Refresher> in layout.tsx updates the page you're previewing when you edit and save in the admin β€” no redeploy.
  • In-place editing: add ?edit_mode=true to any URL (e.g. …/en/JFK?edit_mode=true) for edit overlays. Public visitors still get the clean page.

?edit_mode=true&#32;overlays the editor on the live page.

That's the build

Fifty airports, 35 languages, live β€” described once and filled in by data: one route, four React components, and a handful of CEL bindings. The CMS holds the content, your code renders it, and CEL connects them.

Continue Reading
NextDeploymentsβ€Ί