revamped
This commit is contained in:
parent
322b182ca1
commit
93d4f838b6
50 changed files with 1016 additions and 113 deletions
0
.gitfield/.radicle-push-state
Normal file → Executable file
0
.gitfield/.radicle-push-state
Normal file → Executable file
0
.gitfield/bitbucket.sigil.md
Normal file → Executable file
0
.gitfield/bitbucket.sigil.md
Normal file → Executable file
0
.gitfield/codeberg.sigil.md
Normal file → Executable file
0
.gitfield/codeberg.sigil.md
Normal file → Executable file
0
.gitfield/gitea.sigil.md
Normal file → Executable file
0
.gitfield/gitea.sigil.md
Normal file → Executable file
0
.gitfield/github.sigil.md
Normal file → Executable file
0
.gitfield/github.sigil.md
Normal file → Executable file
0
.gitfield/gitlab.sigil.md
Normal file → Executable file
0
.gitfield/gitlab.sigil.md
Normal file → Executable file
0
.gitfield/local.sigil.md
Normal file → Executable file
0
.gitfield/local.sigil.md
Normal file → Executable file
0
.gitfield/push_log.json.tmp
Normal file → Executable file
0
.gitfield/push_log.json.tmp
Normal file → Executable file
0
.gitfield/pushed.log
Normal file → Executable file
0
.gitfield/pushed.log
Normal file → Executable file
0
.gitfield/radicle.sigil.md
Normal file → Executable file
0
.gitfield/radicle.sigil.md
Normal file → Executable file
0
.gitfield/remember.sigil.md
Normal file → Executable file
0
.gitfield/remember.sigil.md
Normal file → Executable file
0
GITFIELD.md
Normal file → Executable file
0
GITFIELD.md
Normal file → Executable file
110
README.md
Executable file
110
README.md
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
# The Fold Within Earth
|
||||
|
||||
A Markdown-native static site for multi-section content.
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](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
452
app.js
Executable 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>© ${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>We’re 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
0
build.js
Normal file → Executable file
207
build.mjs
Executable file
207
build.mjs
Executable 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
7
config.json
Executable 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
0
hello.md
Normal file → Executable file
48
index.html
Normal file → Executable file
48
index.html
Normal file → Executable 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>
|
||||
0
main.js
Normal file → Executable file
0
main.js
Normal file → Executable file
36
mud.js
Executable file
36
mud.js
Executable 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
0
node_modules/.package-lock.json
generated
vendored
Normal file → Executable file
0
node_modules/.vite/deps_temp_14c96b05/package.json
generated
vendored
Normal file → Executable file
0
node_modules/.vite/deps_temp_14c96b05/package.json
generated
vendored
Normal file → Executable file
0
node_modules/marked/LICENSE.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/LICENSE.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/README.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/README.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/bin/main.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/bin/main.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.cjs
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.cjs
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.cjs.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.d.cts
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.d.cts
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.d.ts
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.esm.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.esm.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.esm.js.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.esm.js.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.umd.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.umd.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.umd.js.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/lib/marked.umd.js.map
generated
vendored
Normal file → Executable file
0
node_modules/marked/man/marked.1
generated
vendored
Normal file → Executable file
0
node_modules/marked/man/marked.1
generated
vendored
Normal file → Executable file
0
node_modules/marked/man/marked.1.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/man/marked.1.md
generated
vendored
Normal file → Executable file
0
node_modules/marked/marked.min.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/marked.min.js
generated
vendored
Normal file → Executable file
0
node_modules/marked/package.json
generated
vendored
Normal file → Executable file
0
node_modules/marked/package.json
generated
vendored
Normal file → Executable file
0
package-lock.json
generated
Normal file → Executable file
0
package-lock.json
generated
Normal file → Executable file
10
package.json
Normal file → Executable file
10
package.json
Normal file → Executable 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
0
posts/hello.md
Normal file → Executable file
0
posts/posts.json
Normal file → Executable file
0
posts/posts.json
Normal file → Executable file
0
posts/the-path-of-self.md
Normal file → Executable file
0
posts/the-path-of-self.md
Normal file → Executable file
0
posts/the-path-of-the-self-.md
Normal file → Executable file
0
posts/the-path-of-the-self-.md
Normal file → Executable file
0
posts/within-the-eternal-now.md
Normal file → Executable file
0
posts/within-the-eternal-now.md
Normal file → Executable file
54
render.js
Executable file
54
render.js
Executable 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
6
sanitize.js
Executable 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
0
src/env.d.ts
vendored
Normal file → Executable file
171
styles.css
Normal file → Executable file
171
styles.css
Normal file → Executable 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
24
util.js
Executable 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 : ''}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue