May 14, 2026 · 9 min read · Use cases
Generating 10,000 destination pages from one API.
Programmatic SEO for travel works when the underlying data is real. It fails when it's GPT slop. Here's the pattern: pull cities by score, fetch typed context per city, render at build time, and skip the LLM-generated paragraphs.
The build pipeline
The mental model is two API calls per page, executed at build time. No runtime LLM, no hallucination surface, no per-request cost.
// Step 1. Get the list of cities to render
GET /v1/cities?iso_alpha2=IN&min_score=50&limit=10000
→ [{ id: "in-jodhpur", name: "Jodhpur", master_score: 84, slug: "jodhpur" }, ...]
// Step 2. For each city, fetch the full context
GET /v1/cities/in-jodhpur/context
→ { city, heritage[], circuits[], airports[], sentiment, narrative }
The narrative field is the part most teams overlook.
The API returns long-form destination prose grounded against the
structured fields, license-tagged for redistribution. You don't
need to write or generate filler text to fill the page.
Static site generator integration
The pattern below is Next.js, but the shape is the same in Astro, Hugo, Eleventy, or your own pipeline.
// app/destinations/[slug]/page.tsx
export async function generateStaticParams() {
const r = await tmai("/v1/cities?iso_alpha2=IN&min_score=50&limit=10000");
return r.map(c => ({ slug: c.slug }));
}
export default async function Page({ params }) {
const ctx = await tmai(`/v1/cities/${params.slug}/context`);
return (
<article>
<h1>{ctx.city.name}, {ctx.city.state}</h1>
<p>{ctx.narrative.lede}</p>
<h2>Heritage</h2>
<ul>{ctx.heritage.map(h => <li key={h.id}>{h.name}</li>)}</ul>
<h2>Getting there</h2>
<p>Nearest airport: {ctx.airports[0]?.name} ({ctx.airports[0]?.iata}).</p>
{ctx.circuits.length > 0 && (
<p>On the {ctx.circuits[0].name}, stop {ctx.circuits[0].sequence}.</p>
)}
</article>
);
}
The schema you write against
The shape of /v1/cities/{id}/context is stable across
countries. Build your template once and it works for every city
in coverage:
city— id, name, admin attribution, lat/lon, iso codesheritage[]— UNESCO and ASI sites, with category and protection yearcircuits[]— named tourism circuits the city sits on, with sequenceairports[]— IATA, name, distance_km, ordered by proximitysentiment— score 0–1 and trailing 30-day trendnarrative— lede, sections, fully license-tagged
Why not just generate the prose with an LLM?
Three reasons. First, Google has been measurably down-weighting pure LLM-generated filler since the 2024 helpful-content updates. Pages that look like every other AI travel page rank like every other AI travel page. Second, the hallucination rate on second-tier Indian cities is high enough that 10–30% of your facts will be wrong on the day you publish. Third, the marginal cost of LLM tokens × 10,000 pages is non-trivial; pulling pre-written, license-tagged narrative is one HTTP call.
Indexability and freshness
Set up a weekly rebuild that re-pulls
/v1/cities/{id}/context for the top 2,000 cities by
master_score, and a monthly rebuild for the long tail. Sentiment,
airport changes, and new heritage inscriptions flow through
automatically. Your sitemap stays accurate; pages that lose
freshness stop ranking.
Schema markup, for free
Because every field is structured, your TouristAttraction
and Place JSON-LD writes itself. Pull lat/lon out of
city, names out of heritage[], and you
get rich-result-eligible markup without a manual review step.
Worked example: 9,000+ Indian city pages
Filter min_score=50 on India and you get roughly 4,200
pages worth shipping. Drop to min_score=30 and you're
closer to 9,000. Ship them as a typed sub-section of your domain,
cross-link by circuit and by state, and let the long tail
accumulate. Three months in, head terms still go to Wikipedia, but
the long tail — "things to do in Jodhpur near Mehrangarh," "ASI
monuments in Hampi" — starts converting.
Free tier covers 1,000 calls/month, enough to prototype a full state. Growth at $249/mo is comfortable for a 10,000-page build with weekly refreshes.