Start Here
Document projects
The files that make up one document, what each is responsible for, and the config that surrounds them.
A workspace holds one document project per document. Each one is a directory
under documents/ with a single entrypoint.
That means a document can be moved, copied or reviewed as one thing, rather than as files scattered across the workspace.
Create one
dxcl document new tenancy-renewal --title "Tenancy Renewal"
Run it with no arguments and it will ask you for the name and title instead.
documents/tenancy-renewal/
document.project.ts
document.tsx
document-style.ts
preview-data.ts
types.ts
derivers/
index.ts
nodes/
greeting.node.tsx
intro.node.tsx
index.ts
The split is deliberate — each file answers exactly one question:
| File | What it holds |
|---|---|
types.ts | The data contract — what a document needs to be written |
preview-data.ts | One instance of that contract, for the preview |
document.tsx | Structure only: which nodes, in which sections, in which order |
nodes/ | One component per node, plus an index.ts that exports them |
document-style.ts | Fonts, spacing and margins for the packed .docx |
derivers/index.ts | Named functions the engine runs per document |
document.project.ts | Ties the above together and names the document |
The entrypoint
import { defineDocumentProject } from "docxcelerate/document";
import { derivers } from "./derivers/index.ts";
import { documentTemplate } from "./document.tsx";
import { documentStyle } from "./document-style.ts";
import { previewData } from "./preview-data.ts";
import type { DocumentData } from "./types.ts";
export default defineDocumentProject<DocumentData>({
id: "tenancy-renewal",
name: "Tenancy Renewal",
version: "0.1.0",
template: documentTemplate,
previewData,
derivers,
style: documentStyle,
previewOptions: {
availableTokens: 800,
},
});
| Field | What it decides |
|---|---|
id | How the document is addressed, in artifacts and by an engine |
name | What a person sees in the preview app |
version | Stamped into every artifact this project builds |
template | The tree, from document.tsx |
previewData | What the preview resolves against |
derivers | Values computed per document rather than per build |
style | The style applied when packing |
previewOptions.availableTokens | The budget useAvailableTokens reports |
The preview app discovers projects by globbing for document.project.ts, so a
new document appears in the picker as soon as it exists. Nothing registers it.
Keep the preview data honest
preview-data.ts is the only data most of a document will ever be built
against, so it is worth more attention than it usually gets:
import type { DocumentData } from "./types.ts";
export const previewData: DocumentData = {
recipientName: "Avery",
city: "Berlin",
};
Short names and placeholder cities hide layout problems that real data exposes. Use the longest name and the largest figure you actually expect.
Why the preview is quick
A preview is rebuilt every time you save. That is the point of it — a document is written by looking at it, and anything the preview waits for is time you spend watching it fail to appear. So the two things that cost real time stand in rather than run:
- Generated nodes show their placeholder.
useAirequires one for exactly this reason. A preview never calls a model, which also means it needs no credentials and cannot be blocked by a rate limit. - Derivers that declared a stand-in use it. A deriver that renders a code,
reads a file, or asks a service says so by supplying a
placeholder, and the preview shows that instead.
// derivers/payment-qr.ts — costly, so it stands in while you write
export default {
name: "paymentQr",
run: async ([iban, amount]) => renderQrPng(iban, amount),
placeholder: "[scan-to-pay code]",
};
// derivers/money.ts — cheap, so the preview shows the real figure
export default {
name: "money",
run: ([amount]) => `£${Number(amount).toLocaleString("en-GB")}`,
};
A deriver with no placeholder is cheap by definition — a total, a currency, a
date — and runs everywhere, because a preview showing the real figure is worth
more than the microsecond it took.
Both are resolved for real when a document is actually written, which is the moment waiting is worth something. The stand-ins change what a value says, never whether it is there: a preview and a written document have the same nodes, so what you proofread is the document somebody receives.
Workspace configuration
docxcelerate.config.json sits at the top of the workspace and covers every
document in it. It stores named presets for build and upload behaviour:
{
"schemaVersion": "docxcelerate.config/v0",
"activePreset": "local",
"presets": {
"local": {
"build": { "outDir": "build" },
"upload": { "endpoint": "", "method": "POST", "headers": {}, "body": "document" }
}
}
}
activePreset selects which one applies, which is how a local setup and a
staging engine sit side by side without editing configuration between runs. With
upload.endpoint empty, everything still builds — you get artifacts on disk
instead of a finished document.
Workspace and config documents every field, and build artifacts covers what a build writes.
Where to go next
- Templates — computed structure, reusable pieces, and what changes when you publish
- The engine — publishing a document and writing from it
- Commands — every CLI flag