# DOCX Generation

> Word document generation and manipulation expertise for creating, editing, and templating professional DOCX files programmatically. Use when building.

- Skill: `yigityildiz0/docx-generation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add yigityildiz0/docx-generation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yigityildiz0/docx-generation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: yigityildiz0 (https://skillmd.com/u/yigityildiz0)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yigityildiz0/docx-generation

---


# DOCX Generation

Structured guidance for programmatic Word document creation, manipulation, and templating. Covers library selection, document structure fundamentals, template-based generation, JavaScript alternatives, professional design patterns, advanced formatting, mail merge pipelines, and testing strategies for document output.

## When to Use This Skill

Use this skill for:

- Building automated report generators that output Word documents
- Creating template-driven contracts, proposals, or invoices
- Implementing mail merge pipelines that produce personalized documents from data sources
- Generating batch documents (certificates, letters, compliance reports) from structured data
- Manipulating existing DOCX files to insert content, update styles, or extract data
- Converting Markdown, HTML, or other formats to professionally styled Word documents
- Building document generation microservices or CLI tools

**Trigger phrases**: "docx", "Word document", "python-docx", "docxtpl", "mail merge", "document template", "report generator", "contract automation", "Word template", "officegen", "OpenXML", "document builder", "batch documents", "DOCX manipulation", "document generation"

## What This Skill Does

Provides document generation patterns including:

- **Library Selection**: Decision matrix for python-docx, docxtpl, Pandoc, officegen, docx (npm), and OpenXML SDK
- **Python Fundamentals**: Document creation with python-docx covering paragraphs, runs, styles, tables, images, and sections
- **Template-Based Generation**: Jinja2-powered DOCX templating with docxtpl for loops, conditionals, images, and subdocuments
- **JavaScript Generation**: Node.js DOCX creation with the docx library covering typed paragraph builders, tables, and headers/footers
- **Design Patterns**: Cover pages, table of contents, headers/footers, page numbering, watermarks, and style hierarchies
- **Advanced Formatting**: Custom styles, theme colors, paragraph spacing, character formatting, section breaks, columns, and footnotes
- **Mail Merge**: Data-driven batch generation with variable substitution, conditional sections, and multi-document output
- **Testing**: Content extraction, style verification, cross-platform rendering validation, and document comparison

## Instructions

### Step 1: Library Selection

Choosing the right DOCX library depends on your language ecosystem, whether you need template-based or programmatic generation, and the complexity of your formatting requirements.

**Decision Matrix**:

| Library | Language | Approach | Strengths | Limitations |
|---------|----------|----------|-----------|-------------|
| python-docx | Python | Programmatic | Full control, styles, images, tables | No template support, verbose for complex layouts |
| docxtpl | Python | Template | Jinja2 in DOCX, designer-friendly | Requires python-docx, limited to template patterns |
| Pandoc | CLI/Any | Conversion | Markdown/HTML to DOCX, reference docs | External binary, limited fine-grained control |
| docx (npm) | Node.js | Programmatic | TypeScript types, declarative API | Steeper learning curve, newer ecosystem |
| officegen | Node.js | Programmatic | Simple API, quick prototyping | Unmaintained, limited style support |
| OpenXML SDK | C#/.NET | Programmatic | Full OOXML access, enterprise standard | Verbose, requires OOXML specification knowledge |

**When to Use Each**:

- **python-docx**: You need full programmatic control and are in a Python stack. Best for custom document builders where every element is data-driven.
- **docxtpl**: You have a Word template designed by a non-developer and need to fill it with data. Best for report generation, contracts, and invoices where layout is fixed but content varies.
- **Pandoc**: You already have content in Markdown, HTML, or reStructuredText and need to produce styled DOCX output. Best for documentation pipelines and static site generators.
- **docx (npm)**: You are in a Node.js/TypeScript stack and need programmatic generation with type safety. Best for serverless document generation APIs.
- **officegen**: You need a quick prototype in Node.js with minimal setup. Not recommended for production due to maintenance status.
- **OpenXML SDK**: You are in a .NET enterprise environment and need full OOXML specification compliance. Best for complex enterprise document workflows.

**Installation Commands**:

```bash
# Python: python-docx (programmatic generation)
pip install python-docx

# Python: docxtpl (template-based generation, includes python-docx)
pip install docxtpl

# Python: both together for hybrid workflows
pip install python-docx docxtpl Pillow

# Node.js: docx (TypeScript-first programmatic generation)
npm install docx

# Node.js: officegen (legacy, quick prototyping only)
npm install officegen

