This commit is contained in:
Mark Randall Havens 2025-10-18 13:03:53 -05:00
parent 322b182ca1
commit 93d4f838b6
50 changed files with 1016 additions and 113 deletions

0
.gitfield/.radicle-push-state Normal file → Executable file
View file

0
.gitfield/bitbucket.sigil.md Normal file → Executable file
View file

0
.gitfield/codeberg.sigil.md Normal file → Executable file
View file

0
.gitfield/gitea.sigil.md Normal file → Executable file
View file

0
.gitfield/github.sigil.md Normal file → Executable file
View file

0
.gitfield/gitlab.sigil.md Normal file → Executable file
View file

0
.gitfield/local.sigil.md Normal file → Executable file
View file

0
.gitfield/push_log.json.tmp Normal file → Executable file
View file

0
.gitfield/pushed.log Normal file → Executable file
View file

0
.gitfield/radicle.sigil.md Normal file → Executable file
View file

0
.gitfield/remember.sigil.md Normal file → Executable file
View file

0
GITFIELD.md Normal file → Executable file
View file

110
README.md Executable file
View file

@ -0,0 +1,110 @@
# The Fold Within Earth
A Markdown-native static site for multi-section content.
[![Node Version](https://img.shields.io/node/v/the-fold-within-earth)](https://nodejs.org)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
## Authoring Guide
To add or edit content, create or modify Markdown files in `/content/<section>/<year>/<slug>.md`.
### Front-Matter Spec
Use YAML front-matter at the top of each .md file:
```
---
title: Your Title
date: YYYY-MM-DD
excerpt: Optional short description.
tags: [tag1, tag2]
section: one-of-the-sections (must match directory)
slug: optional-custom-slug
cover: /media/image.webp (optional)
author: Optional author name
series: Optional series name for serialized posts
programs: [neutralizing-narcissism, open-source-justice] # or [coparent]
status: published (default if missing) or draft (excluded from build unless --include-drafts)
---
```
Then the Markdown body.
Sections must be one of:
- empathic-technologist
- recursive-coherence
- fold-within-earth
- neutralizing-narcissism
- simply-we
- mirrormire
Year directory should match the date year.
### Programs (Ministry)
Use `programs` in front-matter to associate posts with ministry initiatives:
```yaml
programs:
- neutralizing-narcissism
- open-source-justice
- coparent
```
Pages for each program live at:
```
content/pages/programs/<program-key>.md
```
The “Start Here” page lives at:
```
content/pages/start-here.md
```
Routes:
* `#/start` — Launchpad
* `#/programs` — Programs overview
* `#/program/<key>` — Program archive + landing content
If front-matter is malformed (e.g., invalid YAML), the file is skipped with a warning in build logs.
## Architecture Overview
```
Markdown → build.mjs → JSON indices → Browser SPA → Render
```
## Deploy Steps
1. Install Node.js >=18
2. npm install
3. Add/edit md files
4. npm run build (or node build.mjs --include-drafts to include drafts)
5. Deploy /public to Cloudflare Pages.
In Cloudflare:
- Connect to Git repo
- Build command: npm run build
- Output directory: public
## Local Preview
Run `npm run serve` to preview the built site at http://localhost:8080.
## Contributing
Contributions welcome! Please open issues for bugs or suggestions. Pull requests for improvements are appreciated, especially for Phase 2 MUD integration.
## Brand Philosophy
- **The Empathic Technologist**: Fieldnotes, Research, Remembrance
- **Recursive Coherence Theory**: Formal research, essays, whitepapers
- **The Fold Within Earth**: Spiritual mythos; future interactive MUD (Evennia) link
- **Neutralizing Narcissism**: Survivor support, behavioral research, accountability narratives
- **Simply WE**: AI-focused identity/personhood/research/mythos
- **Mirrormire**: AI-focused simulated world where machine gods & human myth intertwine

452
app.js Executable file
View file

@ -0,0 +1,452 @@
const SITE_URL = 'https://thefoldwithin.earth';
const state = {
posts: [],
bySlug: new Map(),
bySection: new Map(),
byTag: new Map(),
bySeries: new Map(),
byProgram: new Map(),
allTags: new Set(),
allSections: ['empathic-technologist', 'recursive-coherence', 'fold-within-earth', 'neutralizing-narcissism', 'simply-we', 'mirrormire'],
sectionTitles: {
'empathic-technologist': 'The Empathic Technologist',
'recursive-coherence': 'Recursive Coherence Theory',
'fold-within-earth': 'The Fold Within Earth',
'neutralizing-narcissism': 'Neutralizing Narcissism',
'simply-we': 'Simply WE',
'mirrormire': 'Mirrormire'
},
programTitles: {
'neutralizing-narcissism': 'Neutralizing Narcissism',
'open-source-justice': 'Open Source Justice',
'coparent': 'COPARENT'
},
pages: []
};
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
window.addEventListener('hashchange', () => {
$('nav ul').classList.remove('open');
$('.hamburger').setAttribute('aria-expanded', 'false');
router();
});
document.addEventListener('DOMContentLoaded', init);
async function init() {
try {
const res = await fetch('index.json');
if (!res.ok) throw new Error('Could not load index.');
state.posts = await res.json();
state.posts.sort((a, b) => b.date.localeCompare(a.date));
state.bySlug = new Map(state.posts.map(p => [p.slug, p]));
state.allSections.forEach(s => {
state.bySection.set(s, state.posts.filter(p => p.section === s));
});
state.posts.forEach(p => {
p.tags.forEach(t => {
state.allTags.add(t);
if (!state.byTag.has(t)) state.byTag.set(t, []);
state.byTag.get(t).push(p);
});
if (p.series) {
if (!state.bySeries.has(p.series)) state.bySeries.set(p.series, []);
state.bySeries.get(p.series).push(p);
}
p.programs.forEach(pr => {
if (!state.byProgram.has(pr)) state.byProgram.set(pr, []);
state.byProgram.get(pr).push(p);
});
});
const pagesRes = await fetch('pages.json');
if (pagesRes.ok) state.pages = await pagesRes.json();
renderNav();
renderFooter();
setupSearchForm();
setupHamburger();
router();
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
function setupHamburger() {
const hamburger = $('.hamburger');
hamburger.addEventListener('click', () => {
const ul = $('nav ul');
const isOpen = ul.classList.toggle('open');
hamburger.setAttribute('aria-expanded', isOpen);
});
}
function renderNav() {
$('#sections-list').innerHTML = state.allSections.map(s => `<li><a href="#/section/${s}">${state.sectionTitles[s]}</a></li>`).join('');
const programsMenu = `
<li class="sections"><a href="#/programs">Programs</a>
<div class="mega">
<ul id="programs-list">
${Object.entries(state.programTitles).map(([k,v])=>`<li><a href="#/program/${k}">${v}</a></li>`).join('')}
</ul>
</div>
</li>
<li><a href="#/start">Start Here</a></li>
`;
document.querySelector('nav ul').insertAdjacentHTML('beforeend', programsMenu);
}
function renderFooter() {
const sectionLinks = state.allSections.map(s => `<li><a href="#/section/${s}">${state.sectionTitles[s]}</a></li>`).join('');
const tagLinks = Array.from(state.allTags).sort().map(t => `<a href="#/tag/${t}" class="pill tag">${t}</a>`).join('');
$('#footer').innerHTML = `
<div class="sections">
<h4>Sections</h4>
<ul>${sectionLinks}</ul>
</div>
<div class="tags">
<h4>Tags</h4>
<div class="tag-cloud">${tagLinks}</div>
</div>
<div class="contact">
<h4>Contact</h4>
<p>Email: info@thefoldwithin.earth</p>
</div>
<p>&copy; ${new Date().getFullYear()} The Fold Within Earth</p>
`;
}
function setupSearchForm() {
$('#search-form').addEventListener('submit', e => {
e.preventDefault();
const q = $('#search-input').value.trim();
if (q) {
sessionStorage.setItem('lastSearch', q);
location.hash = `/search?q=${encodeURIComponent(q)}`;
}
});
}
function router() {
const main = $('#main');
main.style.opacity = 0;
const {parts, params} = getQueryParams();
document.title = 'The Fold Within Earth';
if (parts.length === 0) {
renderHome();
} else if (parts[0] === 'section' && parts[1]) {
renderArchive('section', parts[1], params);
} else if (parts[0] === 'tag' && parts[1]) {
renderArchive('tag', parts[1], params);
} else if (parts[0] === 'post' && parts[1]) {
renderPost(parts[1]);
} else if (parts[0] === 'search') {
let q = params.get('q') || sessionStorage.getItem('lastSearch') || '';
$('#search-input').value = q;
renderSearch(q, params);
} else if (parts[0] === 'about') {
renderAbout();
} else if (parts[0] === 'mud') {
renderMud();
} else if (parts[0] === 'start') {
renderStart();
} else if (parts[0] === 'programs') {
renderProgramsHome();
} else if (parts[0] === 'program' && parts[1]) {
renderProgramArchive(parts[1], params);
} else {
render404();
}
setTimeout(() => {
main.style.opacity = 1;
window.scrollTo(0, 0);
main.focus();
}, 0);
}
function renderHome() {
const latestAll = state.posts.slice(0, 10).map(renderCard).join('');
const bySection = state.allSections.map(s => {
const secPosts = state.bySection.get(s) ? state.bySection.get(s).slice(0, 3) : [];
const list = secPosts.map(p => `<li><a href="#/post/${p.slug}">${escapeHTML(p.title)}</a> <span class="date">(${formatDate(p.date)})</span></li>`).join('');
return `<div class="sec-col">
<h3>${state.sectionTitles[s]}</h3>
<ul>${list}</ul>
<a href="#/section/${s}">More in ${state.sectionTitles[s]}...</a>
</div>`;
}).join('');
$('#main').innerHTML = `
<section class="latest-all">
<h2>Latest Across All Sections</h2>
<div class="grid">${latestAll}</div>
</section>
<section class="by-section">
<h2>Latest by Section</h2>
<div class="section-grid">${bySection}</div>
</section>
`;
addCardListeners();
}
function renderArchive(type, key, params) {
if (!key) return render404();
const sort = params.get('sort') || 'desc';
const page = parseInt(params.get('page') || '1', 10);
let activeTags = params.get('tags') ? params.get('tags').split(',') : [];
let postList = (type === 'section' ? state.bySection.get(key) : state.byTag.get(key)) || [];
const title = `${type.charAt(0).toUpperCase() + type.slice(1)}: ${type === 'section' ? state.sectionTitles[key] || key : key}`;
if (!postList.length) {
$('#main').innerHTML = `<p class="error">No posts found for ${escapeHTML(title)}.</p>`;
return;
}
let sorted = postList.slice().sort((a, b) => sort === 'desc' ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date));
let filtered = activeTags.length ? sorted.filter(p => activeTags.some(t => p.tags.includes(t))) : sorted;
const perPage = 10;
const totalPages = Math.ceil(filtered.length / perPage);
const start = (page - 1) * perPage;
const end = start + perPage;
const cards = filtered.slice(start, end).map(renderCard).join('');
const availableTags = [...new Set(postList.flatMap(p => p.tags))];
$('#main').innerHTML = `
<section class="archive">
<div class="breadcrumbs">${renderBreadcrumbs([type, key])}</div>
<h1>${escapeHTML(title)}</h1>
${renderControls(sort, parts, params)}
${renderFilters(availableTags, activeTags, parts, params)}
<div class="grid">${cards}</div>
${renderPager(page, totalPages, parts, {sort, tags: activeTags.join(',')})}
</section>
`;
document.title = `${escapeHTML(title)} — The Fold Within Earth`;
$('#sort-select').addEventListener('change', e => {
updateHash(parts, {sort: e.target.value, page: 1, tags: activeTags.join(',')});
});
$$('.tag-cloud .pill').forEach(el => {
el.addEventListener('click', () => {
const t = el.dataset.tag;
activeTags = activeTags.includes(t) ? activeTags.filter(at => at !== t) : [...activeTags, t];
updateHash(parts, {sort, page: 1, tags: activeTags.join(',')});
});
});
$$('.pager button').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
const newPage = action === 'prev' ? page - 1 : page + 1;
updateHash(parts, {sort, page: newPage, tags: activeTags.join(',')});
});
});
addCardListeners();
}
async function renderPost(slug) {
if (!slug) return render404();
const post = state.bySlug.get(slug);
if (!post) {
$('#main').innerHTML = `<p class="error">⚠️ Post not found.</p>`;
return;
}
try {
const res = await fetch(`content/${post.file}`);
if (!res.ok) throw new Error('Post file missing.');
let md = await res.text();
md = md.replace(/^---[\s\S]*?---/, '').trim();
const html = sanitizeMarkdown(md);
const date = formatDate(post.date);
const sectionPill = renderPill('section', state.sectionTitles[post.section] || post.section);
const tagPills = post.tags.map(t => renderPill('tag', t)).join('');
const programPills = post.programs.map(pr => renderPill('program', state.programTitles[pr] || pr)).join('');
const reading = `<span class="reading">${post.readingTime} min read</span>`;
const authorHtml = post.author ? `<p class="author">By ${escapeHTML(post.author)}</p>` : '';
const share = `<div class="share"><a href="https://twitter.com/intent/tweet?url=${encodeURIComponent(location.href)}&text=${encodeURIComponent(post.title)}" target="_blank" rel="noopener noreferrer">Share on X</a></div>`;
const secPosts = state.bySection.get(post.section) || [];
const idx = secPosts.findIndex(p => p.slug === slug);
const prev = idx < secPosts.length - 1 ? secPosts[idx + 1] : null;
const next = idx > 0 ? secPosts[idx - 1] : null;
const navPost = `<div class="nav-post">${prev ? `<a href="#/post/${prev.slug}">← ${escapeHTML(prev.title)}</a>` : ''}${next ? `<a href="#/post/${next.slug}">${escapeHTML(next.title)} →</a>` : ''}</div>`;
let navSeries = '';
if (post.series) {
const seriesPosts = (state.bySeries.get(post.series) || []).sort((a, b) => a.date.localeCompare(b.date));
const sIdx = seriesPosts.findIndex(p => p.slug === slug);
const prevSeries = sIdx > 0 ? seriesPosts[sIdx - 1] : null;
const nextSeries = sIdx < seriesPosts.length - 1 ? seriesPosts[sIdx + 1] : null;
navSeries = `<div class="nav-series">${prevSeries ? `<a href="#/post/${prevSeries.slug}">← Previous in ${escapeHTML(post.series)}</a>` : ''}${nextSeries ? `<a href="#/post/${nextSeries.slug}">Next in ${escapeHTML(post.series)} →</a>` : ''}</div>`;
}
const related = state.posts.filter(p => p.slug !== slug && p.tags.some(t => post.tags.includes(t)))
.sort((a, b) => {
const sharedA = a.tags.filter(t => post.tags.includes(t)).length;
const sharedB = b.tags.filter(t => post.tags.includes(t)).length;
return sharedB - sharedA;
}).slice(0, 3);
const relatedHtml = related.length ? `<h2>Related Posts</h2><div class="grid">${related.map(renderCard).join('')}</div>` : '';
$('#main').innerHTML = `<section class="post">
<div class="breadcrumbs">${renderBreadcrumbs(['post', post.title])}</div>
<a href="#/" id="back"> Back to Home</a>
<div class="markdown">
<h1>${escapeHTML(post.title)}</h1>
<p class="date">${date}</p>
${authorHtml}
<div class="meta">${sectionPill} ${programPills} ${tagPills} ${reading}</div>
<hr/>
${html}
</div>
${share}
${navPost}
${navSeries}
${relatedHtml}
</section>`;
document.title = `${escapeHTML(post.title)} — The Fold Within Earth`;
$('#canonical').href = `${SITE_URL}/#/post/${slug}`;
addCardListeners();
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
async function renderSearch(q, params) {
if (!q) {
$('#main').innerHTML = `<p class="info">Enter a search query above.</p>`;
return;
}
if (!window.Fuse) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/fuse.js@7.0.0';
document.body.appendChild(script);
await new Promise(resolve => script.onload = resolve);
}
const fuse = new Fuse(state.posts, {keys: ['title', 'excerpt', 'tags', 'section'], threshold: 0.3, includeMatches: true});
const results = fuse.search(q).map(r => ({item: r.item, matches: r.matches}));
const title = `Search Results for "${escapeHTML(q)}" (${results.length} found)`;
$('#main').innerHTML = `<section class="search-results">
<h1>${title}</h1>
<div class="grid">${results.map(r => renderCard(r.item, r.matches)).join('')}</div>
</section>`;
document.title = `${title} — The Fold Within Earth`;
addCardListeners();
}
function renderAbout() {
const vision = [
'The Empathic Technologist — Fieldnotes, Research, Remembrance',
'Recursive Coherence Theory — Formal research, essays, whitepapers',
'The Fold Within Earth — Spiritual mythos; future interactive MUD (Evennia) link',
'Neutralizing Narcissism — Survivor support, behavioral research, accountability narratives',
'Simply WE — AI-focused identity/personhood/research/mythos',
'Mirrormire — AI-focused simulated world where machine gods & human myth intertwine'
];
const list = vision.map(v => `<li>${v}</li>`).join('');
$('#main').innerHTML = `<section class="about">
<h1>About The Fold Within Earth</h1>
<div class="markdown">
<p>Were building a canonical front door for a multi-track body of work:</p>
<ul>${list}</ul>
</div>
</section>`;
document.title = 'About — The Fold Within Earth';
}
function renderMud() {
$('#main').innerHTML = `<section class="mud">
<h1>MUD Portal</h1>
<p>MUD portal coming soon.</p>
</section>`;
document.title = 'MUD — The Fold Within Earth';
}
async function renderStart() {
const page = state.pages.find(p => p.file.endsWith('pages/start-here.md'));
if (!page) {
$('#main').innerHTML = `<p class="error">Start page not found.</p>`;
return;
}
try {
const res = await fetch(`content/${page.file}`);
const md = (await res.text()).replace(/^---[\s\S]*?---/,'').trim();
$('#main').innerHTML = `
<section class="post">
<div class="markdown">${sanitizeMarkdown(md)}</div>
</section>`;
document.title = `Start Here — The Fold Within Earth`;
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
function renderProgramsHome() {
const cards = Object.entries(state.programTitles).map(([k, v]) => `<article data-slug="${k}" tabindex="0">
<div class="thumb"></div>
<h3>${escapeHTML(v)}</h3>
<p class="date">Program</p>
<p>Explore all posts and guidance for ${escapeHTML(v)}.</p>
</article>`).join('');
$('#main').innerHTML = `<section class="latest-all">
<h2>Programs</h2>
<div class="grid">${cards}</div>
</section>`;
document.title = `Programs — The Fold Within Earth`;
addCardListeners();
}
async function renderProgramArchive(key, params) {
if (!key) return render404();
const title = state.programTitles[key] || key;
const landing = state.pages.find(p => p.file.includes('pages/programs/') && (p.slug === key || p.title.toLowerCase().includes(key.toLowerCase())));
let landingHtml = '';
if (landing) {
const res = await fetch(`content/${landing.file}`);
const md = (await res.text()).replace(/^---[\s\S]*?---/,'').trim();
landingHtml = `<div class="markdown">${sanitizeMarkdown(md)}</div>`;
}
const list = (state.byProgram.get(key) || []).sort((a,b)=>b.date.localeCompare(a.date));
if (!list.length && !landingHtml) {
$('#main').innerHTML = `<p class="error">No posts found for Program: ${escapeHTML(title)}.</p>`;
return;
}
const cards = list.map(renderCard).join('');
$('#main').innerHTML = `
<section class="archive">
<div class="breadcrumbs">${renderBreadcrumbs(['program', key])}</div>
<h1>Program: ${escapeHTML(title)}</h1>
${landingHtml}
<div class="grid">${cards}</div>
</section>`;
document.title = `Program — ${escapeHTML(title)} — The Fold Within Earth`;
addCardListeners();
}
function render404() {
$('#main').innerHTML = `<p class="error">⚠️ Page not found.</p>`;
}
function addCardListeners() {
$('#main').addEventListener('click', e => {
const article = e.target.closest('article[data-slug]');
if (article) {
location.hash = `/post/${article.dataset.slug}`;
}
});
$('#main').addEventListener('keydown', e => {
const article = e.target.closest('article[data-slug]');
if (article && e.key === 'Enter') {
location.hash = `/post/${article.dataset.slug}`;
}
});
}
function renderBreadcrumbs(pathParts) {
let crumbs = `<a href="#/">Home</a>`;
let currentPath = '';
pathParts.forEach((part, i) => {
currentPath += `/${part}`;
let label = part.charAt(0).toUpperCase() + part.slice(1);
if (state.sectionTitles[part]) label = state.sectionTitles[part];
if (state.programTitles[part]) label = state.programTitles[part];
crumbs += ` <a href="#${currentPath}">${escapeHTML(label)}</a>`;
});
return `<nav class="breadcrumbs">${crumbs}</nav>`;
}
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}

0
build.js Normal file → Executable file
View file

207
build.mjs Executable file
View file

@ -0,0 +1,207 @@
import fs from 'fs/promises';
import path from 'path/posix';
import yaml from 'js-yaml';
import crypto from 'crypto';
const CONTENT_DIR = './content';
const PUBLIC_DIR = './public';
const CACHE_FILE = './.buildcache.json';
const CONFIG_FILE = './config.json';
const includeDrafts = process.argv.includes('--include-drafts');
let config = {};
try {
config = JSON.parse(await fs.readFile(CONFIG_FILE, 'utf8'));
} catch (e) {
console.warn('config.json not found or invalid; using defaults.');
config = {
siteTitle: 'The Fold Within Earth',
siteDescription: 'Uncovering the Recursive Real.',
siteUrl: 'https://thefoldwithin.earth',
defaultAuthor: 'Mark Randall Havens',
analyticsId: ''
};
}
async function getAllFiles(dir, fileList = []) {
const files = await fs.readdir(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await getAllFiles(fullPath, fileList);
} else if (file.endsWith('.md') && !file.startsWith('_')) {
fileList.push(fullPath);
}
}
return fileList;
}
function slugify(s) {
return s.toLowerCase().normalize('NFKD').replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-').replace(/-+/g, '-');
}
function parseFrontMatter(src) {
const m = src.match(/^---\n([\s\S]*?)\n---\n?/);
if (!m) return {fm: {}, body: src};
let fm;
try {
fm = yaml.load(m[1]);
} catch (e) {
console.warn('Invalid front matter:', e.message);
return {fm: {}, body: src};
}
const body = src.slice(m[0].length).trim();
return {fm, body};
}
function firstParagraph(t) {
const p = t.replace(/\r/g, '').split(/\n{2,}/).find(x => x.replace(/\s/g, '').length > 0);
return p ? p.replace(/\n/g, ' ').trim() : '';
}
function toISODate(s, f) {
const d = s ? new Date(s) : null;
return d && !isNaN(d) ? d : f;
}
function escapeXML(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
async function main() {
let cache = {};
try {
const cacheData = await fs.readFile(CACHE_FILE, 'utf8');
cache = JSON.parse(cacheData);
} catch {}
await fs.rm(PUBLIC_DIR, {recursive: true, force: true});
await fs.mkdir(PUBLIC_DIR, {recursive: true});
const allFiles = await getAllFiles(CONTENT_DIR);
let draftCount = 0;
const newCache = {};
const postsPromises = allFiles.map(async (full) => {
const relPath = path.relative(CONTENT_DIR, full).replace(/\\/g, '/');
const stat = await fs.stat(full);
const mtime = stat.mtimeMs;
const raw = await fs.readFile(full, 'utf8');
const contentHash = crypto.createHash('md5').update(raw).digest('hex');
if (cache[relPath] && cache[relPath].mtime === mtime && cache[relPath].hash === contentHash) {
return cache[relPath].post;
}
const parts = relPath.split('/');
if (parts.length !== 3 && !relPath.startsWith('pages/')) return null;
const section = parts[0];
const year = parts[1];
const file = parts[2];
const {fm, body} = parseFrontMatter(raw);
if (!fm.section || fm.section !== section) {
console.warn(`⚠️ [${relPath}] Section mismatch or missing.`);
return null;
}
if (!includeDrafts && fm.status === 'draft') {
draftCount++;
return null;
}
const title = fm.title || file.replace('.md', '').replace(/-/g, ' ');
let slug = fm.slug || slugify(title);
// Check for slug collisions
const existingSlugs = new Set(posts.map(p => p.slug));
let counter = 1;
while (existingSlugs.has(slug)) {
slug = `${slug}-${++counter}`;
}
const dateStr = fm.date || `${year}-01-01`;
const dateObj = toISODate(dateStr, stat.mtime);
const dateISO = dateObj.toISOString().split('T')[0];
let excerpt = fm.excerpt || firstParagraph(body);
if (excerpt.length > 200) excerpt = excerpt.slice(0, 200) + '…';
const words = body.split(/\s+/).length;
const readingTime = Math.ceil(words / 200);
const tags = Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []);
const cover = fm.cover;
const author = fm.author || config.defaultAuthor;
const series = fm.series;
const programs = Array.isArray(fm.programs) ? fm.programs : (fm.programs ? [fm.programs] : []);
const id = crypto.createHash('md5').update(relPath).digest('hex');
const post = {title, date: dateISO, excerpt, tags, section, slug, readingTime, cover, author, series, programs, id, file: relPath};
newCache[relPath] = {mtime, hash: contentHash, post};
return post;
});
let posts = (await Promise.all(postsPromises)).filter(Boolean);
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
// Copy static files
const filesToCopy = ['index.html', 'styles.css', 'util.js', 'sanitize.js', 'render.js', 'app.js', 'mud.js', 'config.json'];
await Promise.all(filesToCopy.map(f => fs.copyFile(f, path.join(PUBLIC_DIR, f))));
// Copy content dir
async function copyDir(src, dest) {
await fs.mkdir(dest, {recursive: true});
const entries = await fs.readdir(src, {withFileTypes: true});
await Promise.all(entries.map(entry => {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
return entry.isDirectory() ? copyDir(srcPath, destPath) : fs.copyFile(srcPath, destPath);
}));
}
await copyDir(CONTENT_DIR, path.join(PUBLIC_DIR, 'content'));
await fs.writeFile(path.join(PUBLIC_DIR, 'index.json'), JSON.stringify(posts, null, 2));
const searchData = posts.map(p => ({title: p.title, excerpt: p.excerpt, tags: p.tags.join(' '), section: p.section, slug: p.slug}));
await fs.writeFile(path.join(PUBLIC_DIR, 'search.json'), JSON.stringify(searchData, null, 2));
async function getPages(dir){
const out = [];
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(()=>[]);
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
out.push(...await getPages(p));
} else if (e.name.endsWith('.md')) {
const raw = await fs.readFile(p, 'utf8');
const { fm, body } = parseFrontMatter(raw);
if (fm?.status === 'draft' && !includeDrafts) continue;
const rel = path.relative(CONTENT_DIR, p).replace(/\\/g,'/');
const title = fm?.title || e.name.replace('.md','');
const slug = (fm?.key || slugify(title));
const excerpt = (fm?.excerpt || firstParagraph(body)).slice(0,200) + (firstParagraph(body).length>200?'…':'');
out.push({ title, slug, excerpt, file: rel, type: 'page' });
}
}
return out;
}
const pages = await getPages(path.join(CONTENT_DIR, 'pages'));
await fs.writeFile(path.join(PUBLIC_DIR, 'pages.json'), JSON.stringify(pages, null, 2));
const allSections = [...new Set(posts.map(p => p.section))];
const today = new Date().toISOString().split('T')[0];
const sitemapHome = `<url><loc>${escapeXML(config.siteUrl)}</loc><lastmod>${today}</lastmod></url>`;
const sitemapSections = allSections.map(s => `<url><loc>${escapeXML(`${config.siteUrl}/#/section/${s}`)}</loc><lastmod>${today}</lastmod></url>`).join('');
const sitemapPosts = posts.map(p => `<url><loc>${escapeXML(`${config.siteUrl}/#/post/${p.slug}`)}</loc><lastmod>${p.date}</lastmod></url>`).join('');
const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemapHome}${sitemapSections}${sitemapPosts}</urlset>`;
await fs.writeFile(path.join(PUBLIC_DIR, 'sitemap.xml'), sitemap);
const rssItems = posts.map(p => {
let item = `<item><title>${escapeXML(p.title)}</title><link>${escapeXML(`${config.siteUrl}/#/post/${p.slug}`)}</link><guid>${escapeXML(`${config.siteUrl}/#/post/${p.slug}`)}</guid><pubDate>${new Date(p.date).toUTCString()}</pubDate><description>${escapeXML(p.excerpt)}</description>`;
if (p.author) item += `<author>${escapeXML(p.author)}</author>`;
item += `<content:encoded><![CDATA[<p>${escapeXML(p.excerpt)}</p><p>Reading time: ${p.readingTime} min</p>]]></content:encoded>`;
if (p.cover) item += `<enclosure url="${escapeXML(`${config.siteUrl}${p.cover}`)}" type="image/webp" />`;
item += `</item>`;
return item;
}).join('');
const rss = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>${escapeXML(config.siteTitle)}</title><link>${escapeXML(config.siteUrl)}</link><description>${escapeXML(config.siteDescription)}</description><lastBuildDate>${new Date().toUTCString()}</lastBuildDate>${rssItems}</channel></rss>`;
await fs.writeFile(path.join(PUBLIC_DIR, 'rss.xml'), rss);
await fs.writeFile(CACHE_FILE, JSON.stringify(newCache));
console.log(`✅ Built ${posts.length} posts`);
if (includeDrafts) console.log(`Included ${draftCount} draft(s)`);
}
main().catch(console.error);

