Перейти к содержимому
Docxcelerate

Документы как компоненты.
DOCX на выходе.

Собирайте документы из небольших типизированных компонентов на том же JSX, который вы уже пишете. Легко подключайте ИИ, чтобы он писал содержимое или принимал решения о нём. Используйте наш движок, чтобы генерировать документы в большом объёме.

$ npx docxcelerate init my-documents

v0.6.1

my-documents/ Документов: 4 · файлов: 52
import { Document, PageBreak, Section, template } from "docxcelerate/template";
import {
  Charges,
  Closer,
  EngagementSummary,
  InvoiceMeta,
  Letterhead,
  Parties,
  Payment,
  PaymentLetterhead,
  RunningFooter,
  RunningHeader,
  ScanToPay,
  Terms,
  Totals,
} from "./nodes/index.ts";
import type { InvoiceData } from "./types.ts";

/**
 * Structure only: which nodes, in which order, and where the page turns.
 *
 * The break is part of what this document is, not a way of nudging a paragraph
 * off the bottom of a page. What is owed goes on one page and how to pay it on
 * the next, so that either can be handed to someone on its own.
 *
 * Page one carries no running header: the letterhead already is the top of the
 * page, and printing both names the sender twice. Page two needs one, because a
 * payment page that does not say which invoice it belongs to gets filed against
 * the wrong account — so the header runs everywhere except the first page.
 */
export const documentTemplate = template<InvoiceData>(
  <Document
    id="invoice"
    title="Invoice"
    header={<RunningHeader />}
    firstHeader={false}
    footer={<RunningFooter />}
  >
    <Letterhead />
    <InvoiceMeta />
    <Parties />
    <Section id="summary" title="Engagement summary">
      <EngagementSummary />
    </Section>
    <Charges />
    <Totals />
    <Closer />

    <PageBreak id="to-payment" />

    <PaymentLetterhead />
    <Payment />
    <Section id="scan" title="Scan to pay">
      <ScanToPay />
    </Section>
    <Terms />
  </Document>,
);
Счёт · A4 100%

Настоящий предпросмотр документа в момент отрисовки. Без скриншотов и скрытых уловок.

Документ — это дерево небольших компонентов, отрисованное тем, что разбирается в бумаге. Вы получаете удобство компонентной модели. Человек на другом конце получает файл Word.

Лиам, автор Docxcelerate
Лиам
Автор Docxcelerate
01 Написание
<Document title="Offer of Admission">
<Section title="Your offer">
<Greeting />
<Offer />
<TutorNote />
</Section>
<Section title="Conditions">
<Conditions />
</Section>
</Document>

Пишите документы как сайты

Документ — это дерево типизированных компонентов. Если вы писали на React, эта форма вам уже знакома: пропсы, композиция, небольшие файлы. Поэтому фронтенд-разработчик становится продуктивным в первый же день, а не после изучения очередного языка шаблонов.

02 ИИ
state: { interviewer, portfolioTheme }DATA
tone: "warm, never effusive"
<TutorNote />useSetPrompts
A short personal note from Dr Priya Raman about Maya Oyelaran's interview.
PLACEHOLDER

ИИ на уровне компонента

ИИ подключается через хуки, внутри тех же компонентов, которые вы уже пишете. Компонент передаёт модели свой контекст и то, что нужно написать, поэтому она создаёт ровно эту часть документа, а всё вокруг остаётся детерминированным. Сколько будет сгенерировано, решаете вы, компонент за компонентом.

03 Контроль изменений
nodes/arrears.node.tsx
− within 14 days of this notice
+ within 30 days of this notice
nodes/contact.node.tsx
− call us to discuss
+ call us to agree a payment plan
✓ arrears-notice renders as expected
✓ no other document changed

Документы живут в вашем репозитории

Раз документ — это исходный код, изменение одной фразы становится pull request: с диффом, с ревью и с авторством, которое можно установить и год спустя, когда кто-нибудь спросит, кто поправил формулировку о задолженности. Тесты фиксируют, что документ рендерится так, как вы ожидаете, и CI замечает ошибку раньше получателя.

Движок

Опубликуйте один раз. Масштабируйте сколько нужно.

Именно в движке документы и пишутся по-настоящему. Он подставляет ваши данные, запускает ИИ и возвращает готовый документ. Использовать ИИ может узел любого вида, не только абзац. Ответ модели либо становится текстом, написанным по тем сведениям, которые вы ей дали, либо принимает решение, от которого зависит документ.

