A => README.md +110 -0
@@ 1,110 @@
+# quillhut-static
+
+A minimalist, client-side static site generator that turns Markdown files into a fast, searchable SPA using vanilla JavaScript and Caddy.
+
+## Directory Structure
+
+/opt/www/quillhut-static/ <-- Web Root
+├── index.html <-- The engine & layout
+├── nav.json <-- Header/Footer links
+├── index.md <-- Homepage content
+├── rss.xml <-- Generated Feed
+├── sitemap.xml <-- Generated Sitemap
+├── manifest.json <-- Standalone pages (about, index, etc)
+├── posts/
+│ ├── manifest.json <-- List of all post filenames
+│ ├── post-1.md <-- Individual post
+│ └── post-2.md
+
+## Setup & Installation
+
+### 1. Configure Caddy
+
+Your server must handle "fallback" routing so that if a user refreshes `/posts/my-story`, Caddy knows to serve `index.html`.
+
+#### Caddyfile Example
+
+```bash
+yourdomain.com {
+ root * /opt/www/quillhut-static/
+ file_server
+
+ # Metadata headers
+ header /rss.xml Content-Type "application/rss+xml; charset=utf-8"
+ header /sitemap.xml Content-Type "application/xml; charset=utf-8"
+
+ # SPA Routing
+ try_files {path} /index.html
+}
+```
+
+### 2. Configure `nav.json`
+
+```json
+{
+ "header": [
+ { "name": "home", "path": "/" },
+ { "name": "posts", "path": "/posts" }
+ ],
+ "footer": [
+ { "name": "source", "path": "https://sourcehut.org", "external": true }
+ ]
+}
+```
+
+### 3. Create a Post
+
+Posts live in `/posts/`. Use YAML-like frontmatter:
+
+```markdown
+---
+title: My First Post
+date: 2024-05-22
+tags: tech, web
+author: linuxgoose
+---
+Post content goes here...
+```
+
+### 4. Update the Manifest
+
+Add the filename to `posts/manifest.json`:
+
+```json
+["post-1.md", "post-2.md"]
+```
+
+## Admin Workflow
+
+Because this is a static site without a database, robots (like RSS readers or Google) cannot see the content generated by JavaScript. You must manually generate the "static" XML files when you publish new content.
+Generating RSS & Sitemap
+
+1. Open your website in a browser.
+
+2. Open the Developer Console (`F12` or `Ctrl+Shift+I`).
+
+3. Type the following command and hit Enter:
+
+```bash
+generateRSS(); generateSitemap();
+```
+
+4. Two files (rss.xml and sitemap.xml) will download to your computer.
+
+5. Upload these files to your server's root directory.
+
+## Features
+
+- Zero Build Step: Just upload Markdown and refresh.
+
+- Tag Filtering: Automatic tag cloud generation from post metadata.
+
+- Clean URLs: No .html extensions required. (set `USE_CLEAN_PATHS` to *true*)
+
+- Dark Mode: Automatic support based on system preferences.
+
+- Lightweight: Powered by marked.js and vanilla JS.
+
+## License
+
+Open source under the MIT License. Contributions welcome.<
\ No newline at end of file
A => index.html +464 -0
@@ 1,464 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <base href="/">
+ <title>quillhut-static</title>
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+ <style>
+ :root {
+ --bg: #ffffff; --text: #111; --gray: #555555; --border: #cccccc;
+ --accent: #00B8D9; --code-bg: #f4f4f4; --premium: #d32f2f;
+ }
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #1f2128; --text: #f8f9fa; --gray: #999; --border: #444; --code-bg: #2d2d2d;
+ }
+ }
+ html, body { height: 100%; margin: auto; }
+ body {
+ background-color: var(--bg); color: var(--text); display: flex; flex-direction: column;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ max-width: 960px; padding: .5rem 1rem 0; line-height: 1.5;
+ }
+ a { color: #357edd; text-decoration: none; transition: color .15s ease-in; }
+ a:hover { text-decoration: underline; }
+ @media(prefers-color-scheme: dark) { a { color: #5ca4f8; } }
+
+ nav { display: flex; flex-direction: column; justify-content: space-between; padding: .25rem; }
+ @media screen and (min-width: 48em) { nav { flex-direction: row-reverse; } }
+
+ nav ul { display: inline-flex; font-size: .825rem; list-style: none; margin: 0; padding: 0; color: var(--gray); }
+ nav ul li:not(:first-child)::before { content: "|"; margin: 0 .25rem; }
+
+ .breadcrumbs { display: inline-flex; align-items: baseline; }
+ .brand { font-weight: bold; color: var(--text) !important; }
+ .context .title::before { color: var(--accent); content: "›"; margin: 0 .25rem; font-size: 1rem; }
+ .context .title span { font-size: .825rem; }
+
+ .tag-cloud { margin: 1rem 0; display: flex; gap: 0.5rem; flex-wrap: wrap; }
+ .tag {
+ padding: 0.2rem 0.6rem; background: var(--code-bg); border: 1px solid var(--border);
+ font-size: 0.8rem; cursor: pointer; border-radius: 3px; transition: 0.2s;
+ }
+ .tag.active { background: var(--accent); color: white; border-color: var(--accent); }
+ .post-listing { margin-bottom: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 1rem; }
+
+ .post-info-subtle {
+ font-size: 0.85rem; color: var(--gray); margin-bottom: 1.5rem;
+ display: flex; gap: 10px; align-items: center; opacity: 0.8;
+ }
+ .post-info-subtle .post-tags-list { display: flex; gap: 8px; }
+ .post-info-subtle .post-tag-item { color: var(--accent); }
+
+ main { flex-grow: 1; padding: .25rem; }
+ h1, h2, h3 { font-weight: 500; line-height: 1.2; margin-bottom: .5rem; }
+ hr { border: 0; border-top: 1px solid var(--border); margin: 1rem 0; }
+
+ pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; border: 1px solid var(--border); }
+ code { font-family: monospace; background: var(--code-bg); padding: 0.2rem; }
+
+ footer { color: var(--gray); font-size: .75rem; margin-top: auto; padding: 1rem 0; border-top: 1px solid var(--border); }
+ .footer-links li { display: inline; }
+ .footer-links li:not(:last-child)::after { content: "|"; margin: 0 .5rem; }
+
+ /* DEBUG OVERLAY STYLES */
+ #debug-log {
+ position: fixed; bottom: 10px; right: 10px; background: rgba(0,0,0,0.85); color: #00ff00;
+ font-family: monospace; font-size: 11px; padding: 10px; max-width: 350px;
+ z-index: 9999; border-radius: 5px; border: 1px solid #444; pointer-events: none;
+ max-height: 250px; overflow-y: auto; box-shadow: 0 0 10px rgba(0,0,0,0.5);
+ }
+ .log-entry { margin-bottom: 4px; border-bottom: 1px solid #333; padding-bottom: 2px; }
+ .log-error { color: #ff5555; }
+ .log-warn { color: #ffff55; }
+ </style>
+</head>
+<body>
+
+<div id="debug-log"></div>
+
+<header>
+ <nav>
+ <div class="session">
+ <ul id="header-nav"></ul>
+ </div>
+ <div class="breadcrumbs">
+ <a class="brand" href="/">~quillhut-static</a>
+ <div class="context">
+ <span class="title"><span id="bc-name">index</span></span>
+ </div>
+ </div>
+ </nav>
+ <hr style="display:block; border:0; border-top:1px solid var(--border); margin: 0.5rem 0 1.5rem 0;">
+</header>
+
+<main id="content"></main>
+
+<footer style="display: flex">
+ <div style="text-align: left;">
+ <p>Published with quillhut - an open source static site generator. You can contribute on <a href="https://git-sh.linuxgoose.com/~linuxgoose/quillhut-static">sourcehut</a> (v0.0.1)</p>
+ </div>
+ <div style="text-align: right; margin-left: auto;">
+ <ul id="footer-nav" class="footer-links" style="list-style:none; padding:0; margin:0;"></ul>
+ </div>
+</footer>
+
+<script>
+ const CONFIG = {
+ USE_CLEAN_PATHS: true,
+ POSTS_DIR: 'posts/',
+ MANIFEST: 'posts/manifest.json',
+ NAV_JSON: 'nav.json',
+ DEBUG_MODE: false // Set to true to see logs and a green box overlay
+ };
+
+ const contentDiv = document.getElementById('content');
+ const bcName = document.getElementById('bc-name');
+ const debugDiv = document.getElementById('debug-log');
+ let allPosts = [];
+ let activeTags = new Set();
+
+ // Hide debug box immediately if flag is false
+ if (!CONFIG.DEBUG_MODE && debugDiv) {
+ debugDiv.style.display = 'none';
+ }
+
+ // LOGGING UTILITY - Handles both UI and Console
+ function log(msg, type = 'info') {
+ if (!CONFIG.DEBUG_MODE) return; // Exit early if debug is off
+
+ if (debugDiv) {
+ const entry = document.createElement('div');
+ entry.className = `log-entry log-${type}`;
+ entry.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
+ debugDiv.appendChild(entry);
+ debugDiv.scrollTop = debugDiv.scrollHeight;
+ }
+ console.log(`DEBUG [${type}]: ${msg}`);
+ }
+
+ async function tryFetch(file) {
+ const url = file.startsWith('/') ? file : '/' + file;
+ log(`Fetching: ${url}`);
+
+ try {
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+
+ const text = await r.text();
+
+ if (text.trim().startsWith('<!DOCTYPE html>')) {
+ log(`Rejected: ${url} returned HTML`, 'warn');
+ throw new Error("Invalid file content (HTML returned)");
+ }
+ return text;
+ } catch (e) {
+ log(`Fetch failed: ${url} (${e.message})`, 'error');
+ throw e;
+ }
+ }
+
+ function formatURL(path, isExternal) {
+ if (isExternal) return path;
+ const cleanPath = path.replace(/\.md$/, '');
+ return CONFIG.USE_CLEAN_PATHS ? `/${cleanPath}` : `?p=${cleanPath}`;
+ }
+
+ async function buildNav() {
+ try {
+ const data = JSON.parse(await tryFetch(CONFIG.NAV_JSON));
+ document.getElementById('header-nav').innerHTML = data.header.map(item =>
+ `<li><a href="${formatURL(item.path, item.external)}">${item.name}</a></li>`
+ ).join('');
+ document.getElementById('footer-nav').innerHTML = data.footer.map(item =>
+ `<li><a href="${formatURL(item.path, item.external)}">${item.name}</a></li>`
+ ).join('');
+ log("Navigation menu built");
+ } catch (e) { log("Nav failed to load", 'warn'); }
+ }
+
+ function parseFrontmatter(text) {
+ const regex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
+ const match = text.match(regex);
+ const meta = {};
+ let content = text;
+ if (match) {
+ content = text.replace(regex, '');
+ match[1].split('\n').forEach(line => {
+ const [key, ...val] = line.split(':');
+ if (key && val.length) meta[key.trim()] = val.join(':').trim();
+ });
+ }
+ return { meta, content };
+ }
+
+ async function render() {
+ const params = new URLSearchParams(window.location.search);
+ let route = CONFIG.USE_CLEAN_PATHS ? window.location.pathname : params.get('p');
+
+ if (!route || route === '/' || route === '/index.html' || route === 'index') {
+ route = 'index';
+ }
+
+ route = route.replace(/^\/+|\/+$/g, '');
+ log(`Route detected: ${route}`);
+ bcName.innerText = route;
+
+ if (route === 'posts') {
+ await renderPostsPage();
+ return;
+ }
+
+ try {
+ const fileName = route.endsWith('.md') ? route : `${route}.md`;
+ let rawMd;
+ try {
+ log(`TRYING SUBFOLDER: ${CONFIG.POSTS_DIR}${fileName}`);
+ rawMd = await tryFetch(CONFIG.POSTS_DIR + fileName);
+ } catch (e) {
+ log(`TRYING ROOT: ${fileName}`);
+ rawMd = await tryFetch(fileName);
+ }
+
+ const { meta, content } = parseFrontmatter(rawMd);
+
+ let metaHtml = '';
+ if (meta.date || meta.tags) {
+ const tagList = meta.tags ? meta.tags.split(',').map(t => `<span class="post-tag-item">#${t.trim()}</span>`).join(' ') : '';
+ metaHtml = `<div class="post-info-subtle">${meta.date ? `<span>${meta.date}</span>` : ''}${meta.date && meta.tags ? '<span>|</span>' : ''}<div class="post-tags-list">${tagList}</div></div>`;
+ }
+
+ contentDiv.innerHTML = metaHtml + marked.parse(content);
+ document.title = meta.title ? `${meta.title} | quillhut-static` : `${route} | quillhut-static`;
+ log("Content rendered successfully");
+
+ } catch (err) {
+ log(`FATAL: Could not render ${route}. ${err.message}`, 'error');
+ await render404(route);
+ }
+ }
+
+ async function renderPostsPage() {
+ log("Loading Posts list...");
+ let pageTitle = "Posts";
+ let pageIntro = "";
+
+ try {
+ const raw = await tryFetch('posts.md');
+ const { meta, content } = parseFrontmatter(raw);
+ if (meta.title) pageTitle = meta.title;
+ if (content.trim()) pageIntro = marked.parse(content);
+ } catch (e) { log("Optional posts.md info not found", 'info'); }
+
+ bcName.innerText = pageTitle.toLowerCase();
+ document.title = `${pageTitle} | quillhut-static`;
+
+ contentDiv.innerHTML = `<h1>${pageTitle}</h1>${pageIntro}<div class="tag-cloud" id="tag-cloud"></div><div id="posts-container">Loading posts...</div>`;
+
+ try {
+ const manifestRaw = await tryFetch(CONFIG.MANIFEST);
+ const manifest = JSON.parse(manifestRaw).filter(f => !f.startsWith('.'));
+ allPosts = await Promise.all(manifest.map(async file => {
+ const raw = await tryFetch(CONFIG.POSTS_DIR + file);
+ const { meta } = parseFrontmatter(raw);
+ return { ...meta, file: file.replace('.md', ''), tags: meta.tags ? meta.tags.split(',').map(t => t.trim()) : [] };
+ }));
+ updateDisplay();
+ log(`Manifest loaded: ${allPosts.length} posts found`);
+ } catch (e) {
+ log(`Manifest error: ${e.message}`, 'error');
+ contentDiv.innerHTML += "<p>Error loading manifest.</p>";
+ }
+ }
+
+ function updateDisplay() {
+ const container = document.getElementById('posts-container');
+ const cloud = document.getElementById('tag-cloud');
+ const uniqueTags = [...new Set(allPosts.flatMap(p => p.tags))].sort();
+ cloud.innerHTML = uniqueTags.map(t => `<span class="tag ${activeTags.has(t) ? 'active' : ''}" onclick="toggleTag('${t}')">${t}</span>`).join('');
+ const filtered = allPosts.filter(p => activeTags.size === 0 || p.tags.some(t => activeTags.has(t)));
+ container.innerHTML = filtered.length ? filtered.map(p => `
+ <div class="post-listing">
+ <div class="post-meta">${p.date || 'No Date'} • By ${p.author || 'Anonymous'}</div>
+ <h3><a href="${formatURL(p.file, false)}">${p.title || p.file}</a></h3>
+ <div style="margin-top:5px">${p.tags.map(t => `<small style="color:var(--accent);margin-right:8px">#${t}</small>`).join('')}</div>
+ </div>
+ `).join('') : '<p>No matches found.</p>';
+ }
+
+ window.toggleTag = (tag) => {
+ activeTags.has(tag) ? activeTags.delete(tag) : activeTags.add(tag);
+ updateDisplay();
+ };
+
+ async function render404(file) {
+ try {
+ const errorMd = await tryFetch('404.md');
+ const { content } = parseFrontmatter(errorMd);
+ contentDiv.innerHTML = marked.parse(content);
+ } catch {
+ contentDiv.innerHTML = `<h1>404</h1><p>Path <code>${file}</code> not found.</p>`;
+ }
+ bcName.innerText = "404";
+ }
+
+ document.addEventListener('click', e => {
+ const link = e.target.closest('a');
+ if (CONFIG.USE_CLEAN_PATHS && link && link.href.startsWith(window.location.origin) && !link.getAttribute('target')) {
+ const url = new URL(link.href);
+ const hasExtension = url.pathname.includes('.') && !url.pathname.endsWith('.md');
+ if (!hasExtension) {
+ e.preventDefault();
+ log(`Link clicked: ${url.pathname}`);
+ history.pushState(null, '', link.href);
+ render();
+ }
+ }
+ });
+
+ async function generateRSS() {
+ log("Generating full RSS feed...");
+ try {
+ const manifestRaw = await tryFetch(CONFIG.MANIFEST);
+ // Exclude index.md and any hidden files
+ const manifest = JSON.parse(manifestRaw).filter(f => !f.startsWith('.') && f !== 'index.md');
+
+ const feedItems = [];
+
+ for (const file of manifest) {
+ try {
+ const rawMd = await tryFetch(CONFIG.POSTS_DIR + file);
+
+ // 1. BLOCK ACCIDENTAL HTML INJECTION
+ // If Caddy lied and gave us index.html, this stops the XML from breaking
+ if (rawMd.trim().toLowerCase().startsWith('<!doctype html')) {
+ log(`Skipping ${file}: Request returned HTML index instead of Markdown`, 'warn');
+ continue;
+ }
+
+ const { meta, content } = parseFrontmatter(rawMd);
+
+ // 2. RENDER HTML
+ const htmlContent = marked.parse(content);
+ const slug = file.replace('.md', '');
+ const url = `${window.location.origin}${formatURL(slug, false)}`;
+
+ // 3. CATEGORIES/TAGS
+ const tagsArray = meta.tags ? meta.tags.split(',').map(t => t.trim()) : [];
+ const categoryTags = tagsArray.map(t => `<category><![CDATA[${t}]]></category>`).join('\n ');
+
+ // 4. BUILD ITEM WITH CDATA
+ // CDATA tells the RSS reader: "Treat everything inside as raw text/html, don't try to parse it as XML"
+ feedItems.push(`
+ <item>
+ <title><![CDATA[${meta.title || slug}]]></title>
+ <link>${url}</link>
+ <guid isPermaLink="true">${url}</guid>
+ <pubDate>${new Date(meta.date || Date.now()).toUTCString()}</pubDate>
+ ${categoryTags}
+ <description><![CDATA[${htmlContent}]]></description>
+ </item>`);
+ } catch (e) {
+ log(`Failed to include ${file}: ${e.message}`, 'warn');
+ }
+ }
+
+ const rssTemplate = `<?xml version="1.0" encoding="UTF-8" ?>
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title><![CDATA[quillhut-static]]></title>
+ <link>${window.location.origin}</link>
+ <description><![CDATA[Latest posts from quillhut-static]]></description>
+ <language>en-us</language>
+ <atom:link href="${window.location.origin}/rss.xml" rel="self" type="application/rss+xml" />
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
+ ${feedItems.join('')}
+ </channel>
+ </rss>`;
+
+ const blob = new Blob([rssTemplate], { type: 'application/xml' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = 'rss.xml';
+ link.click();
+ log("Fixed RSS feed generated.");
+ } catch (err) {
+ log(`Failed to generate RSS: ${err.message}`, 'error');
+ }
+ }
+
+ async function generateSitemap() {
+ log("Generating sitemap.xml from manifests...");
+ try {
+ // 1. Fetch both manifests
+ const [rootManifestRaw, postManifestRaw] = await Promise.all([
+ tryFetch('manifest.json'),
+ tryFetch(CONFIG.MANIFEST)
+ ]);
+
+ const rootFiles = JSON.parse(rootManifestRaw).map(f => ({ path: f, isPost: false }));
+ const postFiles = JSON.parse(postManifestRaw).map(f => ({ path: CONFIG.POSTS_DIR + f, isPost: true }));
+
+ const allFiles = [...rootFiles, ...postFiles];
+
+ // 2. Map files to URL entries
+ const urlEntries = await Promise.all(allFiles.map(async (fileObj) => {
+ try {
+ const rawMd = await tryFetch(fileObj.path);
+ if (rawMd.trim().toLowerCase().startsWith('<!doctype html')) return '';
+
+ const { meta } = parseFrontmatter(rawMd);
+
+ // Extract filename for slugging
+ const fileName = fileObj.path.split('/').pop();
+ const slug = fileName.replace('.md', '');
+
+ // Logic: index.md in root is "/"
+ // post-1.md in posts/ is "/post-1"
+ // about.md in root is "/about"
+ const urlPath = (slug === 'index' && !fileObj.isPost) ? '' : formatURL(slug, false);
+ const fullUrl = `${window.location.origin}${urlPath}`;
+
+ return `
+ <url>
+ <loc>${fullUrl}</loc>
+ <lastmod>${meta.date || new Date().toISOString().split('T')[0]}</lastmod>
+ <changefreq>${slug === 'index' ? 'daily' : 'monthly'}</changefreq>
+ <priority>${slug === 'index' ? '1.0' : '0.8'}</priority>
+ </url>`;
+ } catch (e) {
+ return '';
+ }
+ }));
+
+ const sitemapTemplate = `<?xml version="1.0" encoding="UTF-8"?>
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+ ${urlEntries.filter(u => u !== '').join('')}
+ </urlset>`;
+
+ const blob = new Blob([sitemapTemplate], { type: 'application/xml' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = 'sitemap.xml';
+ link.click();
+ log("sitemap.xml generated via manifests.");
+ } catch (err) {
+ log(`Failed to generate sitemap: ${err.message}`, 'error');
+ }
+ }
+
+ async function generateAllMetadata() {
+ await generateRSS();
+ await generateSitemap();
+ log("All metadata files updated!");
+ }
+
+ buildNav();
+ render();
+ window.onpopstate = render;
+</script>
+
+</body>
+</html><
\ No newline at end of file
A => index.md +22 -0
@@ 1,22 @@
+# Hello, World!
+
+Welcome to my personal corner of the web. This site is powered by **quillhut**, a minimalist static site engine that renders Markdown directly in your browser.
+
+### About this site
+This is a boilerplate example of the `index.md` file. It serves as the homepage for the SPA (Single Page Application).
+
+* **Fast:** No heavy frameworks, just vanilla JS.
+* **Markdown-first:** Write in plain text, and it just works.
+* **Clean:** No trackers, no ads, just content.
+
+### Recent Projects
+You can find my latest writing in the [posts](/posts) section. I focus on:
+1. **Minimalist Web Design**
+2. **Linux & Open Source**
+3. **Static Site Architectures**
+
+---
+
+> "Simplicity is the ultimate sophistication." — Leonardo da Vinci
+
+If you want to reach out, feel free to check my [security.txt](/security.txt) for contact details or verify my identity via my [PGP key](/pgp-key.txt).<
\ No newline at end of file
A => manifest.json +4 -0
@@ 1,4 @@
+[
+ "index.md",
+ "posts.md"
+]<
\ No newline at end of file
A => nav.json +9 -0
@@ 1,9 @@
+{
+ "header": [
+ { "name": "Home", "path": "index" },
+ { "name": "Journal", "path": "posts" }
+ ],
+ "footer": [
+ { "name": "source", "path": "https://sourcehut.org", "external": true }
+ ]
+}<
\ No newline at end of file
A => posts/manifest.json +4 -0
@@ 1,4 @@
+[
+"post-1.md",
+"post-2.md"
+]<
\ No newline at end of file
A => posts/post-1.md +26 -0
@@ 1,26 @@
+---
+title: Why I Built My Own SSG
+date: 2026-01-10
+tags: web, javascript, minimalism
+author: linuxgoose
+---
+
+# Why I Built My Own SSG
+
+I wanted a blog that didn't require a complex build pipeline or a 500MB `node_modules` folder. By using **vanilla JavaScript** and **Caddy**, I created a system where the browser does the heavy lifting.
+
+### The Technical Stack
+- **Engine:** Vanilla JS Router
+- **Parser:** [Marked.js](https://marked.js.org/)
+- **Server:** Caddy with SPA routing
+- **Metadata:** YAML-style frontmatter
+
+### Code Example
+Here is how I handle the routing logic in my `index.html`:
+
+```javascript
+function render() {
+ let route = window.location.pathname;
+ // logic to fetch and render markdown
+}
+```<
\ No newline at end of file
A => posts/post-2.md +21 -0
@@ 1,21 @@
+---
+title: The Joy of Small Web
+date: 2026-01-12
+tags: philosophy, web
+author: linuxgoose
+---
+
+# The Joy of Small Web
+
+The modern internet is bloated. Websites are often several megabytes just to display a few paragraphs of text. The **Small Web** movement is about reclaiming the simplicity of the early internet.
+
+### What makes a "Small" website?
+1. **No Tracking:** Respecting user privacy by default.
+2. **Text-Heavy:** Prioritizing information over high-res hero images.
+3. **No Frameworks:** Reducing the "tax" paid by the user's CPU.
+
+When you browse this site, you aren't loading a React bundle. You are loading a single HTML file and a few Markdown strings. It feels **instant** because it is.
+
+---
+
+*If you enjoyed this, check out my other posts in the [archive](/posts).*<
\ No newline at end of file