Hayden Bleasel, an expert developer from OpenAI, released Blume, an open-source documentation framework. Blume shipped to npm as version 1.0.3 the same day. It is as simple as Drop Markdown into a folder and ship a docs site. No app boilerplate is written or maintained afterward. The project is MIT-licensed and open sourced. What is Blume? Blume is a command-line tool paired with a component library for docs. It reads a folder of Markdown or MDX files. From that folder, it produces a production-grade documentation site. That output ships navigation, search, theming, and Open Graph images. Configuration stays optional and is added one file at a time. The code is a TypeScript monorepo; the published package sits at packages/blume. Blume’s own documentation, under apps/docs, is built with Blume itself. It requires Node.js 22.12 or newer. It runs with Bun, pnpm, npm, or yarn. How Blume Works? Under the surface, Blume generates and drives a hidden Astro project. First, the CLI loads blume.config.ts and scans your content into a graph. Next, it writes an Astro project into a .blume/ directory. Astro then renders every page through a single catch-all route. That route imports Blume’s shipped components, the generated data, and your overrides. On each run, .blume/ regenerates, and only changed files are rewritten. As a result, hot reload stays fast during editing. The core theme ships no client framework JavaScript. Consequently, pages score well on Core Web Vitals by default. When you need full control, blume eject promotes the runtime into a standalone Astro app. That ejected project still depends on the blume package. Run blume dev</button> <button id=”prevBtn” disabled>‹ Prev</button> <button id=”nextBtn”>Next ›</button> <button id=”resetBtn”>↺ Reset</button> <div class=”spacer”></div> <label class=”toggle on” id=”aiToggle”><span class=”dot”></span> Show AI outputs</label> </div> <div class=”foot”> <span>Blume v1.0.3 · MIT · Node.js 22.12+ · Astro + Vite</span> <span>Interactive explainer by <a class=”mtp” href=”https://www.marktechpost.com” target=”_blank” rel=”noopener”>Marktechpost</a></span> </div> </div> <script> (function () { var root = document.getElementById(“blume-explainer”); var token = root.querySelector(“#token”); var nodes = root.querySelectorAll(“.node[data-stage]”); var aiNodes = root.querySelectorAll(“.ai-node”); var aiBranches = root.querySelectorAll(“.ai-branch”); var stepLabel = root.querySelector(“#stepLabel”); var stepTitle = root.querySelector(“#stepTitle”); var stepText = root.querySelector(“#stepText”); var termText = root.querySelector(“#termText”); var runBtn = root.querySelector(“#runBtn”); var prevBtn = root.querySelector(“#prevBtn”); var nextBtn = root.querySelector(“#nextBtn”); var resetBtn = root.querySelector(“#resetBtn”); var aiToggle = root.querySelector(“#aiToggle”); // token positions per stage (cx, cy) var pos = [ [85, 90], [250, 90], [470, 90], [700, 70], [700, 163], [842, 90] ]; var steps = [ { label: “Stage 1 / 6”, title: “A folder of Markdown”, term: “docs/ index.mdx guide.mdx api.mdx”, text: “You start with .md or .mdx files in a folder. There is no starter to clone and no app boilerplate to maintain.” }, { label: “Stage 2 / 6”, title: “The CLI loads your config”, term: “blume dev ✓ loaded blume.config.ts”, text: “blume dev loads blume.config.ts. It reads your content sources, theme tokens, and options, all type-checked by a schema.” }, { label: “Stage 3 / 6”, title: “Content is scanned into a graph”, term: “scanning content … ✓ 3 pages, nav inferred”, text: “Blume scans every page into a content graph. Navigation is inferred from your files, so you rarely hand-write it.” }, { label: “Stage 4 / 6”, title: “A hidden Astro project is generated”, term: “writing .blume/ ✓ only changed files”, text: “Blume writes a hidden Astro project into .blume/. It regenerates each run, rewriting only changed files, so hot reload stays fast.” }, { label: “Stage 5 / 6”, title: “Astro + Vite render the pages”, term: “rendering via catch-all route ✓”, text: “Astro renders every page through one catch-all route. It imports Blume’s components, the generated data, and any overrides you add.” }, { label: “Stage 6 / 6”, title: “Static HTML ships to dist/”, term: “blume build ✓ dist/ + search index”, text: “The build outputs static HTML and a local search index into dist/. The core theme ships no client framework JS, helping Core Web Vitals.” } ]; var current = -1; var playing = false; var playTimer = null; function paintNodes(active) { nodes.forEach(function (n) { var s = parseInt(n.getAttribute(“data-stage”), 10); var rect = n.querySelector(“rect”); if (s === active) { rect.setAttribute(“stroke”, “#ff7000”); rect.setAttribute(“stroke-width”, “2.5”); rect.setAttribute(“fill”, “#241108”); } else if (s < active) { rect.setAttribute(“stroke”, “#7a4a1e”); rect.setAttribute(“stroke-width”, “1.5”); rect.setAttribute(“fill”, “#1a120b”); } else { rect.setAttribute(“stroke”, “#33281f”); rect.setAttribute(“stroke-width”, “1.5”); rect.setAttribute(“fill”, “#17120e”); } }); } function moveToken(i) { if (i < 0) { token.style.opacity = 0; return; } token.style.opacity = 1; token.style.transition = “cx .55s cubic-bezier(.4,0,.2,1), cy .55s cubic-bezier(.4,0,.2,1)”; token.setAttribute(“cx”, pos[i][0]); token.setAttribute(“cy”, pos[i][1]); } function render(i) { current = i; if (i < 0) { stepLabel.textContent = “Ready”; stepTitle.textContent = “Press Run to watch the build”; stepText.textContent = “Blume needs only a folder of Markdown. Everything below is generated for you and thrown away on each run, unless you eject.”; termText.textContent = “npx blume init”; paintNodes(-1); moveToken(-1); } else { var s = steps[i]; stepLabel.textContent = s.label; stepTitle.textContent = s.title; stepText.textContent = s.text; termText.textContent = s.term; paintNodes(i); moveToken(i); } prevBtn.disabled = (i <= 0); nextBtn.disabled = (i >= steps.length – 1); reportHeight(); } function next() { if (current < steps.length – 1) render(current + 1); } function prev() { if (current > 0) render(current – 1); } function play() { playing = true; runBtn.textContent = “ Pause”; if (current >= steps.length – 1) render(0); else next(); playTimer = setInterval(function () { if (current >= steps.length – 1) { stop(); return; } next(); }, 1700); } function stop() { playing = false; runBtn.textContent = current >= steps.length – 1 ? “↺ Replay” : “ Run blume dev”; if (playTimer) { clearInterval(playTimer); playTimer = null; } } runBtn.addEventListener(“click”, function () { if (playing) { stop(); } else { if (current >= steps.length – 1) render(-1); play(); } }); nextBtn.addEventListener(“click”, function () { stop(); next(); }); prevBtn.addEventListener(“click”, function () { stop(); prev(); }); resetBtn.addEventListener(“click”, function () { stop(); render(-1); }); aiToggle.addEventListener(“click”, function () { var on = aiToggle.classList.toggle(“on”); aiNodes.forEach(function (n) { n.style.opacity = on ? 1 : 0.18; var r = n.querySelector(“rect”); r.setAttribute(“stroke”, on ? “#7a4a1e” : “#33281f”); }); aiBranches.forEach(function (b) { b.style.opacity =