Config Data Has Invariants. Test Them.
Our local SEO setup runs off a single cities array in src/content/local.ts. Each entry has a slug, a name, a county, a one-paragraph blurb, and a FAQ array. Those fields drive six things: static route generation, sitemap entries, city page content, FAQPage schema, the service area list in the LocalBusiness schema, and the footer copy.
TypeScript knows each entry has slug: string. It doesn't know whether slugs are unique, whether blurbs are long enough to be useful, or whether the FAQ array is populated. Those are invariants, not types.
What Can Go Wrong
If two cities share a slug, Next.js static generation builds both pages. One silently overwrites the other. The losing city gets a 404 at its intended URL. No compiler error. No build warning.
If a blurb is empty or too short, the city page renders with a blank description section and the FAQPage schema contains no questions.
None of this triggers a TypeScript error because an empty string and a zero-length array are valid values for those types. The type tells you what field exists. It doesn't tell you whether the value meets your actual requirements.
The Tests
it("has cities with unique slugs and required content", () => {
const slugs = cities.map((c) => c.slug);
expect(new Set(slugs).size).toBe(slugs.length);
for (const c of cities) {
expect(c.name).toBeTruthy();
expect(c.county).toBeTruthy();
expect(c.blurb.length).toBeGreaterThan(40);
expect(c.faqs.length).toBeGreaterThanOrEqual(1);
}
});
new Set(slugs).size === slugs.length is the uniqueness check. A Set drops duplicates. If the Set is smaller than the original array, two entries share a slug. The assertion fails.
blurb.length > 40 is a minimum content check. TypeScript sees string. The test sees "this blurb is four words and useless." You can adjust the threshold as the format solidifies, but having any floor here means an accidentally empty blurb never ships.
faqs.length >= 1 ensures every city page has schema-ready FAQ content. The TypeScript type is CityFAQ[]. An empty array is valid syntax. A city page with zero FAQs is not a valid city page.
The Sitemap Mirrors It
it("includes new local routes and excludes nonexistent ones", () => {
for (const c of cities) {
expect(urls.some((u) => u.endsWith(`/locations/${c.slug}`))).toBe(true);
}
expect(urls.some((u) => u.endsWith("/services/software"))).toBe(false);
});
The sitemap test iterates the same cities array and confirms each one has a matching entry in the generated sitemap. Adding a city to the config and forgetting to wire the route would fail this test. We also added an explicit false assertion for /services/software after we removed that route, so a stale sitemap entry doesn't quietly survive the next deploy.
The negative assertion is worth having. Most sitemap tests only check what should be there. Checking what should not be there catches the other failure mode: a route you removed that's still advertising itself to crawlers.
The Pattern
When a data array drives multiple output surfaces, the invariants that make those outputs valid are worth testing explicitly. TypeScript validates shape. Tests validate requirements. They're not redundant; they check different things.
We use the same approach for audit JSON: a test that loads the real fixture and asserts minimum completeness. The domain is different but the idea is the same. Audit JSON needs a populated assumption field. City config needs unique slugs and at least one FAQ per city. Both requirements are invisible to the compiler and visible to a three-line test.
The test that catches "Kokomo has no FAQ" costs ten minutes to write. The city page that ships with empty schema costs considerably more to notice and fix.