Skip to content
AstroCraft Docs
On this theme

Layout & Page Shell

Every page is BaseLayout wrapping a single slot. The layout owns <html>, the <head> through BaseHead, the header, the footer and the page-transition curtain; the page owns its title, description and sections.

BaseLayout’s props

Eight, and each is worth knowing:

title and description are required and feed both the <head> and the JSON-LD. image is an optional ImageMetadata for the social card; without one the site’s default is used. noindex flips the robots meta.

footerCta defaults to true and draws the footer’s full-viewport CTA band — /404 and the legal documents pass false. footerCtaHeadline overrides that band’s headline, one string per authored line; the journal routes use it.

schema takes page-specific JSON-LD nodes that merge into the site graph, and article drives og:type=article plus the article:* meta on note pages. See SEO.

Two details in that interface are load-bearing for the CMS rather than stylistic. The props are spelled out rather than extends SeoProps, because this is the composer’s layout — these props are its Page and SEO tabs — and the prop walker in src/admin/js/sections.ts refuses an interface Props extends … outright. The members it could see would look like the whole list, and a prop nobody saw would vanish on the next save; extending anything left both tabs empty. And footerCtaHeadline is a mutable string[] rather than readonly, because a readonly array annotation is not one the walker can turn into a control — the four routes passing a frozen config tuple spread it at the call site.

If you add a prop to BaseLayout, keep both rules or the composer quietly stops seeing the layout.

The container

.site-container is the page-content container: an 80rem clamp with the header’s own px-4 sm:px-7 gutters.

.site-container {
  @apply mx-auto w-full max-w-[80rem] px-4 sm:px-7;
}

It exists because those four utilities were written out in thirty places across Sections/ and pages/, byte-identical in twenty-four of them. It has no exceptions: three files that wanted a different container (Legal, NotFound and the dev catalog) spell theirs out in plain utilities rather than overriding half of this one.

Being in @layer components is what lets a call site override part of it with a plain utility — the utilities layer wins over components whatever the specificity, so the heroes’ lg:px-20 and the gallery’s lg:pl-0 still land.

Site chrome does not use it. The header and footer are full-bleed by design.

The eleven shared sections

Sections/Global/ holds what more than one page draws: Header, Footer, PageTransition, PageHero, Pager, Faq, Testimonials, Partners, FounderQuote, NoteGrid and LayoutToggle.

The rule that put them there is in Sections/README.md: a section used by two or more pages moves to Global/, and until then it lives under its page’s folder. PageHero is the clearest example — four different mocks drew the same 60px serif over a scrimmed photograph, and the fourth page deleted its bespoke hero for the shared one.

Faq takes a name prop because it uses a native exclusive <details> group and the group name must be unique per page. The rows come from config, not from a prop the route invents, because the FAQ is the site’s rather than any one page’s.

The page transition

BaseLayout sets transition:animate="none" on <html>. That is not a taste call: the pixel curtain in PageTransition already covers the screen at swap time, so the router’s default root cross-fade would dissolve two identical covered frames — invisible, and about 200ms of dead time before the curtain may start lifting.

View transitions are switchable in siteSettings.json.ts via useViewTransitions. Prefetch is configured in astro.config.mjs as prefetchAll with the hover strategy — a pointer takes roughly 200ms to travel and press, and spending that on a fetch that was going to happen anyway is the conservative half of the pair. viewport would prefetch every link on screen, which on this site’s index pages is a dozen pages nobody asked for. prefetchAll rather than data-astro-prefetch per link, because the alternative is a thing to remember in 45 primitives and every future section, which is how a policy becomes a bug.

The stylesheet, and the catalog’s cost

The shared stylesheet every page loads is 164,378 bytes as shipped. Deleting the dev-only catalog — src/components/Sections/UiCatalog/ and src/pages/examples/ — takes it to 145,575 bytes, a saving of 11.4%, and drops the @keyframes in it from 108 to 44. The catalog demos nearly the whole motion library, and two thirds of those keyframes are referenced by no built page.

Do not try to get the same saving with @source not instead. It was tried and reverted: the directive is not build-mode conditional, so it strips the demo rules in astro dev too and the catalog silently stops animating. The delete is the remedy.

The same trap has a sharper version for the CMS. A further 27,653 bytes — 16.8% of that stylesheet — exists only because Tailwind scans src/admin/. But src/admin/layouts/BaseHead.astro imports the same global.css and has no sheet of its own, so @source not "../admin" unstyles the CMS in production. That one needs the package to grow its own Tailwind entry; it is an upstream fix, not a config line you can write here.

Keep the catalog while you are still picking primitives. The cost is CSS, not JavaScript, and it is the fastest way to see all 45.

Smooth scroll

Lenis drives inertial scrolling site-wide when useSmoothScroll is on and the reader has not asked for reduced motion. Its five scoped rules are imported from the package rather than copied, so they cannot drift.

One override belongs to the site: html.lenis { scroll-behavior: auto }. Native smooth scroll is wrong while Lenis drives — Lenis writes scrollTop every frame, and a smooth <html> makes the browser animate toward each of those writes instead of landing on them, so the scroll lags the wheel and never settles. lenis.css shipped that rule itself until v1.3 dropped it.

NEXT STEPComponents