For AI agents: the complete documentation index is available at https://ciderpress.dev/llms.txt, the full documentation bundle is available at https://ciderpress.dev/llms-full.txt, and this page is available as Markdown at https://ciderpress.dev/reference/configuration.md.
Get started →

Configuration

All configuration lives in ciderpress.config.ts at your repo root. Use defineConfig for type safety and autocompletion.

import { defineConfig } from 'ciderpress'

export default defineConfig({
  title: 'My Docs',
  description: 'Project documentation',
  pages: [{ title: 'Introduction', path: '/intro', include: 'docs/intro/*.md' }],
})

Configuration is loaded via c12. Supported file formats: .ts, .mts, .js, .mjs, .json, .jsonc, .yml, .yaml.

pages is the only required field. Every other top-level key is optional — minimal config produces a clean site with zero framework branding.

Rich text

Display strings on the home page accept inline markup — no flag, no opt-in. Ciderpress parses it and drops anything unsafe.

This covers the hero, every home.blocks[] band (proof, features, showcase, split, tabs, cta), and workspace cards. Strings outside those surfaces — footer, sidebar.promo, sidebar link text, and announcement — render verbatim today, so markers in them show as literal characters.

SyntaxRenders
**accent**bold and brand-coloured
==highlight==tinted <mark> background
*italic*<em>
`code`inline <code>, sized to the surrounding text
[text](/href)link, SPA-routed when internal
<br>line break
<strong>plain</strong>bold with no colour, when you want weight alone
\*a literal marker character

** is the accent: a heading is already bold, so weight by itself would say nothing there — ** colours the phrase instead, in headings and body copy alike. Reach for <strong> on the rare occasion you want bold without the colour. == keeps the meaning it has in Obsidian, Typora, and markdown-it-mark — a highlight.

Because inline HTML is supported, <span class="cp-accent">text</span> and <mark>text</mark> are equivalent long forms of the two markers. Useful when you want the accent on part of a word, or inside copy that already uses asterisks.

Inline HTML is allowed for a, b, strong, i, em, code, kbd, mark, sup, sub, span, small, u, s, del, and ins, keeping only the class (or classname), title, and (on <a>) href attributes. An <a href> is validated exactly like a markdown link.

Escaping