# CLI: Pandoc (Markdown/HTML to DOCX conversion)
# macOS
brew install pandoc
# Ubuntu/Debian
sudo apt-get install pandoc
# Windows
choco install pandoc

# .NET: OpenXML SDK
dotnet add package DocumentFormat.OpenXml
```

**Hybrid Approach**: For many production systems, the best strategy combines docxtpl for layout-heavy documents (where a designer creates the Word template) with python-docx for fully dynamic documents (where structure itself varies based on data). Use Pandoc as a preprocessing step when source content is in Markdown.

### Step 2: Python python-docx Fundamentals

python-docx provides full programmatic control over Word document creation. Understanding the document object model is essential for building reliable generators.

**Document Object Model**:

```
Document
├── Sections (page layout, orientation, margins)
│   ├── Header
│   └── Footer
├── Paragraphs
│   ├── Runs (text fragments with formatting)
│   └── Paragraph Format (alignment, spacing, indentation)
├── Tables
│   ├── Rows
│   │   └── Cells
│   │       └── Paragraphs (cells contain paragraphs, not raw text)
│   └── Table Style
└── Inline Shapes (images embedded in paragraphs)
```

**Core Document Creation**:

```python
from docx import Document
from docx.shared import Inches, Pt, Cm, Emu, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.section import WD_ORIENT

def create_report(title: str, author: str, content: list[dict]) -> Document:
    """Create a structured report document.

    Args:
        title: Report title for the cover page and headers.
        author: Author name for the document properties.
        content: List of section dicts with 'heading', 'level', and 'body' keys.

    Returns:
        A Document object ready to be saved.
    """
    doc = Document()

    # Set document properties
    doc.core_properties.title = title
    doc.core_properties.author = author

    # Configure default section (first section always exists)
    section = doc.sections[0]
    section.page_width = Inches(8.5)
    section.page_height = Inches(11)
    section.top_margin = Inches(1)
    section.bottom_margin = Inches(1)
    section.left_margin = Inches(1.25)
    section.right_margin = Inches(1.25)

    # Title
    title_para = doc.add_heading(title, level=0)
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # Author line
    author_para = doc.add_paragraph()
    author_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = author_para.add_run(f"Prepared by: {author}")
    run.font.size = Pt(12)
    run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)

    doc.add_page_break()

    # Content sections
    for section_data in content:
        heading_level = section_data.get("level", 1)
        doc.add_heading(section_data["heading"], level=heading_level)

        body = section_data.get("body", "")
        if isinstance(body, str):
            doc.add_paragraph(body)
        elif isinstance(body, list):
            for paragraph_text in body:
                doc.add_paragraph(paragraph_text)

    return doc
```

**Working with Paragraphs and Runs**:

```python
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH

def add_formatted_paragraph(
    doc: Document,
    text_segments: list[dict],
    alignment: int = WD_ALIGN_PARAGRAPH.LEFT,
    space_before: Pt | None = None,
    space_after: Pt | None = None,
    line_spacing: float | None = None,
) -> None:
    """Add a paragraph with mixed formatting using multiple runs.

    Each segment dict has keys: 'text', and optional 'bold', 'italic',
    'underline', 'font_size', 'font_name', 'color' (hex string like 'FF0000').
    """
    para = doc.add_paragraph()
    para.alignment = alignment

    if space_before is not None:
        para.paragraph_format.space_before = space_before
    if space_after is not None:
        para.paragraph_format.space_after = space_after
    if line_spacing is not None:
        para.paragraph_format.line_spacing = line_spacing

    for segment in text_segments:
        run = para.add_run(segment["text"])
        run.bold = segment.get("bold", False)
        run.italic = segment.get("italic", False)
        run.underline = segment.get("underline", False)

        if "font_size" in segment:
            run.font.size = Pt(segment["font_size"])
        if "font_name" in segment:
            run.font.name = segment["font_name"]
        if "color" in segment:
            hex_color = segment["color"].lstrip("#")
            run.font.color.rgb = RGBColor(
                int(hex_color[0:2], 16),
                int(hex_color[2:4], 16),
                int(hex_color[4:6], 16),
            )


