Skip to content
AstroCraft Docs
On this theme

Configuration

Olsa has no CMS and no integration options object. Its configuration is six TypeScript modules in src/config/, imported directly by the pages and sections that need them. They are typed against interfaces in src/config/types/configDataTypes.ts, so a missing field is a compile error rather than an empty string in production, and two of them use the type system to enforce invariants a runtime check could not.

The rule the whole theme follows is that behaviour and copy come from config, never from hard-coded literals in components. Rebranding, adding a nav entry or changing a plan price is an edit in src/config/, not a hunt through .astro files.

siteData.json.ts — identity

The brand facts. name, title and description feed the <title>, the meta description, the JSON-LD WebSite node and the RSS channel. author carries a name, an email and a Twitter handle without the @ (used for twitter:creator).

Three blocks are worth calling out because they are load-bearing beyond the page they appear on:

  • sameAs is an array of social and profile URLs. It ships as [] and it feeds the Organization JSON-LD’s sameAs, which is how a search engine disambiguates your organization from another with the same name. Fill it in, and mirror the same URLs into navData.social so the footer icons and the structured data agree.
  • contact is the public contact block — email, display phone, and a structured street address. It has two consumers: the three channel cards on /contact/, and the Organization node’s email / telephone / PostalAddress. One fact, two renderings, so the page and the structured data cannot disagree. It ships with placeholders on purpose: a [email protected] inbox, a +1 (555) 0123-456 number from the reserved-for-fiction range, and the mock’s Austin address. Replace all three before you launch.
  • demoVideoId is the YouTube id behind the “Watch demo” pill in the hero — the eleven characters after v=. Leave it empty and the pill keeps a placeholder href while DemoDialog renders nothing at all; the branch lives in Sections/Global/_demo.ts. It ships pointing at Blender’s Big Buck Bunny from the rights holder’s own channel, which is placeholder footage in the same way the hero’s logo strip is placeholder brand art.

defaultImage is the fallback social image (/og.jpg in public/) plus its alt text. BaseHead also uses it as the Organization logo until you give it a real one.

One module drives the header, the footer’s three link columns, the social row and the legal row.

The header is the interesting part. Each entry in header is either a plain link or a mega menu, discriminated by the presence of a columns array:

export type NavHeaderEntry = NavLink | NavMenu;

Because it is a union rather than an optional field, a menu can never carry an href and a link can never carry columns — the illegal state is not representable. Navbar.astro narrows with "columns" in entry and renders both shapes from the one array, and the mobile sheet flattens each menu into one titled section per column.

Menu links are NavMenuLinks, which add an optional icon and description for the rich mega-menu cell. The icon field is typed as the generated IconName union, so a typo is a build error rather than a blank square.

The shipped header covers every public page exactly once across two menus and one flat link. Out by design: / (the wordmark is its link), /404/ and the dev-only catalog. Adding a page to the site means adding a line here — that is the convention the file states, and it is why the navigation never quietly falls behind the routes.

The three footer columns are a different story: most of their links are placeholder # hrefs waiting for routes you have not built (Changelog, Careers, Help Center). The social row ships with four # hrefs too. Fill them in, and remember to mirror them into siteData.sameAs.

pricingData.json.ts — plans and the keyed matrix

Two exports in one module so the plan cards and the comparison table cannot drift apart. plans is declared as const satisfies readonly PricingPlan[], which preserves the literal names, and the plan-name union is then derived from the data:

type PlanName = (typeof plans)[number]["name"];   // "Starter" | "Team" | "Enterprise"

The comparison matrix is keyed by that union rather than positional:

export interface ComparisonRow<PlanName extends string> {
  feature: string;
  values: Record<PlanName, ComparisonValue>;
}

Two things follow. Column order is irrelevant, because a cell is found by plan name and not by index. And a missing or misspelled cell is a compile error — which means it surfaces under pnpm check and not under pnpm build. This is the single strongest reason not to skip the type check in your verification loop.

