The data file came first
The whole property is driven by a single JavaScript object: window.DESTINATIONS, an array of records for thirty destinations. Each record looks like this in essence:
{ slug: "bali-indonesia", name: "Bali & Indonesia", region: "Southeast Asia",
continent: "Asia", category: "destination", image: "images/bali-hero.webp",
budget: "$$", difficulty: "Easy", blurb: "Temples, rice terraces, surf..." }
Every field in that record exists because a template somewhere consumes it. The name fills the H1 and the title tag. The region and continent drive breadcrumbs and the hub-page filters. Budget and difficulty become badges on cards. The slug becomes the URL. Writing a new destination is therefore a data-entry task, and that is the entire point: the cost of adding a page collapses to the cost of adding a row, so the site can hold far more destinations than a hand-crafted editorial site of the same effort.
One hub, five variants
A single record is not enough to rank for anything. Someone planning a trip to Bali searches for "Bali itinerary", "Bali budget" and "when to visit Bali" separately from "Bali travel guide". So the template does not produce one page per record — it produces a small cluster. Each destination gets a hub page plus five question-shaped variants:
<slug>— the hub: what to see, how many days, how they fit together.<slug>-things-to-do— the attractions and experiences actually worth the time, ranked rather than listed.<slug>-itinerary— a day-by-day plan answering "how do I actually spend the time".<slug>-budget— costs, currency notes and where the money goes.<slug>-when-to-visit— seasons, crowds and the trade-offs of each month.<slug>-where-to-stay— neighbourhoods, price bands, and what each area is like to actually sleep in.
Thirty destinations at six pages each is 180 pages before the property publishes anything else; a further eight practical guides and five regional food pages bring the total to 200. That arithmetic is the whole argument for the model — the marginal cost of a destination is one row, not six articles.
That cluster pattern is the part worth copying: one entity, several search intents, all interlinked from the hub. On the live site the hubs and their variants sit in one flat directory, which keeps the crawl simple — each page is one click from the destination index and the breadcrumb trail is uniform. Google's SEO starter guide calls this the "page with a clear purpose" pattern; the data model simply made it cheap to repeat consistently.
A content model is not schema.org structured data
"Structured" does two different jobs on a site like this, and conflating them hides real work. window.DESTINATIONS is a content model: it is our own shape for our own data, it exists only at build time, and no search engine ever sees it. It decides which pages exist and what fills each slot. It is not structured data in the search sense, and on its own it earns nothing.
Schema.org markup is a separate layer written into the rendered HTML. Every guide page in this property ships JSON-LD that says what it is — a TravelGuide tied to a Place, published by an Organization — which is what lets a result be understood as a travel guide rather than as an anonymous page. On the question-shaped variants a FAQPage carries the same questions a reader sees on the page. The data file emits none of this; the template emits all of it, once, for every page it generates.
Keeping the two ideas apart matters because a generator multiplies mistakes as reliably as it multiplies pages. Fix a heading in the template and 200 pages improve; get a schema field wrong and 200 pages are wrong. The data model can guarantee that the markup exists and is consistent — it cannot guarantee that it is correct, and no amount of good data entry fixes a bad schema.
Where the template stopped being enough
Here is the honest part: at first the generator filled every page from the same handful of fields, and the result was thin. When we ran a word-count audit over the whole build — by then 140 pages — the median page carried 222 words and 94 of them fell below 300. Every page rendered correctly and two thirds of them were too shallow to be useful on their own.
The audit changed the rule. The schema stayed — it is still what creates the pages — but every hub and every variant gained a written section that the template cannot fake: local detail, practical specifics, the kind of sentences that only exist if somebody actually assembled the trip. The data model is the skeleton; the writing is the muscle. Treating the generator as a publisher rather than a scaffold was the mistake, and measuring word counts per page is how we caught it — not as a target to hit, but as a symptom to read. Our notes on auditing generated content describe the same failure on a larger scale.
Tables, mobile and the long tail of real pages
A data-driven site concentrates its bugs. One layout fault in the template is not one broken page — it is 200 broken pages. Two fixes came out of that.
The first was tables. Itinerary and budget pages want tabular data, and a wide table on a narrow phone breaks the whole layout — the page scrolls sideways or the grid collapses. The fix was defensive CSS rather than per-page markup: grid columns that use minmax(0, 1fr) instead of fixed fractions, so children can shrink below their content width, plus a container that allows horizontal scroll only inside the table itself. A performance pass on the same templates later showed how much of the mobile experience is decided in shared CSS rather than in content.
The second was images. A remote stock API returns the hero image, and it is not always the same image twice — the CDN can serve a different crop on a later request. Downloading artwork during a rebuild therefore risks silently swapping pictures between deploys. The fix is to download once, verify the file, and commit it like any other asset. Never assume a remote URL is stable; treat it as a pointer to fetch, not as a file.
The rule we took away
Structured data is an excellent way to build a site and a poor excuse to skip writing one. The test we now apply to every generated page is the same as the test for a hand-written one: if a reader landed on it from a search result, would it answer the question on its own? A data model can guarantee consistency, URLs, internal links and metadata — it cannot guarantee usefulness. Build the model so pages are cheap, then spend the saved effort making each one worth landing on.