Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If a PDF table-of-contents link lands several pages away from its heading, check the fragment target and wait for Paged.js pagination to finish before calling Puppeteer’s page.pdf(). If the HTML preview is correct but the exported PDF destination is still offset, compare the exact Puppeteer/Chromium pair: reports describe this symptom in Chromium, but the available reports do not establish one universally affected or fixed version. Visible page numbers and clickable PDF destinations are separate things to verify.

First distinguish the two kinds of “wrong page”

A cross-reference can be wrong in two different ways:

  • The printed page number is wrong. Text such as “see page 12” is generated during pagination. Paged.js uses the destination element’s fragment identifier to determine its page number.
  • The clickable link lands in the wrong place. The PDF contains a link destination whose position may differ from the expected heading, even when the HTML link works.

Test both. A correct printed number does not prove that the PDF’s clickable destination is correct, and a working HTML link does not prove Chromium exported the destination at the right coordinates. Reports exist of Next.js, Paged.js, and Puppeteer PDFs whose internal links jumped ahead, as well as Puppeteer reports of anchors landing about 1.5 pages after the expected section. Those are individual reports, not a measured frequency or a universal offset: the exact-stack report and Puppeteer issue #12869.

How Paged.js resolves a page cross-reference

For a page reference, the link needs a fragment target in the same document. The target’s id must be unique, and the link’s href must point to that exact ID. Paged.js’s cross-reference function uses target-counter() to generate the page number where the matching element appears; its documentation also notes that a missing, out-of-document, or not-yet-loaded target can yield 0 for the counter or no text for target-text(). See Paged.js Cross References.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
  • Create a mix using audio, music and voice tracks and recordings.
  • Customize your tracks with amazing effects and helpful editing tools.
  • Use tools like the Beat Maker and Midi Creator.
  • Work efficiently by using Bookmarks and tools like Effect Chain, which allow you to apply multiple effects at a time
  • Use one of the many other NCH multimedia applications that are integrated with MixPad.

A minimal pattern is:

<nav aria-label="Contents">
  <a class="toc-link" href="#installation">Installation</a>
</nav>

<h2 id="installation">Installation</h2>
.toc-link::after {
  content: ", page " target-counter(attr(href url), page);
}

The selector and exact styling are yours to adapt; the important relationship is the matching in-document fragment and unique destination ID. For generated heading links, derive the TOC fragment and heading ID from the same stable value rather than building them separately. Avoid duplicate IDs, stray whitespace, and links whose fragment has been encoded or transformed differently from the actual ID.

Paged.js generates paginated content before Puppeteer prints it. Puppeteer’s Page.pdf() is the final print operation and uses print CSS media. The division of responsibility matters: debug target and pagination logic in Paged.js first, then inspect Chromium’s PDF output if the layout is correct but destinations remain displaced. See Paged.js Generated Content and Puppeteer PDF generation.

Repair the document before changing browser versions

  1. Audit every TOC link. For each href="#some-id", confirm that the rendered document contains exactly one element with id="some-id". Check case, whitespace, encoding, and duplicate IDs. Do this against the final rendered document, not only a source template.
  2. Keep the target in the current document. Paged.js page cross-references are for fragments in that document. A missing fragment or a link to another document cannot supply the current document’s destination page.
  3. Wait until layout-affecting work is done. Do not print while Next.js is still rendering the page, Paged.js is still fragmenting it, or fonts and other layout-affecting assets are still loading. A target that has not loaded when Paged.js resolves the cross-reference can produce an empty value or zero.
  4. Print only after an explicit readiness signal. Arrange for your application to set a marker after its Paged.js rendering process has completed, and have Puppeteer wait for it. The marker below is application-defined; connect it to the completion point used by your installed Paged.js integration rather than assuming a particular undocumented event name.
  5. Re-test visible numbers and clickable destinations separately. Open the resulting PDF in a viewer, check the displayed cross-reference number, then click the link and verify where it lands.

Use an explicit ready marker in the Puppeteer export

The following example shows the print-side contract: the application sets window.__PAGED_READY__ only after rendering and required assets are ready; Puppeteer waits for that marker before saving the PDF. Replace the URL and make sure your page actually sets the marker. If the marker never appears, fail the export rather than silently producing a potentially premature PDF.

// export-pdf.mjs
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/report', {
    waitUntil: 'networkidle0',
  });

  await page.waitForFunction(
    () => window.__PAGED_READY__ === true,
    { timeout: 30000 },
  );

  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
  });
} finally {
  await browser.close();
}

networkidle0 is a navigation wait condition, not proof that Paged.js finished pagination. Likewise, a fixed sleep can be too short on a slow run and unnecessarily long on a fast one. The explicit marker is useful only if it reflects the completion condition in your application. If fonts matter to your layout, ensure your readiness logic accounts for them before setting the marker; a late font swap can change line breaks and pagination.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #3
Corel PDF Fusion Software
  • Save money by using PDF Fusion to view over 100 file formats without having to purchase additional software
  • Merge incompatible files quickly and easily by dragging and dropping in PDF Fusion to create a new PDF documents
  • Save time with PDF Fusion's editing tools to reuse the content from existing documents without starting from scratch

