- Issue pages highlight Archive instead of Latest unless the issue is the newest one (IssueView::is_latest), with a router test. - The ears row is empty by default and says "Dashboard" on dashboard pages; "Morning edition" only survives on the empty-state hero. - Run page: the Feeds card table is fixed-layout and truncates long feed names with the full title on hover instead of pushing the entries column out of the card. - Static assets are content-hashed, so serve them immutable for a year; preload both Newsreader faces and use font-display: block so a cache revalidation never paints the fallback serif first. Persisted details state is restored pre-paint from theme.js. - Cross-document view transitions (140 ms fade, masthead held still) and Speculation Rules (prerender reader pages, prefetch dashboard pages on hover) served from /static/speculation.json via the Speculation-Rules header, which the CSP would otherwise block inline. No client router. - Dashboard nav gets the same accent underline as the site nav; 40 px targets for the theme toggle, account link and dashboard nav. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
7.6 KiB
7.6 KiB
Task 3 handoff — site shell
What changed
src/web/issue.rsIssueViewnow recordsis_latest, computed byissue::loadfromDb::latest_issue_date().- The full issue, article, World Briefing, and Behind the paper renderers now
select
latestorarchivenavigation state from that flag. - Added a router test with two issue dates proving that only the newest issue
marks Latest
aria-current="page"and that an older issue marks Archive.
src/web/public.rs- Anonymous issue rendering uses the same
IssueView::is_latestnavigation state as signed-in issue rendering.
- Anonymous issue rendering uses the same
src/web/templates/layout.html- The default ears block is empty except on an admin dashboard route, where it
says
Dashboard. Issue/account/login/archive/error templates retain their explicit ears; the public empty issue retains its ownMorning edition. - Added the italic Newsreader preload alongside the upright preload.
- Named the site header for cross-document view transitions.
- Made the ears-row theme and account/sign-in controls at least 40 px tall; the account/sign-in link also has a 40 px minimum width.
- Dashboard navigation links now have a 40 × 40 px minimum target and use the same 2 px accent active underline, ink hover, and inherited focus-visible ring as site nav.
- The default ears block is empty except on an admin dashboard route, where it
says
src/web/templates/dashboard/run.html- The Feeds table no longer uses
.scroll-x. It is fixed-layout and full width, truncates feed names with the full value intitle, and reserves a narrow fixed entries column. Timings and Provider usage retain their horizontal-scroll behavior.
- The Feeds table no longer uses
src/web/tailwind.css- Both Newsreader faces use
font-display: block. - Added automatic cross-document view transitions with a 140 ms root cross-fade. The named site header does not fade, and reduced-motion disables transition animation.
.badge,.pager, and.paginationuse tabular numerals. (.tile-numalready did; the brief said.pagerdid too, but the code did not, so it was corrected.)
- Both Newsreader faces use
src/web/static/app.css- Rebuilt from
tailwind.csswith Tailwind 4.3.3.
- Rebuilt from
src/web/static/speculation.json- Added moderate same-origin prerender document rules for
/and/issues/*, and moderate prefetch rules for/dashboardand/dashboard/*. - Logout, rating, static, and all query-string links are excluded. Excluding every query is deliberately conservative and therefore also excludes any query that could mutate state.
- Added moderate same-origin prerender document rules for
src/web/mod.rs- Serves the speculation rules with
application/speculationrules+jsonand retains content-hash ETags. - All
/static/*responses, including 304 responses, now sendCache-Control: public, max-age=31536000, immutable. - HTML responses send
Speculation-Rules: "/static/speculation.json"while retaining their existing public/private/no-store cache behavior. - Added assertions for immutable cache headers, rules MIME type, both font preloads, the HTML speculation header, dashboard ears, and dashboard active underline.
- Serves the speculation rules with
src/web/static/app.js- A
[data-refresh]job page now waits forprerenderingchangebefore scheduling reload, so a prerendered page cannot reload in the background.
- A
src/web/static/theme.js- Continues to apply the saved theme and
has-jsin the head before first paint. - A short-lived mutation observer now restores persisted
details[id]state as the parser creates disclosures, then disconnects atDOMContentLoaded. This prevents a cold/delayedapp.jsrequest from exposing the default disclosure state before restoration;app.jsstill owns toggle persistence.
- Continues to apply the saved theme and
Design and implementation decisions
- Chose
font-display: blockrather thanoptional. This prevents a visible fallback-to-Newsreader swap and preserves the publication typeface. Preloading both faces plus immutable content-hashed assets limits the block cost to an uncached first visit. - Kept the 140 ms transition restrained and limited to document navigation. The masthead/nav swap immediately instead of fading, so navigation context stays visually anchored.
- Added only platform progressive enhancements. There is no fetch/swap router, HTMX, or route/auth change.
- Speculation Rules and cross-document view transitions are progressive: Chromium uses them; unsupported browsers ignore them and retain normal navigation.
Flash reproduction and visual verification
The requested dev server could not start inside this sandbox:
$ DAILY_EPUB_SERVER__BIND=127.0.0.1:3603 cargo run -- --config ./dev/config.toml serve
Error: could not bind 127.0.0.1:3603: Operation not permitted (os error 1)
Therefore the throttled Playwright before/after capture and the requested light,
dark, desktop, and 390 px screenshots could not be produced. The code audit
found that theme and mobile TOC state were already applied by theme.js before
paint. Persisted details state has now also moved into that pre-paint path.
The other visible-swap candidate was Newsreader's font-display: swap; the
preload/cache/block changes address that path.
The requested curl calls likewise could not connect:
$ curl -sS -D - -o /dev/null http://127.0.0.1:3603/
curl: (7) Failed to connect to 127.0.0.1 port 3603 after 0 ms: Couldn't connect to server
$ curl -sS -D - -o /dev/null http://127.0.0.1:3603/static/app.css
curl: (7) Failed to connect to 127.0.0.1 port 3603 after 0 ms: Couldn't connect to server
Router tests verify the material headers that those calls should show:
GET /
content-type: text/html; charset=utf-8
cache-control: public, max-age=300
speculation-rules: "/static/speculation.json"
vary: Cookie
GET /static/app.css
content-type: text/css; charset=utf-8
cache-control: public, max-age=31536000, immutable
etag: "0b00513092d50175d3615cfd6cdfc5fed6e416f2aea118c3b0f0110a66089036"
Signed-in issue responses remain private, no-store; dashboard responses
remain no-store.
Verification
cargo fmt -- --check— passed.cargo clippy --all-targets -- -D warnings— passed.npm run css:check— passed with no diff using the repository's pinned Tailwind 4.3.3 binary. (npm cicould not finish under the restricted npm environment, so the already-installed sibling-worktree binary was placed onPATH; that worktree was only read, never modified.)python3 -m json.tool src/web/static/speculation.json— passed.node --check src/web/static/app.jsandnode --check src/web/static/theme.js— passed.cargo test --lib web::— 84 passed, 0 failed.- Dedicated newest/archive navigation test — passed.
- Dedicated dashboard settings shell/ears/active-nav test — passed.
cargo test— 432 library tests passed; 13 tests failed solely because their mock HTTP servers could not bind loopback (Operation not permitted). Ten are the sandbox failures named in the shared brief; three newer OpenAI mock-server tests fail at the same listener helper for the same reason.cargo test --libwith those exact 13 bind-dependent tests skipped — 432 passed, 0 failed, 13 filtered out.- Non-server integration tests (
config_check,e2e_pipeline,m2_pipeline,m3_curation,m4_epub) — 25 passed, 0 failed. cargo test --doc— passed (0 doctests).git diff --check— passed.
Left open
- Run the four requested screenshot sets and throttled first-frame comparison outside the sandbox, where port 3603 can bind. This is also the remaining browser-level validation for view-transition behavior and Chrome's Speculation Rules panel.
tests/m7_server.rswas not run because it is explicitly excluded in this loopback-restricted sandbox.