A backslash makes any marker literal — \*, \`, \=, \[, \], \<, and \\. Needed when copy has to name a glob or an expression:

description: 'Discovers \\*\\*/\\*.md across the repo'

An asterisk is only read as italic when it hugs its text, so 2 * 3 and a bare *.md already pass through untouched; reach for the backslash when a marker sits directly against a word.

home: {
  hero: {
    tagline: 'Point it at your `markdown`. <strong>No restructuring.</strong>',
  },
  blocks: [
    { type: 'split', title: 'One config, **validated at boot**' },
    { type: 'tabs', title: 'Now with ==OpenAPI==' },
    { type: 'cta', body: 'Questions? [Open an issue](https://github.com/acme/docs/issues).' },
  ],
}

What gets dropped

Parsing produces React elements directly — markup is never injected as raw HTML, so nothing in a config string can execute.

  • <script>, <style>, <iframe>, <object>, <embed>, <template>, <noscript> — removed with their contents
  • Any other unrecognised tag — unwrapped, its text kept (<div>hi</div> renders hi)
  • Every attribute outside the whitelist, including onclick and style
  • Links whose scheme fails validation (javascript:, data:) — the label stays, the anchor goes

Block markdown — lists, headings, blockquotes, tables — is not supported. These fields are single-line display copy; use a markdown page for prose.

Plain-text contexts

The same string is stripped to bare text wherever markup cannot render: the document <title>, <meta name="description">, and the tab strip's aria-label. One value serves both.

description: 'Beautiful Docs, **Zero Effort**',
// hero headline → Beautiful Docs, <strong class="cp-accent">Zero Effort</strong>
// <meta name="description"> → Beautiful Docs, Zero Effort

Hero title accent

The hero headline accents its trailing half automatically. Bold anything in the title and that guess steps aside — you get exactly what you marked:

// automatic — trailing half is accented
description: 'Beautiful Docs, Zero Effort',
// explicit — only the marked words
description: 'Beautiful **Docs**, Zero Effort',

Escape hatch

For layout beyond inline markup, override the theme components rather than reaching for HTML in config: HomeLayout accepts beforeHero / afterHero slots, and Hero, HomeSplit, HomeTabs, CTA, PageRail, RichText, and renderRichText are all exported from @ciderpress/ui.

Site identity

Top-level scalar fields that identify the site itself.

FieldTypeRequiredDescription
titlestringyesSite title shown in browser tab, topbar, and (when copyright is auto) the footer notice
descriptionstringnoMeta description and home page hero headline
basestringnoBase URL the site is deployed under (e.g. '/', '/docs/')
versionstringnoVersion label rendered next to the brand in the topbar (e.g. 'v1.0'). Omit to hide
defineConfig({
  title: 'Acme',
  description: 'Documentation for the Acme platform',
  base: '/',
  version: 'v1.0',
  pages: [/* ... */],
})

seo

Set the production origin once; Ciderpress derives canonical URLs, absolute social metadata, and sitemap.xml from it. Rspress continues to provide the page title, description, and base Open Graph tags.

seo: {
  origin: 'https://docs.acme.com',
  titleTemplate: '%s | Acme',
  socialImage: '/social.png',
  openGraph: {
    siteName: 'Acme',
    locale: 'en_US',
  },
  twitter: {
    card: 'summary_large_image',
    site: '@acme',
  },
  robots: {
    index: true,
    follow: true,
  },
  sitemap: {
    changeFrequency: 'weekly',
    priority: '0.5',
  },
}
FieldTypeDefaultDescription
originstringrequiredProduction origin for canonical and sitemap URLs
titleTemplatestring | falseRspress default%s is replaced by the page title
socialImagestringDefault Open Graph and Twitter image
openGraphOpenGraphConfig | falseenabledSite name, type, and locale defaults
twitterTwitterConfig | falsesummary_large_imageTwitter card and account defaults
robotsRobotsConfigbrowser/search-engine defaultsDefault index and follow directives
sitemapboolean | SitemapConfigtrueGenerate sitemap.xml with the Rspress plugin

Rspress also generates llms.txt, llms-full.txt, and per-page Markdown automatically. These are root-level discovery files and do not require an HTML <head> tag.

redirects

Redirect moved documentation routes after the Ciderpress app loads:

redirects: [
  { from: '^/old-guide$', to: '/guides/new-guide' },
  { from: ['^/v1/install$', '^/v2/install$'], to: '/getting-started/install' },
  { from: '^/legacy/(.*)$', to: '/archive/$1' },
]
FieldTypeDescription
fromstring | string[]Path or regular-expression pattern to match
tostringInternal path or absolute HTTP(S) URL

Rules run in order, and each from value is compiled as a regular expression. Anchor a path with ^ and $ when it should match exactly.

These are client-side redirects. The app loads before the browser replaces its URL, and the server does not return a real 301 or 308. This is usually sufficient for internal documentation. For public documentation, prefer redirects configured in the hosting platform: they run before page load, avoid a visible flash, and preserve search-engine signals.

The host must serve Ciderpress's generated 404.html for unmatched routes. Netlify, Vercel, and GitHub Pages do this by default. If a host does not, configure its fallback route to 404.html.

brand

Brand chrome — icon, wordmark, hero background, favicon, and the inline FOUC loader. Defaults to invisible: omit any field to render nothing in that slot.

brand: {
  icon:    IconConfig,
  logo:    string | LogoFn,
  banner:  string | BannerFn,
  favicon: ImageSource,
  loader:  'apple' | 'classic' | false | LoaderConfig,
}
FieldTypeDescription
iconIconConfigSmall chip rendered before the wordmark in the topbar. See IconConfig
logostring | LogoFnWordmark in the topbar — image path or ({ theme }) => LogoImage | ReactNode
bannerstring | BannerFnHero background — image path or function returning an ImageSource or React node
faviconImageSourceBrowser-tab icon. See ImageSource
loader'apple' | 'classic' | false | LoaderConfigInline FOUC loader. false disables it; pass LoaderConfig for a custom component

BannerFn

type BannerFn = (params: { theme: LogoContext }) => ImageSource | React.ReactNode

The function receives the active theme context and returns either an image source or a React node. Use the React-node variant for procedural canvas, WebGL, or inline SVG backgrounds that respond to theme tokens.

brand: {
  banner: ({ theme }) => <HeroCanvas variant={theme.variant} />,
}

Put component JSX in a .tsx module, import it from ciderpress.config.ts, and return createElement(HeroCanvas, props). Components run in the browser, so keep module initialization browser-safe and access DOM APIs from effects.

home.hero.background takes precedence when set. Otherwise, a plain-string banner is serialized as an image background and a function banner renders at runtime.

LoaderConfig

type LoaderConfig =
  | {
      content: string
      label?: string
      minDisplayMs?: number
      maxDisplayMs?: number
    }
  | {
      component: ComponentType
      label?: string
      minDisplayMs?: number
      maxDisplayMs?: number
    }
VariantDescription
contentStatic SVG/img string rendered as the loader backdrop
componentCustom React component. Renders post-hydration; the pre-hydration fallback is the backdrop only

label is read by screen readers. minDisplayMs and maxDisplayMs clamp visible duration so the loader doesn't flash or hang.

theme

Theming uses a single array of theme entries. The first entry is the default unless one is explicitly marked. Both the named-theme picker and the light/dark variant toggle are independent — themeSwitcher and variantSwitcher.

theme: {
  themes:           ThemeEntry[],
  defaultVariant?:  'light' | 'dark' | 'system',
  themeSwitcher?:   boolean,
  variantSwitcher?: boolean,
  overrides?:       Partial<ThemeColors>,
}
FieldTypeDefaultDescription
themesThemeEntry[]['mulled'] (when theme is omitted)Mix of built-in theme names and custom Theme objects. First entry is default unless one is marked
defaultVariant'light' | 'dark' | 'system'active theme's own defaultVariant ('dark' for every built-in)Initial light/dark variant. 'system' defers to the active theme's declared default
themeSwitcherbooleantrue when themes.length > 1Show the named-theme picker in the topbar
variantSwitcherbooleantrueShow the light/dark toggle in the topbar (auto-hidden when the active theme has only one variant)
overridesPartial<ThemeColors>Override individual color tokens across every theme in themes

ThemeEntry

type ThemeEntry =
  | BuiltInThemeName
  | ThemeInput
  | { name: BuiltInThemeName; default?: boolean }
  | (ThemeInput & { default?: boolean })

Built-in names and full custom theme definitions share the same array. Either form accepts a default: true marker to override the "first entry wins" rule.

theme: {
  themes: [
    'honeycrisp',
    {
      name: 'acme',
      default: true,
      colors: {
        brand: '#ff5a1f',
        text:  '#1a1a1a',
      },
    },
  ],
  defaultVariant:  'dark',
  themeSwitcher:   true,
  variantSwitcher: true,
}

pages

The information architecture tree. Required. Each entry is a Page — the same shape used for leaf documents, sidebar groups, and glob-discovered sections.

Renamed from sections (Page replaces the old Section interface). Children live on Page.pages (renamed from items).

Page

interface Page {
  // ---- Identity ----
  title: TitleConfig
  description?: string
  path?: string
  icon?: IconConfig

  // ---- Source (declare exactly one) ----
  include?: string | string[]
  content?: string | (() => string | Promise<string>)
  pages?: Page[]

  // ---- Navigation behavior ----
  nav?: {
    hidden?: boolean
    collapsible?: boolean
    island?: boolean
    root?: boolean
  }

  // ---- Landing page ----
  landing?: boolean

  // ---- Card behavior ----
  card?: CardConfig

  // ---- Default page metadata ----
  defaults?: Frontmatter

  // ---- Glob-discovery options ----
  discover?: {
    sort?: SortStrategy
    recursive?: boolean
    ignore?: string[]
    indexFile?: string
  }

  // ---- Per-page integration ----
  openapi?: OpenAPISpec
}

Identity

FieldTypeRequiredDescription
titleTitleConfigyesStatic string or derivation rule for auto-discovered children
descriptionstringnoOne-line description for this page's auto-generated landing card and OG meta
pathstringnoURL path this page mounts at (e.g. /guides). Omit for a sidebar-only grouping node
iconIconConfignoIcon rendered on the page's card and (when configured) in the sidebar

Source — declare exactly one

FieldTypeDescription
includestring | string[]File path or glob string(s); children auto-discovered
contentstring | (() => string | Promise<string>)Inline Markdown/MDX string, or async generator
pagesPage[]Explicit child nodes (renamed from items)

Grouped together so per-page chrome flags don't sprawl across the top level of Page.

FieldTypeDefaultDescription
nav.hiddenbooleanfalseHide this page (and children) from the sidebar entirely
nav.collapsiblebooleantrueRender as a collapsible group in the sidebar
nav.islandbooleanfalseRender as a sidebar island — children appear only when the user is inside this branch (renamed from standalone)
nav.rootbooleanfalseMark as a sidebar root — only one root active at a time; the topbar treats it as the active workspace

Landing + card

FieldTypeDefaultDescription
landingbooleantrue for pages with childrenRender an auto-generated landing page at this path listing children as cards
cardCardConfigHow this page appears as a card on its parent's landing
CardConfig
interface CardConfig {
  icon?: IconConfig
  scope?: string
  description?: string
  tags?: string[]
  badge?: { src: string; alt: string }
}
FieldTypeDescription
iconIconConfigCard icon. Defaults to a rotating color based on position in the parent's landing card grid
scopestringScope kicker rendered above the title (e.g. 'apps/', 'packages/')
descriptionstringOne-line description rendered under the card title (overrides the page's own description)
tagsstring[]Tag chips rendered below the description
badge{ src: string; alt: string }Logo badge rendered in the card's top-right corner

Card content resolves from this priority order (highest first): card.description → source file frontmatter descriptionPage.description.

defaults — default page metadata

Renamed from frontmatter. Same Frontmatter type — values here are merged into every child page's frontmatter; per-file YAML wins on conflict.

{
  title: 'API Reference',
  defaults: { aside: 'left', editLink: false },
  pages: [
    { title: 'Auth',  path: '/api/auth',  include: 'docs/api/auth.md' },
    { title: 'Users', path: '/api/users', include: 'docs/api/users.md' },
  ],
}

discover.* — glob-discovery options

Only applies when include is a glob. Renamed from the flat Section.{sort,recursive,exclude,entryFile} fields.

FieldTypeDefaultDescription
discover.sortSortStrategy'default'Sort strategy for discovered children. See SortStrategy
discover.recursivebooleantrueRecurse into subdirectories
discover.ignorestring[]Glob patterns ignored during discovery (renamed from exclude — gitignore vocab)
discover.indexFilestring'overview'Filename treated as the page's own content instead of generating a landing page (renamed from entryFile)

openapi

Per-page OpenAPI spec integration. Generates API operation pages under the page's path. See OpenAPISpec for the shape.

Examples

Leaf page from a single file:

{
  title:   'Architecture',
  path:    '/architecture',
  include: 'docs/architecture.md',
}

Group with explicit children:

{
  title: 'Guides',
  path:  '/guides',
  pages: [
    { title: 'Quick Start', path: '/guides/quick-start', include: 'docs/guides/quick-start.md' },
    { title: 'Deployment',  path: '/guides/deployment',  include: 'docs/guides/deployment.md' },
  ],
}

Glob-discovered section with discovery options:

{
  title:       'Reference',
  path:        '/reference',
  description: 'API and CLI reference.',
  include:     'docs/reference/**/*.md',
  discover: {
    sort:      'alpha',
    recursive: true,
    ignore:    ['**/draft-*.md'],
    indexFile: 'overview',
  },
}

apps, packages, workspaces

Top-level workspace surfaces. Kept flat — apps and packages are the common cases; workspaces is for arbitrary custom groups like "Integrations" or "Plugins".

apps:       Workspace[],
packages:   Workspace[],
workspaces: WorkspaceGroup[],

All three drive the home page card grid (via a showcase block in home.blocks), the auto-generated landing card on their parent, and the workspace introduction page.

Workspace

interface Workspace {
  title: TitleConfig
  description: string
  path: string
  icon?: IconConfig
  tags?: string[]
  badge?: { src: string; alt: string }
  include?: string | string[]
  pages?: Page[]
  defaults?: Frontmatter
  discover?: {
    sort?: SortStrategy
    recursive?: boolean
    ignore?: string[]
    indexFile?: string
  }
  openapi?: OpenAPISpec
}
FieldTypeRequiredDescription
titleTitleConfigyesDisplay name. Accepts the full TitleConfig (was plain string)
descriptionstringyesShort description for cards and the workspace landing page
pathstringyesURL prefix for this workspace's documentation
iconIconConfignoIcon for the home card and sidebar header
tagsstring[]noTech tags — case-insensitive, mapped to icons via the tech registry
badge{ src: string; alt: string }noLogo badge rendered in the card's top-right corner
includestring | string[]noSource file path(s) or glob pattern(s) for content discovery
pagesPage[]noExplicit child pages (mirrors Page.pages)
defaultsFrontmatternoDefault frontmatter injected into every discovered child page (mirrors Page.defaults)
discover(see Page)noGlob-discovery options. Same shape as Page.discover
openapiOpenAPISpecnoOpenAPI spec integration for this workspace

WorkspaceGroup

interface WorkspaceGroup {
  title: string
  description?: string
  icon: IconConfig
  items: Workspace[]
  link?: string
}
FieldTypeRequiredDescription
titlestringyesGroup display name
descriptionstringnoShort description
iconIconConfigyesGroup icon. Accepts the full IconConfig (was IconId only)
itemsWorkspace[]yesWorkspaces in the group (at least one)
linkstringnoURL prefix override (defaults to /${slugify(title)})
workspaces: [
  {
    title: 'Integrations',
    icon: { id: 'pixelarticons:integration', color: 'orange' },
    items: [{ title: 'Stripe', description: 'Payment processing', path: '/integrations/stripe' }],
  },
]

OpenAPISpec

Per-page or per-workspace OpenAPI integration. The same shape lives on Page.openapi and Workspace.openapi — declare it once at the mount point you want the API operation pages to live under.

interface OpenAPISpec {
  spec: string
  path: string
  title?: string
  sidebarLayout?: 'method-path' | 'title'
}
FieldTypeRequiredDescription
specstringyesPath to the OpenAPI document (.json, .yaml, or .yml), relative to the repo root
pathstringyesURL path the API operation pages mount under (must start with /)
titlestringnoSidebar group title (default 'API Reference')
sidebarLayout'method-path' | 'title'noHow operations appear in the sidebar — method-path shows GET /users; title shows the operation summary (default 'method-path')

When declared on a Workspace, path must be nested under the workspace's own path — that's checked at validate time. See the OpenAPI reference for a full walkthrough.

socials

Root-level array of social links. Single source of truth — both topbar.socials and footer.socials reference this list via true.

socials: SocialLink[]
interface SocialLink {
  icon: SocialLinkIcon | { svg: string }
  url: string
  label?: string
}
FieldTypeRequiredDescription
iconSocialLinkIcon | { svg: string }yesBuilt-in icon name or custom SVG
urlstringyesTarget URL
labelstringnoAccessible label (screen readers, hover title)

The Rspress mode/content discriminator is no longer exposed — every link is a URL link.

Built-in SocialLinkIcon values:

import type { SocialLinkIcon } from '@ciderpress/config'

// 'discord' | 'facebook' | 'github' | 'instagram' | 'linkedin' | 'slack'
// | 'x' | 'youtube' | 'gitlab' | 'X' | 'bluesky' | 'npm'

Any icon outside this set must be supplied as { svg: '<svg>...</svg>' }.

Boolean reference pattern

topbar.socials and footer.socials accept either true (reuse root socials) or SocialLink[] (override with a specific list for that surface).

socials: [
  { icon: 'github',  url: 'https://github.com/acme'  },
  { icon: 'discord', url: 'https://discord.gg/acme'  },
],
topbar: { socials: true },                                  // mirror root list
footer: { socials: [{ icon: 'github', url: '...' }] },      // footer-specific

topbar

Top navigation bar — nav items, primary CTA, social row, announcement banner.

topbar: {
  nav:           'auto' | NavItem[],
  cta?:          ButtonConfig,
  socials?:      true | SocialLink[],
  announcement?: AnnouncementConfig,
}
FieldTypeDescription
nav'auto' | NavItem[]Navigation items — see auto rule
ctaButtonConfigPrimary CTA button (also mirrored into the mobile nav)
socialstrue | SocialLink[]true reuses root socials; array overrides for the topbar only
announcementAnnouncementConfigAnnouncement banner rendered above the topbar

Auto-nav emits one top-level entry per root pages entry that has a path. Children are not flattened into dropdowns. Roots with nav.hidden: true are skipped. Workspaces declared via top-level apps / packages / workspaces are not included — they show on the home grid only. For dropdowns or workspace items in the topbar, use the explicit NavItem[] form.

interface NavItem {
  title: string
  link?: string
  items?: NavItem[]
  activeMatch?: string
}
FieldTypeRequiredDescription
titlestringyesDisplay text
linkstringleafTarget URL — required on leaf items, omitted when items is provided
itemsNavItem[]noDropdown children — when present, this entry renders as a menu
activeMatchstringnoRegex pattern matched against the current URL for active-state styling

AnnouncementConfig

interface AnnouncementConfig {
  id?: string
  lead?: string
  message: string
  cta?: { href: string; label: string }
  persistent?: boolean
}
FieldTypeDescription
idstringStable id — when present, dismissal persists in localStorage
leadstringHighlighted lead phrase rendered before the message (e.g. "NEW")
messagestringBody text
cta{ href: string, label: string }Optional CTA appended after the message
persistentbooleanWhen true, hides the dismiss button

Persistent sidebar chrome — links pinned above and below the nav tree, plus the optional promo card.

sidebar: {
  top?:    SidebarLink[],
  bottom?: SidebarLink[],
  promo?:  SidebarPromo,
}
FieldTypeDescription
topSidebarLink[]Links rendered above the sidebar nav tree (renamed from above)
bottomSidebarLink[]Links rendered below the sidebar nav tree (renamed from below)
promoSidebarPromoPromo card pinned to the bottom of the docs sidebar

Sidebar links use ButtonConfig directly — no separate type. The same text/href/variant/shape/icon vocabulary as every other button surface.

sidebar: {
  top: [
    { text: 'Home',   href: '/',                       icon: 'pixelarticons:home',   variant: 'ghost' },
  ],
  bottom: [
    { text: 'GitHub', href: 'https://github.com/acme', icon: 'pixelarticons:github', variant: 'secondary' },
  ],
}

SidebarPromo

interface SidebarPromo {
  title: string
  body: string
  cta: ButtonConfig
}
FieldTypeDescription
titlestringPromo headline
bodystringBody copy
ctaButtonConfigCTA button

badges

Badge configuration — glob rules that apply a badge (or a named status) by route, plus the group flag for collapsible-doc groups. Badges render on the sidebar, breadcrumb, and section cards. A page's own frontmatter or defaults badge/status wins over a rule. See the Badges reference for the full model.

badges: {
  rules: [
    { match: '/api/experimental/**', status: 'alpha' },
    { match: ['/v2/**', '/beta/**'], badge: { text: 'v2', variant: 'info' } },
  ],
  group: true,
}
FieldTypeDescription
rulesBadgeRule[]Glob rules applied by route path (see BadgeRule)
groupbooleanShow a collapsible-doc group's badge on every surface. Defaults to false (hidden everywhere to spare the chevron)

BadgeRule

interface BadgeRule {
  match: string | string[]
  badge?: string | BadgeConfig | array
  status?: string | string[]
}
FieldTypeDescription
matchstring | string[]Glob pattern(s) matched against the route path
badgestring | BadgeConfig | arrayAd-hoc badge(s) applied to matching pages
statusstring | string[]Named status id(s) applied to matching pages

Declare at least one of badge or status. match supports *, **, and ?.

statuses

The named status registry — the semantic layer over badges. A status is a reusable, documented preset referenced by id from a page's status field. Entries merge over the built-in defaults by id (matching ids override, new ids extend).

statuses: [
  {
    id: 'alpha',
    title: 'Alpha',
    description: 'Early and unstable — expect changes.',
    variant: 'warning',
  },
  {
    id: 'design-partner',
    title: 'Design Partner',
    description: 'Available to design partners only.',
    color: '#7c3aed',
  },
]
FieldTypeRequiredDescription
idstringyesReference handle used by status: <id>
titlestringyesChip label
descriptionstringyesHover tooltip
variantBadgeVariantnoTheme-aware color; ignored when color is set
colorstringnoRaw color — overrides variant

Unified footer config — the old top-level footer and site.footer are now one block.

footer: {
  message?:   string,
  copyright?: true | string | CopyrightConfig,
  columns?:   FooterColumn[],
  tagline?:   string,
  brandMark?: string,
  socials?:   true | SocialLink[],
}
FieldTypeDescription
messagestringFooter message text
copyrighttrue | string | CopyrightConfigtrue auto-generates from title + current year; string is verbatim; object is structured
columnsFooterColumn[]Link columns rendered in the footer grid
taglinestringSmall tagline rendered on the right side of the bottom strip
brandMarkstringBrand mark character rendered in the footer's brand block (default 'Z')
socialstrue | SocialLink[]true reuses root socials; array overrides for the footer only

copyright: true produces Copyright © <currentYear> <title>. using the top-level title and the year at build time. Pass a string to override verbatim, or a CopyrightConfig for structured company / DBA / year-range output.

CopyrightConfig

interface CopyrightConfig {
  company?: string
  dba?: string
  year?: number | { from: number }
}
FieldTypeDescription
companystringLegal company name (e.g. 'Acme Inc.')
dbastring"Doing business as" name
yearnumber | { from: number }Single year, or a range from from to the current year
footer: {
  copyright: { company: 'Acme Inc.', dba: 'Acme', year: { from: 2021 } },
  // → "Copyright © 2021–2026 Acme Inc. (Acme)"
}

FooterColumn

interface FooterColumn {
  heading: string
  links: Array<{ text: string; href: string }>
}
FieldTypeDescription
headingstringColumn heading
links{ text: string, href: string }[]Column links

Security note — every href in footer.*, sidebar.*, and topbar.* is validated through a safe-URL helper that rejects javascript:, data:, vbscript:, and file: schemes. Relative paths, fragment anchors, http://, https://, mailto:, and tel: are allowed.

Per-page chrome — the "Edit on GitHub" and "Report an issue" links rendered under every doc page. Flattened to the top level to match the industry pattern (VitePress, Nextra). Set either to false to disable that action site-wide.

editLink?:   false | EditLinkConfig,
reportLink?: false | ReportLinkConfig,

EditLinkConfig

interface EditLinkConfig {
  repo?: string
  branch?: string
  directory?: string
  label?: string
  url?: (page: ResolvedPage) => string
  onResolve?: (page: ResolvedPage) => void
}
FieldTypeDescription
repostring"org/repo" shorthand or full URL — feeds the auto-URL builder
branchstringBranch to link against (default "main")
directorystringSubdirectory inside the repo containing the docs (default: repo root)
labelstringVisible label (default "Edit this page on GitHub")
url(page: ResolvedPage) => stringCustom URL builder — overrides the auto-URL
onResolve(page: ResolvedPage) => voidAnalytics / telemetry hook fired when the link is resolved
editLink: {
  repo:      'acme/docs',
  branch:    'main',
  directory: 'docs',
  onResolve: (page) => track('edit-link.resolved', { path: page.path }),
},

ReportLinkConfig

Identical shape to EditLinkConfig. repo may be either "org/repo" shorthand or a full issues URL; default label is "Report an issue".

reportLink: { repo: 'acme/docs' },
// or disable site-wide:
reportLink: false,

feedback

Controls the "Was this page helpful?" yes/no widget rendered at the bottom of every doc page. Off by default.

feedback?: boolean | { question?: string }
ValueEffect
omitted / falseWidget does not render
trueWidget renders with the default question
{ question: '…' }Widget renders with a custom question
// enable with the default question
feedback: true,
// enable with a custom question
feedback: { question: 'Did this help?' },

home

Home page composition — a special-cased hero header plus an ordered array of blocks. Array order is render order, and any block type may appear more than once (multiple splits, multiple tab bands, etc.). Omit blocks to get the framework default deck: an auto-generated features grid plus a workspace showcase derived from the repo.

home: {
  hero?:   HomeHeroConfig,
  blocks?: HomeBlock[],
}
FieldTypeDescription
heroHomeHeroConfigHeadline, tagline, actions, and the optional demo visual. Always rendered first
blocksHomeBlock[]Ordered landing bands below the hero. Omit for the auto-generated default deck

blocks: [] renders nothing below the hero. Omitting blocks renders the default deck — a features grid plus the workspace showcase. To keep that deck and add to it, write it out and append:

home: {
  blocks: [{ type: 'features' }, { type: 'showcase' }, { type: 'cta', title: 'Ready?' }],
}

HomeBlock

A discriminated union on type. Every variant carries its own flat fields — there are no nested section configs.

typeShapeDescription
'proof'HomeProofBlock"Used by …" strip
'features'HomeFeaturesBlockFeature card grid
'showcase'HomeShowcaseBlockCard grid — defaults to apps + packages + workspaces, accepts arbitrary paths
'split'HomeSplitBlockTwo-column show-and-tell band. Repeatable
'tabs'HomeTabsBlockSelectable tab strip driving one panel. Repeatable
'cta'HomeCtaBlockCall-to-action band
home: {
  hero: { tagline: 'Ship faster.' },
  blocks: [
    { type: 'proof', lead: 'used by', names: ['acme', 'globex'] },
    { type: 'features', items: [/* ... */] },
    { type: 'split', title: 'One config', visual: { type: 'code', code: '// ...' } },
    { type: 'split', title: 'See it live', reverse: true, visual: { type: 'image', src: '/demo.png' } },
    { type: 'tabs', items: [{ label: 'Sync', visual: { type: 'code', code: '// ...' } }] },
    { type: 'cta', title: 'Ready to ship?' },
  ],
}

Every copy-bearing block shares the same flat heading trio — label (small uppercase kicker), title, and body.

HomeHeroConfig

interface HomeHeroConfig {
  label?: string
  tagline?: string
  actions?: ButtonConfig[]
  demo?: false | HomeVisual
  background?: string | HomeHeroBackground
}
FieldTypeDescription
labelstringSmall label above the title (renamed from eyebrow)
taglinestringMarketing line under the title
actionsButtonConfig[]CTA buttons (typically up to 2)
demofalse | HomeVisualVisual rendered below the hero copy. false hides it
backgroundstring | HomeHeroBackgroundBackground image behind the hero copy. Takes precedence over brand.banner when set

Omit demo entirely to keep the framework's built-in terminal animation.

HomeHeroBackground

background: '/hero.jpg'
// shorthand for:
background: { src: '/hero.jpg' }
// lock foreground contrast to dark artwork:
background: { src: '/hero.jpg', mode: 'dark' }
// optional responsive crops:
background: {
  src: '/hero.jpg',
  sources: { tablet: '/hero-tablet.jpg', mobile: '/hero-mobile.jpg' },
}
// switch artwork with the active site variant:
background: {
  dark: {
    src: '/hero-dark.jpg',
    sources: { tablet: '/hero-dark-tablet.jpg', mobile: '/hero-dark-mobile.jpg' },
  },
  light: {
    src: '/hero-light.jpg',
    sources: { tablet: '/hero-light-tablet.jpg', mobile: '/hero-light-mobile.jpg' },
  },
}
FieldTypeDefaultDescription
srcstringImage URL or path, base-prefixed at render
mode'dark' | 'light'Site variantForeground and scrim mode; set this to match the image
sourcesobjectOptional tablet (≤880px) and mobile (≤640px) image overrides
positionstring'center'CSS background-position (e.g. '50% 25%')
sizestring'cover'CSS background-size
repeatstring'no-repeat'CSS background-repeat

Set mode for artwork designed for one color mode. Omit it for artwork that adapts to both site variants. Use { dark, light } to switch the image and foreground contrast with the active site variant. Each variant accepts the same src, sources, position, size, and repeat fields.

HomeVisual

One union backs every visual on the page — hero.demo, split.visual, and each tab's visual. It is discriminated on type, which is required on all three variants.

type HomeVisual = HomeVisualCode | HomeVisualImage | HomeVisualTerminal

interface HomeVisualCode {
  type: 'code'
  code: string
  language?: string
}

interface HomeVisualImage {
  type: 'image'
  src: string
  alt?: string
  width?: number | string
  height?: number | string
}

interface HomeVisualTerminal {
  type: 'terminal'
  command: string
  lines: { kind: 'ok' | 'info' | 'cmt' | 'err'; text: string }[]
  windowTitle?: string
}
VariantRenders
'code'Syntax-highlighted snippet through Rspress's native Shiki pipeline (language defaults ts)
'image'Screenshot or graphic, base-prefixed at render time
'terminal'The framework's terminal chrome painted with your command and output lines

Terminal lines[].kind picks the prefix glyph: ok, info, cmt, err.

HomeProofBlock

{
  type: 'proof',
  lead?: string,
  names?: (string | ProofLogo)[],
}
FieldTypeDescription
leadstringLead phrase (e.g. "used by", "powering teams at")
names(string | ProofLogo)[]Names, logos, or a mix — the band is skipped when empty

An entry may be a bare string or a logo object. Mix both in one strip.

{
  type: 'proof',
  lead: 'used by',
  names: [
    'Acme',
    { src: '/logos/beta.svg', alt: 'Beta' },
    { src: { dark: '/logos/gamma-dark.svg', light: '/logos/gamma-light.svg' }, alt: 'Gamma' },
    { src: '/logos/gamma.svg', alt: 'Gamma', href: 'https://gamma.dev', height: 24, mono: true },
  ],
}
FieldTypeDefaultDescription
srcstring | { dark: string, light: string }Required. Public path, or paths selected from the active site variant; each is base-prefixed on a mounted base
altstringRequired. Accessible name for the logo
hrefstringMakes the logo a link. Validated like any other href; rejected schemes drop the link and keep the mark
heightinteger20Rendered height in pixels; width follows the asset's aspect ratio
monobooleanfalseDraw the asset as a silhouette in the current text colour, so one file works on every theme and both variants

mono reads only the asset's alpha channel, so trim the artwork's viewBox to the mark itself — surrounding padding becomes part of the silhouette.

Use { dark, light } when a full-colour mark needs separate artwork for contrast. A string keeps the same asset in both variants.

HomeFeaturesBlock

{
  type: 'features',
  label?: string,
  title?: string,
  body?: string,
  items?: Feature[],
  columns?: 1 | 2 | 3 | 4,
  truncate?: TruncateConfig,
}
FieldTypeDescription
itemsFeature[]Feature cards. Omit to auto-derive from the first three top-level pages
columns1 | 2 | 3 | 4Grid column count (default 3). Narrow breakpoints collapse to a single column
truncateTruncateConfigMax visible lines before clipping with an ellipsis

Each Feature:

interface Feature {
  title: string
  description: string
  link?: string
  icon?: IconConfig
}

HomeShowcaseBlock

Generalized card grid. The default source is the combined apps + packages + workspaces list; point it at page paths for an arbitrary card set.

{
  type: 'showcase',
  label?: string,
  title?: string,
  body?: string,
  source?: 'workspaces' | string[],
  columns?: 1 | 2 | 3 | 4,
  truncate?: TruncateConfig,
}
FieldTypeDescription
source'workspaces' | string[]Omit / 'workspaces' → apps + packages + workspaces. Array of page paths → arbitrary card set
columns1 | 2 | 3 | 4Grid column count (default 2)
truncateTruncateConfigLine clamps for the card title/description
{ type: 'showcase', columns: 3, source: ['/products/cli', '/products/api', '/products/web'] }

A source path that matches no page or workspace is skipped with a sync warning; if none of them resolve, the band renders nothing rather than falling back to the workspace deck.

HomeSplitBlock

Two-column show-and-tell band — copy on one side, a HomeVisual on the other. Repeatable in any position.

{
  type: 'split',
  title: string,
  label?: string,
  body?: string,
  bullets?: string[],
  cta?: ButtonConfig,
  visual?: HomeVisual,
  reverse?: boolean,
}
FieldTypeRequiredDescription
titlestringyesSection title
labelstringnoSmall label rendered above the title
bodystringnoBody copy rendered under the title
bulletsstring[]noCheckmark list rendered under the body
ctaButtonConfignoCTA button rendered at the bottom of the copy column
visualHomeVisualnoVisual opposite the copy. Omit for a full-width copy band
reversebooleannoFlip the columns — visual left, copy right (default false)

The narrow single-column stack always leads with the copy, so a reversed band never pushes its visual above the headline.

HomeTabsBlock

A strip of selectable tabs driving one panel. Clicking a tab swaps the panel's copy and visual; the first tab is selected on load. Keyboard navigation (arrow keys, Home/End) and the tab/tabpanel ARIA wiring are built in.

{
  type: 'tabs',
  items: HomeTabItem[],
  label?: string,
  title?: string,
  body?: string,
  orientation?: 'vertical' | 'horizontal',
  reverse?: boolean,
}
FieldTypeRequiredDescription
itemsHomeTabItem[]yesTabs in strip order. The band is skipped when empty
orientation'vertical' | 'horizontal'novertical (default) puts the strip beside the panel; horizontal above it
reversebooleannoVertical only — panel left, strip right (default false)

Each HomeTabItem:

interface HomeTabItem {
  label: string
  icon?: IconConfig
  title?: string
  body?: string
  bullets?: string[]
  cta?: ButtonConfig
  visual?: HomeVisual
}
FieldTypeRequiredDescription
labelstringyesTab text in the strip
iconIconConfignoIcon rendered before the label
titlestringnoPanel headline. Defaults to label
bodystringnoPanel body copy
bulletsstring[]noCheckmark list under the body
ctaButtonConfignoCTA button at the bottom of the panel copy
visualHomeVisualnoVisual shown while the tab is selected
{
  type: 'tabs',
  label: 'Capabilities',
  title: 'Pick a thread, follow it through.',
  orientation: 'vertical',
  items: [
    {
      label: 'Sync engine',
      icon: { id: 'pixelarticons:reload', color: 'green' },
      title: 'Your markdown, left where it is',
      body: 'Ciderpress reads your repo in place — no copying, no restructuring.',
      bullets: ['Glob discovery', 'Watch mode on every save'],
      visual: {
        type: 'terminal',
        command: 'ciderpress dev',
        lines: [{ kind: 'ok', text: 'synced 128 pages' }],
      },
    },
    {
      label: 'OpenAPI',
      title: 'Specs become reference pages',
      visual: { type: 'code', language: 'ts', code: "openapi: { spec: 'openapi.yaml' }" },
    },
  ],
}

Narrow breakpoints collapse both orientations to a single stacked column.

HomeCtaBlock

{
  type: 'cta',
  label?: string,
  title?: string,
  body?: string,
  actions?: ButtonConfig[],
}
FieldTypeDescription
labelstringSmall uppercase kicker above the headline
titlestringCTA headline — the band is skipped without it
bodystringSupporting text
actionsButtonConfig[]CTA buttons (typically up to 2)

TruncateConfig

Shared by features and showcase blocks. Values are maximum visible lines before CSS line-clamp clips with an ellipsis.

interface TruncateConfig {
  title?: number
  description?: number
}

discover

Top-level cross-cutting discovery options. Only field is ignore — global glob patterns excluded from every page's auto-discovery.

discover?: {
  ignore?: string[],
}
FieldTypeDescription
discover.ignorestring[]Glob patterns excluded from every page's discovery (gitignore vocab)
discover: {
  ignore: ['**/draft-*.md', '**/internal/**', '**/_*.md'],
}

Per-page discover.ignore is appended to this list — globals always apply.

templates

Directory or directories holding custom document templates used by ciderpress draft. Each is a .md/.mdx file with label/hint frontmatter; the filename is the template type. Paths are relative to the repo root.

templates?: string | string[]
FieldTypeDescription
templatesstring | string[]Directory (or directories) of custom .md/.mdx template files
templates: ['docs/.templates', 'shared/templates'],

A custom template whose filename matches a built-in (e.g. guide.md) overrides it. .mdx templates scaffold to .mdx files. Templates are validated by ciderpress templates check and as part of check/build. See Templates for the authoring format and the SDK.

devServer

Dev-server configuration — controls how ciderpress dev binds and how the dev URL is presented in the terminal and browser auto-open. All fields are optional.

devServer?: {
  url?:  string,
  port?: number,
  host?: string,
  open?: boolean,
}
FieldTypeDefaultDescription
urlstringhttp://${host}:${port}Externally-visible URL. Replaces the default http://${host}:${port} in the "ready: …" terminal message and the browser auto-open target. The dev server still binds locally — this is a display + auto-open hint
portnumber6174Preferred port. ciderpress falls forward through a 5-port range when the preferred port is occupied. CLI --port overrides
hoststring'127.0.0.1'Bind interface — explicit IPv4 loopback so reverse proxies (portless, nginx, Caddy) pointed at 127.0.0.1 can reach the dev server. Set '0.0.0.0' to expose on every network interface (LAN / Docker / VM). CLI --host overrides
openbooleanfalseAuto-open the resolved URL in the default browser when the dev server becomes ready

CLI precedence: --port / --host / --url > devServer.{port,host,url} > built-in defaults.

Example — behind portless.sh

devServer: {
  url: 'https://docs.acme.localhost',
  open: true,
}

The dev server still binds localhost:6174; portless reverse-proxies the HTTPS hostname to that port. See the portless guide for setup.

Example — exposing to LAN / Docker

devServer: {
  host: '0.0.0.0',
  port: 6174,
}

Shared primitives

Types reused across multiple top-level keys. Same shape, same meaning, everywhere.

IconConfig

type IconConfig = IconId | { id: IconId; color?: IconColor } | { src: string; alt?: string }

Uniform across every position — brand.icon, Page.icon, Workspace.icon, WorkspaceGroup.icon, Feature.icon, ButtonConfig.icon. Either a plain Iconify identifier ('pixelarticons:book-open'), an Iconify id with explicit color, or an arbitrary image source.

TitleConfig

type TitleConfig =
  | string
  | {
      from: 'auto' | 'filename' | 'heading' | 'frontmatter'
      transform?: (text: string, slug: string) => string
    }

Uniform across every title field that supports derivation — Page.title and Workspace.title. Plain string for static titles, or a derivation rule for auto-discovered children. The transform hook receives the derived title and the filename slug. (WorkspaceGroup.title is a plain string only.)

fromSource
'auto'Fallback chain: frontmatter → first # heading → filename
'filename'Filename converted to title case (add-route.md"Add Route")
'heading'First # heading in the file
'frontmatter'title field in YAML frontmatter

ButtonConfig

interface ButtonConfig {
  text: string
  href: string
  variant?: 'primary' | 'secondary' | 'ghost'
  shape?: 'square' | 'rounded' | 'circle'
  icon?: IconConfig
}

Unified button vocabulary. Replaces the three old button shapes (HeroAction.theme, SidebarLink.style, and the third unnamed variant). Used by home.hero.actions, the actions / cta fields on home.blocks[], topbar.cta, sidebar.top / sidebar.bottom / sidebar.promo.cta.

FieldTypeDescription
textstringButton label
hrefstringClick target
variant'primary' | 'secondary' | 'ghost'Visual variant (was 'brand' | 'alt' | 'ghost')
shape'square' | 'rounded' | 'circle'Button shape
iconIconConfigOptional leading icon

ImageSource

type ImageSource =
  | string
  | {
      src: string
      alt?: string
      type?: string
      width?: number | string
      height?: number | string
    }

Universal image source — string path or a fully described image object. Used by brand.favicon, brand.banner (string form), Workspace.badge, and anywhere else an image is rendered.

SortStrategy

type SortStrategy =
  'default' | 'alpha' | 'filename' | 'none' | ((a: ResolvedPage, b: ResolvedPage) => number)

Used by Page.discover.sort and Workspace.discover.sort.

ValueBehavior
'default'Sections first, then pinned intro files (introduction, intro, overview, index, readme), then alphabetical by title
'alpha'Sections first, then alphabetical by title
'filename'Sections first, then alphabetical by source filename
'none'Preserve glob-discovery order
comparator(a: ResolvedPage, b: ResolvedPage) => number — sort by your own rule. Each ResolvedPage has title, link, and frontmatter. Your comparator owns the full order; sections-first is not applied

'default' is the implicit fallback when discover.sort is omitted.

Frontmatter

Page.defaults (and Workspace.defaults) take a Frontmatter value. It carries the page metadata fields Rspress understands plus Ciderpress's nested seo overrides.

{
  title: 'API Reference',
  defaults: {
    aside:    'left',
    editLink: false,
    pageType: 'doc',
  },
}

Per-file YAML frontmatter wins on conflict with defaults. See Frontmatter Fields for the full field schema.

References

  • Frontmatter — per-page metadata schema
  • Icon Colors — color values accepted by IconConfig
  • Content — how pages map your existing markdown into the site tree
  • Workspaces — when to use apps, packages, or workspaces
  • Themes — built-in theme names and custom theme definitions

Resources

  • c12 — the config loader used under the hood
  • Iconify — icon identifier search