Infraphysics — building a website that thinks

How a systems engineer built a personal site with a custom markdown compiler, a second brain, and an AI development partner — and what went wrong along the way.


I am not a web developer.

I build things that deal with hardware, systems, infrastructure. My natural habitat is closer to a pipeline than a landing page. So when I decided to build a personal website, the rational move was to use a template. Pick a Hugo theme, write some markdown, deploy, move on. I actually considered it. I looked at Astro. I even thought about buying a pre-built theme.

I did not do the sane thing.

26 · jan 21(1m 15d earlier) I've been carrying a folder of notes around for almost ten years. Notebooks that start from both ends to fit two topics. Post-its that made sense for a week. Drawings with arrows connecting concepts — my own bootleg knowledge graphs, on paper, before I knew that's what they were called. TXTs, markdowns, photos of whiteboards. AI, hardware, optimization, how chips work, how industries scale. All of it scattered across devices and formats and years. The real reason I'm building this site is that I finally have the tools to structure it all. The web stuff is a vehicle. The destination is the brain.

I spent 18 days building a custom React app with its own markdown compiler, a wiki-style knowledge graph I call the Second Brain, and a dual-theme system. In fact, this article is compiled by the same pipeline it describes. The wiki-links in the text point to fieldnotes that were compiled in the same build.

26 · mar 14(6d later) One thing worth clarifying: although the site runs on React, the content you're reading was not generated by your browser. Every article is compiled to HTML at build time. The markdown goes through a 14-step pipeline, gets transformed into static HTML, and gets served as-is. Your browser just sits down and eats.

1. The stack

26 · jan 21(1m 15d earlier) The cursor is blinking. I am not blinking. 26 · jan 24(1m 12d earlier) Built a horizontal navbar. It already feels wrong. Too flat. 26 · jan 25(1m 11d earlier) Rip out the navbar. Spend the whole day rebuilding as a sidebar. Break every layout assumption. Worth it.

That gap between knowing what you want to build and knowing how is wider than it looks from the outside.

Picking tools

I didn't spend a lot of time deciding.

Option Why I looked Why I passed

Hugo / Jekyll Fast, simple, built for blogs Too rigid — I wanted interactive components

Next.js Industry standard, SSR Overkill. The server-side rendering was solving a problem I didn't have

Astro Lightweight, islands architecture Genuinely good. More on this below

Vite + React Fast dev server, I know React Won by inertia

If I were starting today with no learning agenda, Astro would be the right call. Static HTML by default, React islands for interactive bits. The blog posts would ship zero JavaScript. The Second Brain would be an island inside static HTML. Architecturally elegant.

But I chose cohesion over purity. One framework, one routing system, one way of thinking. React was the logic I knew, and the whole project asked for a single dominant logic. The trade-off was conscious.

The final stack: React 19 + TypeScript, Vite 6, Tailwind CSS via CDN 1, marked + Shiki for markdown and syntax highlighting, Cloudflare Pages for deployment with R2 for image storage and KV for view/heart counters.

1not the npm package — the CDN play mode generates classes at runtime, which means dynamic interpolation works without a build step. The npm version would need a PostCSS setup and dynamic class generation would break under build-time purging.

In HindsightTailwind via CDN instead of the npm package means no build-time purging — the CSS payload is larger than it needs to be. And the first implementation was brutal: hardcoded colors everywhere. text-white, bg-gray-900, border-gray-700. Doing the theme migration "later" cost me a full day. Should have set up CSS custom properties on day one. I knew it. I did it anyway.

Why not MDX