Шаблон публикуется в движок один раз. После этого любая система может вызвать его API с набором данных и получить документ. Бесплатный движок можно разместить у себя. Управляемое облако запускает полную версию, в которой есть многое, чего нет в бесплатной. Оно появится скоро.

  1. 01

    Сборка

    Фреймворк превращает ваш документ в пакет. Этот шаг выполняется на вашей машине.

    documents/offer-of-admission/build/
    manifest.json
    preview.json
    document.json
  2. 02

    Публикация

    Вы отправляете пакет в движок. Движок сохраняет его и даёт ему имя.

    docxcelerate.config.json
    "upload": { "endpoint": "https://documents.example.com/api/letters"}
    → document.json stored, and given an address
  3. 03

    Написание

    Ваше приложение отправляет набор данных. Движок возвращает готовый документ.

    POST /api/letters
    { applicantName, conditions, interviewer }
    → 200 offer-of-admission.docx

Начните следующий документ с форой.

В реестре есть темы и компоненты документов. `dxcl add` копирует файл в ваш проект. Никакой зависимости и никакой версии, за которой нужно следить.

your-project — dxcl
$ npx dxcl add slate-report letterhead
+ document-style.ts ← slate-report
+ nodes/letterhead.node.tsx
re-exported from nodes/index.ts
✓ записано файлов: 2 · зависимостей не добавлено
После установки файлы лежат в вашем репозитории. Правьте их как любой другой код.
Для крупных компаний

Сделано для отчётов, которые вы пока пишете вручную

У компании, которая готовит один и тот же отчёт сотни раз в месяц, вёрстка и большая часть формулировок уже есть. Меняется человек, для которого отчёт написан. Соберите этот отчёт заново из компонентов, оставьте каждый прогон одинаковым и отдайте модели только те части, которые зависят от того, кто его читает.

01

Один шаблон, каждый адресат

Опубликуйте шаблон один раз, а дальше вызывайте его на каждого человека. Один отчёт или сто тысяч — это один и тот же вызов, повторённый.

offer-of-admission
1.0.0
02

Каждый раз один и тот же документ

Всё, что вы не пометили как генерируемое, отрисовывается одинаково при каждом прогоне. Меняться могут только выбранные вами части.

03

Вызывается системами, которые у вас есть

Ваша CRM, система ведения дел или биллинг отправляет свои данные и получает .docx. Никто не выгружает таблицу и не открывает Word.

CRMengine × n.docx
04

Воспроизводимо и через год

У шаблонов есть версии, поэтому любой документ можно собрать заново из того же шаблона и тех же данных. Проверка получает сборку, а не архив.

  1. offer-of-admission@1.0.0
  2. offer-of-admission@1.1.0
  3. offer-of-admission@2.0.0
Открытый исходный код

Открытый код — и так задумано надолго

Фреймворк, рендереры, модель узлов и CLI выпущены под лицензией MIT и разрабатываются открыто. Читайте код, который пишет ваши документы, форкайте его или включайте в собственную сборку.

Движок можно бесплатно разместить у себя, поэтому работа с документами в масштабе никогда не зависит от того, останется ли поставщик на рынке и не изменится ли прайс-лист. Наше платное облако добавляет премиальные возможности поверх того же свободного ядра, так что хостинг и масштаб готовы с первого написанного документа. Это удобство, а не вход.

LICENSE 1 из 1

MIT

Copyright (c) 2026 Docxcelerate

Permission is hereby granted, free of charge, to any person obtaining a copy of this software, to use, copy, modify, merge, publish, distribute, sublicense and sell copies of it.

Форкните Встройте Поставляйте

Прочитать →
Agent skills

Отдайте всё это своему агенту

Один Markdown-файл объясняет кодовому агенту, как здесь устроены документы: компонентная модель, правила, на которых агенты спотыкаются, и все команды. Положите его рядом и попросите документ, вместо того чтобы писать первый самому.

