惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
Latest news
Latest news
P
Privacy International News Feed
T
Tenable Blog
Schneier on Security
Schneier on Security
O
OpenAI News
K
Kaspersky official blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
C
Cisco Blogs
L
LangChain Blog
H
Help Net Security
W
WeLiveSecurity
V
Vulnerabilities – Threatpost
C
Cyber Attacks, Cyber Crime and Cyber Security
AWS News Blog
AWS News Blog
博客园 - 叶小钗
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
罗磊的独立博客
C
Check Point Blog
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
MongoDB | Blog
MongoDB | Blog
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy & Cybersecurity Law Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
V2EX - 技术
V2EX - 技术
Spread Privacy
Spread Privacy
博客园 - Franky
T
Threatpost
T
Tor Project blog
P
Proofpoint News Feed
D
DataBreaches.Net
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Heimdal Security Blog
NISL@THU
NISL@THU
大猫的无限游戏
大猫的无限游戏
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
I
Intezer
T
Tailwind CSS Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
美团技术团队
博客园 - 聂微东
T
Threat Research - Cisco Blogs
PCI Perspectives
PCI Perspectives
The Hacker News
The Hacker News
B
Blog RSS Feed

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
A No-Build Markdown Site for Study Notes (or any docs)
Mohamed Idri · 2026-05-02 · via DEV Community

I needed a way to study a stack of notes I had written for a job interview. Eight markdown files, plus some code examples, plus a couple of reference pages. I wanted to read them in a browser with a sidebar, dark mode, and clickable navigation between files. But I did not want to install a static site generator, run a dev server, or maintain a build pipeline for what is basically a personal study folder.

So I built a single-page viewer in one HTML file that reads the markdown directly. The whole site is one index.html plus the markdown files. You open the HTML in a browser, it fetches the markdown, renders it with marked, and you have a real navigable site.

This post walks you through the whole pattern. By the end you will be able to drop the same setup into any folder of notes you want to read like a website.

What we are building

A folder that looks like this:

my-notes/
├── README.md
├── index.html
├── docs/
│   ├── 01-introduction.md
│   ├── 02-setup.md
│   └── 03-deeper-topics.md
└── code-examples/
    └── hello.ts

Enter fullscreen mode Exit fullscreen mode

When you open index.html in a browser you get a sidebar on the left listing every doc, and the rest of the page renders the selected markdown file with code highlighting, tables, and proper typography. Click a link in the sidebar, the URL changes to index.html#docs/02-setup.md, and the content swaps. No page reload, no build step, no node_modules.

Why this approach

Most "docs as code" setups are heavier than they need to be for a small personal project. They assume you want a public site, search, versioning, or a custom theme. For a study folder or a small team handbook, you do not need any of that. You need:

  1. Markdown files you can edit in any editor.
  2. A way to read them in a browser without typing cat file.md in a terminal.
  3. Navigation between files that does not require remembering which folder you are in.
  4. Something that works the same on Windows, macOS, and Linux.

A single HTML file that renders markdown gives you all four. It is also harder to break than a tooling chain, because there is almost nothing to break.

The folder structure

Before the code, let us talk about the organising principles. The exact names do not matter; what matters is the pattern.

One README at the top, as an index

The top-level README.md is not the first chapter. It is a table of contents. Its job is to tell a new reader where to start and what each file contains.

A good index README has:

  • A one-paragraph description of what this folder is.
  • A small table linking each file with a short hook ("what you will learn") and an estimated reading time.
  • Maybe a "TL;DR" if you only have ten minutes.

Here is a snippet of what mine looks like:

## Read in this order

| # | Document | What you'll learn | Time |
|---|----------|-------------------|------|
| 1 | [Introduction](docs/01-introduction.md) | What this is for | 5 min |
| 2 | [Setup](docs/02-setup.md) | How to install everything | 10 min |
| 3 | [Deeper topics](docs/03-deeper-topics.md) | The interesting bits | 25 min |

Enter fullscreen mode Exit fullscreen mode

A reader who lands on the folder for the first time can immediately tell where to go.

Numbered file names for ordering

Notice the 01-, 02-, 03- prefixes. They serve two purposes:

  1. The OS sorts the files alphabetically, so the reading order is also the file-listing order.
  2. The number is a stable handle. You can refer to "doc 04" in conversation and find it instantly.

If you later want to insert a new file between 02 and 03, name it 02b-... or just renumber. Renumbering is annoying but rare in practice.

Group by purpose, not by file type

I see a lot of folders where everything goes in /docs/ because it is markdown. That is fine when you have five files. When you have twenty it stops being fine.

Group by what the content does for the reader, not by file extension:

my-notes/
├── docs/             <- the actual narrative documents
├── code-examples/    <- runnable snippets the docs reference
├── reference/        <- looked-up not read-through (glossary, cheat sheet)
└── assets/           <- images and diagrams

