COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
BRUBKR

Shiki Conflicts, Blueprint Dimming, and Server Component Refactor

Created `.traces/mdx.md` documenting the full MDX pipeline — remark plugins, viz components (`Mermaid`, `D3Viz`, `MathBlock`), the typography component map, and the escape-curlies system that prevents

2026-01-23 // RAW LEARNING CAPTURE
PROJECTBRUBKR

Commits: cff1632, 4b0b31d, plus server component refactor Files touched: src/components/mdx/typography.tsx, src/components/blueprint.tsx, src/components/blueprint-canvas.tsx, src/app/posts/[slug]/page.tsx, src/components/dashboard/projects-grid.tsx, src/lib/projects.ts, .traces/mdx.md


Phase 1: MDX Trace

Created .traces/mdx.md documenting the full MDX pipeline — remark plugins, viz components (Mermaid, D3Viz, MathBlock), the typography component map, and the escape-curlies system that prevents MDX from interpreting { in code blocks.


Phase 2: Shiki + Tailwind Prose Conflict

Investigated whether @tailwindcss/typography prose styles conflict with Shiki themes. Turned out to be a red herring — no @tailwindcss/typography plugin is used. The prose-dark class in the codebase is custom CSS for section/figure counter numbering only.

Actual bug: The Pre component in typography.tsx spreads {...props} AFTER setting its own className. Shiki's rehype plugin generates:

<pre style="background-color: #1f1f1f" class="shiki min-dark">

Those attributes arrive as props and overwrite the component's own classes because spread comes last:

// BEFORE (broken) — Shiki's className/style overwrites ours
export function Pre({ children, ...props }: React.HTMLAttributes<HTMLPreElement>) {
  return (
    <pre className="overflow-x-auto font-mono text-sm" {...props}>
      {children}
    </pre>
  );
}

Fix: Destructure style and className out of props so they never reach the DOM element:

// AFTER (cff1632) — Shiki's style/className discarded
export function Pre({
  children,
  style: _style,
  className: _className,
  ...props
}: React.HTMLAttributes<HTMLPreElement>) {
  return (
    <pre className="overflow-x-auto font-mono text-sm" {...props}>
      {children}
    </pre>
  );
}

Lesson: Shiki's rehype plugin puts inline style on <pre>. CSS class specificity can't win against inline styles. The only fix is to prevent those attributes from spreading onto the element.


Phase 3: Blueprint Background Dimming

Goal: Make the animated Blueprint background less visually distracting for reading long-form content.

Attempt 1: Content backdrop panel with bg-[#050505]/80 rounded-2xl border border-white/[0.03]. User hated it — "too boxy", breaks the open feel.

Attempt 2: CSS mask-image with radial gradient making center fully transparent:

mask-image: radial-gradient(ellipse 60% 70% at 50% 45%, transparent 0%, black 100%)

Failed — page background IS black, so transparent center just shows black. Blueprint disappears entirely in reading zone.

Attempt 3 (shipped): Mask with reduced opacity via rgba instead of full transparency:

mask-image: radial-gradient(
  ellipse 60% 70% at 50% 45%,
  rgba(0,0,0,0.12) 0%,
  rgba(0,0,0,0.2) 40%,
  rgba(0,0,0,0.6) 70%,
  black 100%
)

rgba(0,0,0,0.12) in the mask means the Blueprint renders at 12% opacity in the center — still visible, still alive, but not competing with text.

Also removed a leftover <div className="bg-[#050505]/80 ..."> wrapper in src/app/posts/[slug]/page.tsx from Attempt 1 that was still darkening the blog post content.

Committed as 4b0b31d.

Lesson: CSS mask with rgba(0,0,0,N) where N < 1 dims content to N opacity without removing it. transparent (N=0) hides completely. This is better than opacity because mask-image can be shaped (ellipse, gradient stops).


Phase 4: Server Component Refactor

Goal: Reduce client component surface area. Principle: client boundaries should be "narrow and low in the tree."

Audit

Found 11 'use client' files. Only 3 were refactorable:

FileWhy 'use client'Refactorable?
table.tsxNo reason (no hooks, no browser APIs)Yes — just remove directive
header.tsxusePathname() for route-aware contentYes — per-page pattern
blueprint.tsx450 lines, but only canvas needs clientYes — split static/animated
navigation.tsxusePathname() for active link highlightNo — idiomatic pattern
Others (7 files)Genuine interactivity needsNo

Key Insight: Layouts Don't Re-render

Next.js App Router layouts are static — they don't re-render on client-side navigation. usePathname() inside a client component IS the idiomatic pattern for route-aware UI in layouts. Converting navigation.tsx to a server component would require middleware to inject the current path, and would break client-side nav updates.

However, header.tsx CAN be eliminated by having each page render its own <PageHeader> server component instead of one layout-level client component that maps pathname → content.

Blueprint Split

Separated the 450-line blueprint.tsx into:

blueprint.tsx (server component): Static grids, SVG schematic elements, structural layout, grain texture overlay — everything that doesn't move.

blueprint-canvas.tsx (client component): Only the animated canvas — Packet class, useRef, useEffect, requestAnimationFrame loop. Narrow, leaf-level, minimal JS shipped.

// Layout tree: before
<Layout>                    ← server
  <Blueprint />             ← CLIENT (450 lines, all of it)
  <Navigation />            ← CLIENT (usePathname)
  <Header />                ← CLIENT (usePathname)
  {children}                ← server

// Layout tree: after
<Layout>                    ← server
  <Backdrop>                ← server (static grids, SVG)
    <BlueprintCanvas />     ← CLIENT (only animated canvas, ~80 lines)
  </Backdrop>
  <Navigation />            ← CLIENT (usePathname — idiomatic, kept)
  <PageHeader />            ← server (each page declares its own)
  {children}                ← server

Build Fix: noPropertyAccessFromIndexSignature

Pre-existing TypeScript errors in src/lib/projects.ts blocked the build. With noPropertyAccessFromIndexSignature enabled, Record<string, unknown> requires bracket notation:

// ERROR: Property 'OR' comes from an index signature, so it must be accessed with ['OR']
where.OR = [...]
where.language = language

// FIX:
where['OR'] = [...]
where['language'] = language

Result

Layout-level client components: 3 → 1 (Navigation) + 1 narrow leaf (BlueprintCanvas). Header eliminated from layout entirely. Table directive removed (was never needed).


Technical Learnings

  1. Shiki inline styles: rehype-pretty-code puts style="background-color: ..." on <pre>. No CSS class can override inline styles. Destructure style/className out of props in your component map.

  2. CSS mask opacity trick: mask-image with rgba(0,0,0,0.12) dims to 12% opacity. Unlike opacity property, mask-image can be shaped with gradients for spatial control.

  3. App Router layout statics: Layouts never re-render on navigation. usePathname() in client components is correct for route-aware UI. Don't fight it.

  4. Per-page header pattern: Instead of one client Header that switches on pathname, each page renders a server <PageHeader> with explicit props. Trades one layout client component for zero.

  5. TypeScript strict bracket notation: noPropertyAccessFromIndexSignature + Record<string, unknown> = must use obj['key'] not obj.key. Catches potential typos at compile time.

LOG.ENTRY_END
ref:brubkr
RAW