Q3 Report Example

A 5-page Q3 operations report that measures its own table of contents, keeps the chart with its commentary, and returns the signature box coordinates — all in one API call.

Covers: auto-generated TOC, layout healing, coordinate extraction, and a 3-attempt testCase.

What the document contains

  • A cover page.
  • A table of contents with real page numbers.
  • An executive summary with KPI cards, a revenue/expense chart, and commentary.
  • A financial audit table with totals.
  • A compliance page with a signature block.

What breaks without healing

  • The chart stays on page 3 while the commentary is pushed alone onto page 4.
  • The TOC renders placeholder dashes instead of page numbers.
  • Nothing verifies that the TOC numbers or the signature box are on the final pages.

The three checks

Attempt What the test case finds Returned value
1 #kpi-chart and #commentary are on different pages { patch: { compactKpis: true } }
2 The layout is healed, but the TOC still shows — { patch: { toc: [...] } }
3 Every TOC number matches its heading; chart and commentary share a page { success: true, signature: {...}, totals: {...} }

The order matters: the layout patch runs first, then the TOC is measured against the settled pagination. If you measure the TOC before the compact patch, the page numbers are stale.

Template highlights

TOC rows with fixed height

<div class="toc-row">
  <span class="toc-num">{% if loop.index < 10 %}0{% endif %}{{ loop.index }}</span>
  <span class="toc-row-title">{{ section.title }}</span>
  <span class="toc-dots"></span>
  <span class="toc-page-num">{% if toc and toc[loop.index0] %}{{ toc[loop.index0].page }}{% else %}—{% endif %}</span>
</div>

The row height is fixed, so filling in the page numbers does not change pagination. That is why the TOC can be measured once after the layout settles.

KPI cards, chart, and commentary

<section class="section exec-section{% if compactKpis %} compact{% endif %}">
  <div class="kpi-grid">...</div>
  <div class="chart-card" id="kpi-chart">...</div>
  <div class="commentary" id="commentary">...</div>
</section>

The compact class reduces the KPI and chart heights when the test returns { compactKpis: true }. That frees enough room for the commentary to stay on page 3.

Signature block

<div id="signature-box" class="signature-box">
  <div class="sig-name">{{ signature.name }}</div>
  <div class="sig-title">{{ signature.title }}</div>
  <div class="sig-date">Signed: {{ signature.date }}</div>
</div>

The element is queryable after rendering. Its pages array and geometry are returned in the API response for e-signature placement.

The full test case

This is the exact script sent to the API. It is synchronous, plain JavaScript, and runs in the V8 sandbox.


const exec = doc.querySelector('.exec-section');
const chart = doc.querySelector('#kpi-chart');
const commentary = doc.querySelector('#commentary');

if (!exec || !chart || !commentary) {
  return { success: false, reason: 'Required executive summary elements are missing' };
}

function textOf(node) {
  if (!node) return '';
  if (node.innerText) return node.innerText;
  if (node.textChild && node.textChild.text_content) return node.textChild.text_content;
  return node.text_content || '';
}

const isCompact = (exec.attributes['class'] || '').indexOf('compact') !== -1;

// 1. The chart and its commentary must share a page.
if (chart.pages[0] !== commentary.pages[0]) {
  if (!isCompact) {
    return {
      reason: 'Chart and commentary split across pages',
      patch: { compactKpis: true }
    };
  }
  return {
    success: false,
    reason: 'Chart still split after the compact patch'
  };
}

// 2. Fill the TOC only after the layout has settled.
const tocNums = doc.querySelectorAll('.toc-page-num');
const headings = doc.querySelectorAll('h2.toc-heading');
const tocReady = tocNums.length > 0 && textOf(tocNums[0]).trim() !== '\u2014';

if (!tocReady) {
  const entries = [];
  for (let i = 0; i < headings.length; i++) {
    entries.push({
      title: textOf(headings[i]).trim(),
      page: headings[i].pages[0] + 1
    });
  }
  return {
    reason: 'TOC placeholders still present',
    patch: { toc: entries }
  };
}

// 3. Verify every TOC number against the final layout.
if (tocNums.length !== headings.length) {
  return { success: false, reason: 'TOC rows do not match section headings' };
}

