Skip to content
Docxcelerate

Nodes

The node model

What every node has in common, what makes them differ, and where each one is documented.

A document is a tree of nodes. Every node is three things: an id, a kind, and a rule for producing content.

paragraph<MemberData>({
  id: "greeting",                                // the address
  render: (data) => `Dear ${data.memberName},`,  // the rule
});                                              // the helper is the kind

Resolved against your data, it becomes plain JSON:

{ "id": "greeting", "kind": "paragraph", "mode": "static", "text": "Dear Adaeze Nkemelu," }

No styling, no layout. Those belong to the renderer.

The catalog

Each type has a page of its own, with every option it takes and a preview of each way it can be written.

Structure

section Groups nodes under a titled heading.

The only construct that nests today. Its title carries into the document outline, so the structure you write is the structure the reader sees.

Helpers: section, Section

export const Opening = section<SampleData>({ id: "opening", title: "Your renewal" }, [
  Greeting,
  PriceChange,
]);

Section reference, with previews →

tableOfContents A marker for a contents list, ahead of the renderers that build one. No helper yet

The kind is part of the letter schema and both renderers accept it, but no authoring helper is exported yet. Writing the component by hand works — a node component is a function returning a definition, and the helpers are conveniences over exactly that shape.

export const Contents: NodeComponent<SampleData> = () => ({
  kind: "tableOfContents",
  id: "contents",
  title: "What is in this letter",
});

Table of contents reference, with previews →

Text

paragraph A block of prose, rendered from your data or from prompts.

The workhorse. A static paragraph returns a string from your typed data; a dynamic one carries prompts and a placeholder, and is filled at request time. Both land as the same node kind, differing only by `mode`.

export const Greeting = paragraph<SampleData>({
  id: "greeting",
  render: (data) => `Dear ${data.memberName},`,
});

Paragraph reference, with previews →

Media

image A picture resolved from your data or described by a prompt.

A static image points at something you hold — a signature, a logo, a site photograph — with every field able to vary per recipient. A dynamic image describes what is wanted and leaves the endpoint to make it.

export const Signature = image<SampleData>({
  id: "signature",
  src: (data) => data.signatureUrl,
  alt: (data) => `Signed by ${data.managerName}`,
  width: 180,
  height: 60,
});

Image reference, with previews →

clipArt Named visual blocks — rules, marks, callouts — drawn by the renderer. Planned

Not built yet. Nothing is fetched: the node names a shape and the renderer draws it, so it stays sharp in the DOCX and ships no asset.

Data

graph A bar, line or pie chart declared as data.

Charts are declared, never drawn: `graphType` fixes the form, `data` returns the payload. Holding numbers rather than an image means one declaration serves every renderer and stays diffable in the artifact.

export const VisitsByMonth = graph<SampleData>({
  id: "visits-by-month",
  graphType: "bar",
  data: (data) => ({
    labels: data.visitsByMonth.map((entry) => entry.month),
    series: [{ name: "Visits", values: data.visitsByMonth.map((entry) => entry.visits) }],
  }),
  caption: (data) => `Your visits to ${data.centreName}, last six months`,
});

Graph reference, with previews →

table Rows and columns, with cells that are themselves nodes. Planned

Not built yet. The intent is a node whose cells hold other nodes, so a table composes the way a section does rather than becoming a second content model beside it.

Ids are addresses

A node’s id is how a generation endpoint targets that paragraph, and how two build artifacts line up in a diff. Renaming one breaks whatever points at it from outside, the way renaming an API route does.

Keep them unique across the letter. It costs nothing and makes logs readable.

Static and dynamic

A static node computes its content locally from your data. A dynamic node carries prompts and a placeholder, and is filled in at request time.

You never declare which you want. There is one helper per kind — paragraph, image, graph — and the mode is inferred from the options you give it: supply the local-resolution member (render, src, data) and the node is static; supply prompts instead and it is dynamic. Both resolve to the same kind, differing by mode in the built document.

Supplying both is a compile error, so mode has exactly one source of truth. Static and dynamic covers where the line sits and why.

Nesting

section is the only node that holds children today, and it accepts any kind, including other sections. A table node with nodes in its cells is next, on the same principle: containers hold the components you already write, rather than a second content model beside them.

About these previews

Every preview here is a real build. src/nodes/ in this site’s repository holds one file per variant, written against the published package; a build step resolves each through buildDocument and renders it with the renderer dxcl dev serves. The source shown is the file that ran, and the JSON is what came back — so these pages break loudly rather than going quietly out of date.


Edit this page on GitHub ↗