Skip to content

fix(toc): position-based active heading + preload jetbrains-mono#30

Merged
igorhasse merged 1 commit into
mainfrom
chore/perf-toc-fixes
Apr 28, 2026
Merged

fix(toc): position-based active heading + preload jetbrains-mono#30
igorhasse merged 1 commit into
mainfrom
chore/perf-toc-fixes

Conversation

@igorhasse
Copy link
Copy Markdown
Owner

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

  • After deploy, open `/posts/unreadable` and `/posts/eu-escrevo-ia-edita`. Click any heading in TOC. Then natural-scroll. Highlight should now follow correctly without re-clicking.
  • Re-run Lighthouse on PSI. LCP should drop from 1.8s area to ~1.0s area on mobile (font no longer in critical chain).
  • If you disable Cloudflare's managed robots.txt: SEO score returns to 100.

🤖 Generated with Claude Code

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 igorhasse merged commit 8e751fe into main Apr 28, 2026
3 checks passed
@igorhasse igorhasse deleted the chore/perf-toc-fixes branch April 28, 2026 18:22
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>
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