The registry & defineSection
The section registry is the runtime lookup table that maps section types (string keys like "prose" or "colors") to their Zod schemas and React components. It lives in packages/primitives/src/lib/registry.ts and is the single source of truth for both the viewer render path and the editor.
The registry singleton
Section titled “The registry singleton”The registry is a module-level singleton stored on globalThis under a well-known Symbol.for key:
const REGISTRY_KEY = Symbol.for("@drawnagency/primitives/registry");The Symbol.for key is the safety net against module duplication. If Rollup or Vite produces two copies of the registry module (a risk during SSR code-splitting — see SSR / Netlify gotchas), both copies share the same underlying globalThis[REGISTRY_KEY] instance. Without this, sections registered in one copy would be invisible to the other, producing the “At least 2 section schemas must be registered” runtime error.
Module-level API
Section titled “Module-level API”The following functions operate on the default registry instance and are the normal way to interact with the registry:
import { registerSection, registerSchema, registerRichText, getRichTextFields, getSection, getSchema, getAllSections, getAllSchemas, clearRegistry,} from "@drawnagency/primitives/lib/registry";| Function | Signature | Purpose |
|---|---|---|
registerSection | (def: SectionDefinition) => void | Register a full section definition (schema + component) |
registerSchema | (type: string, schema: ZodType) => void | Register a schema only (no component; used by API validation) |
registerRichText | (type: string, fields: readonly string[]) => void | Declare rich-text field paths for the HTML sanitizer |
getRichTextFields | (type: string) => readonly string[] | Read declared rich-text fields |
getSection | (type: string) => SectionDefinition | undefined | Look up a full section definition |
getSchema | (type: string) => ZodType | undefined | Look up a schema (checks schemas map first, then sections) |
getAllSections | () => SectionDefinition[] | All registered section definitions |
getAllSchemas | () => ZodType[] | All schemas (merged from both registerSchema and registerSection calls) |
clearRegistry | () => void | Clear all registrations (used in tests) |
createRegistry() is also exported for creating isolated registry instances (used in tests).
defineSection
Section titled “defineSection”defineSection is the helper for declaring a section. It takes a typed definition object and returns it as a SectionDefinition — the main value of calling it is TypeScript inference: the content prop type of component is inferred from schema.
import { defineSection } from "@drawnagency/primitives";import { z } from "zod";import MyComponent from "./MyComponent";
export default defineSection({ type: "my_type", label: "My Section", schema: z.object({ text: z.string() }), component: MyComponent, defaults: () => ({ text: "" }), // optional: category: "core", // "core" | "brand-guide" (presentation only) navRole: "h1", // "h1" | "h2" | "h3" — drives sidebar nav richTextFields: ["text"], // field paths that contain sanitized HTML settings: { ... }, // declarative settings panel fields settingsTabs: [...], // group settings fields into tabs inheritableSettings: [...], // keys a parent container can set as child defaults getLabel: (content) => content.text, getThumbnails: (content) => [...],});Some built-in sections use internal helpers that call defineSection under the hood (e.g. defineHeadingSection for heading-type sections), but defineSection is the public API exported from @drawnagency/primitives.
The built-in section manifest
Section titled “The built-in section manifest”All built-in sections are declared in a single ordered array in packages/primitives/src/components/sections/all-sections.ts:
export const allSectionDefs = [...coreSectionDefs, ...brandGuideSectionDefs];Both register.ts (which calls registerSection for each) and register-schemas.ts (which calls registerSchema for each) derive from this one array, so the two registration paths can never drift apart.
Core sections (coreSectionDefs)
Section titled “Core sections (coreSectionDefs)”Generic, brand-agnostic sections. A non-brand site can use these and tree-shake the brand-guide group entirely.
| Type | Description |
|---|---|
link_heading | Top-level section heading; drives <h2> with navRole: "h1" |
sub_heading | Sub-section heading; drives <h3> with navRole: "h2" |
sub_sub_heading | Tertiary heading; drives <h4> with navRole: "h3" |
prose | Rich-text body copy (TipTap) |
media | Single image |
button | CTA button with configurable link |
container | Layout container that wraps child sections |
spacer | Vertical whitespace |
Brand-guide sections (brandGuideSectionDefs)
Section titled “Brand-guide sections (brandGuideSectionDefs)”Opinionated sections for brand guidelines. Importing @drawnagency/primitives/components/sections/brand-guide is a self-contained subtree — builds that omit this import drop the brand-guide sections entirely.
| Type | Description |
|---|---|
colors | Color palette with CMYK/Pantone/hex swatches |
icon_list | List of icons with labels |
dodont_media | Side-by-side do/don’t image examples |
Retired types (do not use): split_content, media_grid, do_dont_grid, do_dont — these were decomposed into container-based compositions by mergeSiteContent() in the loader.
Shell vs editor component split
Section titled “Shell vs editor component split”Each section has two rendering contexts:
- Viewer (shell): Zero JavaScript. React components render server-side to pure HTML. The viewer path in
packages/core/src/pages/[...slug].astrodoes not hydrate any React. - Editor: The full
<EditorShell client:load />is hydrated. The same section component is used in both contexts — it receives anisEditModeprop and anonChangecallback that is only populated in the editor.
The SectionProps<T> type reflects this:
interface SectionProps<T> { content: T; options?: Record<string, unknown>; onChange?: (content: T) => void; // undefined in viewer isEditMode: boolean; openModal?: (title: string, content: ReactNode) => void;}