Building this site: a markdown compiler, a theme system and a second brain
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.
A personal note26 · jan 21Read noteClose noteI'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.
A personal note26 · mar 14Read noteClose noteOne 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, transformed into static HTML, and served as-is. Your browser just sits down and eats.
The stack
A personal note26 · jan 21Read noteClose noteThe cursor is blinking. I am not blinking. A personal note26 · jan 24Built a horizontal navbar. It already feels wrong. Too flat.
A personal note26 · jan 25Rip 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 Hindsight. Tailwind 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, which is appealing until prose starts reading like JSX. What I actually needed was smaller: ordinary Markdown for emphasis and links, plus a few structural extensions for [[wiki-links]], typed notes, contextual updates, mathematics and optional multi-path sections. The decision was practical: the article source should remain text first, and every custom token has to earn the maintenance surface it creates.
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.
A personal note26 · jan 25Read noteClose noteIt 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.
The compiler
I needed a way to write articles in Markdown and get styled HTML. Standard requirement. The useful additions turned out to be superscripts, subscripts, keyboard shortcuts, typed notes, wiki-links to my knowledge graph, context annotations, mathematics and a small number of richer layout primitives.
Standard Markdown does not cover all of that. So instead of using something like remark/rehype with plugins, I started with marked and kept adding layers. Each extension was trivial in isolation. Together they formed a custom language with a much larger interaction surface than any individual regex suggested. The later lesson was subtraction: bold and italic should stay Markdown, links should share one taxonomy, and presentational tricks should disappear when structure can do the job.
The key insight was ordering. A structured block has to be isolated before Markdown parses its interior, while code and mathematics must be protected from every textual transform. Reverse two stages and harmless characters become syntax. Obvious in hindsight, difficult to predict until the pipeline has enough interacting parts.
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.
If 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 pipeline still has several stages 1, but the count is no longer a point of pride. What matters is that the compiler originally built for articles also powers the Second Brain and the localhost editor. One engine is valuable; one engine with fewer special cases is much more valuable.
1protect code and math, compile typed notes, restore protected text, process links, definition and alphabetical lists, contextual annotations, parse Markdown, highlight code and resolve footnotes and graph references.
Inventing a syntax
Once you start adding custom syntax, you can't stop.
It began with presentational shortcuts: explicit colours, custom underline, category accent text, decorative separators. Each was "just one more regex." Together they had to coexist with Markdown, [[wiki-links]], code, mathematics and one another. Most were eventually removed. Emphasis returned to **bold** and *italic*; destination semantics moved into links; section rhythm returned to headings and prose. The compiler became more capable by supporting fewer ways to say the same thing.
At some point I had several competing highlight syntaxes. They had bugs, edge cases, and nothing to justify existing alongside bold. Removing all of them was better than selecting a winner. Killing a feature you built is a specific kind of satisfaction.
A personal note26 · jan 30Read noteClose noteThe 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
<details class="ctx-note"><summary class="ctx-note-summary"><span class="ctx-note-meta"><img src="https://avatars.githubusercontent.com/yago-mendoza" alt="" class="ctx-note-avatar" /><span class="ctx-note-date-row"><span class="ctx-note-title">A personal note</span><span class="ctx-note-date">26 · jan 30</span></span></span><span class="ctx-note-toggle"><span class="ctx-note-read">Read note</span><span class="ctx-note-close">Close note</span></span></summary><div class="ctx-note-body"><div class="ctx-note-body-inner"><div class="ctx-note-text">The custom syntax compiler works. Today was a good day.</div></div></div></details>
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.
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, essays and tutorials). Four content categories, each with its own accent color: projects, essays, 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.
CSS 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.
A personal note26 · jan 30Read noteClose noteDark/light works. Auto-switch on route. Smooth manual toggle. The hardcoded color migration takes the whole morning but the result is clean.
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.
A personal note26 · feb 03Read noteClose noteThe 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: 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 concept. Every 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. A throwaway comparison between a CPU and a GPU is treated like 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. A deliberate link to core, annotated as “shares execution resources,” tells you why two ideas 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.
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 exists. I 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.
A personal note26 · feb 06Read noteClose noteSplit fieldnotes into individual files. Add incremental cache. The build is fast now. Spend the evening just writing new notes because it finally feels frictionless. A personal note26 · feb 17Optimization 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.
A personal note26 · feb 06Read noteClose noteThe 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, but 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 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.
A personal note26 · mar 10Read noteClose noteadded 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.
The 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.
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.
A personal note26 · feb 07Read noteClose noteThe article now exists on the platform it describes. Meta.
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.
A personal note26 · feb 07Read noteClose noteThe 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.
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 essays 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 essays 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.
A personal note26 · mar 11Read noteClose notethe "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.
A personal note26 · mar 10Read noteClose notellms.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.
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.