A cell value is a string, true (renders a check) or false (renders an em dash). Add a plan and every comparison row immediately fails to compile until you have given it a value — which is the point.

faqData.json.ts — one list, tagged per page

Three pages carry an FAQ (home, pricing, contact) and they overlap: several questions appear on two lists, and “Is my data secure?” appears on all three. Rather than three arrays with copies of the same answer waiting to drift, there is one list and each item declares which pages show it:

{ q: "…", a: "…", pages: ["home", "pricing", "contact"] }

faqsFor(page) filters it, and a page renders <Faq items={faqsFor("pricing")} />.

Order in the array is the order on the page. The shipped ordering is arranged so all three subsets come out in their own design’s order, which is why the shared security question sits between two contact-only entries. If you reorder, check all three pages.

legalData.json.ts — terms and privacy

A Record<"terms" | "privacy", LegalPageProps>. Each document has a title, a meta description, an ISO lastUpdated date (formatted at render through formatDate), an intro paragraph and an array of { heading, body[] } sections where each body string becomes its own <p>.

Both documents are placeholder template text and say so in their own intro. Replace them with copy reviewed by a qualified professional before you launch. Nothing here is legal advice.

siteSettings.json.ts — locale and two switches

The smallest file, and the one with the most reach.

export const siteLang = "en" as const;      // <html lang>, RSS <language>
export const siteLocale = "en-US" as const; // Intl date formatting, og:locale, JSON-LD inLanguage

export const siteSettings = {
  useViewTransitions: true,
  useAnimations: true,
} satisfies SiteSettingsProps;

siteLocale is BCP-47 and gets normalized to en_US for og:locale in BaseHead. formatDate in @js/textUtils reads it, so changing it changes every rendered date at once.

useViewTransitions gates whether BaseHead renders <ClientRouter />. useAnimations is the master switch for the decorative motion layer — the scroll reveals, the ambient loops, the panel parallax. It does not govern intentional micro-interactions like a rotating dropdown chevron, and it is completely independent of prefers-reduced-motion, which is always honored by a global CSS guard whatever this flag says. See Motion & Animation.

Note the satisfies rather than a type annotation: it checks the shape while preserving the literal true, so anything branching on the flag can be narrowed at build time.

The theme is single-language. The former i18n layer was removed deliberately, and siteLang / siteLocale are the only locale facts left. Re-adding internationalization means restoring the helper layer, per-locale config and data, hreflang tags in BaseHead and an i18n block in astro.config.mjs — all together. Do not reintroduce it piecemeal.

astro.config.mjs — the settings that are not in src/config

Five things live in the Astro config rather than the typed config layer, because Astro owns them:

  • site — your production domain, still https://example.com. It feeds canonical, og:url, the sitemap, RSS, robots.txt, llms.txt and the JSON-LD @ids.
  • trailingSlash: "always" — one canonical URL shape, agreeing with the directory build, the canonical tag and every generated link.
  • No adapter — deliberately absent, so the theme builds to 100% static HTML and picks no host.
  • security.checkOrigin: true — set explicitly so it survives a config edit and is already correct the day you add an adapter.
  • env.schema — the three optional server-only contact variables.
  • vite.build.assetsInlineLimit: 0 — leave it at zero. Inlined short scripts break under <ClientRouter /> view transitions.

The sitemap integration also carries a filter that drops /examples/, /404/, /signin/ and /signup/, because all four set noindex in their markup and a sitemap entry would contradict that.

Adding your own config file

Follow the shape the others use. Declare an interface in src/config/types/configDataTypes.ts, write the data module as <name>Data.json.ts with a default export typed by it, and import it through the @config/* alias. Use satisfies over an annotation when you want literal types to survive, and derive unions from data ((typeof x)[number]["name"]) rather than maintaining a parallel union by hand.

NEXT STEPDeployment