For a fast link-integrity check, run this in the browser after the final DOM has been assembled. It reports missing destinations and IDs used more than once. It does not validate Paged.js page calculation or PDF link coordinates.

const audit = await page.evaluate(() => {
  const ids = [...document.querySelectorAll('[id]')].map(el => el.id);
  const counts = new Map();
  for (const id of ids) counts.set(id, (counts.get(id) ?? 0) + 1);

  const links = [...document.querySelectorAll('a[href]')];
  const missing = links
    .map(a => a.getAttribute('href'))
    .filter(href => href?.startsWith('#'))
    .filter(href => !document.getElementById(decodeURIComponent(href.slice(1))));

  const duplicates = [...counts]
    .filter(([, count]) => count > 1)
    .map(([id, count]) => ({ id, count }));

  return { missing, duplicates };
});

console.log(audit);

Use this as a diagnostic rather than a complete URL parser: if your application deliberately uses unusual fragment encoding, verify its matching rules against the browser’s actual resolved links. A clean audit establishes only that the inspected anchors resolve uniquely in that DOM. It cannot show whether Paged.js completed pagination or whether the exported PDF’s destinations are correctly placed.

Rank #4
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
  • Transform audio playing via your speakers and headphones
  • Improve sound quality by adjusting it with effects
  • Take control over the sound playing through audio hardware

When the HTML is right but PDF links are still displaced

Once IDs, fragments, and readiness are correct, compare browser environments. Record the Puppeteer version and the exact Chromium executable used for each export. Generate the same small fixture with the deployed pair and another known-good pair. If the symptom occurs with only one pair, pin, upgrade, or roll back that pair while investigating the browser issue. The exact-stack report attributes its symptom to Chromium, but the cited reports do not establish a universally fixed Chromium release or a guaranteed version workaround. Do not present a version change as a fix until your own fixture confirms it.

Keep the rendering environment stable while narrowing the issue. Paged.js warns that output can vary between browsers and operating systems and recommends using the same browser and OS for design and PDF generation: W3C specifications for printing. Record the OS as well as the browser pair, because changing both at once makes a regression harder to isolate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
What you observe Most useful next check
Generated page number is 0 or blank Check that the matching target exists in the current document and has loaded when Paged.js processes the reference.
Link is absent or its fragment does not resolve Compare the final rendered href with destination IDs; check duplicates and transformations.
HTML click works; PDF click lands elsewhere Wait for completed pagination, then compare the Puppeteer/Chromium pair and inspect the PDF in a viewer.
Only some runs or machines differ Stabilize readiness and the browser/OS environment; capture the exact versions for a repeatable fixture.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server, not a replacement for Paged.js pagination or Puppeteer PDF-anchor debugging. It can help capture a clean visual snapshot of a rendered web page, but a screenshot does not test whether a PDF’s clickable destinations are correct. One GET request returns an image or PDF; the API’s screenshot example is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. Its consent-banner handling accepts the banner like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Learn more at ScreenshotNeo. Sign up free for 1,000 screenshots a month, with no card required.

Troubleshooting checklist

  • Page number reads 0: inspect the exact fragment target in the final DOM; verify it exists in the same document and has loaded by the time the cross-reference is processed.
  • TOC link resolves to the wrong heading: search for repeated IDs and compare fragment spelling, case, whitespace, and encoding.
  • HTML is correct, exported link is not: prove pagination is finished before printing, then test the PDF’s clickable destination independently of visible text.
  • PDF changes after an upgrade: reproduce with a minimal document and record Puppeteer, Chromium, and OS before pinning, upgrading, or rolling back any part of the pair.
  • Different machines produce different output: align browser and operating system for design and generation, then retest under that stable setup.

FAQ

Is this necessarily a Next.js problem?

No. Next.js renders the application, Paged.js paginates and generates references, and Puppeteer performs the print operation through Chromium. The observed symptom alone does not identify which layer is responsible; the checks above isolate them in that order.

Do reports of links being off by a couple of pages mean that offset is expected?

No. The reported offsets are descriptions of individual issues, not a general rule or a published statistic for this stack.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I fix a PDF link by changing only the printed page number?

No. Displayed reference text and the PDF’s clickable destination are different outputs; test the visible number and the destination independently.

Is there a confirmed Chromium version that fixes this issue for everyone?

The cited reports do not establish a universal fixed version. Reproduce with the exact browser pair and verify a candidate change against the same fixture.

Quick Recap

Bestseller No. 1
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
MixPad Free Multitrack Recording Studio and Music Mixing Software [Download]
Create a mix using audio, music and voice tracks and recordings.; Customize your tracks with amazing effects and helpful editing tools.
Bestseller No. 3
Bestseller No. 4
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
DeskFX Free Audio Effects & Audio Enhancer Software [PC Download]
Transform audio playing via your speakers and headphones; Improve sound quality by adjusting it with effects

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.