# Usage
doc = Document()
add_formatted_paragraph(doc, [
    {"text": "Important: ", "bold": True, "color": "CC0000", "font_size": 12},
    {"text": "This report contains ", "font_size": 12},
    {"text": "confidential", "italic": True, "underline": True, "font_size": 12},
    {"text": " information.", "font_size": 12},
])
```

**Tables with Merged Cells and Styling**:

```python
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Cm
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def add_data_table(
    doc: Document,
    headers: list[str],
    rows: list[list[str]],
    col_widths: list[float] | None = None,
    header_bg_color: str = "2B579A",
    header_text_color: str = "FFFFFF",
    stripe_color: str = "F2F2F2",
) -> None:
    """Add a formatted data table with header styling and row striping.

    Args:
        doc: Target document.
        headers: Column header labels.
        rows: List of row data (each row is a list of cell strings).
        col_widths: Column widths in inches (optional).
        header_bg_color: Hex color for header row background.
        header_text_color: Hex color for header row text.
        stripe_color: Hex color for alternating row backgrounds.
    """
    table = doc.add_table(rows=1 + len(rows), cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = True

    # Set column widths if provided
    if col_widths:
        for i, width in enumerate(col_widths):
            for row in table.rows:
                row.cells[i].width = Inches(width)

    # Style header row
    header_row = table.rows[0]
    for i, header_text in enumerate(headers):
        cell = header_row.cells[i]
        cell.text = ""
        para = cell.paragraphs[0]
        para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = para.add_run(header_text)
        run.bold = True
        run.font.size = Pt(10)
        run.font.color.rgb = RGBColor(
            int(header_text_color[0:2], 16),
            int(header_text_color[2:4], 16),
            int(header_text_color[4:6], 16),
        )
        _set_cell_shading(cell, header_bg_color)

    # Populate data rows with alternating stripe
    for row_idx, row_data in enumerate(rows):
        row = table.rows[row_idx + 1]
        for col_idx, cell_text in enumerate(row_data):
            cell = row.cells[col_idx]
            cell.text = cell_text
            cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT
            for run in cell.paragraphs[0].runs:
                run.font.size = Pt(9)
        if row_idx % 2 == 1:
            for cell in row.cells:
                _set_cell_shading(cell, stripe_color)


def _set_cell_shading(cell, hex_color: str) -> None:
    """Apply background shading to a table cell."""
    shading = OxmlElement("w:shd")
    shading.set(qn("w:fill"), hex_color)
    shading.set(qn("w:val"), "clear")
    cell._tc.get_or_add_tcPr().append(shading)
```

**Adding Images**:

```python
from docx import Document
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from pathlib import Path
from io import BytesIO

def add_image_with_caption(
    doc: Document,
    image_path: str | Path | BytesIO,
    caption: str,
    width: float = 5.0,
    alignment: int = WD_ALIGN_PARAGRAPH.CENTER,
) -> None:
    """Add an image with a centered caption below it.

    Args:
        doc: Target document.
        image_path: File path or BytesIO stream for the image.
        caption: Caption text displayed below the image.
        width: Image width in inches.
        alignment: Paragraph alignment for both image and caption.
    """
    # Image paragraph
    img_para = doc.add_paragraph()
    img_para.alignment = alignment
    run = img_para.add_run()
    run.add_picture(str(image_path) if isinstance(image_path, Path) else image_path, width=Inches(width))

    # Caption paragraph
    caption_para = doc.add_paragraph()
    caption_para.alignment = alignment
    caption_run = caption_para.add_run(caption)
    caption_run.italic = True
    caption_run.font.size = Pt(9)
    caption_run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
```

**Critical Rules for python-docx**:

- Always create runs explicitly when you need formatting control. `paragraph.text = "..."` creates a single run that loses any existing formatting
- Table cells contain paragraphs, not raw text. Access `cell.paragraphs[0]` to format cell content
- Images are inline shapes attached to runs, not paragraphs. Create a run first, then call `run.add_picture()`
- python-docx does not support generating a Table of Contents natively. You must insert the TOC field code and the TOC is populated when the document is opened in Word
- Saving to the same file that is open in Word will raise a `PermissionError`. Always use a temporary file or ensure the document is closed

### Step 3: Template-Based Generation with docxtpl

docxtpl combines python-docx with Jinja2 templating to fill Word templates with data. This approach is ideal when a designer creates the document layout in Word and developers populate it programmatically.

**Template Syntax in Word Documents**:

Place Jinja2 tags directly in your Word document (`.docx` file opened in Word or LibreOffice):

```
# Simple variable substitution
{{ company_name }}

# Loop over a list (use {%tr for table rows, {%p for paragraphs)
{%tr for item in line_items %}
{{ item.description }}    {{ item.quantity }}    {{ item.price }}
{%tr endfor %}

# Conditional sections
{%p if include_disclaimer %}
This document contains confidential information...
{%p endif %}

# Filters
{{ amount | currency }}
{{ date | format_date }}

# Rich text (preserves formatting from the context)
{{ executive_summary | richtext }}
```

**Basic Template Rendering**:

```python
from docxtpl import DocxTemplate, RichText, InlineImage
from docx.shared import Mm, Pt
from pathlib import Path
from datetime import date

def render_contract(
    template_path: str | Path,
    output_path: str | Path,
    context: dict,
) -> Path:
    """Render a contract document from a Word template.

    Args:
        template_path: Path to the .docx template file.
        output_path: Path where the rendered document will be saved.
        context: Dictionary of variables to inject into the template.

    Returns:
        Path to the saved document.
    """
    tpl = DocxTemplate(str(template_path))
    tpl.render(context)
    output = Path(output_path)
    tpl.save(str(output))
    return output


# Usage
context = {
    "company_name": "Acme Corporation",
    "client_name": "Widget Industries",
    "contract_date": date.today().strftime("%B %d, %Y"),
    "contract_number": "CTR-2026-0042",
    "effective_date": "April 1, 2026",
    "termination_date": "March 31, 2027",
    "line_items": [
        {"description": "Consulting Services", "quantity": 120, "unit": "hours", "rate": 250.00, "total": 30000.00},
        {"description": "Software License", "quantity": 1, "unit": "annual", "rate": 12000.00, "total": 12000.00},
        {"description": "Support Package", "quantity": 12, "unit": "months", "rate": 500.00, "total": 6000.00},
    ],
    "grand_total": 48000.00,
    "payment_terms": "Net 30",
    "include_nda_clause": True,
    "include_sla_appendix": False,
}

render_contract("templates/contract_template.docx", "output/contract_CTR-2026-0042.docx", context)
```

**Rich Text and Inline Images**:

```python
from docxtpl import DocxTemplate, RichText, InlineImage
from docx.shared import Mm, Pt, RGBColor

def build_rich_context(tpl: DocxTemplate, data: dict) -> dict:
    """Build a context dictionary with rich text and inline images.

    Rich text allows mixing fonts, colors, and styles within a single
    template variable. Inline images are sized and positioned within
    the document flow.
    """
    # Rich text with mixed formatting
    summary = RichText()
    summary.add("Status: ", bold=True, font="Calibri", size=Pt(11))
    if data["status"] == "approved":
        summary.add("APPROVED", bold=True, color=RGBColor(0x00, 0x80, 0x00), size=Pt(11))
    else:
        summary.add("PENDING", bold=True, color=RGBColor(0xFF, 0x80, 0x00), size=Pt(11))
    summary.add(f" on {data['status_date']}", size=Pt(11))

    # Inline image from file
    logo = InlineImage(tpl, str(data["logo_path"]), width=Mm(30))

    # Inline image from bytes (useful for chart images generated at runtime)
    chart = None
    if data.get("chart_bytes"):
        from io import BytesIO
        chart = InlineImage(tpl, BytesIO(data["chart_bytes"]), width=Mm(120))

    return {
        "status_summary": summary,
        "company_logo": logo,
        "performance_chart": chart,
        **data,
    }
```

**Custom Jinja2 Filters**:

```python
from docxtpl import DocxTemplate
from jinja2 import Environment
from decimal import Decimal
from datetime import date, datetime

def register_custom_filters(tpl: DocxTemplate) -> None:
    """Register custom Jinja2 filters for document templates.

    Filters transform variable values during rendering. Register them
    on the template's Jinja2 environment before calling render().
    """
    env: Environment = tpl.jinja_env

    def currency_filter(value: float | Decimal, symbol: str = "$", decimals: int = 2) -> str:
        formatted = f"{float(value):,.{decimals}f}"
        return f"{symbol}{formatted}"

    def date_filter(value: date | datetime | str, fmt: str = "%B %d, %Y") -> str:
        if isinstance(value, str):
            value = datetime.fromisoformat(value)
        return value.strftime(fmt)

    def percentage_filter(value: float, decimals: int = 1) -> str:
        return f"{value:.{decimals}f}%"

    def title_case_filter(value: str) -> str:
        return value.title()

    env.filters["currency"] = currency_filter
    env.filters["format_date"] = date_filter
    env.filters["percentage"] = percentage_filter
    env.filters["title_case"] = title_case_filter


# Usage in template: {{ grand_total | currency }}  ->  $48,000.00
# Usage in template: {{ contract_date | format_date }}  ->  March 26, 2026
```

**Subdocuments (Composing Multiple Templates)**:

```python
from docxtpl import DocxTemplate
from pathlib import Path

def render_composite_document(
    master_template: str | Path,
    subdoc_templates: list[dict],
    global_context: dict,
    output_path: str | Path,
) -> Path:
    """Render a master document that includes subdocuments.

    Each subdocument is a separate .docx template rendered with its own
    context and inserted into the master template at a placeholder.

    Args:
        master_template: Path to the master .docx template.
        subdoc_templates: List of dicts with 'placeholder', 'template_path', and 'context'.
        global_context: Variables shared across all templates.
        output_path: Path for the final rendered document.
    """
    tpl = DocxTemplate(str(master_template))

    context = dict(global_context)
    for subdoc_info in subdoc_templates:
        sub = tpl.new_subdoc(str(subdoc_info["template_path"]))
        context[subdoc_info["placeholder"]] = sub

    tpl.render(context)
    output = Path(output_path)
    tpl.save(str(output))
    return output


# Master template contains: {{ appendix_a }}
# This inserts the entire rendered subdocument at that position
render_composite_document(
    master_template="templates/main_report.docx",
    subdoc_templates=[
        {
            "placeholder": "appendix_a",
            "template_path": "templates/appendix_technical.docx",
            "context": {"findings": technical_findings},
        },
        {
            "placeholder": "appendix_b",
            "template_path": "templates/appendix_financial.docx",
            "context": {"budget_data": budget_rows},
        },
    ],
    global_context={"report_title": "Annual Review 2026", "author": "Compliance Team"},
    output_path="output/annual_review_2026.docx",
)
```

**Critical Rules for docxtpl**:

- Use `{%tr ... %}` for table row loops and `{%p ... %}` for paragraph-level loops. Using `{% ... %}` without the `tr` or `p` prefix breaks the XML structure
- Never place two Jinja2 tags in the same Word run if they span structural boundaries (paragraphs, table rows). Each tag should be its own text run in the template
- Rich text variables must be declared as `RichText` objects in the context. Plain strings passed to a `{{ var | richtext }}` filter will fail
- Test templates with edge cases: empty lists (loops produce no output), `None` values (use `{{ var | default("N/A") }}`), and very long strings (may overflow table cells)
- Subdocuments inherit the master document's styles. If the subdocument template uses custom styles not present in the master, those styles will be lost

### Step 4: JavaScript DOCX Generation

The `docx` npm package provides a TypeScript-first declarative API for building Word documents in Node.js. It uses a builder pattern where you compose document elements as nested objects.

**Core Document Structure**:

```typescript
import {
  Document,
  Packer,
  Paragraph,
  TextRun,
  HeadingLevel,
  AlignmentType,
  Table,
  TableRow,
  TableCell,
  WidthType,
  BorderStyle,
  Header,
  Footer,
  PageNumber,
  NumberFormat,
  ImageRun,
  ShadingType,
  convertInchesToTwip,
  Tab,
  TabStopPosition,
  TabStopType,
} from "docx";
import * as fs from "fs";
import * as path from "path";

interface ReportSection {
  heading: string;
  level: (typeof HeadingLevel)[keyof typeof HeadingLevel];
  paragraphs: string[];
}

function createReport(
  title: string,
  author: string,
  sections: ReportSection[],
): Document {
  const children: Paragraph[] = [];

  // Title
  children.push(
    new Paragraph({
      text: title,
      heading: HeadingLevel.TITLE,
      alignment: AlignmentType.CENTER,
      spacing: { after: 200 },
    }),
  );

  // Author line
  children.push(
    new Paragraph({
      alignment: AlignmentType.CENTER,
      spacing: { after: 400 },
      children: [
        new TextRun({
          text: `Prepared by: ${author}`,
          size: 24, // half-points: 24 = 12pt
          color: "666666",
          font: "Calibri",
        }),
      ],
    }),
  );

  // Sections
  for (const section of sections) {
    children.push(
      new Paragraph({
        text: section.heading,
        heading: section.level,
        spacing: { before: 240, after: 120 },
      }),
    );
    for (const text of section.paragraphs) {
      children.push(
        new Paragraph({
          children: [
            new TextRun({
              text,
              size: 22, // 11pt
              font: "Calibri",
            }),
          ],
          spacing: { after: 120 },
        }),
      );
    }
  }

  return new Document({
    creator: author,
    title,
    sections: [
      {
        properties: {
          page: {
            margin: {
              top: convertInchesToTwip(1),
              bottom: convertInchesToTwip(1),
              left: convertInchesToTwip(1.25),
              right: convertInchesToTwip(1.25),
            },
          },
        },
        headers: {
          default: new Header({
            children: [
              new Paragraph({
                alignment: AlignmentType.RIGHT,
                children: [
                  new TextRun({
                    text: title,
                    italics: true,
                    size: 18,
                    color: "999999",
                  }),
                ],
              }),
            ],
          }),
        },
        footers: {
          default: new Footer({
            children: [
              new Paragraph({
                alignment: AlignmentType.CENTER,
                children: [
                  new TextRun({ text: "Page ", size: 18 }),
                  new TextRun({
                    children: [PageNumber.CURRENT],
                    size: 18,
                  }),
                  new TextRun({ text: " of ", size: 18 }),
                  new TextRun({
                    children: [PageNumber.TOTAL_PAGES],
                    size: 18,
                  }),
                ],
              }),
            ],
          }),
        },
        children,
      },
    ],
  });
}

// Save to file
async function saveDocument(doc: Document, filePath: string): Promise<void> {
  const buffer = await Packer.toBuffer(doc);
  fs.writeFileSync(filePath, buffer);
}
```

**Tables in the docx npm Package**:

```typescript
import {
  Table,
  TableRow,
  TableCell,
  Paragraph,
  TextRun,
  WidthType,
  AlignmentType,
  ShadingType,
  BorderStyle,
  convertInchesToTwip,
} from "docx";

interface TableData {
  headers: string[];
  rows: string[][];
}

function createStyledTable(data: TableData): Table {
  const headerCells = data.headers.map(
    (text) =>
      new TableCell({
        children: [
          new Paragraph({
            alignment: AlignmentType.CENTER,
            children: [
              new TextRun({
                text,
                bold: true,
                color: "FFFFFF",
                size: 20,
                font: "Calibri",
              }),
            ],
          }),
        ],
        shading: { fill: "2B579A", type: ShadingType.CLEAR },
        width: { size: 100 / data.headers.length, type: WidthType.PERCENTAGE },
      }),
  );

  const dataRows = data.rows.map(
    (row, rowIdx) =>
      new TableRow({
        children: row.map(
          (cellText) =>
            new TableCell({
              children: [
                new Paragraph({
                  children: [
                    new TextRun({
                      text: cellText,
                      size: 18,
                      font: "Calibri",
                    }),
                  ],
                }),
              ],
              shading:
                rowIdx % 2 === 1
                  ? { fill: "F2F2F2", type: ShadingType.CLEAR }
                  : undefined,
            }),
        ),
      }),
  );

  return new Table({
    rows: [new TableRow({ children: headerCells }), ...dataRows],
    width: { size: 100, type: WidthType.PERCENTAGE },
  });
}
```

**Generating DOCX in Serverless / Express Endpoints**:

```typescript
import express from "express";
import { Document, Packer, Paragraph, HeadingLevel } from "docx";

const app = express();
app.use(express.json());

app.post("/api/generate-report", async (req, res) => {
  const { title, sections } = req.body;

  const doc = new Document({
    sections: [
      {
        children: [
          new Paragraph({ text: title, heading: HeadingLevel.HEADING_1 }),
          ...sections.map(
            (s: { text: string }) => new Paragraph({ text: s.text }),
          ),
        ],
      },
    ],
  });

  const buffer = await Packer.toBuffer(doc);

  res.setHeader(
    "Content-Type",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  );
  res.setHeader("Content-Disposition", `attachment; filename="${title}.docx"`);
  res.send(Buffer.from(buffer));
});
```

**Critical Rules for JavaScript DOCX Generation**:

- Sizes in the docx npm package are in half-points (not points). A 12pt font is `size: 24`
- Use `convertInchesToTwip()` for margins and dimensions. One inch is 1440 twips
- The `Packer.toBuffer()` method is async. Always `await` it
- Table cells must contain at least one `Paragraph`. Empty cells cause invalid documents
- Images require the file bytes passed as a `Buffer` to `ImageRun`, not a file path

### Step 5: Document Design Patterns

Professional documents follow consistent design conventions. These patterns apply regardless of which library you use.

**Cover Page Pattern**:

```python
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from datetime import date

def add_cover_page(
    doc: Document,
    title: str,
    subtitle: str,
    organization: str,
    author: str,
    doc_date: date | None = None,
    logo_path: str | None = None,
    version: str | None = None,
) -> None:
    """Add a professional cover page to the document.

    The cover page uses the first section and adds a page break after.
    """
    if doc_date is None:
        doc_date = date.today()

    # Logo (top center)
    if logo_path:
        logo_para = doc.add_paragraph()
        logo_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = logo_para.add_run()
        run.add_picture(logo_path, width=Inches(2))

    # Spacer
    for _ in range(4):
        doc.add_paragraph()

    # Title
    title_para = doc.add_paragraph()
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title_run = title_para.add_run(title)
    title_run.bold = True
    title_run.font.size = Pt(28)
    title_run.font.color.rgb = RGBColor(0x1A, 0x1A, 0x2E)
    title_run.font.name = "Calibri Light"

    # Subtitle
    subtitle_para = doc.add_paragraph()
    subtitle_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    sub_run = subtitle_para.add_run(subtitle)
    sub_run.font.size = Pt(16)
    sub_run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
    sub_run.font.name = "Calibri"

    # Spacer
    for _ in range(6):
        doc.add_paragraph()

    # Metadata block
    meta_lines = [
        organization,
        f"Prepared by: {author}",
        doc_date.strftime("%B %d, %Y"),
    ]
    if version:
        meta_lines.append(f"Version: {version}")

    for line in meta_lines:
        meta_para = doc.add_paragraph()
        meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        meta_run = meta_para.add_run(line)
        meta_run.font.size = Pt(11)
        meta_run.font.color.rgb = RGBColor(0x44, 0x44, 0x44)

    doc.add_page_break()
```

**Table of Contents Field Code**:

```python
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def add_table_of_contents(doc: Document, title: str = "Table of Contents") -> None:
    """Insert a Table of Contents field code.

    The TOC is a field code that Word evaluates when the document is opened.
    It will not display content in python-docx or PDF converters; it requires
    Word or LibreOffice to update field codes on open.
    """
    doc.add_heading(title, level=1)

    paragraph = doc.add_paragraph()
    run = paragraph.add_run()

    # Begin field
    fld_char_begin = OxmlElement("w:fldChar")
    fld_char_begin.set(qn("w:fldCharType"), "begin")
    run._r.append(fld_char_begin)

    # Field instruction: TOC with heading levels 1-3, hyperlinks
    instr_text = OxmlElement("w:instrText")
    instr_text.set(qn("xml:space"), "preserve")
    instr_text.text = r' TOC \o "1-3" \h \z \u '
    run._r.append(instr_text)

    # Separate
    fld_char_separate = OxmlElement("w:fldChar")
    fld_char_separate.set(qn("w:fldCharType"), "separate")
    run._r.append(fld_char_separate)

    # Placeholder text (replaced when Word updates fields)
    placeholder = OxmlElement("w:t")
    placeholder.text = "Right-click and select 'Update Field' to generate Table of Contents"
    run._r.append(placeholder)

    # End field
    fld_char_end = OxmlElement("w:fldChar")
    fld_char_end.set(qn("w:fldCharType"), "end")
    run._r.append(fld_char_end)

    doc.add_page_break()
```

**Headers and Footers with Page Numbers**:

```python
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def configure_headers_footers(
    doc: Document,
    header_text: str,
    footer_text: str | None = None,
    show_page_numbers: bool = True,
    different_first_page: bool = True,
) -> None:
    """Configure headers and footers for all sections.

    Args:
        doc: Target document.
        header_text: Text displayed in the header (right-aligned).
        footer_text: Optional left-aligned footer text.
        show_page_numbers: Whether to show 'Page X of Y' in the footer.
        different_first_page: If True, the first page has no header/footer
            (useful when the first page is a cover page).
    """
    for section in doc.sections:
        section.different_first_page_header_footer = different_first_page

        # Default header (all pages except first if different_first_page is True)
        header = section.header
        header.is_linked_to_previous = False
        header_para = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
        header_para.text = ""
        header_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
        run = header_para.add_run(header_text)
        run.italic = True
        run.font.size = Pt(9)
        run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)

        # Default footer
        footer = section.footer
        footer.is_linked_to_previous = False
        footer_para = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
        footer_para.text = ""

        if footer_text:
            left_run = footer_para.add_run(footer_text)
            left_run.font.size = Pt(8)
            left_run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)

        if show_page_numbers:
            footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
            _add_page_number_field(footer_para)