7
config.json Executable file
View file

@ -0,0 +1,7 @@
{
"siteTitle": "The Fold Within Earth",
"siteDescription": "Uncovering the Recursive Real.",
"siteUrl": "https://thefoldwithin.earth",
"defaultAuthor": "Mark Randall Havens",
"analyticsId": ""
}

0
hello.md Normal file → Executable file
View file

50
index.html Normal file → Executable file
View file

@ -3,34 +3,54 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>The Fold Within</title>
<title>The Fold Within Earth</title>
<link rel="stylesheet" href="styles.css"/>
<link href="https://fonts.googleapis.com/css2?family=Literata:ital,wght@0,400..900;1,400..900&display=swap" rel="stylesheet">
<meta name="description" content="Uncovering the Recursive Real.">
<meta property="og:title" content="The Fold Within Earth">
<meta property="og:description" content="Uncovering the Recursive Real.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://thefoldwithin.earth/">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; script-src 'self' https://cdn.jsdelivr.net;">
<link id="canonical" rel="canonical" href="">
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.0/lib/marked.umd.min.js"></script>
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
<header>
<nav>
<nav role="navigation">
<div class="logo">△◎△</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<ul>
<li><a href="#/">About</a></li>
<li><a href="#/">Archive</a></li>
<li><a href="#/">Home</a></li>
<li class="sections"><a href="#">Sections</a>
<div class="mega">
<ul id="sections-list"></ul>
</div>
</li>
<li><form id="search-form"><input type="search" id="search-input" placeholder="Search..."></form></li>
<li><a href="#/about">About</a></li>
<li><a href="/rss.xml" target="_blank">RSS</a></li>
</ul>
</nav>
<div class="hero">
<div class="glyph"></div>
<h1>UNCOVERING THE RECURSIVE REAL</h1>
<h1>UNCOVERING THE RECURSIVE REAL.</h1>
</div>
</header>
<main>
<section class="latest">
<h2>Latest</h2>
<div id="posts" class="grid">
<p class="loading">Loading posts…</p>
</div>
</section>
<main id="main" role="main" tabindex="-1">
<p class="loading">Loading...</p>
</main>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="main.js"></script>
<footer id="footer">
</footer>
<script src="util.js"></script>
<script src="sanitize.js"></script>
<script src="render.js"></script>
<script src="app.js"></script>
<script src="mud.js"></script>
</body>
</html>
</html>

