Makko, the people-oriented static site generator made for blogging.
forge.starlightnet.work/Team/Makko
ssg
static-site-generator
makko
starlight-network
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>