fix(toc): position-based active heading + preload jetbrains-mono#30
Merged
Conversation
Two perf/UX fixes flagged after publishing /posts/unreadable: 1. PostToc observer was only updating active heading when an entry transitioned to isIntersecting=true with rootMargin restricted to the top 30%. After click + smooth-scroll, a heading could enter the viewport from below WITHOUT crossing into the top-30% band (because subsequent natural scrolling stops the observer from firing entries whose final state isn't intersecting). Result: highlight got stuck on whatever was active at click moment. Switched to position-based detection: every observer fire re-queries getBoundingClientRect() for all H2s and picks the last one whose top has crossed above 100px. Survives smooth-scroll transitions and natural scrolling alike. 2. Critical request chain (HTML → CSS → font) added 806ms latency to LCP because @font-face for JetBrains Mono is inline in the layout's CRITICAL_CSS but the browser doesn't request the font until it parses the main CSS chunk that uses font-family. Added explicit `<link rel="preload" as="font">` in the layout head so the font fetch starts in parallel with the CSS download. Note on the robots.txt SEO warning Lighthouse flags: that `Content-Signal:` directive is injected by Cloudflare's Managed robots.txt feature, not by `app/robots.ts`. To remove, disable AI Crawl Control on the Cloudflare dashboard for the zone. Out of scope for this code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
igorhasse
added a commit
that referenced
this pull request
Apr 28, 2026
Updates after running PageSpeed Insights against production with the perf fixes from #30 already in place: - Performance: 92 mobile, 100 desktop (PSI numbers, not local Chrome headless). Updated table + opening claim accordingly. - Description rewritten to match: "92/100 no Lighthouse". - Added lighthouse.png screenshot to the bundle. assetsPlugin copies it to public/posts/unreadable/lighthouse.png at build. - Title kept as plain YAML scalar (no quotes); js-yaml accepts this fine, and the user has explicit preference against quotes. Also: - .gitignore now excludes public/posts/ (build-time generated by assetsPlugin from content/posts/<slug>/*.{jpg,png,...}). - RSS regen surface from build. Co-authored-by: Claude Opus 4.7 (1M context) <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.
Summary
Three issues surfaced after `/posts/unreadable` shipped. Two of them code-fixable, one needs a Cloudflare dashboard click.
1. TOC sidebar broke after first click ✓ fixed in code
Bug: the IntersectionObserver only flipped `activeId` when an entry transitioned to `isIntersecting=true` with rootMargin restricted to the top 30% of the viewport. After click + smooth-scroll lands the heading at the top, subsequent natural scrolling could move other H2s through the viewport WITHOUT triggering an intersecting=true at the right moment, so the highlight got stuck.
Fix: position-based detection. Every observer fire calls `getBoundingClientRect()` for all H2s and picks the last one whose top has crossed above 100px. Robust to smooth-scroll transitions and natural scrolling.
2. Critical request chain costing ~800ms LCP ✓ fixed in code
Bug: `@font-face` for JetBrains Mono is declared inline in the layout's `CRITICAL_CSS`, but the browser doesn't actually request the woff2 until it has parsed the main CSS chunk (because that's what uses `font-family: "JetBrains Mono"`). Result: serial chain HTML → CSS → font, adding 806ms to LCP per Lighthouse.
Fix: added `<link rel="preload" as="font" type="font/woff2" crossOrigin="anonymous" href="/fonts/jetbrains-mono-latin.woff2">` at the top of ``. Font fetch now runs in parallel with CSS fetch.
3. `Content-Signal` directive in robots.txt → NOT code, NEEDS Cloudflare dashboard
`app/robots.ts` outputs a clean robots.txt. Cloudflare is prepending "Managed Content" with the `Content-Signal:` directive plus a long Disallow list for AI bots (Amazonbot, ClaudeBot, GPTBot, etc.). Lighthouse correctly flags `Content-Signal:` as an unknown directive (it's a Cloudflare proposal, not a robots.txt standard).
Fix path (manual, NOT this PR): Cloudflare dashboard → Security → AI Audit → toggle off "Add managed robots.txt entries" or similar. Then redeploy. We'll lose the AI bot signaling but Lighthouse SEO goes back to 100.
Test plan
🤖 Generated with Claude Code