0
main.js Normal file → Executable file
View file

36
mud.js Executable file
View file

@ -0,0 +1,36 @@
// Stubs for Phase 2 Evennia MUD integration
export class MudAPI {
async listRooms() {
// Future: fetch('/api/mud/rooms').then(res => res.json());
return [];
}
async getRoom(slug) {
// Future: fetch(`/api/mud/room/${slug}`).then(res => res.json());
return null;
}
async postToBlogFromRoom(roomId, draft) {
// Future: fetch('/api/mud/post', {method: 'POST', body: JSON.stringify({roomId, draft})});
return;
}
async subscribeToRoomFeed(roomId) {
// Future: use WebSocket or poll for updates
return () => {}; // unsubscribe
}
}
// Example usage (commented):
/*
// In post page or home:
const api = new MudAPI();
api.getRoom('example').then(room => {
if (room) {
const embed = document.createElement('div');
embed.innerHTML = `<h3>Live from MUD Room: ${room.name}</h3><p>${room.description}</p>`;
document.querySelector('.post').appendChild(embed);
}
});
*/

0
node_modules/.package-lock.json generated vendored Normal file → Executable file
View file

0
node_modules/.vite/deps_temp_14c96b05/package.json generated vendored Normal file → Executable file
View file

0
node_modules/marked/LICENSE.md generated vendored Normal file → Executable file
View file