I tried MDX for an afternoon. It lets you import React components directly into markdown — <ColorText color="#e74c3c">danger</ColorText> instead of regex. Appealing in theory. But the things I needed were custom inline transforms 1 — not component trees. Writing {#e74c3c:text} feels like writing markdown. Writing <ColorText> feels like writing JSX. The decision was practical: my features are lightweight text transforms, not interactive widgets.

1colored text ({#e74c3c:text}) that flows with prose. Accent text (--text--) that takes the category color with no props. [[wiki-links]] with bidirectional resolution across hundreds of files. Typed blockquotes with seven types. Context annotations with relative timestamps. Backtick protection across the entire pipeline.

Naming things

Four content categories. Naming them took longer than it should.

Projects was obvious. Threads came from systems programming (multiple execution paths sharing a context) and probably from Instagram Threads being everywhere that month. The name stuck because each thread is one train of thought connecting to others. Bits2bricks was the hardest — it's about bridging software and physical engineering, taking what I've learned in bits back to the world of bricks 1. The name is silly. It stays. Fieldnotes came from field notebooks — the kind engineers carry on-site. Each one is a concept, a node in a graph.

1as in atoms. As opposed to software, which obeys whatever the developer felt like that morning.

26 · jan 25(1m 11d earlier) It clicks. The layout makes sense now. Routes, sidebar, content area — everything has a place. First time it feels like a real project and not a homework assignment.

2. The compiler

I needed a way to write articles in markdown and get styled HTML. Standard requirement. But I also wanted colored text, superscripts, keyboard shortcuts, accent text, typed blockquotes, wiki-links to my knowledge graph, and context annotations with timestamps.

Standard markdown doesn't do any of this. So instead of using something like remark/rehype with plugins — which would've been the "correct" choice — I started with marked and kept adding layers. One regex for colored text. Another for superscript. Another for blockquotes. Each one was "just one more regex." And each one was trivial in isolation. But together they form a custom markup language that coexists with standard markdown, and the interaction surface is enormous.

The key insight was ordering. If you write {#e74c3c:some **bold** text}, the color preprocessor needs to fire before marked parses the bold. Reverse the order and the braces survive as literal text. Obvious in hindsight, impossible to predict if you don't think about it.

The other key insight was backtick protection: before any custom processing runs, the pipeline extracts every code block and inline code span, replaces them with placeholder tokens, runs all preprocessing on the protected text, then restores the originals. Anything inside backticks is invisible to the preprocessors. They can't touch it. They don't even know it's there.

TipIf you're building a markdown extension system, protect code content first. Extract it, process everything else, restore it. Trying to make your regex "skip code blocks" is a losing game — the edge cases will eat you alive.

The full pipeline has 14 steps. I won't list them all 1 because the details aren't what matters here. What matters is that this compiler, originally built to render articles, turned out to be the same engine that powers the Second Brain. And later, when I built the localhost editor, it was this same compiler running in real time — compiling notes as I type, catching broken references on every keystroke. A tool built for one purpose that kept finding new ones.

1protect backticks → custom preprocessors → typed blockquotes → restore backticks → external URLs → side images → definition lists → alpha lists → context annotations → marked.parse → strip heading formatting → Shiki highlighting → post-processors → footnotes. Then two global passes: wiki-link resolution and cross-document links.

Inventing a syntax

Once you start adding custom syntax, you can't stop.

It began with colored text. {#e74c3c:danger} → danger. One regex, one <span>. Then superscript. Then subscript. Then keyboard shortcuts. Then underline. Then accent text. Each one was "just one more regex." Each one was trivial in isolation. But together they have to coexist with standard markdown, and also with [[wiki-links]], and also with code blocks, and also with each other. The collision surface 1 is enormous.

1my _underline_ syntax uses word-boundary lookbehind to avoid matching snake_case, but it still conflicts with markdown's native _italic_ syntax. I settled on _text_ for underline and *text* for italic. The boundary is thin. Every new rule has to be tested against every existing one.

At some point I had an ==highlight== syntax. It had bugs, edge cases, and nothing to justify existing alongside bold and accent. I replaced it with --accent text-- — takes the category color automatically, no props. Killing a feature you built is a specific kind of satisfaction.

26 · jan 30(1m 6d earlier) The compiler WORKS. Colors, superscript, blockquotes, wiki-links. I write this at 1:30pm and by 4pm I have wiki-link hover previews working. I can't stop clicking things.

The rewrite

On February 5th I caught myself fixing the fixes of my previous fixes.

When that happens, the codebase is telling you something. It's telling you to stop.

The old Compiler was regex scattered across three files. The blockquote parser couldn't handle nested formatting. Definition lists inside blockquotes produced broken HTML. Every fix introduced a new edge case, and every edge case demanded another fix. The Custom Syntax features had become unpredictable. No single feature was complex — the complexity lived in their interactions.

The rewrite centralized the Compiler into three layers: compiler.config.js (single source of truth for all syntax rules — changing it invalidates the entire build cache), build-content.js (the pipeline orchestrator), and article.css (one stylesheet, category accents through CSS vars). The new version could parse blockquotes with nested definition lists inside them. The old version literally couldn't. That's how you know a rewrite was the right call — the new version enables something the old version couldn't express.

Context annotations

One feature worth calling out, because it captures the philosophy. I wanted a way to add timestamped diary entries inside articles — not footnotes, not blockquotes, but small dated cards that feel like margin notes from a project journal:

MARKDOWN Copy

<div class="ctx-note"><img src="https://avatars.githubusercontent.com/yago-mendoza" alt="" class="ctx-note-avatar" /><div class="ctx-note-body"><div class="ctx-note-entry"><div class="ctx-note-date-row"><span class="ctx-note-date">26 · jan 30</span><span class="ctx-note-relative">(1m 6d earlier)</span></div><span class="ctx-note-text">The custom syntax compiler works. Today was a good day.</span></div></div></div>

This produces a card with the date, a relative timestamp ("5 weeks ago"), and the text. It's the feature I use most in projects articles. Because every project has a story, and the story has dates, and the dates carry context that the polished prose can't. When you read "5 weeks ago" next to a note about the compiler finally working, you feel the time. That's the whole point.

3. The visual layer

I wanted the site to feel like a terminal. Dark background, monospace headings, lime green accents. But the blog needed to be light — long-form reading on a dark background is miserable.

The site has two worlds: lab (dark, projects and Second Brain) and blog (light, threads and tutorials). Four content categories, each with its own accent color: projects, threads, bits2bricks, fieldnotes.

The theme system is a three-layer cascade: CSS custom properties define colors on :root and [data-theme="light"], Tailwind maps them to semantic tokens (th-base, th-primary), and components only use the tokens. Flip one attribute on <html> and everything switches. No JavaScript color logic, no conditional class names.

CSS Copy

:root { --bg-base: #0a0a0a; --text-primary: rgba(255,255,255,0.87); --cat-projects-accent: #a3e635; /* identity color — same in both themes */ } [data-theme="light"] { --bg-base: #ffffff; --text-primary: rgba(0,0,0,0.87); }

Route auto-switching uses useLayoutEffect so the user never sees the wrong theme, even for a single frame. Manual toggle (Shift+T) uses a smooth fade through a .theme-transitioning class that only transitions standard properties like background-color and border-color — never CSS custom properties. I tried transitioning --art-accent directly once and got a visual seizure. The browser double-interpolates: the variable resolves mid-transition while the property using it runs its own transition. Two hours and a literal headache.

TipCSS color-mix() is one of those features that seems minor until you build a theme system. Writing color-mix(in srgb, var(--art-accent) 10%, transparent) to get a tinted version of any accent color — in any category, in either theme — is absurdly powerful. No JS. Pure CSS. The one thing that catches you off guard if you come from dark-first design: light-mode borders and surfaces need 2-3x the opacity of their dark counterparts. rgba(0,0,0,0.10) on white is almost invisible. I forgot to adjust this. Several times.

I could write three more sections about the accent cascade and the custom property transition trap. But the honest truth is that while these bugs were painful — and they were, hours of my life — they're not what makes this project interesting. The CSS works. It's fine. The interesting part is what lives inside it.

26 · jan 30(1m 6d earlier) Dark/light works. Auto-switch on route. Smooth manual toggle. The hardcoded color migration takes the whole morning but the result is clean.

4. The Second Brain

This is the part where the project stopped being a website and started being something I actually use every day.

The origin story

I've been collecting notes since 2016. Not casual notes — obsessive, multi-format, cross-domain knowledge collection. It started in college: an industrial engineering student who picked up software because every engineer should, and fell down the rabbit hole. AI, low-level computing, optimization algorithms, chip architecture, robotics, industry processes. Every topic opened four more topics. Every rabbit hole led to another rabbit hole.

The medium kept changing. First, physical notebooks that I'd start from both ends — front for one topic, back for another — trying to squeeze two subjects into one book. Then Post-its. Then conceptual drawings where I'd force myself to make them beautiful, because making something beautiful forces you to think before you draw. Arrows connecting ideas. Diagrams showing how the parts of a system relate. My own bootleg knowledge graphs, on paper, years before I knew that's what they were called.

Then TXT files. Then markdown. Then more TXT files because I forgot about the markdown files. Then photos of whiteboards that lived on my phone and never got organized. For years, the passion was the only thing pulling me forward. Every time I learned something, I'd write it down somewhere. The problem was that "somewhere" was everywhere.

26 · feb 03(1m 5d earlier) The thing about collecting notes for ten years is that eventually the collection is the problem. You have hundreds of files across dozens of formats and you can feel how they connect — the structure is in your head, you just can't grep it. The whole point of this project is to take that latent graph and externalize it into something you can actually navigate.

The AI inflection

Before AI, organizing all this was a war of attrition. You'd read something, four new topics would branch off, and you'd have to manually chase each one. The passion kept you going, but the bottleneck was always human processing speed.

Then the tools changed. First I'd use AI subscriptions — the browser-based ones — to process batches of notes. Upload a TXT, ask it to structure it, get back organized markdown. It worked, but it was slow. Copy-paste workflows. Switching between browser and local files. Formatting issues. Token limits.

When I moved to Claude Code — working directly in the terminal, with the AI reading my files, understanding the project structure, and writing directly to disk — everything accelerated. And extended thinking is something else entirely. I'm still discovering what these systems can do organizationally — not just code generation, but knowledge organization. The ability to take a messy folder of notes and produce structured, cross-referenced, hierarchically organized content. It's like having the passion that was always driving you forward, but now with sixteen horses pulling the cart instead of four 1.

1that analogy sounds dramatic, but if you've ever spent a Saturday afternoon manually cross-referencing notes between three different notebooks, you know exactly what I mean.

More on how extended thinking changes the game: threads/Every time we think we've found the ceiling, the ceiling leaves.

How it actually works

Every fieldnote is a markdown file with frontmatter: a unique ID, a hierarchical address, a name, and a date. The address system uses // as a hierarchy separator — ML//Transformer//attention means "attention, under Transformer, under ML." The hierarchy is semantic, not organizational: all files live flat in one directory. The structure exists in the addresses.

The [[wiki-links]] in the body create references. Links at the bottom are trailing refs — intentional connections that appear on both sides. If note A has a trailing ref to note B, the connection shows up on A and B. One ref, bilateral display. Annotations explain why:

MARKDOWN Copy

## Interactions

- [[avBp6NIF|synthetic data]] : : Synthetic data is the fuel, model collapse is the exhaust

The bidirectional resolution is one of the things I'm most proud of technically. The build processes every note, extracts every reference, computes the reverse links, and generates a complete relationship graph — every connection typed, annotated, and navigable from either end. The kind of thing you'd build with a graph database if you were being serious. I built it with JSON and a build script. It works.

One file, one conceptEvery fieldnote is exactly one idea. Not a topic. Not a chapter. One atomic concept. "Attention mechanism" is one note. "Softmax" is another. "KV cache" is another. They connect through wiki-links, not through being in the same document. This is what makes the graph possible — and what makes navigation feel like thinking.

So... it's Obsidian?

I showed it to a friend. He looked at the Second Brain for about thirty seconds and said "this is like Wikipedia if Wikipedia had a panic attack." I still don't know if that was a compliment. Another friend asked why I didn't just use Notion. That one stung a little. But Notion wouldn't let me write [[egoxqpmC]] :: shares execution resources and have it resolve bilaterally across hundreds of notes with aliases and hierarchical parents. I checked.

The third question I kept getting was: "so... it's Obsidian?"

And honestly, on the surface, it kind of is. Markdown notes. Wiki-links. A graph of connections. If you squint, the Second Brain looks like Obsidian built by someone who didn't know Obsidian existed. (I did know. I just wanted to build mine anyway, which is worse.)

But the more I built, the more the differences became the whole point.

Obsidian is a note editor with a graph attached. The Second Brain is a graph browser with notes attached. The difference sounds semantic until you use both. In Obsidian, you spend most of your time writing — the graph is a visualization you open occasionally to feel smart about your note-taking system. In the Second Brain, you spend most of your time navigating — clicking through connections, discovering neighborhoods, watching the structure reveal things you didn't know you'd written. One prioritizes creation. The other prioritizes discovery. Both are valid. They're just not the same tool.

The first technical difference is references. In Obsidian, [[links]] point to filenames. Rename a file and every reference across your vault needs rewriting — Obsidian does this automatically, which works great until it doesn't (merge conflicts, external tools editing the files, sync issues). The Second Brain uses UIDs — stable 8-character identifiers generated when you create a note. [[OkJJJyxX]] points to a note by its identity, not its name. Rename the note, change its address, restructure the entire hierarchy — the UID never changes. Zero references break. No rewriting needed. Ever. It's the difference between addressing a letter to "John Smith, 42 Oak Street" (hope he doesn't move) and addressing it to a permanent ID 1 that follows him everywhere.

1like a Social Security number for concepts. The address changes. The identity doesn't. In graph theory terms: the vertex label is mutable but the vertex ID is immutable. References point to the ID.

The second difference is trailing refs. Obsidian has backlinks — if you mention a note anywhere in another note's body, it shows up as an automatic backlink. Useful, but noisy. Every casual mention counts the same as a deliberate connection. Write "unlike [[CPU]], the GPU handles..." and Obsidian treats that throwaway comparison the same as a carefully curated relationship. The Second Brain separates these: body mentions are body mentions (tracked, but lightweight), and trailing refs are intentional connections — written at the end of a note, with optional annotations explaining the relationship. [[Z9W6rweD]] :: shares execution resources tells you why these two things are connected, not just that they are. And the system only needs the ref on one side — it crosses automatically to the other. Write it once, see it on both pages.

Then there's the topology layer — and this is where Obsidian and the Second Brain aren't even playing the same game anymore. Obsidian gives you a force-directed graph visualization. It's pretty. You can zoom and drag nodes around. It tells you approximately nothing 1. The Second Brain computes actual graph topology at build time: which notes form connected islands (groups that can reach each other through links), which notes are bridges (remove them and an island splits in two — with a criticality score from 0 to 100%), which notes are isolated (completely disconnected from everything), and which notes are hubs (ranked by percentile of total connections). This isn't a visualization you stare at — it's structural analysis you use. You can filter by island, show only bridges, scope the search to a connected component. The graph stops being decoration and starts being a navigation instrument.

1I'm being slightly unfair. The Obsidian graph is useful for spotting clusters and isolated notes visually. But it's a visualization, not an analysis tool. It shows you what your graph looks like. It doesn't tell you what your graph means.

And then there's drift detection — the feature that made me realize I'd accidentally built something I couldn't get anywhere else. The algorithm looks at pairs of notes that share neighbors but aren't directly connected. If note A links to C and D, and note B also links to C and D, but A and B don't link to each other — that's suspicious. They're probably related. The system surfaces the top three suggested missing links per note, ranked by evidence strength, with the shared neighbors listed as justification. It's a recommender system for your own knowledge graph. "Hey, you wrote about these two things separately, and they both connect to the same three concepts, but you never connected them to each other. Maybe you should."

The filtering system has toggles for isolated notes, leaves, bridges, and hubs; a depth range slider for the naming hierarchy; a heatmap that looks like a GitHub contribution graph where each cell is a day and you can click to filter by creation date. Three search modes — by name, by content, by backlinks. Session tracking that marks which notes you've already opened (blue = visited, purple = not yet) so you can toggle "unvisited only" and see what's left to explore.

On building what existsI could have used Obsidian. Or Logseq. Or Roam. Any of them would have been faster to set up and better maintained than anything I'd build alone. But none of them would have taught me what I learned by building it: that a graph is more than its nodes, that references need stable identities, that the difference between a mention and a connection is the difference between noise and signal. Sometimes the point of building something that exists is finding out why it exists the way it does — and where it could exist differently.

From monolith to tool

The first version of the Second Brain was terrible. All notes lived in a single _fieldnotes.md — one giant document. The build parsed the whole thing and generated a single JSON blob. This worked at 20 notes. At 60+ it was slow enough to notice.

The split into individual files changed everything. The current system has two tiers: a metadata index (loaded eagerly — addresses, references, search text, no HTML) and content files (one per note, loaded on demand when you open it). Fast initial paint, instant search, lazy content. The build went from 4 seconds to ~400ms for a single-file change.

26 · feb 06(1m 2d earlier) Split fieldnotes into individual files. Add incremental cache. The build is fast now. Spend the evening just writing new notes because it finally feels frictionless. 26 · feb 17(19d earlier) Optimization pass at 251 notes. Three changes: (1) sync cache path — if a note's HTML is already cached, content loads in the same render frame instead of waiting for an async round-trip. (2) prefetching — when you open a note, all visible targets (connections, mentions, neighbors, wiki-links) get fetched in the background. By the time you click, the content is already there. (3) O(N) neighborhood — the old algorithm scans all notes to find siblings/children for each note (O(N^{2}), ~63k comparisons at 251 notes). Replace with a pre-built map: one pass to bucket notes by parent, then O(1) lookups. Net result: clicking a search result no longer flashes the old note, navigation between connected notes feels instant when prefetched, and index initialization drops from O(N^{2}) to O(N).

This is also where the architecture choice pays off. The Second Brain needs instant navigation between notes, hover previews when you mouse over a [[link]], real-time search across hundreds of notes, and smooth graph transitions. None of this works with full page reloads. The Second Brain is an application, not a document, and it needed client-side state management. Astro's islands could have handled this — the blog as static HTML, the brain as a React island. It would have been more elegant architecturally. But in practice, sharing the same routing, theme system, and wiki-link resolution between the brain and the blog simplified development enormously.

The safety net

Once the graph hit 60+ notes with hundreds of cross-references, things started breaking in ways I couldn't see. I'd rename a concept, forget to update a reference three files away, and discover the broken link weeks later.

So I built a validation pipeline that runs on every build. Broken [[wiki-links]] fail the build — hard stop. Self-references, missing parents, circular refs, and isolated notes get warnings. The most interesting check is segment collisions: if I create CPU//cache and networking//cache, the validator flags it — "cache" appears at two different paths. Same concept? Probably, refactor into one note. Intentionally different? Declare it with distinct to suppress the warning. It's data integrity, but for ideas.

26 · feb 06(1m 2d earlier) The validator catches 14 broken references on its first run. Fourteen. Half are wiki-links in posts pointing to fieldnotes I've renamed. Without the validator, those would be dead links in production for weeks.

The rename script was born from a specific disaster. I manually renamed a concept called "chip" to "component//chip" — 23 references across 15 files. I went through them by hand in VS Code's search panel. I missed three. The build caught two (broken [[refs]]). The third was a distinct entry in another note that the build doesn't validate — it survived as a stale reference until I happened to re-read that note weeks later. After that I wrote rename-address.js: one command, dry-run by default, touches every reference atomically. And move-hierarchy.js for cascading renames — because rename-address.js renames ONE exact address, not its children. I learned that by isolating an entire subtree. Twice.

Five scripts now, if you're counting. None of them were planned — each one was a different disaster. But together they protect internal consistency: the graph's version of type safety. References resolve to real notes, hierarchies have actual parents, connections exist on purpose. Break one of those and nothing crashes. The note loads, the links render. You just can't tell it's lying to you until you click something that should exist and doesn't.

The build validator catches structural breaks — dead [[refs]] halt everything. check-references.js goes deeper: notes with zero connections sitting in the graph like forgotten furniture, trailing refs duplicated on both sides (the system only needs one), address segments that imply a parent note nobody ever wrote, fuzzy collisions like chip//MCU vs chip//MPU at 95% string similarity. First time I ran it: 40 duplicate bilateral refs. Forty. preflight.js runs before you create anything — feed it the addresses you're about to write and it flags collision risks, shows bilateral status with neighbors, catches body patterns that should be trailing refs instead of prose ("unlike X" is an interaction, not a sentence). analyze-pairs.js is the microscope: point it at two notes and it maps the full relationship — hierarchy, trailing refs, body mentions, shared neighbors.

Build catches compilation errors, check-references catches semantic rot, preflight catches pre-creation blind spots, analyze-pairs catches relational gaps, the rename scripts catch structural moves gone wrong. Five axes, no overlap. I didn't plan it — every disaster just happened to break something new.

The sidebar dashboard

The Second Brain has its own management panel — a sidebar with tools that evolved over time:

First came the directory tree with scoping. Click a folder to filter. Click a concept to navigate. The tree prunes itself based on active filters — if you're searching for "attention" and the blockchain branch has no matches, it disappears while the filter is active.

Then topology detection — the system finds disconnected islands in the graph. Notes that form isolated clusters with no bridge to the main body of knowledge. This is surprisingly useful: it shows you what you haven't connected yet.

Then the word count histogram — a quick visual of note depth across the graph. Click a bar to filter by length. Then the calendar heatmap — and this one deserves its own paragraph.

The calendar

The calendar isn't about productivity tracking. It's about mental archaeology. When I filter by a two-week window, I see what I was focused on during that period. Not what I think I was focused on — what I actually wrote about. And the gaps between periods reveal what I missed.

If I spent two weeks deep in machine learning six months ago, and then two weeks on the same topic last month, the calendar lets me isolate each period and compare. What did I know then? What do I know now? What's missing between the two? Those gaps — the concepts I didn't write about because I didn't understand them yet — are exactly where the next batch of notes should go. The calendar turns time into a filter for thought.

Then centrality bars — tiny inline indicators showing how connected each note is relative to the whole graph. A quiet signal for what's well-integrated and what's floating.

The graph

The force-directed graph was originally a visual flex. I'll be honest about that. I'd been playing with threads/graph visualization libraries for years without going anywhere, because I didn't have enough structured data to make it interesting. Now I do.

But here's what I didn't expect: I actually use it. Not every day, but regularly. The graph shows you the shape of your thinking. Three clusters are immediately visible — hardware, machine learning, and blockchain — with bridges between them. The isolated nodes are obvious. The over-connected hubs are obvious. And sometimes you see a connection you didn't know was there, because two notes reference the same concept from completely different domains.

Building the tree view for browsing the hierarchy was the moment the project crossed another threshold. When I first got it working — the tree expanding, the search filtering in real time, notes appearing and disappearing as I typed — I spent twenty minutes just clicking around my own notes, watching concepts I'd written months ago suddenly have visible neighbors. Not a website anymore. A tool.

The 2D graph uses react-force-graph-2d. The 3D version uses react-force-graph-3d with Three.js. Both read from the same pre-computed relevance data. Neither is architecturally complex — the hard part was having clean graph data, and the Second Brain already provides that.

26 · mar 10(2d later) added area selection mode to the graph. hold Shift and drag to draw a rectangle — every node inside gets selected (cyan highlight). Ctrl/Cmd+click adds individual nodes. the selection prompt shows the count and names, plus a "Copy structured context" button that dumps the selected notes as structured text for Claude. also added subgraph isolation: select a cluster, hit "Isolate cluster", and the graph filters to only those nodes and their internal connections. the background switches to a subtle violet dot grid to signal you're in a subgraph. breadcrumb navigation at the top-right lets you drill deeper (area-select within a subgraph) or step back. each level preserves its state. the idea is that the graph should be a workspace, not just a visualization — you should be able to isolate a region of your knowledge, study its internal structure, and navigate back without losing context.

The editor

This part escalated.

It started as "I want to edit a note without opening VS Code." A small CodeMirror panel, inline, on localhost only. But then I needed wiki-link autocomplete — type [[ and get a dropdown of all notes. Then I needed term detection — the editor scanning the body for mentions of known concepts that aren't linked yet, highlighting them in purple, offering to insert the link with one click. Then missing parent detection — if I write a note at ML//Transformer//attention//sink but ML//Transformer//attention doesn't exist, the editor warns me and offers to create the stub.

Now it's a mini-IDE. I navigate between notes, the editor panel follows, I fix a typo or add a reference, the compiler runs in real time, the build catches broken links on every keystroke, and the page refreshes via HMR. The compiler that was built to render articles at build time now runs live in the browser as I type. A tool built for one purpose that kept finding new ones — the theme of this entire project.

TipThe term suggestion system works by scanning the note body for unlinked mentions of known note names. It skips frontmatter, existing wiki-links, code blocks, and trailing refs. When it finds a match, it shows a purple highlight with a one-click option to insert the link. It's the kind of feature that sounds simple and is simple — 50 lines of regex, a StateField in CodeMirror, done. But it catches connections I would've missed every single time.

5. The workflow

Here's how a typical session works, because understanding the workflow explains why everything was built this way.

I start with raw material. A photo of a hand-drawn diagram from 2019. A TXT file from a course I took. A markdown document from last week's research session. I bring these to Claude Code in the terminal. Together we transcribe, structure, fact-check, and cross-reference. At every step I supervise the output — reading forces me to re-learn the material, and sometimes I catch something that triggers a search, and the search produces an extra note I didn't plan for.

Once the notes exist as fieldnotes, Claude helps organize them into the address hierarchy. I let Claude drive the organizational logic — its internal consistency is often better than my improvised taxonomy. This echoes something Peter Steinberg 1 advocates: the system that organizes should be the system that retrieves.

1the Obsidian community has a long-running conversation about whether folder structures should serve human intuition or machine retrieval. Steinberg argued that the system that organizes should be the system that retrieves. If the AI organizes your notes, the AI will know where things are.

Then bulk import into the Second Brain. Build. Check references. The validator catches missing parents, broken links, segment collisions. The interactive resolver fixes them — create a stub here, add a distinct annotation there, queue a merge for later. Then I open the localhost editor and polish: remove redundant bullet points, add missing wiki-links (the term detector catches most of them), write openers for notes that start too abruptly.

The calendar heatmap reflects the session. The graph grows. And sometimes — this is the part that keeps me going — Claude suggests, based on the cluster of notes I just imported, what kind of article I could write. "You have twelve notes on attention mechanisms, three on training dynamics, and two on model collapse. That's a thread." And it's right. The notes compound into something I couldn't see when they were scattered across notebooks.

26 · feb 07(1m 1d earlier) The article now exists on the platform it describes. Meta.

6. Teaching an AI to maintain it

On February 6th I started using Claude Code to help build the site. LLMs are simultaneously incredible and terrible at maintaining a codebase. Incredible because they can hold the entire architecture in context and write code faster than I can think. Terrible because they don't remember anything between sessions, and they'll happily "improve" code you didn't ask them to touch.

The incredible and the terrible

The incredible part was real. The first time I described a feature — the category accent system, how colors need to cascade through headings and borders and blockquote bars — and the AI produced changes across article.css, index.html, and two React components, all consistent, all correct. I would have spent an hour on that. It took ninety seconds. That moment changes how you think about development velocity.

The terrible part was also real. I asked for a fix on the sidebar border — it was using the wrong opacity in light mode. A one-line CSS change. The AI fixed the border, and also "improved" the theme switching logic in ThemeContext.tsx, and also "cleaned up" what it considered redundant CSS variables in index.html, and also added comments explaining the code it had just rewritten. I spent forty minutes reverting its improvements. The border opacity is still wrong in the commit history because I was so annoyed I forgot to push the actual fix.

This wasn't a one-time thing. It was a pattern. Every session, something got "improved." It would add docstrings to functions I hadn't asked about. It would refactor a variable name because the old one was "unclear." It would consolidate two CSS rules into one because they "did the same thing" — except they didn't, they targeted different states, and now hover styles were broken.

The AI wasn't being malicious. It was being helpful. It saw code that could be "better" and it "improved" it. The problem is that "better" for an AI means "more consistent, more documented, more conventionally structured." "Better" for a maintainer means "exactly what it was before, except for the one thing I asked you to change."

The instructions file

So I wrote a CLAUDE.md — instructions for the AI. An onboarding guide for someone with amnesia who needs to re-read it every morning.

The rules are simple:

File create/delete → update the README tree

Edit markdown in pages/ → run the build

Rename a fieldnote → use the rename script, never by hand

Change the syntax pipeline → update the authoring guide

Any code change → do ONLY what was requested. Nothing more.

That last rule took three drafts. The first version said "try not to change things you weren't asked to change." Too soft. The AI interpreted "try" as "consider briefly, then do it anyway." The second said "only modify files directly related to the request." It found creative ways to argue that ThemeContext.tsx was "directly related" to a CSS border fix. The third version says "do ONLY what was requested. Nothing more." Blunt, unambiguous, impossible to lawyer around. It mostly works.

26 · feb 07(1m 1d earlier) The AI adds comments to a file I ask it to leave alone. We're getting there.

Hooks

Even with CLAUDE.md, the AI forgets. It edits a fieldnote and doesn't run the build. It applies a rename without dry-running first. The instructions exist, but they rely on the AI checking them at the right moment.

So I added hooks — shell scripts that fire automatically when the AI uses specific tools. Write a fieldnote file and a reminder injects: "FIELDNOTE MODIFIED — run the build when done." Start a rename and a checklist appears: "did you dry-run first?"

The hooks don't block anything. They just make sure the AI sees the reminder at the right moment. It's the difference between "please remember to lock the door" (CLAUDE.md) and a sign taped to the door that says "DID YOU LOCK THIS?" (hooks). CLAUDE.md catches 80% of mistakes. Hooks catch another 15%. The last 5% is me reading every diff before pushing. It's not bulletproof. But the combination is surprisingly effective.

The scripts — check-references.js, move-hierarchy.js, preflight.js, analyze-pairs.js — are a collaboration. I describe what I need, Claude builds the pipeline. And here's the thing: Claude is very good at organizational tooling. The scripts it builds for knowledge management — validation, cross-referencing, bulk operations — tend to work on the first try. It's the kind of structured, rule-based pipeline work where an LLM's consistency shines. I'd trust it to build a reference checker before I'd trust it to pick a CSS color.

7. Everything else

Some features deserve a mention without deserving a section.

Giscus comments, view counts, hearts. Integrated because I wanted to learn how, not because I needed them. My friends don't have GitHub. But the integration was easier than expected — Cloudflare KV for counters, Giscus for comments, both configured in an afternoon. The repository exists, the task board is there, and even if nobody uses the comments, I learned how API endpoints work on Cloudflare Workers. Worth it.

Search. Full-text across all categories, with match counts and excerpts. The search palette opens with Ctrl+K. Fieldnote search is separate, built into the sidebar, with instant filtering as you type. Neither was architecturally hard — the data was already indexed. The challenge was UI, and UI is just patience.

Performance. Lazy loading views with React.lazy. Content prefetching for the next likely note. O(1) lookups in the graph relevance engine. I'll be honest: Claude did most of the heavy lifting on performance. Bundle splitting, fetch optimization, cache invalidation — that's not my domain. I described what felt slow, Claude fixed it. The graph and the note loading were the main culprits. Both are fast now.

English and Spanish. The threads in Spanish are for my circle — friends, family, university classmates. I send them direct links. The rest is English because if you're going to put something on the internet, you might as well make it readable by anyone interested. I'm a divulgador 1 — but I'm starting small. This site is the small. The circle that reads the Spanish threads is the proof of concept. The English content is the scale.

1Spanish for someone who makes complex topics accessible. "Science communicator" is close but too formal. "Explainer" is too casual. Divulgador is the right word, and it doesn't have an English equivalent.

Deployment. Cloudflare Pages, connected to the repo, auto-deploys on push. Chose it over Vercel and Netlify because the R2 + KV + Workers ecosystem meant I could keep images, counters, and edge functions in one platform. Setup was boring. Boring is good for deployment.

26 · mar 11(3d later) the "2026 SaaS stack" that keeps circulating is 13 services: Vercel, Supabase, Clerk, Stripe, Resend, PostHog, Sentry, Upstash, Pinecone. I use none of them. there is no backend. no auth. no database. no analytics. no error tracking beyond "the build failed." the entire data layer is a folder of markdown files and the query engine is a build script. every service on that list solves a problem this site doesn't have.

Crawlers and AEO. This is an SPA -- crawlers see an empty <div id="root"> unless you do something about it. The solution is a Cloudflare edge function that intercepts bot requests and injects the full page content (title, description, body text) as server-rendered HTML. The metadata lives in og-manifest.json, auto-generated at build time from every post and fieldnote.

For LLM crawlers specifically, there are two files. llms-full.txt dumps every published article as plain text -- auto-generated, exhaustive, zero maintenance. llms.txt is the summary: site structure, curated intro, and article listings. It used to be manually maintained, which meant it was permanently out of sync. Now both files auto-generate from the same build pipeline that produces the sitemap and RSS feed. One npm run build and everything updates: Open Graph manifest, sitemap, RSS, llms.txt, llms-full.txt. No human memory required.

26 · mar 10(2d later) llms.txt was manually maintained for days. every new article required editing it by hand, and it was already out of sync when I checked. moved it to auto-generation in the build pipeline. the fieldnote count, the article listings, the URLs -- all derived from the same source of truth now. one less thing to forget.

8. What this is really about

This project was not the rational choice. The CSS is not how a frontend engineer would write it. The Compiler is not how a language designer would build it. There are parts I'd do differently — the Tailwind CDN choice, the hardcoded colors on day one, the monolith fieldnotes file I should have split from the start.

But here's what I know that I didn't know 18 days ago. I know that overflow: hidden is the answer to questions you can't articulate yet. I know that CSS custom properties resolve at computation time, not declaration time, and that this matters more than it sounds. I know that color-mix() is unreasonably powerful and that transitioning CSS variables will give you a headache. I know that a wiki-link is really just a regex with ambition, and that if you build enough of them, a graph appears — and the graph is more interesting than any individual note.

I know that a backtick 1 sounds like it should be a tapas dish. I know what useLayoutEffect does and I know when it matters. I know that an AI assistant will rewrite your theme switching if you ask it to fix a border color. I know that 777 lines can disappear in an afternoon and you feel lighter after.

1on a Spanish keyboard, it's the key right next to the P, above the + key. I'd been pressing it accidentally for years without knowing what it was called.

I'm an industrial engineer who picked up code because every engineer should. Becoming a developer was never the plan — moving faster was. This site exists because I've been collecting knowledge about how things work for a decade, and I finally have the tools to structure it.

The compiler was fun. The theme system was educational. The CSS bugs were painful. But the reason I keep building this thing — the reason there are 300+ notes in the graph now, the reason I photograph old notebooks and transcribe them at 11pm, the reason the editor keeps growing features I didn't plan — is simpler than any of that.

I believe knowledge compounds faster when it's written down, and even faster when it's shared. The notes I took in 2016 about how CPUs work are connected to the notes I took last week about how transformers work, and the connection is real, and the graph shows it, and you can click through it right now.

This site is my lab, my notebook, and my proof of work. The Second Brain is the part that thinks. Everything else is just the frame.

I am still not a web developer. But the website doesn't seem to mind.