Documenten als componenten.
DOCX als resultaat.
Stel documenten samen uit kleine, getypeerde componenten, met de JSX die je toch al schrijft. Gebruik de ingebouwde AI-functies om dynamische documenten te maken en schaal documentgeneratie op met onze engine.
npx docxcelerate init my-documents {
"schemaVersion": "docxcelerate.config/v0",
"activePreset": "local",
"presets": {
"local": {
"build": { "outDir": "build" },
"upload": {
"endpoint": "",
"method": "POST",
"headers": {},
"body": "document"
}
},
"staging": {
"build": { "outDir": "build" },
"upload": {
"endpoint": "https://documents.staging.example.com/api/letters",
"method": "POST",
"headers": { "Authorization": "Bearer ${LETTERS_TOKEN}" },
"body": "document"
}
}
}
} import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* Formatting is ordinary TypeScript — no template language, so an Oxford comma
* and a singular/plural switch cost one line each.
*/
export const Conditions = paragraph<OfferData>({
id: "conditions",
render: (data) => {
const list = formatList(data.conditions);
const verb = data.conditions.length === 1 ? "condition is" : "conditions are";
return `This offer is conditional. The ${verb} ${list}. We will confirm ` +
`your place automatically once your results reach us.`;
},
});
function formatList(items: string[]): string {
if (items.length <= 1) return items[0] ?? "";
return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
} import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* An optional field changes the paragraph rather than leaving a gap — the kind
* of branch that is awkward in a mail-merge and trivial here.
*/
export const Fees = paragraph<OfferData>({
id: "fees",
render: (data) => {
const base = `You have been assessed as a ${data.feeStatus} fee payer, so ` +
`tuition for your first year will be ${data.tuitionFee}.`;
if (!data.scholarship) {
return `${base} Details of loans, bursaries and hardship funding are in ` +
`the enclosed funding guide.`;
}
return `${base} We are also pleased to award you the ` +
`${data.scholarship.name}, worth ${data.scholarship.amount} for the ` +
`duration of your course. No separate application is needed.`;
},
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const Greeting = paragraph<OfferData>({
id: "greeting",
render: (data) => `Dear ${data.applicantName},`,
}); export { Conditions } from "./conditions.node.ts";
export { Fees } from "./fees.node.ts";
export { Greeting } from "./greeting.node.ts";
export { NextSteps } from "./next-steps.node.ts";
export { Offer } from "./offer.node.ts";
export { SignOff } from "./signoff.node.ts";
export { TutorNote } from "./tutor-note.node.ts"; import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const NextSteps = paragraph<OfferData>({
id: "next-steps",
render: (data) =>
`To accept, reply through the applicant portal by ${data.replyBy}. ` +
`If you would like to visit ${data.college} before deciding, our open ` +
`afternoons run every Thursday through April and you are very welcome.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const Offer = paragraph<OfferData>({
id: "offer",
render: (data) =>
`I am delighted to offer you a place on the ${data.programme} at ` +
`${data.college}, beginning ${data.startDate}. Your application reference ` +
`is ${data.offerRef}; please quote it in any correspondence with us.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const SignOff = paragraph<OfferData>({
id: "sign-off",
render: (data) =>
`Yours sincerely, ${data.signatory.name}, ${data.signatory.title}, ${data.college}.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* The one paragraph worth generating. Everything else in this document is
* deterministic, so this is the only node that needs an endpoint at all.
*/
export const TutorNote = paragraph<OfferData>({
id: "tutor-note",
placeholder: (data) =>
`A short personal note from ${data.interviewer} about ${data.applicantName}'s interview.`,
generalPrompt: (data) =>
`Write two warm, specific sentences from ${data.interviewer} to ` +
`${data.applicantName}, referring to their interest in ${data.portfolioTheme}. ` +
`Mention one thing they should read before term starts.`,
systemPrompt: () =>
"You are an admissions tutor. Be warm but never effusive, and never promise outcomes.",
negativePrompt: () =>
"Do not restate the offer, the conditions, or the reply deadline.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { OfferData } from "./types.ts";
export default defineDocumentProject<OfferData>({
id: "offer-of-admission",
name: "Offer of Admission",
version: "1.0.0",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import {
Conditions,
Fees,
Greeting,
NextSteps,
Offer,
SignOff,
TutorNote,
} from "./nodes/index.ts";
import type { OfferData } from "./types.ts";
export const documentTemplate = template<OfferData>(
<Document id="offer-of-admission" title="Offer of Admission">
<Section id="your-offer" title="Your offer">
<Greeting />
<Offer />
<TutorNote />
</Section>
<Section id="conditions" title="Conditions">
<Conditions />
</Section>
<Section id="fees-and-funding" title="Fees and funding">
<Fees />
</Section>
<Section id="next-steps" title="Next steps">
<NextSteps />
<SignOff />
</Section>
</Document>,
); import type { OfferData } from "./types.ts";
export const previewData: OfferData = {
applicantName: "Maya Oyelaran",
programme: "MEng Structural Engineering",
college: "Ashcroft College",
startDate: "28 September 2026",
offerRef: "ADM-2026-4417",
conditions: [
"grades A*AA at A-level, including Mathematics and Physics",
"IELTS 7.0 overall, with no component below 6.5",
"a satisfactory enhanced DBS check",
],
replyBy: "15 May 2026",
portfolioTheme: "timber gridshell roofs and low-carbon structural systems",
interviewer: "Dr Priya Raman",
tuitionFee: "£9,535",
feeStatus: "Home",
scholarship: { name: "Ashcroft Engineering Scholarship", amount: "£3,000 per year" },
signatory: { name: "Dr Priya Raman", title: "Director of Undergraduate Admissions" },
}; export interface OfferData {
applicantName: string;
programme: string;
college: string;
startDate: string;
offerRef: string;
conditions: string[];
replyBy: string;
/** Context for the tutor's note — never printed verbatim. */
portfolioTheme: string;
interviewer: string;
tuitionFee: string;
feeStatus: "Home" | "International";
scholarship?: { name: string; amount: string };
signatory: { name: string; title: string };
} import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
/**
* An absent optional field should change the sentence, not print "undefined"
* or leave a blank line.
*/
export const Access = paragraph<RepairsData>({
id: "access",
render: (data) =>
data.accessNotes
? `We hold the following access note for your home: ${data.accessNotes} ` +
`Please let us know if this is out of date.`
: `Someone aged 18 or over needs to be home for the visit. If that is ` +
`not possible, call us and we will arrange access another way.`,
}); import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const Appointment = paragraph<RepairsData>({
id: "appointment",
render: (data) =>
`We have booked a ${data.trade} to visit ${data.address} on ` +
`${data.visitDate}, ${data.visitWindow}. Your job reference is ` +
`${data.jobRef}.`,
}); import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const Greeting = paragraph<RepairsData>({
id: "greeting",
render: (data) => `Dear ${data.residentName},`,
}); export { Access } from "./access.node.ts";
export { Appointment } from "./appointment.node.ts";
export { Greeting } from "./greeting.node.ts";
export { WhatToExpect } from "./what-to-expect.node.ts"; import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const WhatToExpect = paragraph<RepairsData>({
id: "what-to-expect",
placeholder: (data) => `What to expect during a ${data.trade}'s visit.`,
generalPrompt: (data) =>
`In three short sentences, explain what a ${data.trade} will do during a ` +
`routine repair visit, how long it usually takes, and what the resident ` +
`should move out of the way beforehand.`,
systemPrompt: () =>
"You write for social housing residents. Use plain English and short sentences.",
negativePrompt: () => "Do not repeat the appointment date or the job reference.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { RepairsData } from "./types.ts";
export default defineDocumentProject<RepairsData>({
id: "repairs-appointment",
name: "Repair Appointment",
version: "2.1.0",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import { Access, Appointment, Greeting, WhatToExpect } from "./nodes/index.ts";
import type { RepairsData } from "./types.ts";
export const documentTemplate = template<RepairsData>(
<Document id="repairs-appointment" title="Your repair appointment">
<Section id="appointment" title="Your appointment">
<Greeting />
<Appointment />
</Section>
<Section id="on-the-day" title="On the day">
<WhatToExpect />
<Access />
</Section>
</Document>,
); import type { RepairsData } from "./types.ts";
export const previewData: RepairsData = {
residentName: "Tomasz Wójcik",
address: "12 Bracken Road, Leeds LS6 3QT",
jobRef: "RPR-88213",
trade: "plumber",
visitDate: "Tuesday 25 August",
visitWindow: "between 8am and 12pm",
accessNotes: "Key safe at the side gate; the code is with the scheme manager.",
}; export interface RepairsData {
residentName: string;
address: string;
jobRef: string;
trade: "plumber" | "electrician" | "joiner";
visitDate: string;
visitWindow: string;
accessNotes?: string;
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Greeting = paragraph<PolicyData>({
id: "greeting",
render: (data) => `Dear ${data.holderName},`,
}); export { Greeting } from "./greeting.node.ts";
export { PremiumChange } from "./premium-change.node.ts";
export { Renewal } from "./renewal.node.ts";
export { Shopping } from "./shopping.node.ts"; import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
/**
* A price rise is the sentence customers actually read. Computing the
* direction and the percentage locally means it can never contradict the
* figures printed beside it.
*/
export const PremiumChange = paragraph<PolicyData>({
id: "premium-change",
render: (data) => {
const delta = data.newPremium - data.lastPremium;
const percent = Math.abs((delta / data.lastPremium) * 100).toFixed(1);
if (Math.abs(delta) < 0.01) {
return `Your premium is unchanged at ${money(data.newPremium)} a year.`;
}
const direction = delta > 0 ? "risen" : "fallen";
return `Your premium has ${direction} by ${percent}%, from ` +
`${money(data.lastPremium)} to ${money(data.newPremium)} a year. ` +
`That works out at ${money(data.newPremium / 12)} a month.`;
},
});
function money(value: number): string {
return value.toLocaleString("en-GB", { style: "currency", currency: "GBP" });
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Renewal = paragraph<PolicyData>({
id: "renewal",
render: (data) =>
`Your ${data.cover.toLowerCase()} policy ${data.policyNumber} renews on ` +
`${data.renewalDate}. Unless you tell us otherwise, cover continues ` +
`automatically with an excess of ${money(data.excess)}.`,
});
function money(value: number): string {
return value.toLocaleString("en-GB", { style: "currency", currency: "GBP" });
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Shopping = paragraph<PolicyData>({
id: "shopping-around",
placeholder: () => "A short note on comparing this renewal with other quotes.",
generalPrompt: (data) =>
`Write two sentences reminding ${data.holderName} that they can compare ` +
`this renewal against other quotes, and that doing so will not affect ` +
`their existing cover.`,
systemPrompt: () =>
"You write regulated insurance correspondence. Be neutral and never discourage switching.",
negativePrompt: () => "Do not quote a price, a percentage, or a competitor name.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { PolicyData } from "./types.ts";
export default defineDocumentProject<PolicyData>({
id: "policy-renewal",
name: "Policy Renewal",
version: "0.4.2",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import { Greeting, PremiumChange, Renewal, Shopping } from "./nodes/index.ts";
import type { PolicyData } from "./types.ts";
export const documentTemplate = template<PolicyData>(
<Document id="policy-renewal" title="Your renewal">
<Section id="renewal" title="Your renewal">
<Greeting />
<Renewal />
<PremiumChange />
</Section>
<Section id="your-options" title="Your options">
<Shopping />
</Section>
</Document>,
); import type { PolicyData } from "./types.ts";
export const previewData: PolicyData = {
holderName: "Eleanor Whitfield",
policyNumber: "HC-2291-8840",
cover: "Contents and buildings",
renewalDate: "1 October 2026",
lastPremium: 284.4,
newPremium: 311.16,
excess: 250,
}; export interface PolicyData {
holderName: string;
policyNumber: string;
cover: "Contents" | "Buildings" | "Contents and buildings";
renewalDate: string;
lastPremium: number;
newPremium: number;
excess: number;
} Een echte documentpreview die live wordt gerenderd, geen screenshots en geen verborgen trucs.
Schrijf documenten zoals websites
Een document is een boom van getypeerde componenten. Als je ooit React hebt geschreven, ken je deze vorm al — props, compositie, kleine bestanden — dus een frontend-engineer is de eerste middag al productief in plaats van eerst een templatetaal te moeten leren.
Ontworpen voor AI
Gegenereerde tekst is een nodetype, geen aanbouwsel. Markeer de ene alinea die echt een model nodig heeft, geef die prompts en een placeholder, en laat de rest deterministisch — zo lost AI het moeilijke deel op zonder dat het hele document aan zijn genade is overgeleverd.
Documenten staan in je repository
Omdat een document broncode is, is het wijzigen van een zin een pull request — gedift, beoordeeld, en een jaar later nog steeds herleidbaar wanneer iemand vraagt wie de formulering over betalingsachterstanden heeft aangepast. Tests leggen vast dat een document rendert wat je verwacht, zodat CI de fout opmerkt vóór de ontvanger dat doet.
Docxcelerate behandelt een document zoals een UI-framework een scherm behandelt: kleine componenten, samengevoegd tot een boom, gerenderd door iets dat verstand heeft van papier. Jij krijgt het gemak van een componentmodel, en de ontvanger krijgt een Word-document.
De engine
In de engine worden documenten daadwerkelijk geschreven. Hij vult je gegevens in, voert de AI uit en geeft het voltooide document terug. Elk soort node kan AI gebruiken, niet alleen alinea's. Het antwoord van het model wordt ofwel de tekst, geschreven op basis van de informatie die je meegeeft, ofwel een beslissing waarvan het document afhangt.
Je publiceert een template één keer naar de engine. Daarna kan elk systeem zijn API aanroepen met een set gegevens en krijgt het een document terug. Er is een gratis engine die je zelf kunt hosten. De managed cloud draait de volledige versie, met veel dat de gratis versie niet heeft, en komt binnenkort.
- 01 Bouwen Het framework maakt van je document een pakket. Deze stap draait op je eigen machine.
- 02 Publiceren Je stuurt het pakket naar een engine. De engine slaat het op en geeft het een naam.
- 03 Schrijven Je applicatie stuurt een set gegevens. De engine geeft het voltooide document terug.
Lees het helemaal
Elk nodetype, elke CLI-vlag en elk bestand dat een build wegschrijft — met previews die door de echte renderer zijn gerenderd, zodat niets op de pagina een helper kan beschrijven die niet meer bestaat.