This page is the operational playbook for coding agents that need to create or update Alinea content JSON directly. It focuses on deterministic rules that match Alinea core behavior and this repository's content shapes.
When documenting or generating Alinea content, use source of truth in this order:
The Alinea full-llms.txt file holds all documentation (including this playbook section). Use this as primary source of truth.
In case of ambiguity or missing documentation, consult the alinea core source code.
Projects using alinea will typically have alinea as a bundled node_modules dependency, which means the (compiled) source code can be accessed directly.
In case the source code can not be retrieved, or the compiled code is unclear, consult the source code on https://github.com/alineacms/alinea
Consult schema implementations and concrete JSON fixtures/content in the target project (for example content/main/**, content/demo/**) to find helpful examples and try to following the same structure when suggesting changes.
The source code of the alinea documentation website is also publicly available on https://github.com/alineacms/alineacms.com. The documentation website is itself created with Alinea + Next.js and can serve as a useful example.
In new Next.js projects, agents should follow the tutorial file structure as closely as possible to keep the codebase readable and maintainable.
In existing codebases, first scan the current structure and then align new files and changes to the established coding guidelines and architectural principles.
Top-level entries are JSON records with required meta fields. In Alinea core this is defined by EntryMeta in src/core/EntryRecord.ts.
{
"_id": "<createId()>",
"_type": "<schema type name>",
"_index": "<fractional index>",
"_root": "pages",
"_seeded": "/index.json",
"title": "..."
}_id: unique entry id from createId() (alinea/core/Id).
_type: exact schema type key.
_index: fractional ordering key. Generate with generateKeyBetween from alinea/core/util/FractionalIndexing.
_root: required for root-level entries (entries without a parent). Value is the workspace root key, for example pages or media.
_seeded: only for entries that correspond to seeded config pages (Config.page(...)). Keep path stable.
# New id
node --input-type=module -e "import {createId} from 'alinea/core/Id'; console.log(createId())"
# Index between two siblings
node --input-type=module -e "import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'; console.log(generateKeyBetween('a0', 'a1'))"
# Append after last sibling
node --input-type=module -e "import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'; console.log(generateKeyBetween('a0', null))"Alinea sorts sibling entries by _index ascending. Never hand-pick _index by eye when inserting between entries.
Links appear in two places: rich text marks and link fields (Field.link / Field.link.multiple).
[
{
"_type": "paragraph",
"content": [
{
"_type": "text",
"text": "Internal doc",
"marks": [
{
"_type": "link",
"_id": "<createId()>",
"_link": "entry",
"_entry": "<target entry id>"
}
]
},
{
"_type": "text",
"text": " and external site",
"marks": [
{
"_type": "link",
"_id": "<createId()>",
"_link": "url",
"href": "https://example.com",
"target": "_blank",
"title": ""
}
]
}
]
}
]Rich text link mark shape is defined by LinkMark in src/core/TextDoc.ts: _type: link, _id, _link (entry | file | url), and optional _entry.
// Field.link('Link') -> single entry link
{
"_id": "<createId()>",
"_type": "entry",
"_entry": "<target entry id>",
"label": "Optional extra field"
}
// Field.link('Link') -> single external url
{
"_id": "<createId()>",
"_type": "url",
"_url": "https://example.com",
"_title": "Example",
"_target": "_blank",
"label": "Optional extra field"
}
// Field.link.multiple('Links') row
{
"_id": "<createId()>",
"_index": "<fractional index>",
"_type": "entry",
"_entry": "<target entry id>",
"label": "Optional extra field"
}For Field.link.multiple, each row is also a list row, so _index is required.
List rows are defined by ListRow in src/core/shape/ListShape.ts. Every row must include _id, _type, _index.
{
"items": [
{
"_id": "<createId()>",
"_index": "a0",
"_type": "Item",
"title": "First"
},
{
"_id": "<createId()>",
"_index": "a1",
"_type": "Item",
"title": "Second"
}
]
}Union values (from UnionShape) require _id and _type. If a union is inside a list, it still needs list row _index as well.
Alinea rich text is a TextDoc array (src/core/TextDoc.ts). Common internal nodes used by this project include heading, paragraph, text, bulletList, orderedList, listItem, hardBreak, and block nodes like CodeBlock/ImageBlock with _id.
[
{
"_type": "heading",
"level": 2,
"content": [{"_type": "text", "text": "Heading"}]
},
{
"_type": "paragraph",
"textAlign": "left",
"content": [
{"_type": "text", "text": "Normal text "},
{
"_type": "text",
"text": "bold",
"marks": [{"_type": "bold"}]
},
{"_type": "text", "text": " "},
{
"_type": "text",
"text": "italic",
"marks": [{"_type": "italic"}]
},
{"_type": "hardBreak"},
{
"_type": "text",
"text": "anchor",
"marks": [
{
"_type": "link",
"_id": "<createId()>",
"_link": "url",
"href": "https://example.com",
"target": "_blank",
"title": ""
}
]
}
]
},
{
"_type": "bulletList",
"content": [
{
"_type": "listItem",
"content": [
{
"_type": "paragraph",
"content": [{"_type": "text", "text": "Bullet item"}]
}
]
}
]
},
{
"_type": "orderedList",
"start": 1,
"content": [
{
"_type": "listItem",
"content": [
{
"_type": "paragraph",
"content": [{"_type": "text", "text": "Ordered item"}]
}
]
}
]
},
{
"_type": "CodeBlock",
"_id": "<createId()>",
"code": "console.log('block nodes need _id')",
"language": "javascript",
"fileName": "",
"compact": false
}
]If generating from HTML, Alinea's parser maps common tags to these node/mark types (src/core/field/RichTextField.ts), for example <p> -> paragraph, <a> -> link, <ul>/<ol>/<li> -> list nodes, <strong> -> bold.
Config.workspace and Config.root define where content is stored and which root key each entry belongs to (src/core/Workspace.ts, src/core/Root.ts).
// Example workspace layout in this repository
content/
main/
pages/
docs.json
docs/
reference/
cli.json
media/
screenshot.json
demo/
pages/
index.json
recipes/
chocolate-chip.json
media/
...
// Workspace key -> source
main -> content/main
demo -> content/demo
// Root key -> folder under each workspace source
pages -> <workspace>/pages
media -> <workspace>/mediaManual generation rules:
When creating a root-level entry file, include _root with the matching root key.
Place files under the workspace source directory and root directory that match config.
For seeded pages, keep _seeded stable and matching the configured seed path.
Do not remove nested identity fields (_id, _index, _type) from list rows, union values, or rich text block nodes.
# Normalize metadata via Alinea fix path
alinea build --fix
# In this repository
bun run build -- --fix
# Final validation
bun run buildBefore commit: ensure JSON parses, _type matches schema, _index order is correct among siblings, and no duplicate _id values were introduced in edited scope.