def _add_page_number_field(paragraph) -> None:
    """Insert 'Page X of Y' using Word field codes."""
    run = paragraph.add_run()
    run.font.size = Pt(8)
    run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)

    run2 = paragraph.add_run("Page ")
    run2.font.size = Pt(8)

    # Current page number field
    fld_begin = OxmlElement("w:fldChar")
    fld_begin.set(qn("w:fldCharType"), "begin")
    run3 = paragraph.add_run()
    run3._r.append(fld_begin)

    instr = OxmlElement("w:instrText")
    instr.text = " PAGE "
    run4 = paragraph.add_run()
    run4._r.append(instr)

    fld_sep = OxmlElement("w:fldChar")
    fld_sep.set(qn("w:fldCharType"), "separate")
    run5 = paragraph.add_run()
    run5._r.append(fld_sep)

    fld_end = OxmlElement("w:fldChar")
    fld_end.set(qn("w:fldCharType"), "end")
    run6 = paragraph.add_run()
    run6._r.append(fld_end)

    run7 = paragraph.add_run(" of ")
    run7.font.size = Pt(8)

    # Total pages field
    fld_begin2 = OxmlElement("w:fldChar")
    fld_begin2.set(qn("w:fldCharType"), "begin")
    run8 = paragraph.add_run()
    run8._r.append(fld_begin2)

    instr2 = OxmlElement("w:instrText")
    instr2.text = " NUMPAGES "
    run9 = paragraph.add_run()
    run9._r.append(instr2)

    fld_sep2 = OxmlElement("w:fldChar")
    fld_sep2.set(qn("w:fldCharType"), "separate")
    run10 = paragraph.add_run()
    run10._r.append(fld_sep2)

    fld_end2 = OxmlElement("w:fldChar")
    fld_end2.set(qn("w:fldCharType"), "end")
    run11 = paragraph.add_run()
    run11._r.append(fld_end2)
