How to Fix linkToDestination Not Working in pdfmake
Fix pdfmake internal links by matching linkToDestination to an id, using the right version syntax, and isolating viewer or layout problems.
Use linkToDestination on the clickable text object and put the identical string in id on the destination node. In pdfmake 0.3.x, this is the smallest working pattern:
const docDefinition = {
content: [
{
text: 'Go to Header',
linkToDestination: 'header'
},
{
text: 'Header content',
id: 'header'
}
]
};
The two values must match exactly, including capitalization and punctuation. Also verify that your installed pdfmake version matches the documentation you are using. The current links page is for 0.3.x, while pdfmake publishes separate documentation for older 0.1.x/0.2.x releases. See the official 0.3.x links documentation and the versioned documentation landing page.
Minimal runnable Node.js example
Install pdfmake, save this as internal-link.js, and run it with Node.js:
npm install pdfmake
const pdfmake = require('pdfmake');
const docDefinition = {
content: [
{
text: 'Jump to the header',
linkToDestination: 'header',
color: 'blue',
decoration: 'underline'
},
{ text: '\\nIntroductory paragraph.' },
{
text: 'Header content',
id: 'header',
style: 'heading'
},
{ text: 'The destination is the node carrying id: header.' }
],
styles: {
heading: { fontSize: 18, bold: true, margin: [0, 20, 0, 8] }
}
};
pdfmake.createPdf(docDefinition).write('internal-link.pdf').then(() => {
console.log('Wrote internal-link.pdf');
}).catch((error) => {
console.error(error);
process.exitCode = 1;
});
pdfmake is a JavaScript PDF-generation library that runs in browsers and on the server. Its 0.3.x server-side methods return promises; consult the server-side methods documentation for the output method appropriate to your application.
How the three link properties differ
| Property | Value | Target | Typical use |
|---|---|---|---|
link |
String URL | External website or resource | Open an HTTPS page |
linkToPage |
Number | PDF page number | Jump to a known page |
linkToDestination |
String | Node whose id matches |
Jump to a named heading or section |
Do not replace a destination name with a page number. Use linkToPage for a page reference and linkToDestination for a named in-document destination. The property type for linkToDestination is a string.
Step-by-step repair checklist
- Put the property on the clickable object. The object needs a
textvalue andlinkToDestination. - Put
idon the destination content. Add it to the text, stack, column, table cell content, or other content node that should receive focus. - Match the strings exactly.
header,Header, andheader-1are different identifiers. - Check the installed version. Run
npm list pdfmakeand compare the result with the documentation branch you copied. - Generate a minimal PDF. Remove tables, page breaks, images, custom fonts, and dynamic content until only one link and one destination remain.
- Open the output in another PDF viewer. A viewer may expose or hide navigation behavior differently; testing another viewer helps separate document-generation problems from display problems.
Common document shapes
Link to a heading
{
text: 'Chapter 2',
linkToDestination: 'chapter-2'
},
{
text: 'Chapter 2: Configuration',
id: 'chapter-2',
style: 'heading'
}
Build a table of contents from data
const sections = [
{ id: 'intro', label: 'Introduction', body: 'Start here.' },
{ id: 'api', label: 'API reference', body: 'Reference material.' }
];
const toc = sections.map(section => ({
text: section.label,
linkToDestination: section.id,
color: 'blue',
margin: [0, 2, 0, 2]
}));
const content = [
{ text: 'Contents', style: 'heading' },
...toc,
...sections.flatMap(section => [
{ text: section.label, id: section.id, style: 'heading', pageBreak: 'before' },
{ text: section.body }
])
];
const docDefinition = { content, styles: { heading: { fontSize: 18, bold: true } } };
Generate the destination nodes with the same IDs used to build the links. Keep IDs stable when content is reordered; a named destination is more resilient than a hard-coded page number.
Link from a text fragment
{
text: [
'Read the ',
{ text: 'appendix', linkToDestination: 'appendix' },
' for details.'
]
},
{
text: 'Appendix',
id: 'appendix',
style: 'heading'
}
Why a link can appear broken
| Symptom | Likely cause | Fix |
|---|---|---|
| Clicking does nothing | The destination string and id differ |
Copy the same string to both properties and check case, spaces, and punctuation. |
| External URL opens, internal link does not | link was used where a named destination was intended |
Use linkToDestination: 'name' and id: 'name'. |
| Link jumps to the wrong place | The ID is attached to a different content node than expected, or the ID is reused | Move id to the intended destination node and make IDs unique. |
| Code copied from a different guide fails | Installed package and documentation branch do not match | Check the package version and use its matching pdfmake documentation. |
| Minimal example works but full document fails | Complex layout, dynamic data, page breaks, or a malformed node obscures the problem | Add document sections back one at a time until the failing structure is identified. |
| Generated PDF behaves differently between viewers | Viewer navigation behavior differs | Compare the same file in another viewer and retain a minimal reproducible file. |
| Build fails before a PDF is produced | Runtime, import, font, or bundler configuration error rather than a link-definition error | Fix the build error first, then test the two-node link example independently. |
Version checks: 0.3.x versus older documentation
The syntax above follows the documented 0.3.x page. pdfmake keeps separate 0.1.x/0.2.x and 0.3.x documentation, so a copied example can be wrong for the package actually installed. Check both your lockfile and runtime package:
npm list pdfmake
node -p "require('pdfmake/package.json').version"
If your project uses a browser bundle, confirm that the bundle and its font setup come from the same release line. The official client-side guide documents the browser files and versioned CDN paths.
Edge cases to review
- Duplicate IDs: use one unique ID per destination. Reusing an ID makes the target ambiguous.
- Whitespace and punctuation: treat IDs as exact strings. Avoid generating one form for links and another for headings.
- Dynamic sections: derive both the link and target from one data field so they cannot drift.
- Page breaks: named destinations avoid calculating page numbers, but the final destination still depends on where the node is laid out.
- Nested content: if an ID on a deeply nested structure behaves unexpectedly, move it temporarily to a simple text node and retest.
- SVG: the changelog separately records support for link properties in SVG in the 0.3.0 beta series. Do not assume SVG behavior is identical to the documented text-object example; test that structure separately.
Performance, reliability, and maintenance
- Named destinations avoid a preprocessing pass that calculates page numbers, which is useful when sections move as text changes.
- Generate a small smoke-test PDF in CI containing one internal link and one target after dependency upgrades.
- Keep a reproducible document definition, package version, runtime (browser or Node.js), and viewer name when reporting a failure.
- Do not infer a universal viewer or browser cause from one failure. The official documentation defines the object shape but does not provide an exhaustive compatibility matrix.
- For large documents, build the link and target objects from the same normalized section list and validate that every link ID has exactly one target before generation.
Or skip the browser setup
If your actual task is capturing a web page as a PDF or image rather than generating a PDF document with pdfmake, ScreenshotNeo provides a single-request screenshot and PDF API. It accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing result in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Read the ScreenshotNeo API documentation for the complete option list. A direct PDF request can be made with the same endpoint:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -d format=pdf -o page.pdf
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"},
timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('page.pdf', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Does linkToDestination take a page number?
No. It takes a destination-name string. Use linkToPage when you intentionally want a numeric page reference.
Can the target be a heading style?
Yes. The target needs an id; styling such as style: 'heading' is independent.
Should IDs include a leading hash?
No hash is required by the documented syntax. Use the same plain string on linkToDestination and id.
What should I include in a bug report?
Include the pdfmake version, browser or Node.js runtime, minimal document definition, generated PDF, and the viewer where the behavior occurs.
Is a successful external link proof that internal links are configured correctly?
No. External link, page-number linkToPage, and named-destination linkToDestination are separate properties and should be tested independently.