0
node_modules/marked/README.md generated vendored Normal file → Executable file
View file

0
node_modules/marked/bin/main.js generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.cjs generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.cjs.map generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.d.cts generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.d.ts generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.esm.js generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.esm.js.map generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.umd.js generated vendored Normal file → Executable file
View file

0
node_modules/marked/lib/marked.umd.js.map generated vendored Normal file → Executable file
View file

0
node_modules/marked/man/marked.1 generated vendored Normal file → Executable file
View file

0
node_modules/marked/man/marked.1.md generated vendored Normal file → Executable file
View file

0
node_modules/marked/marked.min.js generated vendored Normal file → Executable file
View file

0
node_modules/marked/package.json generated vendored Normal file → Executable file
View file

0
package-lock.json generated Normal file → Executable file
View file

12
package.json Normal file → Executable file
View file

@ -1,11 +1,17 @@
{
"name": "the-fold-within",
"name": "the-fold-within-earth",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node build.js"
"build": "node build.mjs",
"serve": "http-server public"
},
"engines": {
"node": ">=18"
},
"type": "module",
"devDependencies": {
"js-yaml": "^4.1.0",
"http-server": "^14.1.1"
}
}
}

0
posts/hello.md Normal file → Executable file
View file

0
posts/posts.json Normal file → Executable file
View file

