Data and custom sections
Keep content as JSON-compatible data and render it with components supplied by your application. This makes the same model usable for local files, a CMS or a future editing interface.
<script lang="ts">
import { ContentPage } from '@page-sections/svelte';
import { Sections, type PageData } from '@page-sections/svelte';
const page: PageData = {
title: 'Garden notebook',
sections: [
{ type: 'heading', id: 'page-title', title: 'Garden notebook', headingLevel: 1 },
{
type: 'richText',
id: 'intro',
title: 'Introduction',
body: 'Notes from **this season**.'
},
{
type: 'grid',
id: 'details',
title: 'Details',
blocks: [
{
id: 'left',
span: { xs: 4, s: 3 },
sections: [
{
type: 'richText',
id: 'hours',
title: 'Hours',
body: 'Open on Saturdays.'
}
]
},
{
id: 'right',
span: { xs: 4, s: 3 },
sections: [
{
type: 'richText',
id: 'address',
title: 'Address',
body: '42 Garden Lane.'
}
]
}
]
}
]
};
</script>
<ContentPage {page} showContentMenu><Sections sections={page.sections} /></ContentPage
> Page and section models
ContentPageData has required title and optional pageTitle, breadcrumbs,
bodyLayout (wide/narrow) and bodySpacing. PageData extends it with required sections: SectionData[]. ContentPage itself does not read page.sections; render
Sections explicitly.
SectionData = SectionTypeMap[keyof SectionTypeMap]. Every built-in entry
extends the shared SectionDefinition.
| Interface / discriminator | Additional fields beyond SectionDefinition |
|---|---|
HeroSectionData / hero | Optional image: ImageData, size: CardSize, textBackground: boolean, actions: ContentAction[], composition: overlay/artwork, imageSide: left/right |
RichTextSectionData / richText | Either body with optional format (markdown/text), or blocks: RichTextBlock[] |
PanelSectionData / panel | Required blocks; optional tone (standard/muted), actions |
ImageSectionData / image | Required image: ImageData; optional description: string, imageSide: left/right |
VideoSectionData / video | Required video: VideoData; optional description: string, videoSide: left/right |
CardGridSectionData / cardGrid | Required list: CardItem[]; optional cardSize, cardVariant, mediaAspect, mediaFit, itemSpan, textAlign |
CarouselSectionData / carousel | Required list: CardItem[]; optional cardSize, cardVariant, mediaAspect, mediaFit, textAlign, controls, initialItemKey |
GridSectionData / grid | Required blocks: { id: string; span?: ContentSpan; sections: SectionData[] }[] |
Grid renders Section + ContentGrid + ContentBlocks and recursively renders each block's sections. A block id is its keyed-loop identity, not a rendered anchor. Nested sections still register in the page, so their IDs must be unique across the page. Nested text/panel sections omit extra outer padding by default. Nested grids/collections establish their own container and one gutter pair. Heading levels inherit from the enclosing section and stop at h6.
There is no plain section discriminator and no exported GridSection component. Use
declarative Section or a custom type for arbitrary content.
Sections component props
| Prop | Type | Default |
|---|---|---|
sections | SectionData[] | Required |
renderers | Record<string, Component<any>> | Empty object |
The code discriminator renders CodeSection. Optional codeHighlights: Record<string, HighlightedCode> supplies derived syntax tokens keyed by section ID and is forwarded into nested grids. Sections
keys entries by id. A matching custom renderer takes precedence over a built-in and receives the
complete section prop. Unknown types throw an error during rendering. Recursive grid
sections forward the same renderer map. No fetching, JSON validation, schema migration or routing
is performed.
Add a custom section
Create these three files in the same directory. The data remains serializable; component constructors stay in the renderer map.
import type { SectionDefinition } from '@page-sections/core';
export interface QuoteSection extends SectionDefinition {
type: 'quote';
quote: string;
author: string;
}
declare module '@page-sections/core' {
interface SectionTypeMap {
quote: QuoteSection;
}
} <script lang="ts">
import { Section } from '@page-sections/svelte';
import type { QuoteSection } from './quote.js';
let { section }: { section: QuoteSection } = $props();
</script>
<Section {section}>
<blockquote>
<p>{section.quote}</p>
<footer>{section.author}</footer>
</blockquote>
</Section> <script lang="ts">
import { ContentPage, Sections, type PageData } from '@page-sections/svelte';
import Quote from './Quote.svelte';
import type {} from './quote.js';
const page: PageData = {
title: 'Shared wisdom',
sections: [
{ type: 'heading', id: 'page-title', title: 'Shared wisdom', headingLevel: 1 },
{
type: 'quote',
id: 'advice',
title: 'Advice',
quote: 'Start with one small patch.',
author: 'The garden team'
}
]
};
</script>
<ContentPage {page} showContentMenu
><Sections sections={page.sections} renderers={{ quote: Quote }} /></ContentPage
> The augmentation must be included in the application's TypeScript compilation. Renderer
typing is intentionally broad (Component<any>); the map does not enforce
correspondence between a key and its component props. Give each renderer a precise section
prop type as above and validate external data before rendering it.