Skip to content
AstroCraft Docs
On this theme

Project Structure

Olsa has one organising idea, and once you see it the rest of the tree explains itself: a page is a thin shell that owns its layout and its SEO and composes sections; a section owns markup and layout; a card knows about a data shape; a primitive knows nothing at all. Every file under src/components/ sits at exactly one of those four levels, and the folder it lives in tells you which.

Each of the three component folders carries its own README.md stating the contract for that level. Those files are the source of truth in the repository; this page is the map.

The tree

src/
├── actions/
│   └── contact.ts                    # the contact form's server half — shipped unmounted
├── assets/
│   ├── images/                       # photography and renders, optimized by astro:assets
│   ├── logos/<id>.svg                # integration brand glyphs, keyed by collection id
│   ├── cta-grid.svg                  # the GrainyPanel floor vector
│   └── logo-wordmark.svg             # the brand wordmark, inlined raw
├── components/
│   ├── Sections/<Page>/<Name>.astro  # layout-free page sections; Global/ for shared ones
│   ├── Cards/<Name>Card.astro        # content-aware compositions built on ui/card
│   ├── ui/<name>/<Name>.astro        # the 44 UI primitives
│   └── svg/icons/                    # the icon system: Icon.astro + generated registry
├── config/
│   ├── siteData.json.ts              # brand, author, contact, default OG image, demo video
│   ├── navData.json.ts               # header entries (links + mega menus), footer, social, legal
│   ├── pricingData.json.ts           # plans + the name-keyed comparison matrix
│   ├── faqData.json.ts               # one FAQ list, tagged per page
│   ├── legalData.json.ts             # terms + privacy content
│   ├── siteSettings.json.ts          # siteLang / siteLocale + two feature flags
│   └── types/configDataTypes.ts      # the interfaces the data files satisfy
├── data/
│   ├── blog/<slug>.md                # blog collection entries (flat files)
│   ├── authors/<slug>.md
│   └── integrations/<slug>.md
├── js/
│   ├── schema.ts                     # JSON-LD builders (import-free on purpose)
│   ├── contact.ts                    # contact schema, spam gates, escaping, Resend call
│   ├── rss.ts                        # the RSS 2.0 document
│   ├── blogUtils.ts                  # published-post invariant + byline resolution
│   ├── integrationUtils.ts           # directory order + the logo glob
│   └── textUtils.ts                  # slugify, formatDate
├── layouts/
│   ├── BaseLayout.astro              # <html>, the body slots, the single CSS import
│   └── BaseHead.astro                # every <head> tag, JSON-LD, the pre-paint theme script
├── pages/                            # thin route shells + the four generated endpoints
├── styles/
│   ├── global.css                    # the single entry: semantic tokens, shared classes
│   ├── tailwind-theme.css            # palette aliases + the @theme inline bridge
│   ├── fonts.css                     # the Host Grotesk @font-face
│   └── motion/                       # the owned animate-* catalog
└── content.config.ts                 # the three Zod collection schemas

The four tiers, top down

Routes live in src/pages/ and are deliberately thin. A route owns BaseLayout with a real title and description, any noindex / schema / article SEO props, whatever data lookup those tags need, and the list of sections it composes. It holds no markup of its own. src/pages/index.astro is thirty lines and eleven of them are imports.

Sections live in src/components/Sections/, one folder per page plus Global/ for the ones used by more than one page. A section is a layout-free block of content — a hero, a feature row, a pricing table, a legal article. A section never imports BaseLayout. There are twelve section folders today: About/, Auth/, Blog/, Contact/, Features/, Global/, Home/, Integrations/, Legal/, NotFound/, Pricing/ and the dev-only UiCatalog/.

The rule for promotion is mechanical: a section used by two or more pages moves to Global/. That is why Cta, Faq, Footer, Navbar, PageHero, Pricing, SectionHeader, IconCards, Breadcrumbs and DemoDialog live there while Home/Benefits.astro does not.

Cards live in src/components/Cards/. A card is a content-aware composition — it knows about a CollectionEntry<"blog"> or a pricing tier — built from the generic ui/card primitives. Five cards ship (BlogCard, FeaturedPostCard, IntegrationCard, PricingCard, WhyCard) alongside three sub-parts that are pieces those cards compose rather than cards in their own right (WhyCardArt, IntegrationLogo, AuthorLine).

The folder records one judgment call worth reading before you add a card. FeaturedPostCard is a sibling of BlogCard rather than a featured variant of it, because its layout tree genuinely diverges. IntegrationCard is one component with a featured variant, because the tree is identical and only the scale changes. Split on the layout tree, not on the label.

Primitives live in src/components/ui/, one folder per primitive, and know nothing about your content. They are covered in UI Components and enumerated in the Components Reference.

Two ways data reaches a section, never both

A section either receives its content as typed props from the route, or reads config itself. Pick one per datum.

src/pages/terms.astro is the props-in exemplar: it picks legalData.terms, uses it for the SEO tags and passes it down as <LegalArticle page={page} />. One section serves both legal documents and nothing is read twice.

Sections/Home/Hero.astro is the other side: it imports siteData directly for the brand name, because the route has no use for that string.

src/pages/404.astro shows why the rule exists. Its description is both the meta description and the visible paragraph, so it lives in the route and flows down as a prop — the two can’t drift.

Sub-parts

Anything independently swappable that belongs to exactly one section sits beside it as a sibling file and is imported relatively. Sections/NotFound/NotFoundIllustration.astro is the clearest case: replacing the artwork is replacing one file. Sections/Pricing/ComparisonCell.astro, Sections/Contact/ContactField.astro and Sections/Auth/AuthField.astro follow the same shape.

Modules with a leading underscore are internal helpers, not components: Sections/Contact/_form.ts owns the contact form’s wiring switch, Sections/Global/_demo.ts owns the demo-video decision, Sections/Auth/_surface.ts and Sections/Contact/_surface.ts carry shared class strings.

Path aliases

tsconfig.json defines the aliases, and they resolve relative to the config file — there is no baseUrl, which TypeScript deprecated. Prefer them over deep relative imports:

Alias Resolves to
@config/* src/config/*
@js/* src/js/*
@layouts/* src/layouts/*
@components/* src/components/*
@assets/* src/assets/*
@images/* src/assets/images/*
@videos/* src/assets/videos/*
@/* src/*

Relative imports stay for genuinely local files — a section importing its own sub-part, a primitive importing a sibling part of the same compound.

Where the numbers land

At the time of writing the theme ships 149 .astro components across the four tiers, 28 built pages, three content collections, six config modules, 44 UI primitives, 571 icons and seven runnable checks. None of those are load-bearing figures — they are here so you can tell at a glance whether you are looking at the whole thing or a subset of it.

NEXT STEPConfiguration