Enter fullscreen mode Exit fullscreen mode

A reader reaches for code-examples/ because they want code, not because it is markdown.

One file per concept, short and focused

If a doc is over 600 lines, split it. Each markdown file should answer one question. The interview prep had eight files, each between 100 and 400 lines. None of them tried to be a textbook.

Writing principles I used in the docs

A quick aside before the HTML viewer.

Lead with a sentence that previews the doc

Every doc opens with a single sentence in italics or quotes that tells you what to expect. That way a reader skimming the index can decide whether to commit five minutes to reading.

Tables for reference, prose for narrative

Tables are great for "look up what this does." They are terrible for "explain this concept." Mixing both keeps a doc readable. The tech-stack page in my prep folder is mostly prose with one or two tables. The glossary is one big table.

Show code with the why

Every code block should be preceded by a sentence that says what it is and why. Code without context is just decoration.

Avoid em dashes and other typography tricks

I deliberately stick to commas, parentheses, and full stops. Em dashes look fancy but they often hide a sentence that should have been split in two.

The single-file HTML viewer

Now the fun part. Here is the whole HTML file in pieces.

The skeleton

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My Notes</title>
    <style>
      /* styles go here */
    </style>
  </head>
  <body>
    <nav>
      <h1>My Notes</h1>
      <a href="#README.md">Home</a>
      <ol>
        <li><a href="#docs/01-introduction.md">Introduction</a></li>
        <li><a href="#docs/02-setup.md">Setup</a></li>
        <li><a href="#docs/03-deeper-topics.md">Deeper topics</a></li>
      </ol>
    </nav>
    <main id="content">
      <div class="loader">Loading...</div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/marked@13.0.0/marked.min.js"></script>
    <script>
      /* router goes here */
    </script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

That is it for the structure. Two main blocks (a sidebar and a content pane), one CDN script for markdown rendering, one script for the routing logic.

Layout with CSS Grid

The sidebar plus content layout is one CSS Grid declaration:

body {
  margin: 0;
  font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  display: grid;
  grid-template-columns: 280px 1fr;
  min-height: 100vh;
}

Enter fullscreen mode Exit fullscreen mode

280px 1fr means the sidebar is a fixed 280 pixels wide, and the rest of the row stretches to fill the remaining space. Three lines, no flexbox, no media queries needed at this stage.

Sticky sidebar

You want the sidebar to stay visible when the content scrolls:

