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.

svelte
<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 / discriminatorAdditional fields beyond SectionDefinition
HeroSectionData / heroOptional image: ImageData, size: CardSize, textBackground: boolean, actions: ContentAction[], composition: overlay/artwork, imageSide: left/right
RichTextSectionData / richTextEither body with optional format (markdown/text), or blocks: RichTextBlock[]
PanelSectionData / panelRequired blocks; optional tone (standard/muted), actions
ImageSectionData / imageRequired image: ImageData; optional description: string, imageSide: left/right
VideoSectionData / videoRequired video: VideoData; optional description: string, videoSide: left/right
CardGridSectionData / cardGridRequired list: CardItem[]; optional cardSize, cardVariant, mediaAspect, mediaFit, itemSpan, textAlign
CarouselSectionData / carouselRequired list: CardItem[]; optional cardSize, cardVariant, mediaAspect, mediaFit, textAlign, controls, initialItemKey
GridSectionData / gridRequired 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

PropTypeDefault
sectionsSectionData[]Required
renderersRecord<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.

quote.ts
ts
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;
  }
}
Quote.svelte
svelte
<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>
App.svelte
svelte
<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.