.claude/skills/docxcelerate/
---
name: docxcelerate
description: Write and maintain Docxcelerate documents — DOCX letters composed from typed JSX components, with prose an engine generates per recipient. Use when a workspace has docxcelerate.config.json, documents/*/document.project.ts or *.node.tsx files, when code imports from docxcelerate, docxcelerate/template or docxcelerate/document, or when asked to create a document, add or edit a node, write prompts for generated prose, style the packed .docx, or publish a document to the engine.
---

# Docxcelerate

A document is **a JSX tree plus a data type**. Components return nodes; building
the tree against data produces a `DocumentModel` — plain JSON, no styling and no
layout. Renderers turn that JSON into a `.docx` or a preview page.

If you know a frontend framework, the shape maps over: `document.tsx` is the
entrypoint, `nodes/*.node.tsx` are components, `useState` is where data enters,
and everything else is ordinary TypeScript.

The word "template" here means a document tree, not a string-substitution
language. There is no template language — an `if` is an `if`, `.map()` is
`.map()`, and formatting is a function call.

## Read this before writing code

Four rules cause nearly every mistake an agent makes in this framework.

1. **`useState` is the only door data comes through.** Its initializer receives
   the document data. Nothing else reaches for it.
2. **Every hook runs before the first `await` and before any branch or return.**
   Same rule as React, same reason. There is no `useMemo` — compute in the
   `useState` initializer, which runs once by construction.
3. **Static or dynamic is inferred, never declared.** A node given text (or an
   `Image` a `src`, or a `Graph` its `data`) resolves locally. A node given
   prompts is filled in by the engine. Supplying both on one element is an error.
4. **Never add a `@jsxImportSource` pragma comment.** The workspace
   `tsconfig.json` already sets `jsxImportSource: "docxcelerate/template"` for
   every file. A pragma is only for a foreign project that points
   `jsxImportSource` somewhere else.

## Where things live

```text
my-documents/                        # dxcl init writes this — an ordinary Vite project
  docxcelerate.config.json           # build + upload presets, workspace-wide
  documents/
    tenancy-renewal/
      document.project.ts            # the entrypoint; ties everything below together
      document.tsx                   # structure only — which nodes, which sections, what order
      types.ts                       # the data contract
      preview-data.ts                # one realistic instance of that contract
      document-style.ts              # fonts, spacing, margins for the packed .docx
      nodes/
        greeting.node.tsx            # one node per file
        index.ts                     # re-exports every node
      derivers/index.ts              # named functions the engine runs per document
```

The split is the point — each file answers one question. Keep prose out of
`document.tsx`; a template that inlines its text stops being readable about
halfway down.

Imports: **`docxcelerate/template`** for authoring (elements, hooks, `template`),
**`docxcelerate/document`** for `defineDocumentProject` and style types,
**`docxcelerate`** for `buildDocument` and the domain types.

## A node

```tsx
import { Paragraph, useFormat, useState } from "docxcelerate/template";
import type { TenancyData } from "../types.ts";

export const Balance: Paragraph = () => {
  const { currency } = useFormat();
  const [state] = useState((data: TenancyData) => ({
    name: data.recipientName,
    due: data.balanceDue,
  }));

  if (state.due === 0) {
    return <Paragraph id="balance-settled">Nothing outstanding, {state.name}.</Paragraph>;
  }

  return <Paragraph id="balance-arrears">You owe {currency(state.due)}.</Paragraph>;
};
```

`Paragraph` is both the element and the component type, so
`const Balance: Paragraph` declares what this yields — returning a `<Section>`
from it is a compile error. Give each branch arm **its own id**: that is what
lets a resolved document record which one this recipient got.

The `if` publishes: the build compiles it into a condition, so both arms travel
to the engine and it decides per recipient. The `currency()` call does not —
computing on request data needs a deriver. See
[references/publishing.md](references/publishing.md).

## A node whose prose is generated

Set prompts instead of text, and a placeholder so previews stay readable.

```tsx
import { Paragraph, useSetPlaceholders, useSetPrompts, useState } from "docxcelerate/template";
import type { OfferData } from "../types.ts";

export const TutorNote: Paragraph = () => {
  const [state] = useState((data: OfferData) => ({
    applicant: data.applicantName,
    interviewer: data.interviewer,
  }));

  useSetPrompts({
    systemPrompt: "You are an admissions tutor. Warm, never effusive. Promise nothing.",
    generalPrompt: `Write two specific sentences from ${state.interviewer} about ` +
      `${state.applicant}'s interview.`,
    negativePrompt: "Do not restate the offer, the conditions, or the reply deadline.",
  });
  useSetPlaceholders(`A short note from ${state.interviewer}.`);

  return <Paragraph id="tutor-note" />;
};
```

Only `generalPrompt` is required. `infoPrompt` is context the model should have
but not restate; `negativePrompt` is what to avoid; `systemPrompt` is role and
tone; `examplePrompt` is a finished answer to match rather than a description of
one, which is the cheapest way to pin down an opening, an order and a length. All
six slots (those five plus `placeholder`) can also be given as props, which reads
better when they are short — and props win over the hook, so a caller can
override what a shared hook set around it.

Previews resolve dynamic nodes to their placeholder, never to generated prose.
Nothing leaves the machine, and the same build gives the same page every time.

## The elements

| Element | Holds | Notes |
| --- | --- | --- |
| `Document` | sections and nodes | `id` and `title` both required; one per template; `header`/`footer` take running furniture |
| `Section` | any nodes, including sections | the **only** container; `title` required and becomes a heading |
| `Paragraph` | text children, or prompts | `text` prop says the same as children |
| `Image` | `src`, `alt`, `width`, `height`, or prompts | `src` becomes `path`; only a `data:` URI travels — see below |
| `Graph` | `graphType`, `data`, `title`, `caption`, or prompts | packed as a real Word chart, never a picture of one — the numbers travel with it |
| `Table` | `Row`s, and any `.map()` producing them | `columns` declared once, in mm or `"auto"`, with `align` |
| `Row` | `Cell`s | `header` marks a heading row; only *leading* ones repeat across pages |
| `Cell` | text, or paragraphs when a line is not enough | `span`, and `align` when it departs from its column |
| `TableOfContents` | nothing | a marker; renderers print the title and stop |
| `PageBreak` | nothing | for a break that is part of what the document *is* |
| `PageNumber` | nothing | `format` (`current`/`total`/`currentOfTotal`) and `separator`; counted by the renderer |

**An `<Image>` only travels if it carries its bytes.** A `data:` URI does; a
path or a URL draws on screen, where a browser can fetch it, but packs into Word
as a note rather than a picture — the packer never reaches for a file, because
the engine writing the document is not on the machine the file was on. Word will
not embed an SVG alone either, so give one a `fallbackSrc` raster: the screen
draws the SVG and the Word file gets the raster.

Every element also takes **`variant`** — a name the theme looks up, never an
appearance: `<Cell variant="badge">`, `<Paragraph variant="band">`. The colours
live in the style's `blocks`, so a document restyles without a node changing,
and a name the theme has not heard of draws as an ordinary block rather than
failing. Never write a colour into a component.

A block style says `fill`, `color`, `border` (with `borderWidthPt`,
`borderSides`), `paddingPt`, `fontSizePt`, `weight`, `transform`,
`letterSpacingEm` and `bleed`. **All of them mean the same thing on screen and
in the `.docx`** — a fill is shading, a border is a real border, a bleed is a
negative indent past the margin. If a property cannot be expressed in Word it
does not exist here, because a style that quietly did nothing in the format the
framework produces is worse than one that was never offered.

Ids are addresses: an engine targets a node by id and two build artifacts line
up in a diff by id, so **treat a rename as a breaking change**. **Do not write
ids by default.** A node without one is named after its heading, or after the
component that yielded it — `<Greeting />` becomes `greeting`, a section titled
"Fees and funding" becomes `fees-and-funding` — and repeats are numbered
(`greeting-2`). Those names come from what a node is rather than where it sits,
so they survive insertion and reordering. Write one only to pin an address a
request asks for by name. This also keeps
`.map()` and branches from demanding names you do not have. Reusing an id is an
error reported with both positions.

`{condition && <Node />}` reads the way it does everywhere else, and publishes
the way an `if` does — the build compiles it into a condition rather than
deciding once. Falsy children are skipped, so a `&&` yielding anything other
than a node still just drops out. There is no `key` prop — an element accepts
only its own props, so `key={…}` is a type error. Text lives only inside a
`<Paragraph>`, and a paragraph holds text rather than elements.

## Commands

```sh
npx docxcelerate init my-documents      # scaffold a workspace and npm install it
npm run dev                             # preview on 127.0.0.1:4507
dxcl document new tenancy-renewal --title "Tenancy Renewal"
dxcl document node documents/tenancy-renewal next-steps --type paragraph
```

`npx docxcelerate`, not `npx dxcl` — npx resolves the package name and the
binary inside is `dxcl`. Any command run with no arguments asks for what it
needs instead of failing.

`dxcl document node` writes `nodes/<name>.node.tsx` and updates
`nodes/index.ts`. It deliberately does **not** place the node in the template —
after generating one, add it to `document.tsx` where it belongs.

Every flag is in [references/cli.md](references/cli.md).

## Before writing a node from scratch

The package ships a small registry of themes and prebuilt nodes. `dxcl list`
prints it, `dxcl show <id>` prints one entry in full, and `dxcl add <id>`
installs it.

```sh
dxcl list                               # 5 themes, 6 components
dxcl show payment-summary               # what it does and what it reads
dxcl add slate-report letterhead        # a theme and a node, into this project
```

A component is **copied in as source** — `nodes/<name>.node.tsx`, re-exported
from `nodes/index.ts`. It has no version and is never upgraded behind you, so
editing it afterwards is the expected next step rather than a fork. Two things
it leaves to you, both printed as follow-up when it installs: the fields it
reads have to be added to `types.ts` and `preview-data.ts`, and the node itself
has to be placed in `document.tsx`.

A theme is written out as `document-style.ts`, which `document.project.ts`
already passes through — so the next preview is themed. It replaces a
`document-style.ts` nothing has touched; once you have edited that file,
replacing it needs `--force`.

Reach for the registry first when a request names something ordinary — a
letterhead, an address block, a signature, small print. Every entry is listed in
[references/cli.md](references/cli.md) and browsable at
[docxcelerate.com/components](https://docxcelerate.com/components/).

## Publishing changes the rules

Everything above assumes you hold the data. Publishing to the engine builds the
artifact **once**, against stand-ins for a request nobody has made yet, so a
decision that depends on request data has to travel to the engine instead:

- **Interpolating** a value publishes fine — it becomes a `{{data.x}}` token.
  **Computing** on one does not; use a **deriver**, which the engine runs per
  document. Never hand-write a `{{data.…}}` token; interpolation produces it.
- **A preview never waits.** It is rebuilt on every save, so generated nodes show
  the placeholder `useAi` required and derivers that declared a `placeholder`
  stand in rather than run. Cheap derivers still run, so the figures are real.
  Give any deriver that renders, reads or fetches a `placeholder`; leave it off
  for a total or a currency format.
- **`.map()` over request data is how a loop is written.** It walks the list when
  the data is real and is published as a loop the engine walks when it is not.
  Never hand-write a `{{ctx.…}}` token inside one — the entry writes its own
  references. Anything needing the entries first (`.filter`, `.length`, `for…of`)
  belongs in a deriver.
- **A decision is written as an ordinary conditional.** An `if` that returns, a
  ternary, and `cond && <Node />` are all compiled into the condition the engine
  evaluates per document, so both arms travel with the test that selects them.
  A conditional picking a *value* rather than a node is a deriver's job. This
  needs the transform in the build — `docxcelerateTransform()` for Vite,
  `docxcelerateEsbuildTransform()` for esbuild, from `docxcelerate/transform`.
  Without it the decision is made once, at build time, for every recipient.

Read [references/publishing.md](references/publishing.md) before touching a
document that ships to an engine, or when a document is right in preview and
wrong in production.

## Before you call it done

- Each branch arm has its own id; no id is used twice; no `key` props.
- Hooks all called before any `await`, branch or `return`.
- No node carries both text and prompts.
- Every dynamic node has a placeholder, and the document still reads with
  placeholders in place. If it does not, the structure is doing too little work.
- `preview-data.ts` uses the longest name and largest figure you actually
  expect — short names and placeholder cities hide layout problems.
- New nodes are exported from `nodes/index.ts` **and** placed in `document.tsx`.
- `npm run documents:check` type-checks every document.

## Going deeper

- [references/api.md](references/api.md) — every entrypoint, element prop, hook and build function
- [references/patterns.md](references/patterns.md) — copyable recipes: repeats, graphs, house style, shared state, computed sections
- [references/publishing.md](references/publishing.md) — derivers, build artifacts, preview vs engine vs final
- [references/cli.md](references/cli.md) — every `dxcl` command and flag
- [docxcelerate.com/docs](https://docxcelerate.com/docs/start-here/) — the full documentation

Скопируйте папку в .claude/skills/ для одного проекта или в ~/.claude/skills/ для всех. Она подхватится сама, как только появится проект документов.

Это обычный Markdown, а рядом с ним лежат четыре справочных файла в skills/docxcelerate/.