Contact Us

Layouts

DOC-00008 reference implementor, developer

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:

  1. BaseLayout — HTML document shell. Owns <head>, global CSS, FOUC prevention, and analytics. Every page passes through this.
  2. Template layouts — Page composition. PageGridLayout and BlankLayout wrap BaseLayout and compose the visible page structure. PageGridLayout handles all standard page patterns via conditional named slots; BlankLayout is for no-chrome pages.
  3. Layout primitivesPageGrid (CSS grid infrastructure) and Container (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> from siteConfig.locale
  • CSS entry point (global.css)
  • FOUC prevention script — sets data-theme attribute before first paint
  • <SEO>, <Analytics>, <Favicon> head components
  • Reserved search-modal named slot at end of <body> for search modal integration.

Props

PropTypeDefaultNotes
titlestringrequiredPage title for <title> and OG
descriptionstringrequiredMeta description and OG
canonicalstringautoOverride canonical URL
noindexbooleanfalseEmit noindex,nofollow robots tag
ogImagestringconfigOverride OG image
type'website' | 'article''website'OG type
datePublishedDateReserved for structured data output
dateModifiedDateReserved for structured data output

FOUC Prevention Script

The is:inline script in <head> resolves the theme before any stylesheet paints:

  1. If themeSwitcher === 'none': clears theme-preference from localStorage, locks to defaultTheme (or 'light' if defaultTheme === 'system').
  2. Reads localStorage.getItem('theme-preference').
  3. Reads prefers-color-scheme: dark media query.
  4. Resolution order: stored preference → system preference → defaultTheme fallback.
  5. Sets data-theme attribute 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:

  • LayoutSection components frame their own inner content width/gutters.
  • Direct non-LayoutSection children of <main id="main-content"> receive a default layout frame (inherited contentWidth width + --pt-layout-container-gutter).
  • Add data-no-layout-frame to a direct child to opt out of this automatic framing.

Props

All BaseLayout props, plus:

PropTypeDefaultNotes
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)
sidebarWidthstringCSS value for --page-sidebar-width (e.g. '20rem')
asideWidthstringCSS value for --page-aside-width (e.g. '20rem')

Slots

Slots are listed in DOM (tab) order. CSS Grid areas control visual placement independently.

SlotGrid AreaElementCondition
bannerbanner<div data-area="banner">If populated
headerheader<section data-area="header" aria-label="Site Header">If populated
subheadersubheader<header data-area="subheader" role="banner">If populated
main-headermain-header<section data-area="main-header">If populated
asideaside<aside data-area="aside">If populated
defaultmain<main id="main-content">Always
main-footermain-footer<section data-area="main-footer">If populated
sidebar-headersidebar-header<aside data-area="sidebar-header">If populated
sidebarsidebar<aside data-area="sidebar">If populated
sidebar-footersidebar-footer<aside data-area="sidebar-footer">If populated
footerfooter<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 the subheader slot wrapper (<header role="banner">); contains the page heading/hero
  • navigation — provided by the <nav> element inside SiteHeader; SkipLink also renders inside SiteHeader
  • <main id="main-content"> — always present
  • role="complementary" — provided by aside component when aside slot populated
  • role="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 pagecontentWidth="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

SlotNotes
defaultRenders 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

PropTypeNotes
classstringAdditional CSS class (e.g. page-grid--sidebar-end)
stylestringInline 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 0fr when populated; the main column narrows accordingly to keep the content rail within --pt-fluid-vw-max. When contentWidth="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.

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

PropTypeDefaultNotes
size'content' | 'content-wide' | 'wide' | 'full' | 'inherit''wide'Width variant. inherit uses page-level section width var
gutter'default' | 'none''default'Horizontal padding behavior
classstringAdditional CSS classes

Size Variants

VariantMax WidthUse 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
fullnoneFull-bleed sections
inherit--page-section-content-max fallback --pt-layout-container-maxLayoutSection default frame size

By default, all variants include horizontal gutter padding from --pt-layout-container-gutter. Set gutter="none" to remove horizontal padding.

REQ-00012 normative Theme switching shall be supported at runtime.
REQ-00013 implemented Theme switching shall persist user preference across sessions.
REQ-00014 implemented Theme switching shall be implemented via document-level class or attribute changes.
REQ-00017 implemented Dark mode selection shall resolve using persisted preference followed by system preference.
REQ-00190 implemented The theme shall define standardized layout width constraints. At minimum a content width, a wide width, and a prose/reading width shall be defined as canonical layout tokens.
REQ-00191 implemented Content shall support narrow, default, wide, and full alignment modes. Components shall respect these alignment rules.
REQ-00263 implemented In multi-column page layouts, DOM order shall reflect logical reading and tab order (e.g., sidebar/TOC before main content). CSS Grid shall be used to decouple visual layout from DOM order. Article metadata shall be placed in the page layout's main-header slot.

Search

Search across pages and articles. Use arrow keys to navigate results.

Search across pages and articles.

Loading search...

Search is unavailable. Please try again later.

    No results for ""

    Try different keywords or fewer words.