Skip to content

Content

Shells & components

The chrome around a page. Frame is the headless composition; DocsShell and BlogShell are ready-made forms built on the same bricks (Doc, Sidebar, Pager, Search), plus an opt-in theme.

On this page

The site layer ships the site’s structure as data; the components render it. One headless frame, two ready-made shells, and a handful of bricks turn a Site into a page that looks like docs. Every piece is overridable or ejectable.

When to use

Use DocsShell when you want a working docs layout in one import, BlogShell for the blog form. Drop to Frame when you want the composition but your own header and sidebar. Use the bricks (Doc, Sidebar, Pager) directly when you compose the page yourself.

One frame, two shells

Frame is the headless composition: a sidebar and main, with every region overridable. Pass a component to replace a region, false/null to remove it, nothing for the default. It imports no CSS and makes no design decisions:

<script>
	import { Frame } from 'ogygia/content';
	import { docs } from '$lib/site.server';   // in a csr=false layout, server-only is fine
	import MyHeader from '$lib/MyHeader.svelte';
</script>

<Frame site={docs} header={MyHeader} footer={null}>
	{@render children()}
</Frame>

The shells are compositions of Frame and the public bricks, not privileged code. DocsShell is the VitePress form: top header (brand, links, search, theme toggle), left sidebar, content column, a mobile bottom bar with a slide-up sheet. BlogShell is the blog form: the same header and mobile chrome, no sidebar, article-width column. Anything they do, your own composition can do. They exist so you don’t have to.

<script>
	import DocsShell from 'ogygia/content/docs-shell';
	import 'ogygia/content/theme.css';   // the design language (tokens + element styles)
	import 'ogygia/content/shell.css';   // shell layout (header + sidebar + grid + mobile chrome)
	import { meta } from '$lib/site.remote';
	let { children } = $props();

	const shellMeta = await meta();
</script>

<DocsShell meta={shellMeta} base="/docs" title="Docs">
	{@render children()}
</DocsShell>
<script>
	import { BlogShell } from 'ogygia/content';
	import 'ogygia/content/theme.css';
	import 'ogygia/content/shell.css';
	let { children } = $props();
</script>

<BlogShell base="/blog" title="Example blog" links={[{ text: 'Docs', href: '/docs' }]}>
	{@render children()}
</BlogShell>

Both accept {site} directly (simple path, csr=false layouts) or {meta} from the meta remote (the corpus-stays-server-side path). Every region a shell renders is a conditional snippet prop: pass a snippet to replace it, null to remove it, nothing for the built-in. This site removes the stock header (header={null}) and brings its own.

Styling is opt-in

The shells ship structure + .og-* hooks and import no CSS. Want the stock look? Import the two stylesheets once, in your root layout. Want your own? Don’t, and style the .og-* classes yourself. You only pay for the CSS you ask for.

  • theme.css is the design language: the token palette (--og-*) and element styles shared by every shell.
  • shell.css is the shells’ layout: header, sidebar, grid, mobile chrome.

They’re separate imports so you can keep the tokens and swap the layout, or keep the layout and repaint with your own tokens. Every rule lives in @layer ogygia, so any unlayered rule you write wins. No specificity fights, no !important:

/* your skin: unlayered → beats every @layer ogygia rule with plain selectors */
.og-sidebar { border-right: 1px dashed var(--og-line); }

Alternate token themes live under ogygia/content/themes/*. Swap the import, keep everything else:

<script>
	import 'ogygia/content/themes/thalassa.css';   // or alexandria, daidalos, nephele, …
	import 'ogygia/content/shell.css';
</script>

The bricks

Each is a plain component you can use without a shell:

  • <Doc {view} /> lays out one page from a docs.page() view: eyebrow, title, the rendered body, the On this page rail from view.headings, and prev/next. Props toggle the crumbs, the pager, the keep-reading list.
  • <Sidebar />: the nav tree with the active row tracked. A client island, so its marker glides across SPA navigations.
  • <Pager />: the prev/next footer, from the outline’s prevNext.
  • <OnThisPage headings={…} />: the scroll-spy rail. <Doc> mounts it; use it standalone if you compose your own article.
  • <Switcher />: the version/locale picker on a dimensioned site. A Bits UI dropdown, so it’s an island.
  • <Search /> / <SearchPage />: the ⌘K palette and the no-JS results page; see Search.
  • <BlogList {posts} /> / <BlogPost {view} /> are the blog bricks: a dated index and the article form, both reading the same PageView shape <Doc> does.
  • <TabGroup> / <Tab>: tabbed panels, also minted from ::: code-group markdown with zero imports (the tabs above are exactly that).

Doc, on its own

The article layout works with just a view, no shell required:

<script>
	import { Doc } from 'ogygia/content';
	import * as docs from '$lib/site.remote';

	const view = await docs.page(slug);
</script>

<Doc {view} keepReading={view.trail?.related} />

The body inside view is a baked region ticket, so any island the source .svx imported wakes on the page exactly as an in-pass render would. These docs are <Doc>: the article you are reading, its on-this-page rail, and the pager below are this brick.

Overriding markdown elements

The components map on site() replaces how markdown elements render, with real component values in app code, not import-path config. The compiled markup routes overridable tags through one slot; at render it looks the tag up in the map:

export const docs = site({
	outline: guides,
	components: { img: OptimizedImage, code: FancyCode }
});

The built-in default is a → Link: id-form links ([Guide](routing)) resolve against the site’s address space and stay correct under any mount. A shell provides the map via context; tier-2 renders can provide it themselves.

Do / don’t

  • Do reach for DocsShell first. It’s a working docs site in one import; drop to Frame or the bricks only when you need to.
  • Do import the CSS in your root layout, once. The shells import none themselves. That’s what lets you bring your own.
  • Don’t thread site through every component. A shell sets it on context; the bricks inside read it without props.
  • Don’t re-implement the on-this-page rail or the pager. view.headings and view.trail already carry the data; <Doc> renders both.