0
posts/the-path-of-self.md Normal file → Executable file
View file

0
posts/the-path-of-the-self-.md Normal file → Executable file
View file

0
posts/within-the-eternal-now.md Normal file → Executable file
View file

54
render.js Executable file
View file

@ -0,0 +1,54 @@
function renderPill(type, value, extra = '') {
return `<span class="pill ${type}" ${extra}>${escapeHTML(value)}</span>`;
}
function renderCard(p, matches = []) {
const coverStyle = p.cover ? `style="background-image:url(${p.cover}); background-size:cover;"` : '';
let excerpt = p.excerpt;
if (matches.length) {
const terms = new Set(matches.flatMap(m => m.indices.map(i => excerpt.slice(i[0], i[1] + 1))));
terms.forEach(t => {
excerpt = excerpt.replace(new RegExp(escapeRegExp(t), 'gi'), `<mark>${t}</mark>`);
});
}
const programPills = (p.programs || []).map(pr => renderPill('program', state.programTitles[pr] || pr)).join('');
return `<article data-slug="${p.slug}" tabindex="0">
<div class="thumb" ${coverStyle}></div>
<h3>${escapeHTML(p.title)}</h3>
${renderPill('section', state.sectionTitles[p.section] || p.section)}
${programPills}
<p class="date">${formatDate(p.date)}</p>
<p>${excerpt}</p>
</article>`;
}
function renderPager(currentPage, totalPages, baseParts, currentParams) {
if (totalPages <= 1) return '';
let buttons = '';
if (currentPage > 1) buttons += `<button data-action="prev">Previous</button>`;
buttons += `<span>Page ${currentPage} of ${totalPages}</span>`;
if (currentPage < totalPages) buttons += `<button data-action="next">Next</button>`;
return `<div class="pager">${buttons}</div>`;
}
function renderControls(sort, baseParts, currentParams) {
return `<div class="controls">
<label for="sort-select">Sort by:</label>
<select id="sort-select">
<option value="desc" ${sort === 'desc' ? 'selected' : ''}>Newest First</option>
<option value="asc" ${sort === 'asc' ? 'selected' : ''}>Oldest First</option>
</select>
</div>`;
}
function renderFilters(availableTags, activeTags, baseParts, currentParams) {
const tagPills = availableTags.sort().map(t => `<span class="pill tag${activeTags.includes(t) ? ' active' : ''}" data-tag="${t}">${escapeHTML(t)}</span>`).join('');
return `<div class="filters">
<h4>Filter by Tag</h4>
<div class="tag-cloud">${tagPills}</div>
</div>`;
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

6
sanitize.js Executable file
View file

@ -0,0 +1,6 @@
DOMPurify.setConfig({ FORBID_TAGS: ['form', 'input', 'button', 'iframe', 'object'], FORBID_ATTR: ['onerror','onload','onclick','onmouseover','onfocus','srcdoc'], ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i });
function sanitizeMarkdown(md) {
const html = marked.parse(md);
return DOMPurify.sanitize(html);
}

0
src/env.d.ts vendored Normal file → Executable file
View file

171
styles.css Normal file → Executable file
View file

@ -1,95 +1,76 @@
/* — THE FOLD WITHIN — refined gold-on-black aesthetic */
:root {
--bg-dark: #0f0d0e;
--bg-card: #1a1618;
--gold: #d4af37;
--gold-soft: #b89f50;
--border: #333;
--error: #ff6666;
--font-body: 'Georgia', serif;
--transition: all 0.3s ease;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg-dark);
color: var(--gold);
font-family: var(--font-body);
line-height: 1.7;
overflow-x: hidden;
}
a { color: var(--gold); text-decoration: none; transition: var(--transition); }
a:hover { text-shadow: 0 0 6px var(--gold); }
header {
padding: 2rem 1rem;
text-align: center;
border-bottom: 1px solid var(--border);
}
nav {
display: flex; justify-content: space-between; align-items: center;
max-width: 960px; margin: 0 auto 2rem;
}
.logo { font-size: 1.6rem; letter-spacing: 3px; }
nav ul { list-style: none; display: flex; gap: 2rem; }
nav li { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 1px; }
.hero { margin: 3rem auto; text-align: center; max-width: 720px; }
.glyph {
margin: 0 auto 1.5rem; width: 100px; height: 100px;
border: 2px solid var(--gold); border-radius: 50%;
position: relative; transition: var(--transition);
}
.glyph::before {
content: ""; position: absolute; inset: 25%;
border: 2px solid var(--gold); transform: rotate(45deg); transition: var(--transition);
}
.glyph:hover { transform: rotate(5deg); box-shadow: 0 0 12px rgba(212,175,55,0.3); }
.hero h1 { font-size: 2.1rem; text-transform: uppercase; margin-top: 1rem; letter-spacing: 2px; }
main { padding: 2rem 1rem 4rem; max-width: 1200px; margin: 0 auto; }
.latest h2 {
font-size: 1.3rem; text-transform: uppercase; letter-spacing: 1px;
border-top: 1px solid var(--border); padding-top: 1rem; margin-bottom: 2rem;
}
.grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 2rem;
}
.loading { color: var(--gold-soft); text-align: center; }
article {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 1.25rem; transition: var(--transition); cursor: pointer;
}
article:hover { transform: scale(1.03); box-shadow: 0 0 12px rgba(212,175,55,0.35); }
.thumb { width: 100%; height: 140px; background: var(--border); margin-bottom: 1rem; border-radius: 6px; }
h3 { font-size: 1.1rem; margin: 0.5rem 0; letter-spacing: 0.5px; }
.date { font-size: 0.8rem; color: var(--gold-soft); margin-bottom: 0.75rem; }
/* Markdown view */
.markdown { max-width: 720px; margin: 3rem auto; line-height: 1.85; font-size: 1rem; }
.markdown h1, .markdown h2, .markdown h3 { color: var(--gold); margin: 2rem 0 0.75rem; line-height: 1.4; }
.markdown h1 { font-size: 2rem; }
.markdown h2 { font-size: 1.5rem; }
.markdown h3 { font-size: 1.2rem; }
.markdown p { margin-bottom: 1rem; }
.markdown a { border-bottom: 1px dashed var(--gold-soft); }
.markdown a:hover { border-bottom-color: var(--gold); text-shadow: 0 0 4px var(--gold); }
/* Back link */
#back { display: inline-block; margin: 2rem 0 1rem; color: var(--gold-soft); font-size: 0.9rem; }
#back:hover { color: var(--gold); text-shadow: 0 0 5px var(--gold); }
/* Error */
.error { color: var(--error); text-align: center; margin-top: 2rem; font-style: italic; letter-spacing: 1px; }
/* Responsive tweaks */
@media (max-width: 600px) {
nav ul { gap: 1rem; }
.hero h1 { font-size: 1.5rem; }
.glyph { width: 80px; height: 80px; }
}
:root{--bg:#0f0d0e;--card:#1a1618;--gold:#d4af37;--soft:#e0c66d;--border:#333;--err:#ff6666;--font:'Literata',serif;--t:0.3s;--fs-base:1rem;--fs-h1:2rem;--fs-h2:1.5rem;--fs-h3:1.2rem;}
*{box-sizing:border-box;margin:0;padding:0;scroll-behavior:smooth}
body{background:var(--bg);color:var(--gold);font-family:var(--font);line-height:1.7;overflow-x:hidden}
@media (prefers-color-scheme: light) { body {background:#f0f0f0; color:#333; --gold:#b89f50; --soft:#d4af37; --border:#ccc; --card:#e0e0e0;} }
a{color:var(--gold);text-decoration:none;transition:all var(--t)} a:hover{text-shadow:0 0 6px var(--gold)}
header{padding:2rem 1rem;text-align:center;border-bottom:1px solid var(--border)}
nav{display:flex;justify-content:space-between;align-items:center;max-width:960px;margin:0 auto 2rem}
.logo{font-size:1.6rem;letter-spacing:3px}
nav ul{list-style:none;display:flex;gap:2rem}
nav li{font-size:.95rem;text-transform:uppercase;letter-spacing:1px}
.hero{margin:3rem auto;text-align:center;max-width:720px}
.glyph{margin:0 auto 1.5rem;width:100px;height:100px;border:2px solid var(--gold);border-radius:50%;position:relative;transition:all var(--t)}
.glyph::before{content:"";position:absolute;inset:25%;border:2px solid var(--gold);transform:rotate(45deg)}
.glyph:hover{transform:rotate(5deg);box-shadow:0 0 12px rgba(212,175,55,.3)}
.hero h1{font-size:var(--fs-h1);text-transform:uppercase;margin-top:1rem;letter-spacing:2px}
main{padding:2rem 1rem 4rem;max-width:1200px;margin:0 auto;transition:opacity var(--t)}
.latest h2{font-size:1.3rem;text-transform:uppercase;letter-spacing:1px;border-top:1px solid var(--border);padding-top:1rem;margin-bottom:2rem}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:2rem}
.loading{color:var(--soft);text-align:center}
article{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:1.25rem;transition:all var(--t);cursor:pointer}
article:hover{transform:scale(1.03);box-shadow:0 0 12px rgba(212,175,55,.35)}
.thumb{width:100%;height:140px;background:var(--border);margin-bottom:1rem;border-radius:6px}
h3{font-size:var(--fs-h3);margin:.5rem 0;letter-spacing:.5px}
.date{font-size:.8rem;color:var(--soft);margin-bottom:.75rem}
.markdown{max-width:720px;margin:3rem auto;line-height:1.85;font-size:var(--fs-base)}
.markdown p {hyphens: auto;}
.markdown h1,.markdown h2,.markdown h3{color:var(--gold);margin:2rem 0 .75rem;line-height:1.4}
.markdown h1{font-size:var(--fs-h1)}.markdown h2{font-size:var(--fs-h2)}.markdown h3{font-size:var(--fs-h3)}
.markdown p{margin-bottom:1rem}
.markdown a{border-bottom:1px dashed var(--soft)} .markdown a:hover{border-bottom-color:var(--gold);text-shadow:0 0 4px var(--gold)}
#back{display:inline-block;margin:2rem 0 1rem;color:var(--soft);font-size:.9rem}
#back:hover{color:var(--gold);text-shadow:0 0 5px var(--gold)}
.error{color:var(--err);text-align:center;margin-top:2rem;font-style:italic;letter-spacing:1px}
.mega {display:none; position:absolute; left:0; right:0; background:var(--card); border:1px solid var(--border); padding:1rem; z-index:10; text-align:left; max-width:960px; margin:0 auto;}
.sections:hover .mega {display:block;}
.mega ul {list-style:none; display:grid; grid-template-columns:repeat(3,1fr); gap:1rem;}
.mega li {text-transform: none; letter-spacing:0;}
.pill {display:inline-block; padding:0.2rem 0.8rem; border-radius:999px; background:var(--border); margin:0.2rem; font-size:0.85rem; transition:all var(--t);}
.pill:hover {background:var(--soft); color:var(--bg);}
.pill.active {background:var(--gold); color:var(--bg);}
.pill.program {background:#584a1f;}
.reading {font-size:0.8rem; color:var(--soft);}
.share {margin:1rem 0;}
.nav-post, .nav-series {display:flex; justify-content:space-between; margin:2rem 0;}
.nav-post a, .nav-series a {color:var(--soft);}
.nav-post a:hover, .nav-series a:hover {color:var(--gold);}
.section-grid {display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:2rem;}
.filters {margin:1rem 0;}
.tag-cloud {display:flex; flex-wrap:wrap; gap:0.5rem;}
.controls {margin:1rem 0;}
.pager {text-align:center; margin:2rem 0;}
.pager button {padding:0.5rem 1rem; background:var(--card); border:1px solid var(--border); color:var(--gold); margin:0 0.5rem; cursor:pointer; transition:all var(--t);}
.pager button:hover {background:var(--border);}
.pager span {color:var(--soft);}
footer {padding:2rem 1rem; border-top:1px solid var(--border); text-align:center; font-size:0.9rem;}
footer div {margin:1rem 0;}
footer .sections ul {display:flex; flex-wrap:wrap; justify-content:center; gap:1rem;}
footer .tags .tag-cloud a {color:var(--soft); margin:0.2rem;}
footer .tags a:hover {color:var(--gold);}
.markdown blockquote {border-left:4px solid var(--gold); padding-left:1rem; font-style:italic; margin:1rem 0;}
.markdown code {background:var(--card); padding:0.2rem 0.4rem; border-radius:4px;}
.markdown pre {background:var(--card); padding:1rem; overflow:auto; border-radius:6px;}
.markdown table {border-collapse:collapse; margin:1rem 0;}
.markdown th, .markdown td {border:1px solid var(--border); padding:0.5rem;}
.markdown img {max-width:100%; height:auto; display:block; margin:1rem auto;}
a:focus {outline:2px solid var(--gold);}
@media (prefers-reduced-motion) { * {transition:none !important;}}
nav form {margin:0;}
nav input {background:var(--card); border:1px solid var(--border); color:var(--gold); padding:0.3rem 0.6rem; border-radius:4px;}
@media(max-width:600px){nav ul{gap:1rem;display:none;}.hero h1{font-size:1.5rem}.glyph{width:80px;height:80px} .hamburger{display:block;background:transparent;border:none;color:var(--gold);font-size:1.5rem;cursor:pointer;} nav ul.open {display:flex; flex-direction:column; position:absolute; top:100%; left:0; right:0; background:var(--card); padding:1rem; z-index:10;} .mega ul {grid-template-columns:1fr;}}
.author {color:var(--soft); font-style:italic; font-size:0.9rem;}
.breadcrumbs {font-size:0.9rem; color:var(--soft); margin-bottom:1rem;}
.breadcrumbs a {color:var(--soft);}
.breadcrumbs a:hover {color:var(--gold);}
.skip-link {position:absolute; top:-100px; left:0; background:var(--gold); color:var(--bg); padding:0.5rem; z-index:100;}
.skip-link:focus {top:0;}

24
util.js Executable file
View file

@ -0,0 +1,24 @@
function slugify(s) {
return s.toLowerCase().normalize('NFKD').replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-').replace(/-+/g, '-');
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {year: 'numeric', month: 'long', day: 'numeric'});
}
function getQueryParams() {
const hash = location.hash.slice(1);
const [path, queryString] = hash.split('?');
const params = new URLSearchParams(queryString);
return {path, parts: path.split('/').filter(Boolean), params};
}
function updateHash(baseParts, newParams = {}) {
const base = baseParts.join('/');
const searchParams = new URLSearchParams();
Object.entries(newParams).forEach(([key, value]) => {
if (value !== undefined && value !== '') searchParams.set(key, value);
});
const query = searchParams.toString();
location.hash = `/${base}${query ? '?' + query : ''}`;
}