nav {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

Enter fullscreen mode Exit fullscreen mode

position: sticky plus top: 0 plus height: 100vh makes the sidebar pin itself to the top and become its own scroll container. Three properties, no JavaScript.

Dark mode for free

You can detect the user's system preference with prefers-color-scheme:

:root {
  --bg: #0e1116;
  --text: #e6edf3;
  --link: #58a6ff;
  --border: #30363d;
  --code-bg: #1c2128;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #ffffff;
    --text: #1f2328;
    --link: #0969da;
    --border: #d1d9e0;
    --code-bg: #f6f8fa;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

Enter fullscreen mode Exit fullscreen mode

Define your colours as CSS custom properties (variables) once. Override them inside a prefers-color-scheme: light block. Use the variables everywhere else. The OS theme switch flips the whole page automatically.

If you want a manual toggle, you can store a class on <html> (light or dark) and override the variables based on the class instead of the media query. But for a personal study folder, following the OS preference is enough.

Mobile responsive in three lines

@media (max-width: 800px) {
  body { grid-template-columns: 1fr; }
  nav { position: static; height: auto; }
  main { padding: 24px; }
}

Enter fullscreen mode Exit fullscreen mode

On narrow screens, collapse the grid to a single column, unstick the sidebar, and shrink padding. Done.

The router (the JavaScript part)

The interesting trick is using the URL hash as the route. When you click <a href="#docs/01-introduction.md">, the browser changes the hash but does not reload the page. We listen for that change and load the right markdown file.

Step 1: load and render

async function load(path) {
  if (!path) path = 'README.md';
  const content = document.getElementById('content');
  content.innerHTML = '<div class="loader">Loading...</div>';

  const response = await fetch(path);
  if (!response.ok) {
    content.innerHTML = `<h2>Could not load ${path}</h2>`;
    return;
  }

  const markdown = await response.text();
  content.innerHTML = marked.parse(markdown);
  window.scrollTo(0, 0);
}

Enter fullscreen mode Exit fullscreen mode

fetch(path) reads the markdown file from disk (or rather, from wherever the HTML is being served). marked.parse(markdown) turns the markdown text into HTML. We dump that HTML into the content area.

Step 2: react to URL changes

function onHashChange() {
  const path = location.hash.slice(1); // strip the leading "#"
  load(path);
}

window.addEventListener('hashchange', onHashChange);
onHashChange(); // also run once on initial page load

Enter fullscreen mode Exit fullscreen mode

When the user clicks a sidebar link, location.hash changes, the hashchange event fires, and we reload the content. When they reload the page, the same code runs once and shows the right doc based on the URL.

Step 3: rewrite relative links inside the markdown

This is the bit that trips most people up. If your doc says:

See [setup](02-setup.md) for details.

Enter fullscreen mode Exit fullscreen mode

That link, after rendering, points to 02-setup.md, which the browser tries to fetch as a real page. It will fail (or load the raw markdown), not navigate inside our viewer.

We need to rewrite those links to use the hash:

content.querySelectorAll('a[href]').forEach((anchor) => {
  const href = anchor.getAttribute('href');
  if (href.startsWith('http') || href.startsWith('#')) return;
  if (!href.endsWith('.md') && !href.includes('.md#')) return;

  // Resolve relative to the current doc's directory
  const currentDir = (location.hash.slice(1) || 'README.md').split('/').slice(0, -1);
  const segments = href.split('/');
  for (const seg of segments) {
    if (seg === '..') currentDir.pop();
    else if (seg !== '.') currentDir.push(seg);
  }
  anchor.setAttribute('href', '#' + currentDir.join('/'));
});

Enter fullscreen mode Exit fullscreen mode

This walks every link the renderer just produced. If it points to a markdown file with a relative path, we resolve it relative to the current doc's directory and prepend a #. Now [setup](02-setup.md) from inside docs/01-introduction.md becomes #docs/02-setup.md, which our router knows how to handle.

You only need this if you write relative links between your docs. If every link in the markdown is absolute (/docs/...), you can skip it.

The one caveat: file:// and CORS

If you double-click index.html in your file manager, the browser opens it from file://. Some browsers (Chrome, in particular) block fetch() from file:// for security reasons. Your viewer will load with the sidebar, then show "Could not load" when it tries to fetch the markdown.

There are two easy fixes.

Fix 1: serve the folder with any tiny HTTP server

# Pick whichever you have
npx http-server .
python -m http.server 8000

Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080 (or :8000) instead of the file path. Done.

Fix 2: use Firefox

Firefox allows fetch() from file:// by default. If you do not want a server, just use Firefox.

I documented this clearly in the loader's error message so a reader is never confused:

content.innerHTML =
  `<h2>Could not load ${path}</h2>` +
  `<p>If you opened this file directly via <code>file://</code>, ` +
  `your browser may be blocking <code>fetch</code>. Run a tiny ` +
  `server in this folder instead:</p>` +
  `<pre>npx http-server .</pre>`;

Enter fullscreen mode Exit fullscreen mode

A graceful error message is the best documentation.

Styling tips for nice rendering

Marked outputs plain HTML (<h1>, <table>, <pre>, <code>). You can style them like any other page. A few things make the result look more like GitHub or a real docs site:

main h1, main h2 {
  border-bottom: 1px solid var(--border);
  padding-bottom: 6px;
}
main pre {
  background: var(--code-bg);
  padding: 14px 16px;
  border-radius: 6px;
  border: 1px solid var(--border);
  overflow-x: auto;
  line-height: 1.5;
}
main code {
  background: var(--code-bg);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 0.92em;
}
main blockquote {
  border-left: 4px solid var(--accent);
  padding: 4px 16px;
  color: var(--muted);
}
main table {
  border-collapse: collapse;
  width: 100%;
}
main th, main td {
  border: 1px solid var(--border);
  padding: 8px 12px;
}

Enter fullscreen mode Exit fullscreen mode

Underlined H1 and H2, padded code blocks with rounded corners, blockquotes with a left accent stripe, properly bordered tables. Maybe twenty lines of CSS gets you a result that looks intentional.

When you outgrow this

This pattern is great until you need:

  • Search across all docs.
  • Build-time syntax highlighting (the marked CDN does not include it, although you can add highlight.js the same way).
  • Versioning (multiple snapshots of the same docs).
  • A public site with a domain and analytics.

At that point, take a look at:

  • MkDocs with the Material theme. Best balance of "barely any config" and "production-ready output."
  • Docusaurus if you want React components inside your markdown.
  • Astro if you want a faster site with more flexibility.
  • GitHub Pages plus Jekyll if you just want to publish a folder of markdown to the internet.

For everything else, the no-build approach holds up surprisingly well.

Wrap up

A folder of markdown plus a single HTML viewer gets you most of what a docs site does, with no install step and no maintenance burden. The pattern is:

  1. Numbered markdown files in a docs/ folder.
  2. A README.md at the top that is an index, not a chapter.
  3. One index.html with CSS Grid layout, a sticky sidebar, and CSS variables for dark mode.
  4. A small router that listens to hashchange, fetches the markdown, parses it with marked, and rewrites relative links.
  5. A clear error message when file:// blocks fetch.

If you want to study something, document something, or share notes with a small team, this is the smallest amount of effort that gets you a real navigable site. Try it on your next folder of notes.

Happy writing.