<!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";
}
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>