Building this site with Claude: design mockups to production architecture

2026.04.19 · 12 min · web, react, vite, architecture, prerender, katex

I used Claude to design the LCARS aesthetic, then engineered a zero-dependency Vite pipeline, custom Markdown AST parser, and static prerendering engine. Here is what the systems architecture looks like and why each decision got made.

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. But rather than using AI as an unconstrained code generator, 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: - No heavyweight component libraries (no Tailwind, MUI, or Bootstrap bloat). - Strict LCARS geometric proportions: pill badges, chamfered corner cutouts, panel borders, and distinctive curved SVG elbow banners (`lcars-elbow`). - CSS custom properties for the entire color token architecture, with dynamic switching between Night Ops (dark amber/orange terminal) and Daylight modes. - Typography: monospaced secondary fonts for telemetry and addresses (`Geist Mono` / `JetBrains Mono`) paired with high-contrast display headings (`Space Grotesk`).

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 leverages 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`. 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: - Subscript underscores (`dheadd_{\text{head}}`) are interpreted as italic delimiters (``). - Matrix asterisks and multiplication operators are parsed as bold delimiters (``). - LaTeX escape backslashes (`\alpha`, `\sum`) are stripped by HTML sanitizers.

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

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

// Pass 1: Extract Display Math (<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">...</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.1056em;"></span><span class="mord">...</span></span></span></span></span>) and Inline Math (<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">...</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.1056em;"></span><span class="mord">...</span></span></span></span>) 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. Furthermore, hash-based URLs (`/#/blog/b1`) are 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: - Crawlers and no-JS clients instantly receive fully rendered semantic markup and metadata. - Shared URLs generate rich OpenGraph preview cards with titles, descriptions, and thumbnails. - When an interactive visitor loads the page, React 19 mounts directly into `

`, seamlessly activating client-side routing and UI state without layout shift. - `dist/404.html` acts as the GitHub Pages fallback handler.

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.