Project Structure
src/
├── components/
│ ├── Sections/<Page>/ layout-free page sections — 98 files in 18 folders, plus Global/
│ ├── Cards/ 7 content-aware card compositions
│ ├── ui/<name>/ the primitive library — 37 folders, 84 components
│ └── svg/icons/ the <Icon> system — 578 inlined glyphs
├── config/ 19 typed data modules — the source of truth, never literals in components
├── data/ content collections (blog, authors), Zod-validated
├── js/ favorites, schema, toc, textUtils + their *.test.ts checks
├── layouts/ BaseLayout + BaseHead (every meta/SEO tag lives here)
├── pages/ 25 route files — thin shells owning BaseLayout + SEO
└── styles/ global.css entry, tailwind-theme.css tokens, motion/ catalog
The three tiers
A page in Indexa is a route shell, a stack of sections, and the primitives those sections are built from. The direction never reverses.
A page under src/pages/ owns three things and no more: BaseLayout, its SEO props, and the order of its sections. used-cars.astro is thirty lines and most of them are imports. That is the target shape, not an accident of a small page — cars/[ref].astro handles 69 records and is still under fifty lines, because everything it decides is which sections a record gets.
A section under src/components/Sections/<Page>/ is one band of the page: full-bleed if the design says so, laid out inside .site-container, and free of any knowledge about what sits above or below it. Sections that appear on more than one page live in Sections/Global/ — SiteHeader, SiteFooter, AlertBand, HowItWorks, IndexColumns, FaqSection, SectionHead, NextRail, LedgerCard, BandWave, IndexMark. The shared HowItWorks band is the clearest case: the four-step explainer renders on the valuation page and the reviews hub from the same file, with each page’s own four steps.
A card under src/components/Cards/ is a content-aware composition built on the card primitive — CarRecordCard, SpecialistCard, ArticleCard, DealCard, BodyTypeCard, EssentialCard, TestimonialCard. A card knows what a car record or a post is; a primitive does not, and that is the line between the two folders.
A primitive under src/components/ui/<name>/ knows nothing about Indexa at all. It takes native props plus variants, renders tokens, and merges your classes. Its contract is written down in src/components/ui/README.md and repeated in Components.
Config is the source of truth
Nineteen modules under src/config/ hold the site’s copy and data — one per page family, plus siteData, siteSettings and the shared carImages photo pool. A section receives its content as props; it does not reach for a string.
This is what makes the codebase searchable. When the header’s index count is wrong, it is wrong in navData.json.ts and nowhere else. When a browse rail offers a filter value no record carries, carsData.test.ts fails before you see an empty result list. Seven of those modules have a *.test.ts sitting beside them for exactly that reason — see Commands.
The types live in one place too: src/config/types/configDataTypes.ts. A record is a CarRecordProps, a footer link is a NavLinkItem, and an internal href is a SiteHref that types the trailing slash — so a link written without one fails astro check rather than 404-ing in production.
Path aliases
@config/*, @js/*, @layouts/*, @components/*, @assets/* and @/* come from tsconfig.json paths. Prefer them over deep relative imports; the one exception is a module that must load under plain node --experimental-strip-types for a self-check, which needs relative imports and explicit .ts extensions. src/js/favorites.ts and src/config/siteSettings.json.ts are both written that way on purpose, and both say so at the top.
The two files that are not source
src/components/Sections/UiCatalog/ and src/pages/examples/ are the dev catalog. They build no pages in production, but Tailwind still scans their markup, so their demo classes sit in the stylesheet every page loads. Deleting both takes the shared CSS from 113,715 to 95,172 bytes and drops 69 unused @keyframes. Keep them while you are still choosing primitives — the cost is CSS, not JavaScript. Layout & Page Shell has the measurement.