Skip to content

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
physics515 merged 5 commits into
mainfrom
routine/site-2026-10-12
Oct 13, 2026
Merged

physics515 merged 5 commits into
mainfrom
routine/site-2026-10-12

Conversation

@physics515

@physics515 physics515 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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:navigation flake that could turn CI red. Ticked.

Files: app/plugins/readme-restore.client.ts (new), app/composables/useReadmeFold.ts, ROADMAP.md

What 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 contradictory overflow-anchor bisect: injecting any style at DOMContentLoaded changed the timing, and a no-op control style suppressed the drift too.

What decides it. Pinning manual from document start: 0 drifts in 42 reloads; the pass-through control: 14 in 42. Two things have to be true together:

  • Nuxt's auto write must be held off in the same task. Re-setting manual in 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.
  • The entry must leave the previous document as manual. Without this, the first reload still drifted.

The fix.

  • A client plugin runs before Nuxt's router plugin (order: -25, ahead of its pre at -20). It holds manual through 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 at app:mounted.
  • The fold sets manual when 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.md

What was wrong. In both engines, a reload of /projects at 1,200 px landed at 0, and so did an about page with its README folded. In Chromium, Back from another site to /projects, /news or 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, because START_LOCATION's path is /. vue-router's saved position can't help: Chromium drops its beforeunload write, so history.state.scroll is false on 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:

Case main this branch
Chromium /projects reload 0 1,200
Chromium folded onyums reload 0 1,500
Firefox, same two cases 0 restored
Chromium Back from another origin (/projects, /news, onyums/blog) 0 1,200 / 604 / 734

Firefox already restored those Back cases from its back-forward cache, and still does.

Guard. check:navigation now 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.md

What was wrong. Chromium applies the site's scroll-behavior: smooth to 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: auto until scrolling has been still for 300 ms (3 s at most).

Result: 34 scroll events → 1 on / and /projects in 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):

  • Typecheck clean, build green.
  • 276 tests in 30 files.
  • og:check, font:check, sizes:check, contrast and audit:runtime all pass.

Real server, final build:

  • /healthz 200, / 200, /projects 200, all 8 /projects/<slug>/about and /blog pages 200.
  • /projects/<slug> 301.
  • /no-such-page and /projects/no-such-project 404, and the onion admin probes 404.
  • /onion-frame 503 (no local gateway, same as main).
  • Overall 25×200, 8×301, 8×404, 1×503, identical to last night.

Checks, with fixture content:

  • check: 22 pages, 79 URLs.
  • a11y: 23 pages.
  • a11y:browser and a11y:firefox: 23 pages × 3 widths, clean.
  • check:navigation in Firefox: 160/160.
  • check:navigation in 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

  1. A cold load of 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.
    • It failed in 1 of 6 Chromium check:navigation runs 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.
    • It is a fresh navigate load, 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.
    • Alone, 25 cold loads of that URL landed at 16 or 12 px, never out of band.
    • CI can go red on it. It is a new [ ] with next steps.
  2. One unit-test run reported 272 tests instead of 276. Five reruns all reported 276. Not explained.
  3. Housekeeping: an orphaned headless Firefox of mine (ba-bidi profile) was left alive from an earlier pass; I killed it.

Research

Done vs open

Done:

  • Increments 1–3.
  • Onion probe's first scheduled runs read and ticked: 82.6 s on a second try after a 120 s connect timeout, and 11.1 s on the first try.
  • Snapshot refreshed (Skidbladnir v1.4.3).

New [ ]:

  • The cold-load miss.
  • Read a few days of onion probe runs.
  • marked 18.1.1.
  • Upstream Nuxt report (owner).

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

  1. Cold-load miss: run the case inside the check's own sequence with its frame trace, on both builds, and find what the stalled run waited on.
  2. Take marked 18.1.1 with a byte diff of every README and post.
  3. Owner: decide whether to report the Nuxt reload-to-top upstream.

Shipped

  • CI: green (build 26 m 27 s, image builds, onion gateway). Merged as d1f41c8, with a tree identical to the verified branch.
  • Release: prerelease nightly-2026-10-12.
  • Deploy: the prior image was tagged basicautomation-site:previous (023c0fd13d51) first. The new :local (636d5e5dab10) is built from merged main and reported healthy in ~10 s.
  • Live gate:
    • https://basicautomation.io/ 200, with the <title> "basic automation — software for the productive".
    • /projects/skidbladnir/about 200, /projects 200.
    • /healthz 200: status ok, data live, onion launched.
  • Live behaviour: the reload probe against production now gives /projects 1200→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.
  • No rollback was needed.

🤖 Generated with Claude Code

physics515 and others added 5 commits October 12, 2026 22:43
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>
@physics515
physics515 merged commit d1f41c8 into main Oct 13, 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