Makko, the people-oriented static site generator made for blogging.

Cleanup, added shell prototype

+406 -8
+1
.gitignore
··· 7 7 old_src 8 8 result 9 9 doc/web 10 + .zed
-2
doc/blog/english.md
··· 8 8 created: 2025-07-17T21:09:58+00:00 9 9 --- 10 10 11 - ![Makko, a pink rabbit with huge glasses, trying to balance.](/media/docs.svg) 12 - 13 11 ### Some notes before you begin: 14 12 Makko assumes you have some level of knowledge working with websites, specifically some knowledge about writing and reading basic HTML. You can refer to MDN which has guides for HTML and more [here on their website](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content) 15 13
+1
doc/blog/styles.css
··· 213 213 gap: 0.8lh; 214 214 margin: 0; 215 215 padding-left: 40px; 216 + padding-top: 10px; 216 217 } 217 218 218 219 li {
+6
doc/blog/toc.js
··· 71 71 const tocContainer = document.createElement("div"); 72 72 tocContainer.id = "toc-container"; 73 73 tocContainer.innerHTML = ` 74 + <img 75 + src="/media/docs.svg" 76 + height="128px" 77 + alt="Makko, a pink rabbit with huge glasses, trying to balance." 78 + loading="lazy" 79 + ></img> 74 80 <div id="toc-content"> 75 81 ${buildTocHTML(tree)} 76 82 </div>
+1 -1
doc/makko.json
··· 23 23 "on_modify": null 24 24 }, 25 25 "hashes": { 26 - "TGm1pyN0WiY": "ckFxMoBFaXU" 26 + "TGm1pyN0WiY": "dCZMIYD12b0" 27 27 } 28 28 }
+1
doc/templates/feed.html
··· 19 19 loading="lazy" 20 20 alt="The Makko mascot: an anthropomorphic pink bunny, wearing a markdown file as a robe/cape, with the Makko logo next to her." 21 21 /> 22 + 22 23 <p> 23 24 makko is the markdown-based, people-oriented static site 24 25 generator, that allows you to setup your dream blog in a matter
+28
doc/templates/post.html
··· 14 14 15 15 <link rel="stylesheet" href="/highlight.css" /> 16 16 <link rel="stylesheet" href="/styles.css" /> 17 + 18 + <style> 19 + @media (max-width: 1500px) { 20 + body { 21 + margin: 0; 22 + margin-left: auto; 23 + } 24 + } 25 + 26 + @media (max-width: 1200px) { 27 + #toc-container { 28 + display: none; 29 + } 30 + 31 + body { 32 + margin: auto; 33 + } 34 + } 35 + </style> 17 36 </head> 18 37 19 38 <body> ··· 21 40 <a href="/">INDEX</a> 22 41 <img src="/media/gradient.webp" /> 23 42 </nav> 43 + 44 + <noscript> 45 + <img 46 + src="/media/docs.svg" 47 + height="128px" 48 + loading="lazy" 49 + alt="Makko, a pink rabbit with huge glasses, trying to balance." 50 + ></img> 51 + </noscript> 24 52 25 53 {{#post}} 26 54
+351
src/gui/shell.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Makko!</title> 7 + <style> 8 + :root { 9 + --background: light-dark(white, #131313); 10 + --foreground: light-dark(#131313, white); 11 + --accent: light-dark(hsl(269, 80%, 40%), hsl(269, 65%, 75%)); 12 + --accent-transparent: light-dark( 13 + hsla(269, 60%, 70%, 20%), 14 + hsla(269, 80%, 40%, 20%) 15 + ); 16 + --opacity: 0.8; 17 + --lines: light-dark(var(--foreground), var(--accent)); 18 + color-scheme: light dark; 19 + } 20 + 21 + * { 22 + margin: 0; 23 + padding: 0; 24 + box-sizing: border-box; 25 + } 26 + 27 + body { 28 + font-family: 29 + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, 30 + Consolas, "DejaVu Sans Mono", monospace; 31 + overflow: hidden; 32 + } 33 + 34 + .banner { 35 + position: fixed; 36 + top: 0; 37 + left: 0; 38 + width: 100%; 39 + color: var(--foreground); 40 + padding: 8px 12px; 41 + z-index: 999999; 42 + transition: opacity 0.3s ease; 43 + height: 50px; 44 + display: flex; 45 + align-items: center; 46 + border-bottom: 1px solid var(--lines); 47 + } 48 + 49 + .banner.offline { 50 + opacity: 0.7; 51 + } 52 + 53 + .status-text { 54 + white-space: nowrap; 55 + } 56 + 57 + .url-bar { 58 + flex: 1; 59 + width: 100%; 60 + overflow-x: auto; 61 + white-space: nowrap; 62 + } 63 + 64 + .url-path { 65 + display: flex; 66 + gap: 6px; 67 + padding: 6px 10px; 68 + } 69 + 70 + .url-bar::-webkit-scrollbar { 71 + height: 4px; 72 + } 73 + 74 + .url-bar::-webkit-scrollbar-track { 75 + background: #333; 76 + } 77 + 78 + .url-bar::-webkit-scrollbar-thumb { 79 + background: #666; 80 + border-radius: 2px; 81 + } 82 + 83 + .url-bar a { 84 + color: var(--accent); 85 + text-decoration: none; 86 + } 87 + 88 + .url-bar a:hover { 89 + text-decoration: underline; 90 + } 91 + 92 + .hamburger { 93 + display: none; 94 + background: none; 95 + border: none; 96 + color: var(--foreground); 97 + font-size: 20px; 98 + cursor: pointer; 99 + padding: 4px 8px; 100 + } 101 + 102 + .sidebar { 103 + position: fixed; 104 + top: 50px; 105 + left: 0; 106 + width: 250px; 107 + height: calc(100vh - 50px); 108 + background: var(--background); 109 + color: var(--foreground); 110 + padding: 16px; 111 + overflow-y: auto; 112 + transition: transform 0.3s ease; 113 + z-index: 999998; 114 + border-right: 1px solid var(--lines); 115 + } 116 + 117 + .sidebar h3 { 118 + font-size: 0.8em; 119 + margin-bottom: 12px; 120 + color: var(--foreground); 121 + text-transform: uppercase; 122 + } 123 + 124 + .sidebar-links { 125 + list-style: none; 126 + } 127 + 128 + .content-frame { 129 + position: fixed; 130 + top: 50px; 131 + left: 250px; 132 + width: calc(100% - 250px); 133 + height: calc(100vh - 50px); 134 + border: none; 135 + transition: 136 + left 0.3s ease, 137 + width 0.3s ease; 138 + } 139 + 140 + a, 141 + a:visited { 142 + text-decoration: underline dashed; 143 + } 144 + 145 + a:visited { 146 + color: var(--accent); 147 + } 148 + 149 + a { 150 + color: var(--accent); 151 + background-color: var(--accent-transparent); 152 + padding: 4px 8px; 153 + } 154 + 155 + @media (max-width: 768px) { 156 + .hamburger { 157 + display: block; 158 + } 159 + 160 + .sidebar { 161 + transform: translateX(-100%); 162 + } 163 + 164 + .sidebar.open { 165 + transform: translateX(0); 166 + } 167 + 168 + .content-frame { 169 + left: 0; 170 + width: 100%; 171 + } 172 + } 173 + </style> 174 + </head> 175 + <body> 176 + <div class="banner"> 177 + <button class="hamburger">☰</button> 178 + <div class="url-bar"> 179 + <span class="url-path">/</span> 180 + </div> 181 + <span class="status-text">LIVE!</span> 182 + </div> 183 + 184 + <div class="sidebar"> 185 + <h3>Navigation</h3> 186 + <ul class="sidebar-links"> 187 + <li><a href="/">Home</a></li> 188 + <li><a href="/about.html">About</a></li> 189 + <li><a href="/blog/">Blog</a></li> 190 + </ul> 191 + </div> 192 + 193 + <iframe 194 + class="content-frame" 195 + src="index.html?makko-disable-banner=1" 196 + ></iframe> 197 + 198 + <script> 199 + (function () { 200 + const statusText = document.querySelector(".status-text"); 201 + const banner = document.querySelector(".banner"); 202 + const iframe = document.querySelector(".content-frame"); 203 + const sidebar = document.querySelector(".sidebar"); 204 + const hamburger = document.querySelector(".hamburger"); 205 + const urlPath = document.querySelector(".url-path"); 206 + 207 + let lastState = null; 208 + let currentSrc = iframe.src; 209 + 210 + hamburger.addEventListener("click", function () { 211 + sidebar.classList.toggle("open"); 212 + }); 213 + 214 + document.addEventListener("click", function (e) { 215 + if ( 216 + window.innerWidth <= 768 && 217 + sidebar.classList.contains("open") && 218 + !sidebar.contains(e.target) && 219 + e.target !== hamburger 220 + ) { 221 + sidebar.classList.remove("open"); 222 + } 223 + }); 224 + 225 + function updateUrlBar(url) { 226 + const urlObj = new URL(url); 227 + const pathname = urlObj.pathname; 228 + 229 + if (pathname === "/") { 230 + urlPath.innerHTML = '<a href="/">/</a>'; 231 + return; 232 + } 233 + 234 + const parts = pathname 235 + .split("/") 236 + .filter((p) => (p == "/" ? null : p)); 237 + let html = '<a href="/">/</a>'; 238 + let accumulatedPath = ""; 239 + 240 + parts.forEach((part, index) => { 241 + accumulatedPath += "/" + part; 242 + const isLast = index === parts.length - 1; 243 + 244 + if (isLast && !part.includes(".")) { 245 + // Directory without trailing slash 246 + html += `<a href="${accumulatedPath}/">${part}</a>`; 247 + } else if (isLast) { 248 + // File 249 + html += `<a href="${accumulatedPath}">/${part}</a>`; 250 + } else { 251 + // Directory in the middle 252 + html += `<a href="${accumulatedPath}/">${part}</a>`; 253 + } 254 + html += " "; 255 + }); 256 + 257 + urlPath.innerHTML = html; 258 + } 259 + 260 + // Navigate to URL in iframe 261 + function navigateTo(url) { 262 + const urlObj = new URL(url, window.location.origin); 263 + urlObj.searchParams.set("makko-disable-banner", "1"); 264 + iframe.src = urlObj.href; 265 + currentSrc = urlObj.href; 266 + updateUrlBar(urlObj.href); 267 + 268 + if (window.innerWidth <= 768) { 269 + sidebar.classList.remove("open"); 270 + } 271 + } 272 + 273 + // Handle sidebar link clicks 274 + sidebar.addEventListener("click", function (e) { 275 + const link = e.target.closest("a"); 276 + if (link && link.href) { 277 + e.preventDefault(); 278 + navigateTo(link.href); 279 + } 280 + }); 281 + 282 + // Handle URL bar clicks 283 + urlPath.addEventListener("click", function (e) { 284 + const link = e.target.closest("a"); 285 + if (link && link.href) { 286 + e.preventDefault(); 287 + navigateTo(link.href); 288 + } 289 + }); 290 + 291 + // Intercept navigation within the iframe 292 + iframe.addEventListener("load", function () { 293 + try { 294 + const iframeDoc = 295 + iframe.contentDocument || 296 + iframe.contentWindow.document; 297 + const iframeUrl = iframe.contentWindow.location.href; 298 + updateUrlBar(iframeUrl); 299 + 300 + // Intercept all clicks on links 301 + iframeDoc.addEventListener( 302 + "click", 303 + function (e) { 304 + const target = e.target.closest("a"); 305 + if (target && target.href) { 306 + const url = new URL(target.href); 307 + 308 + // Only intercept same-origin links 309 + if (url.origin === window.location.origin) { 310 + e.preventDefault(); 311 + navigateTo(url.href); 312 + } 313 + } 314 + }, 315 + true, 316 + ); 317 + } catch (e) { 318 + console.log("Cannot intercept cross-origin iframe"); 319 + } 320 + }); 321 + 322 + async function poll() { 323 + try { 324 + const response = await fetch("/.makko/state"); 325 + const result = await response.text(); 326 + const parsed = parseInt(result.trim(), 10); 327 + 328 + if (!Number.isNaN(parsed)) { 329 + banner.classList.remove("offline"); 330 + statusText.textContent = "LIVE!"; 331 + 332 + if (lastState !== null && parsed !== lastState) { 333 + navigateTo(currentSrc); 334 + } 335 + 336 + lastState = parsed; 337 + } else { 338 + throw new Error("Invalid response"); 339 + } 340 + } catch (e) { 341 + banner.classList.add("offline"); 342 + statusText.textContent = "OFFLINE..."; 343 + } 344 + } 345 + 346 + updateUrlBar(currentSrc); 347 + setInterval(poll, 300); 348 + })(); 349 + </script> 350 + </body> 351 + </html>
+17 -5
src/network.zig
··· 12 12 changed: std.atomic.Value(i16), 13 13 log: Log, 14 14 directory: std.fs.Dir, 15 + port: u16, 15 16 16 17 pub fn update(self: *Handle) void { 17 18 self.log.header("Reloading"); ··· 76 77 .changed = .{ .raw = 0 }, 77 78 .directory = makko.paths.output, 78 79 .log = makko.log, 80 + .port = port, 79 81 }; 80 82 81 83 var server = try Server.init( ··· 145 147 // in case we want to access either /directory/ or just /, we redirect to 146 148 // /directory/index.html and /index.html 147 149 fn redirect(_: *Handle, req: *httpz.Request, res: *httpz.Response) !void { 150 + const query = try req.query(); 151 + const enable_banner = query.get("makko-disable-banner") == null; 152 + 148 153 const alloc = try std.mem.concat(res.arena, u8, &.{ 149 - req.url.path, "index.html", 154 + req.url.path, 155 + "index.html", 156 + if (!enable_banner) "?makko-disable-banner=1" else "", 150 157 }); 151 158 res.headers.add("Location", alloc); 152 159 res.status = 308; ··· 159 166 // checks if NOT accessing from eiher 127.0.0.1 or 0.0.0.0 160 167 const is_local = addr == 0x0100007f or addr == 0; 161 168 if (!is_local) { 162 - // kick em out! nobody but the hoster should be able to access this. 163 - res.body = "<h1>NERDZ NOT ALLOWED!!!!!</h1>"; 164 - res.status = 401; 169 + const new_addr = 170 + try std.fmt.allocPrint(res.arena, "http://localhost:{}/.makko", .{handle.port}); 171 + 172 + res.headers.add("Location", new_addr); 173 + res.status = 308; 165 174 return; 166 175 } 167 176 168 - _ = handle; 177 + const writer = res.writer(); 178 + try writer.writeAll(@embedFile("gui/shell.html")); 179 + res.content_type = .HTML; 180 + res.status = 200; 169 181 } 170 182 171 183 fn state(handle: *Handle, _: *httpz.Request, res: *httpz.Response) !void {