Ray Shao Back to the room Desktop
BlogBuilding this site with Claude: design mockups to production architecture

← Blog

Building this site with Claude: design mockups to production architecture

2026.04.19 / 12 min

On this page 7 sections
  1. Using AI as a design collaborator
  2. The Vite build pipeline & module graph architecture
  3. Cache invalidation via asset co-location
  4. Zero-dependency Markdown AST parser in parseBlocks.ts
  5. Solving KaTeX mathematical formula integration
  6. Diagnosing the browser runtime failure
  7. Static prerendering engine (scripts/prerender.js)
  8. Summary & Systems Takeaways

The site started as an architectural dialogue with Claude. I wanted an interface inspired by the LCARS terminal aesthetic from Star Trek: tactile, functional, information-dense, and unapologetically engineered. I treated Claude as a senior systems collaborator: defining constraints, iterating on CSS token architectures, and building a lightweight static pipeline that requires zero runtime CMS or external backend dependencies.

The first prototype came together in an afternoon. Transforming that prototype into a fast, crawler-visible, production-grade portfolio required engineering a custom Markdown AST parser, mathematical typesetting integration, and an automated post-build prerendering engine.

Using AI as a design collaborator

Before writing any TypeScript, I used Claude to establish a rigorous design system. The constraints were clear:

Claude produced the core design token definitions (--accent, --accent-dim, --fg, --fg-dim, --fg-muted, --bg, --panel, --hairline) and CSS Grid layouts. The signature LCARS cut corners and panel borders were implemented directly via CSS clip-path: polygon(...) and structural borders. Defining the design tokens and structural vocabulary upfront meant every subsequent component snapped into a coherent visual system.

The Vite build pipeline & module graph architecture

The site runs on React 19, Vite 6, and TypeScript. Because the site deploys directly to GitHub Pages as a static host, the filesystem itself acts as the database at compile time.

Instead of maintaining brittle manual arrays of projects and blog posts, the application uses Vite’s compile-time module evaluation:

// src/data/blog/index.ts
import type { Post } from '../types';

const modules = import.meta.glob('./*/index.ts', { eager: true }) as Record<
  string,
  { default: Post }
>;

export const POSTS: Post[] = Object.values(modules)
  .map(m => m.default)
  .sort((a, b) => Number(b.stardate) - Number(a.stardate));

Adding a new blog post or engineering project requires dropping a new folder (b8/ or p17/) into src/data/. During vite build, Vite’s Rollup engine traverses the directory tree, discovers every entry point, parses the content, and bundles the application without touching a single registry file.

Cache invalidation via asset co-location

Placing static images in a generic public/uploads/ directory causes two severe issues: asset files become disconnected from their markdown source, and Vite copies public files without content hashing. If you update an architectural diagram, browsers continue serving the stale cached image.

To solve this, assets are strictly co-located within each article’s directory:

src/data/blog/b1/
  ├── b1.md
  ├── index.ts
  └── mi50.jpg

Inside index.ts, assets are dynamically resolved through the module graph:

const imgs = import.meta.glob('./*.{jpg,png,gif,webp,svg,mp4,webm,mov}', {
  eager: true,
  query: '?url',
  import: 'default',
}) as Record<string, string>;

const imageMap = Object.fromEntries(
  Object.entries(imgs).map(([p, u]) => [p.replace('./', ''), u])
);

export default parsePost(raw, imageMap);

Vite processes every image and video through its asset pipeline, emits fingerprinted filenames (e.g., mi50-T-pCqVn2.jpg) into dist/assets/, and guarantees immediate cache busting whenever an asset changes.

Zero-dependency Markdown AST parser in parseBlocks.ts

Writing technical blog posts directly in TypeScript files creates unacceptable authoring friction: template literal escaping, nested objects, and unreadable git diffs.

Instead of pulling in heavy Markdown compilers like remark, rehype, and unified (which drag in dozens of transitive npm dependencies and inflate bundle size), I built a dedicated, zero-dependency Markdown AST parser in src/data/parseBlocks.ts (~200 lines of TypeScript).

The parser converts raw markdown into a typed discriminated union Block[]:

export type Block =
  | { type: 'h2'; text: string }
  | { type: 'h3'; text: string }
  | { type: 'h4'; text: string }
  | { type: 'p'; text: string }
  | { type: 'lede'; text: string }
  | { type: 'code'; code: string; lang?: string }
  | { type: 'list'; items: string[] }
  | { type: 'callout'; kind: 'warn' | 'info' | 'ok'; title?: string; text: string }
  | { type: 'image'; src: string; alt?: string; caption?: string }
  | { type: 'video'; src: string; caption?: string }
  | { type: 'table'; headers: string[]; rows: string[][] }
  | { type: 'meta'; rows: { k: string; v: string }[] }
  | { type: 'kicker'; text: string }
  | { type: 'signoff'; text: string };

