The layout system provides the HTML document shell and page-level composition templates. Every page in the site renders through BaseLayout (the document wrapper) and one of the template layouts that compose the visible page structure.
Who this is for
- Implementors building new pages or page templates
- Developers customizing layout behavior for specific routes
- AI agents wiring content collection routes to layouts
Architecture Overview
The layout stack has three layers:
- BaseLayout — HTML document shell. Owns
<head>, global CSS, FOUC prevention, and analytics. Every page passes through this. - Template layouts — Page composition.
PageGridLayoutandBlankLayoutwrapBaseLayoutand compose the visible page structure.PageGridLayouthandles all standard page patterns via conditional named slots;BlankLayoutis for no-chrome pages. - Layout primitives —
PageGrid(CSS grid infrastructure) andContainer(width constraints) are the building blocks template layouts use internally.
All layout files live in src/core/components/layouts/ and are CORE-OWNED.
BaseLayout
File: src/core/components/layouts/BaseLayout.astro
The HTML document wrapper. Responsible for:
<!doctype html>and<html lang>fromsiteConfig.locale- CSS entry point (
global.css) - FOUC prevention script — sets
data-themeattribute before first paint <SEO>,<Analytics>,<Favicon>head components- Reserved
search-modalnamed slot at end of<body>for search modal integration.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
title | string | required | Page title for <title> and OG |
description | string | required | Meta description and OG |
canonical | string | auto | Override canonical URL |
noindex | boolean | false | Emit noindex,nofollow robots tag |
ogImage | string | config | Override OG image |
type | 'website' | 'article' | 'website' | OG type |
datePublished | Date | — | Reserved for structured data output |
dateModified | Date | — | Reserved for structured data output |
FOUC Prevention Script
The is:inline script in <head> resolves the theme before any stylesheet paints:
- If
themeSwitcher === 'none': clearstheme-preferencefrom localStorage, locks todefaultTheme(or'light'ifdefaultTheme === 'system'). - Reads
localStorage.getItem('theme-preference'). - Reads
prefers-color-scheme: darkmedia query. - Resolution order: stored preference → system preference →
defaultThemefallback. - Sets
data-themeattribute on<html>.
The script uses define:vars to inject config values without leaking them into the module graph.
PageGridLayout
File: src/core/components/layouts/PageGridLayout.astro
The single standard page template. Exposes all 11 PageGrid areas as conditional named slots. Common page patterns (sidebar, listing, three-column, landing) are handled through slot selection — no separate layout files needed. BlankLayout is the only other template (genuinely different behavior: no PageGrid, no chrome).
The header and footer slots are conditional — all slots produce no DOM output when unpopulated. Most landmark elements are owned by the components placed in slots, not by the layout. The exception is the subheader slot, which the layout wraps in <header role="banner"> to provide the page heading landmark. Components nested in PageGrid divs must declare explicit ARIA roles since they are not direct children of <body>.
SkipLink integration is handled by the site header component.
Default-slot framing behavior:
LayoutSectioncomponents frame their own inner content width/gutters.- Direct non-
LayoutSectionchildren of<main id="main-content">receive a default layout frame (inheritedcontentWidthwidth +--pt-layout-container-gutter). - Add
data-no-layout-frameto a direct child to opt out of this automatic framing.
Props
All BaseLayout props, plus:
| Prop | Type | Default | Notes |
|---|---|---|---|
contentWidth | 'content' | 'content-wide' | 'wide' | 'full' | 'wide' | Default width for LayoutSection inner frames in the default slot via CSS variable inheritance. |
sidebarPosition | 'start' | 'end' | 'start' | Navigation column left (start) or right (end) |
sidebarWidth | string | — | CSS value for --page-sidebar-width (e.g. '20rem') |
asideWidth | string | — | CSS value for --page-aside-width (e.g. '20rem') |
Slots
Slots are listed in DOM (tab) order. CSS Grid areas control visual placement independently.
| Slot | Grid Area | Element | Condition |
|---|---|---|---|
banner | banner | <div data-area="banner"> | If populated |
header | header | <section data-area="header" aria-label="Site Header"> | If populated |
subheader | subheader | <header data-area="subheader" role="banner"> | If populated |
main-header | main-header | <section data-area="main-header"> | If populated |
aside | aside | <aside data-area="aside"> | If populated |
| default | main | <main id="main-content"> | Always |
main-footer | main-footer | <section data-area="main-footer"> | If populated |
sidebar-header | sidebar-header | <aside data-area="sidebar-header"> | If populated |
sidebar | sidebar | <aside data-area="sidebar"> | If populated |
sidebar-footer | sidebar-footer | <aside data-area="sidebar-footer"> | If populated |
footer | footer | <div data-area="footer"> | If populated |
ARIA Landmarks
Most landmark elements are owned by slotted components, not the layout. Components placed in slots must declare explicit roles since PageGrid areas are not direct <body> children.
role="banner"— provided by thesubheaderslot wrapper (<header role="banner">); contains the page heading/heronavigation— provided by the<nav>element inside SiteHeader; SkipLink also renders inside SiteHeader<main id="main-content">— always presentrole="complementary"— provided by aside component whenasideslot populatedrole="contentinfo"— provided by SiteFooter
Slot Recipes
Common page patterns are implemented by selecting which slots to populate:
Standard page — default slot only; contentWidth controls default frame width:
<PageGridLayout title="..." description="..." contentWidth="wide">
<h1>Page title</h1>
<p>Raw slotted content is framed automatically.</p>
</PageGridLayout>
Opt out of auto frame on a direct child:
<PageGridLayout title="..." description="..." contentWidth="wide">
<div data-no-layout-frame>Edge-to-edge component</div>
</PageGridLayout>
Sidebar / docs page — populate sidebar slot; grid auto-expands sidebar column:
<PageGridLayout
title="..."
description="..."
contentWidth="content-wide"
sidebarPosition="start"
>
<DocsNav slot="sidebar" />
<LayoutSection>
<p>Page content</p>
</LayoutSection>
</PageGridLayout>
Listing page — filters in aside, toolbar+items in default slot, pager in main-footer:
<PageGridLayout title="..." description="...">
<div slot="subheader">...</div>
{/* page title / hero */}
<div slot="aside">Filters</div>
<div>toolbar + items grid</div>
{/* default slot */}
<div slot="main-footer">Pager</div>
</PageGridLayout>
Article detail page — breadcrumbs/metadata in main-header, TOC in aside, content in default slot, related articles in main-footer. Tab order follows: metadata → TOC → article body:
<PageGridLayout title="..." description="..." contentWidth="wide">
<Hero slot="subheader" heading="..." />
<div slot="main-header">Breadcrumbs + metadata</div>
<TocAside slot="aside" headings={headings} />
<LayoutSection>
<article><Content /></article>
</LayoutSection>
<div slot="main-footer">Related articles + prev/next</div>
</PageGridLayout>
Marketing / landing page — contentWidth="full" with per-section width control:
<PageGridLayout title="..." description="..." contentWidth="full">
<LayoutSection contentWidth="wide">Hero</LayoutSection>
<LayoutSection contentWidth="wide" background="soft">Feature</LayoutSection>
<LayoutSection contentWidth="content-wide" background="contrast">
CTA band
</LayoutSection>
</PageGridLayout>
Collection Integration
Route files set the collection’s default layout pattern. Optional frontmatter fields (contentWidth, sidebarPosition, hideNav) allow per-entry overrides:
<PageGridLayout
contentWidth={entry.data.contentWidth ?? "content-wide"}
sidebarPosition={entry.data.sidebarPosition ?? "start"}
>
{!entry.data.hideNav && <DocsNav slot="sidebar" />}
<Content />
</PageGridLayout>
See src/content.config.ts for the schema fields.
BlankLayout
File: src/core/components/layouts/BlankLayout.astro
Wraps BaseLayout with no structural chrome — no header, footer, or grid. Use for landing pages, embeds, or full-content-control routes where global CSS, analytics, and theme script are still needed.
Props
Same as BaseLayout.
Slots
| Slot | Notes |
|---|---|
| default | Renders directly inside <body> |
PageGrid
File: src/core/components/layouts/PageGrid.astro
CSS named grid with 11 areas and a five-column full-bleed structure. All areas use data-area attributes for placement; only <main> uses a semantic element selector. Sidebar/aside columns auto-expand from 0fr via CSS classes (page-grid--has-sidebar, page-grid--has-aside) set by PageGridLayout.
Grid Areas
banner, header, subheader, sidebar-header, main-header, sidebar, main, aside, sidebar-footer, main-footer, footer
Props
| Prop | Type | Notes |
|---|---|---|
class | string | Additional CSS class (e.g. page-grid--sidebar-end) |
style | string | Inline styles (e.g. --page-sidebar-width) |
Five-Column Structure and Gutter Model
The grid uses five columns. Understanding how they interact with Container padding is essential for troubleshooting layout spacing.
col 1: minmax(0, 1fr) ← bleed rail (centering only)
col 2: 0fr | sidebar-width ← sidebar (auto-expands when populated)
col 3: minmax(0, max-width) ← main content
col 4: 0fr | aside-width ← aside (auto-expands when populated)
col 5: minmax(0, 1fr) ← bleed rail (centering only)
Bleed rails (columns 1 and 5) exist for centering. On wide viewports they grow via 1fr to push the content area to the center. On narrow viewports they shrink to 0. They do not provide gutter spacing — that is the Container’s job.
Container provides all gutters. Every LayoutSection wraps its content in a Container which applies padding-inline: var(--pt-layout-container-gutter). This single source of truth provides:
- Edge spacing between content and the viewport edge
- Inter-column spacing between content and adjacent sidebar/aside columns
Full-span rows (banner, header, subheader, footer) span all five columns (1 / -1). Their LayoutSection backgrounds extend viewport-wide; Container padding provides the only inset.
Content rows (main-header, main, main-footer, sidebar, aside) occupy their named grid areas in columns 2–4. When <main> has no sidebar or aside, it spans all columns (1 / -1) so its LayoutSection backgrounds also go edge-to-edge. When sidebar or aside is present, <main> is constrained to the main grid area.
Sidebar and aside columns auto-expand from 0fr when populated. Their content components provide their own internal padding (e.g., DocsSidebarNav has px-sm on items, TocAside has p-md). They do not use Container.
Responsive Behavior
- Desktop (>767px): Five-column full-bleed grid as described above. Sidebar and aside columns auto-expand from
0frwhen populated; the main column narrows accordingly to keep the content rail within--pt-fluid-vw-max. WhencontentWidth="full", rails collapse and the main column becomes uncapped. - Three-column collapse (≤1023px): When both sidebar and aside are present, the grid collapses to a single-column stack to avoid extreme compression.
- Mobile (≤767px): Single-column vertical stack with all areas in document order.
Sidebar-End Variant
Add class page-grid--sidebar-end to place the sidebar column on the right side instead of the left. Used by PageGridLayout when sidebarPosition="end".
Container
File: src/core/components/layouts/Container.astro
Width constraint wrapper with horizontal centering and gutter padding.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
size | 'content' | 'content-wide' | 'wide' | 'full' | 'inherit' | 'wide' | Width variant. inherit uses page-level section width var |
gutter | 'default' | 'none' | 'default' | Horizontal padding behavior |
class | string | — | Additional CSS classes |
Size Variants
| Variant | Max Width | Use Case |
|---|---|---|
content | --pt-layout-content-max (65ch) | Prose-optimized reading width |
content-wide | --pt-layout-content-wide-max (80ch) | Wide prose or docs |
wide | --pt-layout-container-max (80rem) | Standard page container |
full | none | Full-bleed sections |
inherit | --page-section-content-max fallback --pt-layout-container-max | LayoutSection default frame size |
By default, all variants include horizontal gutter padding from --pt-layout-container-gutter. Set gutter="none" to remove horizontal padding.