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.
/{language}/{airport_code}.cms-renderer.curl -fsSL https://bun.sh/install | bashgh) signed in, and a Vercel account linked to GitHub.language) ship built-in.cms-renderer SDK with a read-tier API key.End state: ~50 airports in the CMS, the app wired to read them, and the design system in place.
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.
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.
airport componentIn 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 airport component β six fields, with code set as the Route Slug.
bun run generate-schemas
This writes airportSchema (Zod) and Airport (type) to generated/cms-schemas.ts, and confirms your credentials work.
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
datafolder, 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.
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/fontfor 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.
Build the rendering layer: the UI elements, the React components, the route, the CEL bindings, and translation.
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 β brandheadline β title, subtitlebody β code, city, country, latitude (Number), longitude (Number)footer β textbody.latitude and body.longitude must be Number to match the airport component.
Each UI element is its own component β here body, set Active with the UI Element tag.
All four UI elements alongside the airport component.
bun run generate-schemas
Prompt Claude:
Build four React components β nav, headline, body, footer β in
components/. Each takes a singlecontentprop typed asBlockComponentProps<T>fromcms-renderer/lib/types, whereTis the element's generated type, and reads its fields offcontent. 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 acode Β· city, countrysubtitle. 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.
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.
Adding a UI element to the page from the Custom tab.
The four UI elements added to the JFK binding in the Page Builder.
Pick the JFK binding β Add UI Element β Custom tab β add nav, headline, body, footer in order. Profound propagates the set to every airport binding.
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).
| Field | CEL value |
|---|---|
headline.title | documents.get("airport", meta.params.airport_code).name |
headline.subtitle | documents.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.country | documents.get("airport", meta.params.airport_code).<field> |
body.latitude / body.longitude | documents.get("airport", meta.params.airport_code).latitude (and .longitude) |
nav.brand, footer.text | static strings |
Publish the page (top-right).
Open localhost:3000/JFK, then /SFO, /LAX β same template, different airport.
The finished page rendering JFK's data, in English.
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.
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 {language} segment to the route.
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):
| Field | Translated CEL |
|---|---|
headline.title | documents.translated("airport", meta.params.airport_code, meta.params.language).name |
headline.subtitle | documents.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.country | documents.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.
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.
Both ship with the scaffold.
<Refresher> in layout.tsx updates the page you're previewing when you edit and save in the admin β no redeploy.?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 overlays the editor on the live page.
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.