Configuration
Indexa keeps its copy and its data in typed modules under src/config/, never as literals in components. Each one exports a value typed against an interface in src/config/types/configDataTypes.ts, so a missing field or a wrong shape is a pnpm check error rather than a blank space on a page.
The two you will edit first
siteData.json.ts is the brand: name, title, description, the author block, the default social image, and sameAs. That last one is empty on purpose — sameAs is what tells a search engine which real-world organization the JSON-LD Organization node refers to, and an invented profile URL is worse than none. Fill it with your actual profiles before launch.
siteSettings.json.ts holds four facts and nothing else:
export const siteLang = "en" as const; // <html lang>
export const siteLocale = "en-US" as const; // Intl dates, og:locale, JSON-LD inLanguage
export const siteSettings = {
useViewTransitions: true,
useAnimations: true,
} satisfies SiteSettingsProps;
useViewTransitions mounts or drops <ClientRouter /> in BaseHead. useAnimations is the master switch for decorative motion — the scroll-reveal wrapper, the header’s load-in, the section entrances. Turning it off leaves the site fully functional and completely still. Neither switch touches accessibility: prefers-reduced-motion is honoured by a global guard regardless of what these say, which Motion explains.
The satisfies rather than a type annotation is deliberate — it checks the shape while keeping the literal true, so a component can narrow on it.
The content modules
| Module | Owns |
|---|---|
navData.json.ts |
Header links, the two strip CTAs, the index-status figures, the four footer columns, the legal row, the company line |
homeData.json.ts |
All ten home sections — hero, body types, dossier, deals, makes, testimonials, valuation, essentials, latest, FAQ |
carsData.json.ts |
carRecords (37 used records), browseData (the /used-cars/ header, toolbar, rail and copy), featuredCar, findCar |
hubsData.json.ts |
newCarRecords and electricCarRecords (16 each) plus a browseData-shaped block for each hub |
carImages.ts |
The shared photo pool, keyed by CarImageKey — kept out of carsData so that file stays importable under Node type stripping |
collectionsData.json.ts |
The editorial collection page: hero, method, six picks, glance, siblings, editor’s note |
specialistsData.json.ts |
specialistsCategory (hero, intro, eight rated firms, verification, cities, trades) and specialistsCity (the London page and its five-group rail) |
reviewsData.json.ts |
The reviews hub: masthead, featured review, verdicts, the shared four-step band, owners, make columns, FAQ |
searchData.json.ts |
/search/ — the query, its token parse, the result refs, near-misses, the three ranking rules |
savedData.json.ts |
The favourites dashboard’s copy for both states, the drift ledger, the position tiles, the watch band |
signInData.json.ts, submitData.json.ts, advertiseData.json.ts, valueMyCarData.json.ts |
One application page each, forms included |
blogData.json.ts |
The blog index: masthead, featured slot, topics, toolbar, ledger, authors band, related |
infoData.json.ts |
Twelve information pages, keyed by slug and served by one dynamic route |
legalData.json.ts |
Terms and privacy, section by section |
Two of these deserve a note. infoData is keyed by slug and src/pages/[info].astro enumerates its keys, so adding an information page means adding an object — no new route file. And carsData/hubsData are the same shape on purpose: the three browse hubs render through one set of sections, which is what Browse Hubs is about.
Editorial placeholders
Several figures in the data are the design’s own numbers awaiting a live index feed: the header’s 452,681 records and 14:00 refresh, the browse header’s match count, /search/’s total of 4,106, the rail’s “all 84 makes”. They are marked in the source where they appear. They are internally consistent — the tests pin the ones that have to agree with the dataset — but they are not derived from it, so replace them when you wire a real feed rather than assuming they will follow.
The same policy covers the fictional contact details and the footer’s company number. Nothing in Indexa invents a real-world identity for you.
The one environment variable
SITE_URL is your production domain, and it feeds six things at once: canonical URLs, og:url, the JSON-LD graph, the sitemap, robots.txt and llms.txt. It is read in astro.config.mjs and falls back to https://example.com:
const site = process.env.SITE_URL ?? "https://example.com";
A fresh clone builds on that placeholder so you can see the site before owning a domain. A production deploy refuses to: the config throws if the placeholder survives into a build that Netlify, Vercel or your own DEPLOY_ENV=production has marked as production. Local builds and deploy previews are unaffected. .env.example documents the two variables and which hosts need the second one.
That gate exists because all six consumers fail silently. A canonical tag pointing at example.com looks perfectly fine in review.