Skip to content
mroot.co
← All writing
Case StudiesJun 27, 2026 · 2 min read

One content source, two sites: how mroot.co works

The portfolio you're reading. A Vite plugin turns Markdown into a typed data module, and both the regular site and the IDE mode render from it — no duplicated content.

Project: mroot.coopen →

This is the case study for the site you're reading right now. mroot.co runs two completely different experiences off one codebase: a normal marketing site, and a working IDE — file explorer, tabs, terminal, command palette — that shows the exact same projects and writing as actual files. Writing a new project means writing one Markdown file, not maintaining two versions of the same content.

Content is compiled, not shipped as Markdown

A small Vite plugin reads everything under content/ at build time, parses the frontmatter, and emits a plain data module exposed as virtual:content. No Markdown parser ships to the browser; the site just imports typed arrays.

vite/content-plugin.ts
function buildModuleSource(contentDir: string): string {  const projects = loadProjects(contentDir);  const writing = loadWriting(contentDir);  return `export const PROJECTS = ${JSON.stringify(projects)};export const WRITING = ${JSON.stringify(writing)};// …profile, skills, songs`;}

src/data.ts re-exports those, and every page — regular or IDE — reads from there. That's the whole reason there's no duplicated content: there's only one source, and it's typed.

The two modes share a URL space

Deep links work the same in both modes. The router pathname maps to the id the IDE already understands, so /projects/atlas-market opens the right file whether you're in website mode or IDE mode.

src/lib/routeToIdeId.ts
export function routeToIdeId(pathname: string): string {  const path = pathname.replace(/\/+$/, '') || '/';  if (path in STATIC_ROUTES) return STATIC_ROUTES[path];  const projectMatch = path.match(/^\/projects\/([^/]+)$/);  if (projectMatch) return 'proj:' + projectMatch[1];  return 'readme';}

IDE mode is a preference stored in localStorage, so a visitor who flips into it stays there across pages and reloads.

Static all the way

The build prerenders every route to HTML with vite-react-ssg, then a postbuild step writes the sitemap and 404. It deploys to Cloudflare Pages on push. The dev server watches content/, so editing a Markdown file hot-reloads the explorer, the writing index, and the command palette at once.

What I'd improve

  • The IDE code window on each project page is generated from frontmatter, not real source; pulling a genuine snippet per project would be more honest.
  • The App.tsx view-model is one large object; splitting it per surface would make it easier to follow.
  • There's no automated test around the content plugin, which is the load-bearing piece — a schema check on frontmatter would catch mistakes before they hit the build.

More on the design decisions behind the rest of the site is on the about page; the other write-ups under /writing are all built from this same pipeline.

Marc Delacruz — full-stack, security-minded.Get in touch →