Documentos como componentes.
DOCX como resultado.
Compón documentos a partir de componentes pequeños y tipados, con el JSX que ya escribes. Aprovecha las funciones de IA integradas para crear documentos dinámicos y escala la generación de documentos con nuestro motor.
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;
} Una vista previa real del documento renderizándose, sin capturas de pantalla ni trucos ocultos.
Escribe documentos como sitios web
Un documento es un árbol de componentes tipados. Si has escrito React, ya conoces esta forma — props, composición, archivos pequeños — así que una persona con perfil de frontend es productiva la primera tarde en lugar de tener que aprender antes un lenguaje de plantillas.
Diseñado para la IA
El texto generado es un tipo de nodo, no un añadido. Marca el único párrafo que de verdad necesita un modelo, dale prompts y un marcador de posición, y deja el resto determinista — así la IA resuelve la parte difícil sin poner todo el documento a su merced.
Los documentos viven en tu repositorio
Como un documento es código fuente, cambiar una frase es un pull request — con su diff, revisado, y todavía atribuible un año después cuando alguien pregunte quién modificó la redacción sobre los impagos. Las pruebas afirman que un documento genera lo que esperas, así que la CI detecta el error antes que quien lo recibe.
Docxcelerate trata un documento como un framework de interfaz trata una pantalla: componentes pequeños, compuestos en un árbol, renderizados por algo que entiende de papel. Tú obtienes la ergonomía de un modelo de componentes, y quien lo recibe obtiene un documento de Word.
El motor
El motor es donde los documentos se escriben de verdad. Rellena tus datos, ejecuta la IA y devuelve el documento terminado. Cualquier tipo de nodo puede usar IA, no solo los párrafos. La respuesta del modelo o bien se convierte en el texto, escrito a partir de la información que le das, o bien toma una decisión de la que depende el documento.
Publicas una plantilla en el motor una sola vez. A partir de ahí, cualquier sistema puede llamar a su API con un conjunto de datos y recibir un documento. Hay un motor gratuito que puedes alojar tú. La nube gestionada ejecuta el completo, con mucho que la versión gratuita no tiene, y llegará pronto.
- 01 Compilar El framework convierte tu documento en un paquete. Este paso se ejecuta en tu máquina.
- 02 Publicar Envías el paquete a un motor. El motor lo guarda y le da un nombre.
- 03 Escribir Tu aplicación envía un conjunto de datos. El motor devuelve el documento terminado.
Léelo entero
Cada tipo de nodo, cada flag de la CLI y cada archivo que escribe una compilación — con vistas previas generadas por el renderizador real, de modo que nada en la página pueda describir un helper que ya no existe.