```

**Watermark Pattern**:

```python
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def add_watermark(doc: Document, text: str = "DRAFT", color: str = "C0C0C0") -> None:
    """Add a diagonal text watermark to all pages.

    Watermarks are implemented as VML shapes in the default header.
    This works in Word and most DOCX renderers.
    """
    for section in doc.sections:
        header = section.header
        header.is_linked_to_previous = False
        paragraph = header.paragraphs[0] if header.paragraphs else header.add_paragraph()

        # VML shape for watermark
        pict = OxmlElement("w:pict")
        shape = OxmlElement("v:shape")
        shape.set("id", "watermark")
        shape.set("style", (
            "position:absolute;margin-left:0;margin-top:0;"
            "width:500pt;height:200pt;rotation:315;"
            "z-index:-251657216;mso-position-horizontal:center;"
            "mso-position-vertical:center;"
            "mso-position-horizontal-relative:margin;"
            "mso-position-vertical-relative:margin"
        ))
        shape.set("fillcolor", f"#{color}")
        shape.set("stroked", "f")

        textpath = OxmlElement("v:textpath")
        textpath.set("string", text)
        textpath.set("style", "font-family:Calibri;font-size:1pt")
        shape.append(textpath)
        pict.append(shape)

        run = paragraph.add_run()
        run._r.append(pict)
