Skip to content

fix(file-viewer): honor #page= fragments in PDF links - #841

Open
omnbird wants to merge 2 commits into
agegr:mainfrom
omnbird:fix/pdf-page-fragment
Open

omnbird wants to merge 2 commits into
agegr:mainfrom
omnbird:fix/pdf-page-fragment

Conversation

@omnbird

@omnbird omnbird commented Sep 14, 2026

Copy link
Copy Markdown

Problem

Opening a PDF through the in-app viewer always lands on page 1, even when the markdown
link points at a specific page:

[report.pdf p.183](file:///home/me/books/report.pdf#page=183)

resolveLocalFileHref() resolves that href to /home/me/books/report.pdf, and the viewer
builds its iframe URL from the path alone — so the #page=183 fragment never reaches the
PDF viewer. A relative link (books/report.pdf#page=183) behaves the same way.

Only the in-app viewer is affected. A link that the browser opens itself — a top-level
/api/files/...?type=read#page=N, which resolveLocalFileHref() deliberately ignores and
leaves as a plain target="_blank" anchor — does jump, because the browser's native PDF
viewer handles #page=N on its own.

Cause

resolveLocalFileHref() strips #… while resolving the href to a filesystem path
(lib/file-links.ts), and DocumentViewer then builds its iframe URL from that path alone
via getFileApiUrl(filePath, "read", …), which has no fragment parameter. There is no route
for the fragment from the markdown href to the iframe URL.

Change

Carry the page selector from the markdown href through to the iframe URL:

  • lib/file-links.ts — add parsePdfPageFragment(): accepts #page=N (positive integer),
    returns null for anything else.
  • components/MarkdownBody.tsxonOpenFile(filePath, page?).
  • components/AppShell.tsx, components/file-tab-state.ts, components/TabBar.tsx — store
    page on the tab and bump viewerRevision when it changes, so the viewer remounts and
    jumps when another page of the same file is opened.
  • components/FileViewer.tsx — new initialPage prop; DocumentViewer appends #page=N
    for PDFs only.

Behaviour for every other fragment (#L42, #section, …) is unchanged.

Verification

Static:

  • node_modules/.bin/tsc --noEmit — clean
  • eslint on all touched files — clean
  • npm test — 1026/1026 pass, including new cases for #page= parsing (rejects
    #page=0, #page=-1, #page=abc, #page=12&zoom=100) and for the page-change remount

Runtime (e2e/pdf-page-fragment.mjs, full Chromium build):

  • [PDF page 183](...report.pdf#page=183) opens the viewer with #page=183 in the iframe
    URL, and the rendered document shows page 183
  • a plain link to the same PDF renders the viewer with no fragment (page 1)
  • clicking the fragment link again after that jumps back to page 183

The e2e check needs the full Chromium build. The Chromium headless shell has no PDF
viewer and downloads the file instead, so the iframe renders nothing and the page can
not be observed.

Markdown links such as `[report.pdf#page=12](/api/files/...?type=read#page=12)`
opened the in-app viewer at page 1: the href is resolved to a filesystem path
before it reaches the viewer, so the fragment never made it to the iframe URL.

Parse the page selector in MarkdownBody, carry it through the file tab state,
and append it to the `type=read` iframe URL so the browser's PDF viewer jumps
to the requested page. Opening a different page of the same file remounts the
viewer, while any other fragment (for example `#L42`) is ignored as before.
Seeds a session with a generated multi-page PDF whose pages print their own
number, then clicks a `#page=N` markdown link and asserts the viewer iframe URL.

Requires the full Chromium build: the Chromium headless shell has no PDF viewer
and downloads the file instead, so nothing renders in the iframe.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant