@genetik/content
The content package provides the flat content model, parsing of content JSON strings, validation of content against a schema, and normalization (inline → flat + id generation). It depends on @genetik/schema for block types and config validation.
Concepts
- Canonical form: Content is stored and rendered as an entry id plus a flat node map (
nodeId → node). Slots hold id references (a single id or an array of ids), not nested nodes. - ContentNode: Each node has
id,block,config, and slot names (from the block type) whose values arestringorstring[]. - Validation:
validateContent(schema, content)checks structure, block types, config (via schema), slot shapes, and link integrity (no dangling ids). - Normalization: When the schema allows inline (slot
referenceMode"inline" or "both"), slot values may be inline node(s).normalizeContent(schema, input)flattens them, assigns ids (via nanoid or a custom generator), and returns canonical content.
Installation
pnpm add @genetik/content
Types
import type { GenetikContent, ContentNode, SlotValue } from "@genetik/content";
// Content: entry point + flat map
const content: GenetikContent = {
entryId: "root",
nodes: {
root: {
id: "root",
block: "text",
config: { content: "Hello" },
},
},
};
// Slot values in canonical form: id or array of ids
const slotValue: SlotValue = "child-id";
const slotValues: SlotValue = ["id-1", "id-2"];
Parsing
When you have a raw JSON string (e.g. from user input or an API), use parseContentJson to parse it and check the minimal shape (entryId and nodes). It does not validate against a schema or normalize; use validateContent and normalizeContent for that.
import { parseContentJson } from "@genetik/content";
const result = parseContentJson(rawString);
if (result.ok) {
const content = result.content; // GenetikContent
// pass to validateContent, normalizeContent, or renderContent
} else {
console.error(result.error); // e.g. "Invalid JSON" or "Content must have entryId (string)"
}
Validation
import { validateContent } from "@genetik/content";
import { createSchema } from "@genetik/schema";
const schema = createSchema({
blocks: [
{
id: "text",
configSchema: {
type: "object",
properties: { content: { type: "string" } },
required: ["content"],
},
slots: [],
},
],
});
const result = validateContent(schema, {
entryId: "root",
nodes: {
root: { id: "root", block: "text", config: { content: "Hi" } },
},
});
if (result.valid) {
// content is valid
} else {
console.error(result.errors);
// e.g. { path: "nodes.x.config", message: "..." }
}
Validation ensures:
entryIdis present and exists innodes- Every node has
id,block,config;blockis a known type;configpasses the block's JSON Schema - Slot values are
string(single id) orstring[](list) according to the slot definition - No extra keys on a node that are not slots of its block type
- Every id referenced in any slot exists in
nodes(link integrity)
Normalization
When a slot has referenceMode "inline" or "both", you can pass content with inline nodes in that slot. Normalization flattens them and assigns ids.
import { normalizeContent, validateContent } from "@genetik/content";
import { createSchema } from "@genetik/schema";
const schema = createSchema({
blocks: [
{
id: "text",
configSchema: { type: "object", properties: { content: { type: "string" } }, required: ["content"] },
slots: [],
},
{
id: "card",
configSchema: { type: "object", properties: { title: { type: "string" } } },
slots: [{ name: "children", multiple: true }],
},
],
options: { slotReferenceMode: "both" },
});
const input = {
entryId: "root",
nodes: {
root: {
id: "root",
block: "card",
config: { title: "Card" },
children: [
{ block: "text", config: { content: "Inline child" } },
],
},
},
};
const canonical = normalizeContent(schema, input);
// canonical.nodes now has root + a new node for the inline text (with generated id)
const result = validateContent(schema, canonical);
// result.valid === true
Optional custom id generator:
const canonical = normalizeContent(schema, input, {
generateId: () => `my-id-${Math.random().toString(36).slice(2)}`,
});
API summary
| Export | Description |
|---|---|
parseContentJson(raw) | Parses a JSON string into content. Returns { ok: true, content } or { ok: false, error }. Does not validate or normalize. |
normalizeContent(schema, input, options?) | Flattens inline nodes to canonical form; assigns ids (nanoid or custom). |
validateContent(schema, content) | Validates content against a schema. Returns { valid, errors }. |
GenetikContent | Type: { entryId: string; nodes: Record<string, ContentNode> }. |
GenetikContentInput | Type: input content (slots may contain inline nodes where schema allows). |
ContentNode | Type: node with id, block, config, and slot keys. |
ContentNodeInput | Type: input node (same as ContentNode; slot values may be inline). |
SlotValue | Type: string | string[]. |
InlineNode, InlineSlotValue | Types for inline slot values. |
NormalizeOptions | Type: { generateId?: () => string }. |
ParseContentResult | Type: { ok: true; content: GenetikContent } | { ok: false; error: string }. |
ContentValidationError | Type: { path: string; message: string }. |
ContentValidationResult | Type: { valid: boolean; errors: ContentValidationError[] }. |
Package location and build
Source: packages/content in the monorepo. The package is built with tsdown (ESM + CJS + types). Run pnpm build from the package directory or pnpm --filter @genetik/content build from the repo root.