```

**Style Hierarchy**: Word documents have a three-level style hierarchy. Document defaults define the base font and paragraph formatting for the entire document. Named styles (Heading 1, Normal, etc.) inherit from document defaults and can override any property. Direct formatting (applied via runs and paragraph format objects) overrides named styles. Best practice is to define named styles for repeatable formatting and minimize direct formatting, which makes documents easier to maintain and restyle.

### Step 6: Advanced Formatting

Complex documents require fine-grained control over styles, spacing, columns, and structural elements beyond basic paragraphs and tables.

**Custom Style Definitions**:

```python
from docx import Document
from docx.shared import Pt, RGBColor, Inches, Cm
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.text import WD_ALIGN_PARAGRAPH

def define_custom_styles(doc: Document) -> None:
    """Define a set of custom styles for consistent document formatting.

    Custom styles are added to the document's style catalog and can be
    applied by name to any paragraph or run.
    """
    styles = doc.styles

    # Body text style
    body_style = styles.add_style("Custom Body", WD_STYLE_TYPE.PARAGRAPH)
    body_style.base_style = styles["Normal"]
    body_style.font.name = "Calibri"
    body_style.font.size = Pt(11)
    body_style.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
    body_style.paragraph_format.space_after = Pt(6)
    body_style.paragraph_format.space_before = Pt(0)
    body_style.paragraph_format.line_spacing = 1.15

    # Callout / highlight box style
    callout_style = styles.add_style("Callout", WD_STYLE_TYPE.PARAGRAPH)
    callout_style.base_style = styles["Normal"]
    callout_style.font.name = "Calibri"
    callout_style.font.size = Pt(10)
    callout_style.font.italic = True
    callout_style.font.color.rgb = RGBColor(0x1A, 0x5B, 0x9C)
    callout_style.paragraph_format.left_indent = Inches(0.5)
    callout_style.paragraph_format.space_before = Pt(12)
    callout_style.paragraph_format.space_after = Pt(12)

    # Code block style (monospace)
    code_style = styles.add_style

…(truncated)