Key architectural capabilities of the parser:

  1. Lede Inference: When frontmatter specifies a kicker, the parser automatically promotes the opening paragraph to an LCARS lede block, applying high-visibility styling.
  2. Media Dispatch by Extension: Authors write standard image syntax ![caption](demo.mp4). The parser inspects the file extension: if it matches .mp4, .webm, or .mov, it emits a video block; otherwise, it resolves the fingerprinted URL in imageMap and emits an image block.
  3. Structured Callouts: Synthesizes ::: warn Title\n...\n::: blocks directly into LCARS diagnostic callout containers.

Vite’s ?raw import loads the .md files as plain strings at compile time: zero runtime network requests and zero client-side parsing overhead.

Solving KaTeX mathematical formula integration

Systems engineering posts require rigorous mathematical modeling: memory bandwidth ceilings, shared memory allocation breakdowns, and Poisson arrival distributions.

Integrating LaTeX math into Markdown often fails because standard Markdown parsers mangle math syntax:

To guarantee mathematical precision, parseBlocks.ts implements a two-pass token masking engine:

function renderMathInBlock(markdown: string): string {
  const mathTokens: string[] = [];

  // Pass 1: Extract Display Math ($$...$$) and Inline Math ($...$)
  let masked = markdown.replace(/\$\$([\s\S]+?)\$\$/g, (_, math) => {
    const idx = mathTokens.length;
    mathTokens.push(katex.renderToString(math.trim(), { displayMode: true, throwOnError: false }));
    return `KATEXMATH${idx}HTAMXETAK`;
  });

  masked = masked.replace(/(?<![\$\\])\$(?!\$)([^\$\n]+?)(?<![\$\\])\$(?!\$)/g, (_, math) => {
    const idx = mathTokens.length;
    mathTokens.push(katex.renderToString(math.trim(), { displayMode: false, throwOnError: false }));
    return `KATEXMATH${idx}HTAMXETAK`;
  });

  // Pass 2: Run standard marked parser safely
  const parsed = marked.parse(masked) as string;

  // Pass 3: Restore rendered KaTeX HTML
  return parsed.replace(/KATEXMATH(\d+)HTAMXETAK/g, (_, idx) => mathTokens[Number(idx)]);
}

The mathematical formulas are pre-compiled to HTML and shielded behind opaque boundary tokens before marked touches the text. Formulas render with pixel-perfect TeX typography and zero client-side layout shift.

Diagnosing the browser runtime failure

During early development, I experimented with using gray-matter, the industry-standard frontmatter parser. While it functioned under Node.js CLI testing, compiling it in Vite triggered an immediate browser crash:

Uncaught ReferenceError: Buffer is not defined
  at Module.eval (gray-matter/lib/parse.js:14)

gray-matter relies internally on Node’s native Buffer API. While Vite bundles dependencies, it purposefully does not inject heavy Node.js polyfills into client builds. Polyfilling Buffer would have bloated the production bundle.

The solution was writing my own clean, 10-line parseFrontmatter regex parser that executes identically in both Node.js build scripts and browser runtimes:

kicker: BUILD LOG · FILE MSN-001-A
signoff: End log · SD 102387 · RAY
meta: Stardate|102387, Hardware|MI50 · gfx906, Resolved|2026-03-01

Static prerendering engine (scripts/prerender.js)

A pure Single Page Application (SPA) on GitHub Pages serves a bare shell:

<div id="root"></div>

When web crawlers, search indexing bots, or social unfurlers (Slack, Discord, LinkedIn OpenGraph crawlers) inspect a raw SPA URL, they receive an empty page without text or meta tags. Hash-based URLs (/#/blog/b1) are also invisible to external preview scrapers.

To make the site fully indexable and shareable, I authored scripts/prerender.js, executed automatically during npm run postbuild.

┌────────────────────────────────────────────────────────────────────────┐
│ POSTBUILD PRERENDERING PIPELINE (scripts/prerender.js)                 │
├────────────────────────────────────────────────────────────────────────┤
│ 1. Read dist/index.html (Compiled SPA Shell)                           │
│ 2. Save unmodified shell to dist/404.html (Client-Side SPA Routing)   │
│ 3. Discover all Project (.md) and Blog (.md) entries in src/data/      │
│ 4. For each route:                                                     │
│    • Extract frontmatter (title, desc, tags, stardate)                 │
│    • Compile markdown body to semantic HTML with KaTeX math            │
│    • Inject semantic markup into <div id="root">                       │
│    • Inject OpenGraph & Twitter meta tags (<title>, og:title, og:desc) │
│    • Write static directory index: dist/blog/b1/index.html             │
└────────────────────────────────────────────────────────────────────────┘

The prerenderer writes real HTML files for every route:

Summary & Systems Takeaways

  1. The Filesystem Is the Database: Compile-time globbing (import.meta.glob) and Markdown AST compilation eliminate CMS complexity and database latency.
  2. Zero-Dependency Simplicity: A ~200-line AST parser in parseBlocks.ts replaced an entire ecosystem of unified/remark plugins while remaining completely typed.
  3. Math Isolation via Token Masking: Pre-rendering KaTeX to inert placeholder tokens prevents Markdown compilers from mangling complex LaTeX syntax.
  4. Hybrid SPA + Prerender: Pairing client-side React hydration with a post-build static HTML generator delivers instant page loads, rich unfurls, and full search engine visibility on static infrastructure.

All postsBack to top