Generate Office documents (DOCX, XLSX, PDF, PPTX) with TypeScript. Pure JS libraries that work everywhere: Claude Code CLI, Cloudflare Workers, browsers. Uses docx (Word), xlsx/SheetJS (Excel), pdf-lib (PDF), pptxgenjs (PowerPoint). Use when: creating invoices, reports, spreadsheets, presentations, form filling, exporting data to Office formats, or troubleshooting "Packer.toBuffer", "XLSX.utils", "PDFDocument", "pptxgenjs" errors.
84
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
Status: Production Ready Last Updated: 2026-01-12 Dependencies: None (pure JavaScript libraries) Latest Versions: docx@9.5.0, xlsx@0.18.5, pdf-lib@1.17.1, pptxgenjs@4.0.1
Generate Microsoft Office documents and PDFs programmatically with TypeScript. All libraries are pure JavaScript with zero native dependencies, enabling universal runtime support:
| Format | Library | Workers | Browser | Node.js |
|---|---|---|---|---|
| DOCX | docx | ✅ | ✅ | ✅ |
| XLSX | xlsx (SheetJS) | ✅ | ✅ | ✅ |
pdf-lib | ✅ | ✅ | ✅ | |
| PPTX | pptxgenjs | ⚠️* | ✅ | ✅ |
*PPTX in Workers: Works for local images/data. Remote image fetching needs workaround (uses https module).
# Install all four (or pick what you need)
npm install docx xlsx pdf-lib pptxgenjsimport { Document, Packer, Paragraph, TextRun } from 'docx';
import { writeFileSync } from 'fs';
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new TextRun({ text: 'Hello World', bold: true, size: 48 }),
],
}),
],
}],
});
// Node.js: Save to file
const buffer = await Packer.toBuffer(doc);
writeFileSync('hello.docx', buffer);
// Browser/Workers: Get as blob
const blob = await Packer.toBlob(doc);import * as XLSX from 'xlsx';
// Create workbook with data
const data = [
['Name', 'Amount', 'Date'],
['Invoice #1', 1500, '2026-01-12'],
['Invoice #2', 2300, '2026-01-13'],
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Invoices');
// Node.js: Save to file
XLSX.writeFile(workbook, 'invoices.xlsx');
// Browser/Workers: Get as buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]); // Letter size
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText('Hello World', {
x: 50,
y: 700,
size: 24,
font,
color: rgb(0, 0, 0),
});
// Get as bytes (works everywhere)
const pdfBytes = await pdfDoc.save();
// Node.js: Save to file
writeFileSync('hello.pdf', pdfBytes);import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Your Name';
pptx.title = 'Sample Presentation';
// Add a slide
const slide = pptx.addSlide();
slide.addText('Hello World', {
x: 1, y: 1, w: 8, h: 1.5,
fontSize: 36, bold: true, color: '363636',
});
// Node.js: Save to file
await pptx.writeFile({ fileName: 'hello.pptx' });
// Browser: Trigger download
await pptx.writeFile({ fileName: 'hello.pptx' });The docx package uses a builder pattern:
import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx';
const doc = new Document({
sections: [{
children: [
new Paragraph({
text: 'Monthly Report',
heading: HeadingLevel.HEADING_1,
}),
new Paragraph({
children: [
new TextRun('This is a '),
new TextRun({ text: 'bold', bold: true }),
new TextRun(' and '),
new TextRun({ text: 'italic', italics: true }),
new TextRun(' text example.'),
],
}),
],
}],
});import { Document, Table, TableRow, TableCell, Paragraph, WidthType } from 'docx';
const table = new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Header 1')] }),
new TableCell({ children: [new Paragraph('Header 2')] }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Data 1')] }),
new TableCell({ children: [new Paragraph('Data 2')] }),
],
}),
],
});
const doc = new Document({
sections: [{ children: [table] }],
});import { Document, Paragraph, ImageRun } from 'docx';
import { readFileSync } from 'fs';
const imageBuffer = readFileSync('logo.png');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [
new ImageRun({
data: imageBuffer,
transformation: { width: 200, height: 100 },
type: 'png',
}),
],
}),
],
}],
});// Node.js - Save to file
import { writeFileSync } from 'fs';
const buffer = await Packer.toBuffer(doc);
writeFileSync('document.docx', buffer);
// Browser - Download
const blob = await Packer.toBlob(doc);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.docx';
a.click();
// Cloudflare Workers - Return as Response
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': 'attachment; filename="document.docx"',
},
});SheetJS (xlsx) uses utility functions:
import * as XLSX from 'xlsx';
const data = [
['Product', 'Price', 'Quantity', 'Total'],
['Widget A', 10, 5, { f: 'B2*C2' }], // Formula
['Widget B', 15, 3, { f: 'B3*C3' }],
['', '', 'Grand Total:', { f: 'SUM(D2:D3)' }],
];
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sales');const invoices = [
{ id: 1, customer: 'Acme Corp', amount: 1500, date: '2026-01-12' },
{ id: 2, customer: 'Globex', amount: 2300, date: '2026-01-13' },
];
const ws = XLSX.utils.json_to_sheet(invoices);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Invoices');// Set column widths (in characters)
ws['!cols'] = [
{ wch: 10 }, // Column A
{ wch: 20 }, // Column B
{ wch: 15 }, // Column C
];const wb = XLSX.utils.book_new();
const summaryData = [['Total Sales', 3800]];
const detailData = [['Item', 'Amount'], ['Item 1', 1500], ['Item 2', 2300]];
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summaryData), 'Summary');
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(detailData), 'Details');// Node.js - Save to file
XLSX.writeFile(workbook, 'report.xlsx');
// Browser/Workers - Get buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Cloudflare Workers - Return as Response
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="report.xlsx"',
},
});pdf-lib uses a page-based approach:
// Common sizes in points [width, height]
const LETTER = [612, 792]; // 8.5" x 11"
const A4 = [595.28, 841.89]; // 210mm x 297mm
const LEGAL = [612, 1008]; // 8.5" x 14"import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]);
// Embed standard fonts
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Draw text (y=0 is BOTTOM of page)
page.drawText('Invoice #12345', {
x: 50,
y: 750, // Near top of page
size: 24,
font: helveticaBold,
color: rgb(0, 0, 0),
});
page.drawText('Thank you for your business!', {
x: 50,
y: 50, // Near bottom of page
size: 12,
font: helvetica,
color: rgb(0.5, 0.5, 0.5),
});// Rectangle
page.drawRectangle({
x: 50,
y: 600,
width: 200,
height: 100,
borderColor: rgb(0, 0, 0),
borderWidth: 1,
color: rgb(0.95, 0.95, 0.95), // Fill color
});
// Line
page.drawLine({
start: { x: 50, y: 500 },
end: { x: 550, y: 500 },
thickness: 1,
color: rgb(0, 0, 0),
});// From URL/fetch
const imageBytes = await fetch('https://example.com/logo.png').then(r => r.arrayBuffer());
const image = await pdfDoc.embedPng(imageBytes);
// Or embedJpg for JPEG
// const image = await pdfDoc.embedJpg(imageBytes);
page.drawImage(image, {
x: 50,
y: 700,
width: 100,
height: 50,
});import { PDFDocument } from 'pdf-lib';
// Load existing PDF with form
const existingPdfBytes = await fetch('form.pdf').then(r => r.arrayBuffer());
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get form and fields
const form = pdfDoc.getForm();
const nameField = form.getTextField('customer_name');
const dateField = form.getTextField('date');
// Fill fields
nameField.setText('John Doe');
dateField.setText('2026-01-12');
// Flatten form (make fields non-editable)
form.flatten();
const pdfBytes = await pdfDoc.save();import { PDFDocument } from 'pdf-lib';
const pdf1Bytes = await fetch('doc1.pdf').then(r => r.arrayBuffer());
const pdf2Bytes = await fetch('doc2.pdf').then(r => r.arrayBuffer());
const mergedPdf = await PDFDocument.create();
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2 = await PDFDocument.load(pdf2Bytes);
// Copy all pages from both documents
const pages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
const pages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
pages1.forEach(page => mergedPdf.addPage(page));
pages2.forEach(page => mergedPdf.addPage(page));
const mergedBytes = await mergedPdf.save();pptxgenjs uses a slide-based approach:
import pptxgen from 'pptxgenjs';
const pptx = new pptxgen();
pptx.author = 'Author Name';
pptx.title = 'Presentation Title';
pptx.subject = 'Subject';
pptx.company = 'Company';
// Title slide
const titleSlide = pptx.addSlide();
titleSlide.addText('Quarterly Report', {
x: 0.5, y: 2, w: 9, h: 1,
fontSize: 44, bold: true, color: '0066CC', align: 'center',
});
titleSlide.addText('Q1 2026', {
x: 0.5, y: 3.5, w: 9, h: 0.5,
fontSize: 24, color: '666666', align: 'center',
});const contentSlide = pptx.addSlide();
// Title
contentSlide.addText('Key Highlights', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 32, bold: true, color: '333333',
});
// Bullet points
contentSlide.addText([
{ text: 'Revenue up 25% YoY', options: { bullet: true, fontSize: 20 } },
{ text: 'Customer base grew to 10,000', options: { bullet: true, fontSize: 20 } },
{ text: 'New product launch successful', options: { bullet: true, fontSize: 20 } },
{ text: 'Expanded to 5 new markets', options: { bullet: true, fontSize: 20 } },
], { x: 0.5, y: 1.5, w: 8, h: 4, valign: 'top' });const tableSlide = pptx.addSlide();
tableSlide.addText('Sales Summary', {
x: 0.5, y: 0.5, w: 9, h: 0.8,
fontSize: 28, bold: true,
});
const tableData = [
[{ text: 'Region', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q1', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },
{ text: 'Q2', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } }],
['North America', '$2.5M', '$2.8M'],
['Europe', '$1.8M', '$2.1M'],
['Asia Pacific', '$1.2M', '$1.5M'],
];
tableSlide.addTable(tableData, {
x: 0.5, y: 1.5, w: 9,
border: { pt: 1, color: 'CCCCCC' },
fontFace: 'Arial',
fontSize: 14,
align: 'center',
valign: 'middle',
});const chartSlide = pptx.addSlide();
chartSlide.addText('Revenue Trend', {
x: 0.5, y: 0.5, w: 9, h: 0.6,
fontSize: 28, bold: true,
});
chartSlide.addChart(pptx.ChartType.line, [
{ name: 'Revenue', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [100, 120, 150, 180] },
{ name: 'Expenses', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [80, 85, 90, 95] },
], {
x: 0.5, y: 1.2, w: 9, h: 4,
showLegend: true,
legendPos: 'b',
showTitle: false,
});import { readFileSync } from 'fs';
const imageSlide = pptx.addSlide();
// From file (Node.js)
imageSlide.addImage({
path: 'logo.png',
x: 0.5, y: 0.5, w: 2, h: 1,
});
// From base64
const imageBase64 = readFileSync('chart.png').toString('base64');
imageSlide.addImage({
data: `image/png;base64,${imageBase64}`,
x: 0.5, y: 2, w: 4, h: 3,
});
// From URL (Node.js only - uses https module)
imageSlide.addImage({
path: 'https://example.com/image.png',
x: 5, y: 2, w: 4, h: 3,
});const shapeSlide = pptx.addSlide();
// Rectangle
shapeSlide.addShape(pptx.ShapeType.rect, {
x: 0.5, y: 0.5, w: 3, h: 2,
fill: { color: '0066CC' },
line: { color: '004499', pt: 2 },
});
// Circle/Oval
shapeSlide.addShape(pptx.ShapeType.ellipse, {
x: 4, y: 0.5, w: 2, h: 2,
fill: { color: '00AA00' },
});
// Arrow
shapeSlide.addShape(pptx.ShapeType.rightArrow, {
x: 1, y: 3, w: 3, h: 1,
fill: { color: 'FF6600' },
});// Define a master slide
pptx.defineSlideMaster({
title: 'COMPANY_MASTER',
background: { color: 'FFFFFF' },
objects: [
{ text: { text: 'Company Name', options: { x: 0.5, y: 0.2, w: 4, h: 0.3, fontSize: 10, color: '999999' } } },
{ line: { x: 0.5, y: 0.6, w: 9, h: 0, line: { color: '0066CC', pt: 2 } } },
],
});
// Use the master
const slide = pptx.addSlide({ masterName: 'COMPANY_MASTER' });// Node.js - Save to file
await pptx.writeFile({ fileName: 'presentation.pptx' });
// Browser - Trigger download
await pptx.writeFile({ fileName: 'presentation.pptx' });
// Get as base64 (for email, API, etc.)
const base64 = await pptx.write({ outputType: 'base64' });
// Get as Blob (browser)
const blob = await pptx.write({ outputType: 'blob' });
// Get as ArrayBuffer
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
// Cloudflare Workers - Return as Response
const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });
return new Response(arrayBuffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'Content-Disposition': 'attachment; filename="presentation.pptx"',
},
});All four libraries work in Cloudflare Workers (PPTX with caveats for remote images).
import { Hono } from 'hono';
import { Document, Packer, Paragraph, TextRun } from 'docx';
const app = new Hono();
app.get('/api/invoice/:id', async (c) => {
const invoiceId = c.req.param('id');
const doc = new Document({
sections: [{
children: [
new Paragraph({
children: [new TextRun({ text: `Invoice #${invoiceId}`, bold: true, size: 48 })],
}),
],
}],
});
const buffer = await Packer.toBuffer(doc);
return new Response(buffer, {
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename="invoice-${invoiceId}.docx"`,
},
});
});
export default app;For complex layouts with CSS, use Cloudflare Browser Rendering (paid feature):
import puppeteer from '@cloudflare/puppeteer';
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
// Set HTML content
await page.setContent(`
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>Service</td><td>$500</td></tr>
</table>
</body>
</html>
`);
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
},
});
},
};wrangler.jsonc binding:
{
"browser": {
"binding": "BROWSER"
}
}✅ Use await Packer.toBuffer() for DOCX (it's async)
✅ Remember PDF coordinates start at BOTTOM-LEFT
✅ Use type: 'buffer' for XLSX in Workers/browser
✅ Embed fonts in PDF before using them
✅ Set proper Content-Type headers for downloads
✅ Use await pptx.writeFile() or await pptx.write() for PPTX
✅ Use base64 images in PPTX for Workers (avoid remote URLs)
❌ Use Packer.toBuffer() without await (returns Promise)
❌ Assume PDF y=0 is at top (it's at bottom)
❌ Use writeFile in Workers (use Response instead)
❌ Forget to set Content-Disposition for downloads
❌ Use Node.js fs module in browser/Workers
❌ Use PPTX path URLs in Workers (https module not available)
Error: [object Promise] in file or empty document
Why: Packer.toBuffer() is async but called without await
Prevention: Always await Packer.toBuffer(doc)
Error: Text appears at bottom of page or off-page Why: PDF coordinates have origin at bottom-left, not top-left Prevention: For text near top, use high y values (e.g., y=750 for letter size)
Error: Cell shows formula text instead of result Why: SheetJS doesn't execute formulas, Excel does on open Prevention: This is expected - formulas calculate when opened in Excel
Error: Downloaded file is empty or corrupted Why: Returning wrong type or missing Content-Type header Prevention: Return buffer directly with proper headers
Error: https is not defined or image not appearing
Why: pptxgenjs uses Node.js https module for remote images
Prevention: Use base64 data URIs or local images in Workers environment
docx-basic.ts - Complete Word document with headings, tables, imagesxlsx-basic.ts - Excel workbook with formulas and formattingpdf-basic.ts - PDF with text, images, shapespptx-basic.ts - PowerPoint with slides, charts, tablesworkers-pdf.ts - Cloudflare Workers PDF generation exampledocx-api.md - Quick reference for docx npm packagexlsx-api.md - Quick reference for SheetJS functionspdf-lib-api.md - Quick reference for pdf-lib methodspptxgenjs-api.md - Quick reference for pptxgenjsverify-deps.sh - Check library versions are currentThis skill focuses on document creation with portable TypeScript libraries:
| Feature | This Skill | Anthropic's Skills |
|---|---|---|
| Create DOCX/XLSX/PDF/PPTX | ✅ | ✅ |
| Works in Cloudflare Workers | ✅ | ❌ |
| Plugin installable | ✅ | ❌ |
| TypeScript-first | ✅ | ❌ (Python) |
| Edit existing DOCX with tracked changes | ❌ | ✅ |
| Excel formula validation | ❌ | ✅ |
| OCR scanned PDFs | ❌ | ✅ |
For advanced editing scenarios (tracked changes, formula validation), consider Anthropic's official skills with Python tooling.
{
"dependencies": {
"docx": "^9.5.0",
"xlsx": "^0.18.5",
"pdf-lib": "^1.17.1",
"pptxgenjs": "^4.0.1"
}
}npm install docx xlsx pdf-lib)fa91c34
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.