Repository navigation
routine 2026-10-12: reloads put the reader back — no 2 px creep on an open README, every page restores (not only /), the browser's restore jumps - #23
Merged
Conversation
Two restores ran on every reload of an about page with its README open:
the fold's own and Chromium's. Nuxt's router plugin leaves
history.scrollRestoration at `auto` for the first document, so the browser
still restored the entry after the fold had opened and scrolled, and landed
2 px further on; the next reload saved that and crept on. That is the
check:navigation flake that could turn CI red.
Traced frame by frame: the fold's scrollTo({ top: 4200 }) read 4200, the
first scroll event 4202, and no element moved in between, so it was never
layout or scroll anchoring. Chromium settles it in the task that sets
`auto`, and the entry must also leave the previous document as `manual`:
- app/plugins/readme-restore.client.ts runs before Nuxt's router plugin
(order -25) and holds `manual` through its write, only for a reload or
Back/Forward of an entry whose README was open and whose place is stored.
- app/composables/useReadmeFold.ts sets `manual` when the fold opens and
hands back what it took when folded again; foldWillRestore() and a
stale stored place is dropped once folded.
onyums at 390 px: 14 of 42 reloads drifted before, 0 of 42 after (DPR 1
and CI's DPR 3 phone profile). Reloads of other pages land as on main's
build, side by side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reload of /projects at 1,200 px landed at 0, and so did an about page
with its README folded, in Chromium and Firefox; in Chromium so did Back
to /projects, /news or a blog tab from another site. Only / came back.
Nuxt's router plugin runs one start-up navigation through the scroll
behaviour from START_LOCATION, and with no saved position it answers
{ top: 0 }, cancelling the browser's own restore. / was spared only by
Nuxt's same-path rule (START_LOCATION's path is /). vue-router's saved
position can't step in: Chromium drops its beforeunload write, so
history.state.scroll is false on every reload.
- app/router.options.ts: on the page's own load, with neither a saved
position nor a hash, leave the scroll alone. A fresh visit is at the
top anyway; a hash is still Nuxt's to scroll to.
- scripts/check-navigation.mjs: reload each folded about page halfway
down and expect it to stay there. Main's build fails 16 of them; this
build passes 160 in Chromium and in Firefox.
- ROADMAP.md, README.md: the item, and the restore animation found on the
way as a new open item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chromium applies the site's `scroll-behavior: smooth` to its own restore, so a reload of / at 1,200 px scrolled from the top to 1,200 over ~550 ms (34 scroll events); Firefox jumps. vue-router's restores were already made instant in app/router.options.ts, but the browser's can't be asked. app/plugins/restore-jump.client.ts holds `scroll-behavior: auto` on a reload or Back/Forward to a document until scrolling has been still for 300 ms (3 s at most), then hands smooth scrolling back. Chromium, / and /projects at 1,200 px: 34 scroll events -> 1, landing at 1,200; Firefox unchanged. A fresh visit and a page without script are untouched. ROADMAP.md: the item; an upstream report of Nuxt's reload-to-top for the owner (nuxt/nuxt#36346 was closed as an i18n duplicate, and this site has no i18n); and an intermittent cold-load #heading landing 2 px short, seen once in three Chromium runs on a path this change does not touch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cold-load miss measured - The onion workflow's first scheduled runs reported (82.6 s on a second try, 11.1 s on the first); reading a few days of them is its own item, and GitHub's schedule ran twice in ~20 h, which it documents as possible. - marked 18.1.1 changes README output; held a night with a byte-diff plan. - The cold-load #heading miss: 1 in 6 Chromium runs on this build, none in 3 on main's, plus one stalled run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skidbladnir moves to v1.4.3 in the snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three fixes to how pages come back on a reload or on Back. Run window: 21:10 → 01:09 CDT.
Increment 1: a reload of an open README lands where the reader was (2c2018d)
Roadmap item: "A reload of an unfolded README lands 2–9 px past the reader's place". This is the
check:navigationflake that could turn CI red. Ticked.Files:
app/plugins/readme-restore.client.ts(new),app/composables/useReadmeFold.ts,ROADMAP.mdWhat was wrong. Two restores ran on each such reload: the fold's own, and Chromium's. Nuxt's router plugin leaves
history.scrollRestoration = 'auto'for the first document, so Chromium also restored the page. It did so after the fold had already scrolled, and landed 2 px further. The next reload saved that position, so each reload crept 2 px more.How it was found. A frame-by-frame trace: the fold's
scrollTo({top: 4200})read 4200, and the very next scroll event read 4202. A full-depth layout diff found no element that moved in between, so it was never layout or scroll anchoring. That also explains last night's contradictoryoverflow-anchorbisect: injecting any style atDOMContentLoadedchanged the timing, and a no-op control style suppressed the drift too.What decides it. Pinning
manualfrom document start: 0 drifts in 42 reloads; the pass-through control: 14 in 42. Two things have to be true together:autowrite must be held off in the same task. Re-settingmanualin a microtask gave 0 in 26; in the next task, 13 in 28. Setting it from a plugin after the router's gave 16 in 42, and once mounted, 17 in 42.manual. Without this, the first reload still drifted.The fix.
order: -25, ahead of itspreat -20). It holdsmanualthrough Nuxt's write, but only when the fold will restore this entry: a reload or Back/Forward,readmeOpen === true, and a stored position. It releases atapp:mounted.manualwhen it opens, and gives back what it took when folded again.Result: onyums at 390 px, 0 drifts in 42 reloads, at DPR 1 and at CI's phone profile (DPR 3, mobile).
Increment 2: a reload puts the reader back on every page, not only
/(3a420e7)Roadmap item: new (found while verifying increment 1). Ticked.
Files:
app/router.options.ts,scripts/check-navigation.mjs,ROADMAP.md,README.mdWhat was wrong. In both engines, a reload of
/projectsat 1,200 px landed at 0, and so did an about page with its README folded. In Chromium, Back from another site to/projects,/newsor a blog tab also landed at 0. Only/came back.Why. Nuxt's router plugin runs one start-up navigation from
START_LOCATION. With no saved position, it answers{top: 0}, and that cancelled the browser's own restore./was spared only by Nuxt's same-path rule, becauseSTART_LOCATION's path is/. vue-router's saved position can't help: Chromium drops itsbeforeunloadwrite, sohistory.state.scrollisfalseon every reload.The fix. On the page's own load, with neither a saved position nor a hash, the scroll is left alone.
Before → after, 390 px, side by side with main's build on this host:
/projectsreload/projects,/news,onyums/blog)Firefox already restored those Back cases from its back-forward cache, and still does.
Guard.
check:navigationnow reloads each folded about page halfway down. Main's build fails it on all 16 page × width cases (three runs); this build passes 160 checks in Chromium and in Firefox.Increment 3: the browser's own restore is a jump, not an animation (7c4d76e)
Roadmap item: new (found while tracing increment 2). Ticked.
Files:
app/plugins/restore-jump.client.ts(new),ROADMAP.mdWhat was wrong. Chromium applies the site's
scroll-behavior: smoothto its own restore. A reload of/at 1,200 px scrolled from the top over ~550 ms, in 34 scroll events. Firefox jumps.The fix. On a reload or Back/Forward, the page holds
scroll-behavior: autountil scrolling has been still for 300 ms (3 s at most).Result: 34 scroll events → 1 on
/and/projectsin Chromium. In Firefox the restore often lands before the plugin listens, so smooth scrolling comes back at the 3 s fallback; checked at 3.5 s. A fresh visit and a page without script are untouched.Verification
Final tree (e6bb4e6):
og:check,font:check,sizes:check,contrastandaudit:runtimeall pass.Real server, final build:
/healthz200,/200,/projects200, all 8/projects/<slug>/aboutand/blogpages 200./projects/<slug>301./no-such-pageand/projects/no-such-project404, and the onion admin probes 404./onion-frame503 (no local gateway, same as main).Checks, with fixture content:
check: 22 pages, 79 URLs.a11y: 23 pages.a11y:browseranda11y:firefox: 23 pages × 3 widths, clean.check:navigationin Firefox: 160/160.check:navigationin Chromium: green on increments 1 and 2. On increment 3's build, see the open bug below.Screenshots (headless Chromium, desktop 1280 and phone 390, of home, Skidbladnir and Nisaba) are in the run's scratch dir. I looked at them: the flat single background is intact, the masthead glass is unchanged, and nothing overflows.
Bugs found, not fixed
about#<heading>can land 2 px short of the masthead. Nanna, phone width,#api-key-invalid: the heading sat 2 px under the masthead edge, outside the 0–200 band.check:navigationruns on this build. Another run stalled on the same page until its 15-minute timeout; the host was at load 9 with other agents' browsers running. The other 4 passed.navigateload, where all three changes return early. But main's build had no such miss in 3 runs, so I can't show it predates tonight.[ ]with next steps.ba-bidiprofile) was left alive from an earlier pass; I killed it.Research
[ ], since reporting it upstream speaks for the org. Scroll is not restored after refresh nuxt/nuxt#36346 · @nuxtjs/i18n break scroll restoration nuxt-modules/i18n#4157scheduleruns can be delayed under load and dropped if load is high enough. The onion probe ran twice in its first ~20 h. https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedulewalkTokensquadratic fix does not apply here. Held a night with a byte-diff plan. https://github.com/markedjs/marked/releases/tag/v18.1.1 · walkTokens takes quadratic time because it copies the results array for every token markedjs/marked#4120Done vs open
Done:
New
[ ]:Still owner-gated or upstream: unchanged from #22.
STOP REASON: 4 h cutoff (21:10 → 01:09 CDT). Three increments landed. The fourth slice, the cold-load miss, was measured but not fixed, and is written up as the next item.
Recommended next step
Shipped
nightly-2026-10-12.basicautomation-site:previous(023c0fd13d51) first. The new:local(636d5e5dab10) is built from merged main and reportedhealthyin ~10 s.https://basicautomation.io/200, with the<title>"basic automation — software for the productive"./projects/skidbladnir/about200,/projects200./healthz200: statusok, datalive, onionlaunched./projects1200→1200, folded onyums 1500→1500 and open onyums 4200→4200. Before the deploy, the same probe on the live site gave 0, 0 and 4200.🤖 Generated with Claude Code