Skip to content
Docxcelerate

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:

FileWhat it holds
types.tsThe data contract — what a document needs to be written
preview-data.tsOne instance of that contract, for the preview
document.tsxStructure only: which nodes, in which sections, in which order
nodes/One component per node, plus an index.ts that exports them
document-style.tsFonts, spacing and margins for the packed .docx
derivers/index.tsNamed functions the engine runs per document
document.project.tsTies 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,
  },
});
FieldWhat it decides
idHow the document is addressed, in artifacts and by an engine
nameWhat a person sees in the preview app
versionStamped into every artifact this project builds
templateThe tree, from document.tsx
previewDataWhat the preview resolves against
deriversValues computed per document rather than per build
styleThe style applied when packing
previewOptions.availableTokensThe 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. useAi requires 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

Edit this page on GitHub ↗ Read this page as Markdown