Skip to content

Give every archival PDF a page to be read on - #29

Merged
lmoresi merged 6 commits into
mainfrom
feature/pdf-reader-page
Aug 20, 2026
Merged

Give every archival PDF a page to be read on#29
lmoresi merged 6 commits into
mainfrom
feature/pdf-reader-page

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

A DOI click lands a reader on figshare's item page, which is a file browser: it
shows a PDF and a zip and asks which one you wanted. We cannot restyle that page.
What we can do is stop our links handing over a file when the reader wanted to
look at the document.

/<slug>/read/ shows the archival PDF embedded, under the note's title,
authors, date and DOI, with three buttons: Download PDF, Markdown source,
and Read on the site.

  • The site's own PDF links (front page, notes listing, topic pages) point there
    instead of at the file.
  • The theme's Downloads menu is retargeted in the browser, using the same
    after-hydration approach as the comments — React owns the document and undoes
    anything changed before it hydrates. Only anchors into the theme's /build/
    export path are touched, so a PDF linked from an article's prose is untouched.
  • The markdown source is staged at /<slug>/<slug>.md to give that button
    something to point at.

Why our own copy rather than figshare's widget

Measured, not assumed:

ndownloader.figshare.com/files/<id> content-disposition: attachment, x-frame-options: SAMEORIGIN — cannot be framed here, only downloaded
the figshare embed widget frames fine, but it is a widget on our page; it cannot change what a DOI click shows
record 33216996 two files (PDF + archive zip), typed "online resource" — which is what makes figshare show a file list rather than a viewer
/<slug>/<slug>.pdf (ours) application/pdf, no disposition — renders inline, frames from our origin, no third-party script, works in a preview build

Tests

The ordering is the part that is easy to lose in a one-line build task: the
reader page is only written where the PDF is already staged, so
stage_downloads.py must run first — asserted for the build task and for the
preview path. Plus the page carrying both downloads, the way back, and a
noindex (the article stays canonical). 133 pass.

Preview: /<any-slug>/read/, e.g. /running-underworld-in-a-browser/read/.

Underworld development team with AI support from Claude Code

A DOI click lands a reader on the repository's item page, which is a file
browser: it shows a PDF and a zip and asks which one you wanted. We cannot
restyle that page -- it is the repository's -- but we can stop sending readers a
file when what they wanted was to look at the document.

`/<slug>/read/` shows the archival PDF embedded, under the note's title, authors,
date and DOI, with three buttons: Download PDF, Markdown source, and Read on the
site. The site's own PDF links now go there rather than at the file, and the
theme's Downloads menu is retargeted at it in the browser -- the same
after-hydration trick the comments use, since React owns the document and undoes
anything changed before it hydrates. Only anchors into the theme's `/build/`
export path are touched, so a PDF linked from an article's prose is left alone.

The markdown source is staged beside the PDF at `/<slug>/<slug>.md` to give that
second button something to point at. It is what someone reusing a figure, a table
or an equation actually wants, and it was already in the repository.

Embedding our own copy rather than the repository's widget is not only a styling
choice: `ndownloader.figshare.com` answers with `content-disposition: attachment`
and `x-frame-options: SAMEORIGIN`, so the archived file cannot be framed from
this origin at all. Ours is served as `application/pdf` with no disposition, so
it renders inline, needs no third-party script, and works in a preview build.

Tests cover the ordering that is easy to lose -- the reader page is only written
where the PDF is already staged, so `stage_downloads.py` must run first, in the
build task and in the preview path alike -- and that the page carries both
downloads, the way back, and a noindex (the article remains canonical).

Underworld development team with AI support from Claude Code
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Preview: https://underworld-technical-notes.github.io/underworldcode.org-preview/c468a06e88/

No article changed on this branch. Built from a46672d. Shows notes at draft and review, which the published site withholds. Not indexed, no comments, and not the citable version.

Every href and the embed source were absolute (/<slug>/<slug>.pdf). The preview
site serves the whole site from a hashed subdirectory, so an absolute path walks
out of it to the domain root and the buttons 404 on exactly the build where they
would be reviewed. They are now ../<slug>.pdf and ../, which work in both.

The Downloads retarget had the same fault -- it built "/" + slug + "/read/" --
and now derives the href from the current path, prefix and all.

The test asserts the outcome rather than the strings: no absolute article path
appears on the page.

Underworld development team with AI support from Claude Code
…n race it

Louis asked where the button is. It was inside the theme's Downloads menu, behind
an icon in the frontmatter row -- which is not an answer.

A visible "PDF" link now sits in that row beside the licence badge, styled with
the site's own tokens, and goes to /<slug>/read/.

The menu entry is handled differently too. The theme renders those entries from
its hydration payload only when the menu OPENS, so there is no anchor in the page
to rewrite beforehand, and the MutationObserver rewrite this replaces was racing
the reader's second click by its own 120 ms debounce. A capture-phase click
listener has no race: it sees the click on any anchor into /build/*.pdf and sends
it to the reader page instead.

Tests assert the visible link, the capture-phase interception, and that the href
is still built from the current path so the preview subdirectory survives. The
first draft of that last test asserted the ABSENCE of an absolute form and
matched the comment explaining why we do not use one; it asserts the positive.

Underworld development team with AI support from Claude Code
With a visible PDF link going to /<slug>/read/, where the PDF and the markdown
source are both a button away, the theme's Downloads menu is a quieter second
route to the same two files. It is gone.

Removed at both ends. The menu is rendered from the `exports` array in the page's
hydration payload, so that is emptied -- 82 pages. The button MyST had already
rendered into the static HTML is deleted with it -- 80 pages: React would
reconcile it away once hydration ran, but not before it had been on screen, and
never at all for a reader without Javascript.

The button is found by its own accessible label rather than by a class. The
theme's classes are utility soup and its element ids are generated per render,
but `<span class="sr-only">Downloads</span>` is there because a screen reader
needs it to be.

The capture-phase click interception stays as belt and braces: if a theme upgrade
renders an export link from somewhere this does not reach, it still leads to the
reader page rather than to a bare file.

Verified on a full build: no Downloads label, no populated exports, no /build/
PDF references, the licence badge untouched and the visible PDF link in place.

Underworld development team with AI support from Claude Code
#28 landed the absolute-path guard on the same file this branch appends its
reader-page tests to. Both are new tests and neither replaces the other, so the
resolution keeps both. 136 pass.

Underworld development team with AI support from Claude Code
@lmoresi
lmoresi merged commit 5f6ed47 into main Aug 20, 2026
3 checks passed
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