const toc = [];
for (let i = 0; i < headings.length; i++) {
  const actual = headings[i].pages[0] + 1;
  const shown = parseInt(textOf(tocNums[i]).trim(), 10);
  if (shown !== actual) {
    return {
      success: false,
      reason: 'TOC mismatch for ' + textOf(headings[i]).trim() + ': shown ' + shown + ', actual ' + actual
    };
  }
  toc.push({ title: textOf(headings[i]).trim(), page: actual });
}

// 4. Return coordinates for downstream placement.
const signature = doc.querySelector('#signature-box');
const totals = doc.querySelector('#grand-total');

if (!signature || !totals) {
  return { success: false, reason: 'Signature or totals block is missing' };
}

return {
  success: true,
  toc: toc,
  signature: {
    page: signature.pages[0] + 1,
    x: Math.round(signature.geometry.x * 100) / 100,
    y: Math.round(signature.geometry.y * 100) / 100,
    width: Math.round(signature.geometry.width * 100) / 100,
    height: Math.round(signature.geometry.height * 100) / 100
  },
  totals: {
    page: totals.pages[0] + 1,
    x: Math.round(totals.geometry.x * 100) / 100,
    y: Math.round(totals.geometry.y * 100) / 100
  }
};

Generate the report

Save the template as a dashboard template, or pass it as htmlContent with the data object. The example uses spatial: true so the PDF and the layout map are returned together.

cURL

curl -X POST "https://api.forkpdf.com/v1/generate-pdf" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-project-id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "YOUR_TEMPLATE_ID",
    "format": "A4",
    "margins": { "top": "18mm", "right": "16mm", "bottom": "18mm", "left": "16mm" },
    "spatial": true,
    "testCase": "PASTE_THE_TEST_CASE_ABOVE"
  }'

SDK

const fs = require('fs');
const { ForkPDFClient } = require('forkpdf');
const data = require('./data');

// Paste the test case from above here.
const TEST_CASE = `...`;

const client = new ForkPDFClient(
  process.env.FORKPDF_API_KEY,
  process.env.FORKPDF_PROJECT_ID
);

const result = await client.generate({
  htmlContent: fs.readFileSync('./report.html', 'utf8'),
  data,
  format: 'A4',
  margins: { top: '18mm', right: '16mm', bottom: '18mm', left: '16mm' },
  spatial: true,
  unzip: true,
  outputDir: './output',
  testCase: TEST_CASE
});

console.log(result.testResults);

Result

The successful response includes the TOC entries, the signature coordinates, the totals block, and the number of attempts:

{
  "success": true,
  "toc": [
    { "title": "Executive Summary", "page": 3 },
    { "title": "Financial Audit", "page": 4 },
    { "title": "Compliance & Sign-off", "page": 5 }
  ],
  "signature": {
    "page": 5,
    "x": 338.86,
    "y": 622.34,
    "width": 199.4,
    "height": 70.47
  },
  "totals": {
    "page": 4,
    "x": 326.11,
    "y": 319.48
  },
  "total_attempts": 3
}

Output

Before / after: the unhealed page 3 has the chart with no commentary; the healed page 3 keeps them together.

Before and after comparison of the Q3 report executive summary page

TOC: the page numbers are measured after the layout settles.

Q3 report table of contents with resolved page numbers 3, 4, and 5

Signature coordinates: the red box is the return value of #signature-box.geometry.

Signature box outlined with its returned PDF coordinates

Why the TOC is measured second

Filling the TOC with page numbers does not change pagination because the rows are fixed height. But the earlier compactKpis patch does change pagination. The test case therefore heals the layout first and measures the headings on the next attempt. If you reverse the order, the TOC may say page 4 while the section moved to page 3.

Gotchas

  • pages is 0-indexed. Add 1 when displaying a page number; heading.pages[0] + 1 is the TOC value.
  • geometry is in PDF points, not CSS pixels. Use printableWidth/printableHeight for page math, not pageWidth/pageHeight.
  • On element nodes, text_content is often empty. Use innerText for the full string; textChild.text_content is only the first text node.
  • The layout patch must run before the TOC measurement.
  • Fixed-height TOC rows keep pagination stable when the numbers are filled in.

Download the example

Download the full source (.zip)

Run it locally

# from the unzipped example folder
npm install
cp .env.example .env
# add your API key and project ID
npm start

The source script writes output/report-healed.pdf, output/report-broken.pdf, output/data.pb, and output/results.json.