Homepage: seamless theme music that keeps playing on /members/

Adds the eom.dev theme (Strudel) as a 16-bar Web Audio loop (Opus, AAC
fallback) with a Theme on/off button. Playback starts on the first
interaction unless the visitor turned it off. While it plays, links
between the homepage and membership pages are fetched and swapped in
place, so the music doesn't restart; sign-in, checkout, billing and
forms remain real navigations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 19:03:24 -05:00
co-authored by Claude Opus 5.5
parent 0b008c776a
commit 849a020cbc
8 changed files with 771 additions and 432 deletions
+16
View File
@@ -84,6 +84,22 @@ can use the permanent channel and playlist links. Requests use 10-second timeout
and a 2 MiB response limit. Titles are rendered as text, and watch/thumbnail URLs
are restricted to the PeerTube origin. No descriptions, HTML, or players are embedded.
### Theme music
The homepage plays the eom.dev theme (composed in Strudel) as a seamless 16-bar
loop: `htdocs/static/theme-player.mjs` with `htdocs/static/audio/eom-theme-loop.ogg`
(Opus) and an AAC `.m4a` fallback. Web Audio loops it without a gap. Browsers
block sound until the first click or key press, so it starts then unless the
visitor switched it off with the "Theme" button (remembered in localStorage).
While it plays, links between the homepage and `/members/` pages are fetched
and swapped into the same document, so the music does not restart on the
membership page. Forms, sign-in, checkout, billing and other sites stay normal
navigations (`HARD_NAV` in the module), and with the music off every link is a
normal navigation. The membership templates do not load the player; a visitor
who opens `/members/` directly gets no music. Sources and longer versions are
kept outside this repository (`eom-theme.strudel.js`).
### Local preview and content checks
With Node.js 24 or later, run `node scripts/preview.mjs` from this repository
+1
View File
@@ -323,6 +323,7 @@
</main>
<script type="module" src="/static/peertube-feeds.mjs"></script>
<script type="module" src="/static/gitea-projects.mjs"></script>
<script type="module" src="/static/theme-player.mjs"></script>
<!--#include virtual="/common/footer.html" -->
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
+302
View File
@@ -0,0 +1,302 @@
/**
* eom.dev theme music for the homepage.
*
* Plays a seamless 16-bar loop of the eom.dev theme (Web Audio, so the loop has no
* gap). While it plays, links between the homepage and the membership pages are
* loaded in place (fetch + swap) instead of reloading the document, so the music
* keeps going on /members/. Anything else (forms, sign-in, checkout, other sites)
* is a normal navigation.
*
* Browsers block sound until the visitor interacts with the page, so playback
* starts on the first click or key press unless the visitor has turned it off.
* The choice is kept in localStorage ("eom-theme-music": "on" | "off").
*/
const SOURCES = [
['/static/audio/eom-theme-loop.ogg', 'audio/ogg; codecs=opus'],
['/static/audio/eom-theme-loop.m4a', 'audio/mp4; codecs=mp4a.40.2'],
];
const VOLUME = 0.3;
const FADE_S = 1.5;
const PREF_KEY = 'eom-theme-music';
const RESUME_KEY = 'eom-theme-music-position';
const PLAYER_ID = 'eom-theme-player';
// Membership routes that must be real navigations (redirects off-site, POST-only, webhooks).
const HARD_NAV = /^\/members\/(login|logout|oidc|checkout|billing|stripe|shkeeper|crypto\/checkout)\b/;
const storage = {
get(key, area = localStorage) { try { return area.getItem(key); } catch { return null; } },
set(key, value, area = localStorage) { try { area.setItem(key, value); } catch { /* private mode */ } },
};
class ThemeMusic {
constructor() {
this.ctx = null;
this.buffer = null;
this.source = null;
this.gain = null;
this.startedAt = 0; // ctx time when the current source started
this.offset = 0; // loop position (s) at startedAt
this.playing = false;
this.loading = null;
}
position() {
if (!this.buffer) return 0;
const t = this.playing ? this.offset + (this.ctx.currentTime - this.startedAt) : this.offset;
return t % this.buffer.duration;
}
async load() {
if (this.buffer) return this.buffer;
this.loading ??= (async () => {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.gain = this.ctx.createGain();
this.gain.gain.value = 0;
this.gain.connect(this.ctx.destination);
const probe = document.createElement('audio');
const candidates = SOURCES.filter(([, type]) => probe.canPlayType(type));
for (const [url] of candidates.length ? candidates : SOURCES) {
try {
const res = await fetch(url);
if (!res.ok) continue;
this.buffer = await this.ctx.decodeAudioData(await res.arrayBuffer());
return this.buffer;
} catch { /* try the next format */ }
}
throw new Error('theme music could not be decoded');
})();
return this.loading;
}
async play(at = this.offset) {
await this.load();
if (this.ctx.state === 'suspended') await this.ctx.resume();
if (this.ctx.state !== 'running') throw new Error('audio blocked until user interaction');
if (this.playing) return;
const src = this.ctx.createBufferSource();
src.buffer = this.buffer;
src.loop = true;
src.connect(this.gain);
this.offset = at % this.buffer.duration;
this.startedAt = this.ctx.currentTime;
src.start(0, this.offset);
this.source = src;
this.playing = true;
const g = this.gain.gain;
g.cancelScheduledValues(this.ctx.currentTime);
g.setValueAtTime(g.value, this.ctx.currentTime);
g.linearRampToValueAtTime(VOLUME, this.ctx.currentTime + FADE_S);
}
pause() {
if (!this.playing) return;
this.offset = this.position();
this.playing = false;
const now = this.ctx.currentTime;
const g = this.gain.gain;
g.cancelScheduledValues(now);
g.setValueAtTime(g.value, now);
g.linearRampToValueAtTime(0, now + 0.3);
this.source.stop(now + 0.32);
this.source = null;
}
}
const music = new ThemeMusic();
let button;
let starting = false; // a start is in flight (audio context resuming)
let renderedPath = location.pathname + location.search;
function render() {
if (!button) return;
button.setAttribute('aria-pressed', String(music.playing));
button.title = music.playing ? 'Pause the eom.dev theme' : 'Play the eom.dev theme';
button.querySelector('.label').textContent = music.playing ? 'Theme on' : 'Theme off';
}
async function start() {
starting = true;
try { await music.play(); } catch { /* still blocked; wait for a gesture */ }
starting = false;
render();
}
function toggle() {
if (music.playing) {
music.pause();
storage.set(PREF_KEY, 'off');
render();
} else {
storage.set(PREF_KEY, 'on');
start();
}
}
function mountButton() {
if (document.getElementById(PLAYER_ID)) return;
const style = document.createElement('style');
style.id = `${PLAYER_ID}-style`;
style.textContent = `
#${PLAYER_ID}{position:fixed;right:1rem;bottom:1rem;z-index:1000;display:inline-flex;align-items:center;gap:.45rem;
padding:.5rem .8rem;border-radius:999px;border:1px solid var(--border,#3c3d56);background:var(--surface-raised,#292940);
color:var(--text,#cdd6f4);font:600 .85rem/1 system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 18px rgb(0 0 0 / .25)}
#${PLAYER_ID}:hover{border-color:var(--accent,#cba6f7)}
#${PLAYER_ID}:focus-visible{outline:2px solid var(--accent,#cba6f7);outline-offset:2px}
#${PLAYER_ID} .bars{display:inline-flex;gap:2px;align-items:flex-end;height:.9rem}
#${PLAYER_ID} .bars i{width:3px;height:30%;background:var(--accent,#cba6f7);border-radius:1px}
#${PLAYER_ID}[aria-pressed="true"] .bars i{animation:eomBars 1.07s ease-in-out infinite}
#${PLAYER_ID}[aria-pressed="true"] .bars i:nth-child(2){animation-delay:-.27s}
#${PLAYER_ID}[aria-pressed="true"] .bars i:nth-child(3){animation-delay:-.54s}
@keyframes eomBars{0%,100%{height:30%}50%{height:100%}}
@media (prefers-reduced-motion:reduce){#${PLAYER_ID} .bars i{animation:none!important;height:60%!important}}
@media print{#${PLAYER_ID}{display:none}}`;
document.head.append(style);
button = document.createElement('button');
button.id = PLAYER_ID;
button.type = 'button';
button.innerHTML = '<span class="bars" aria-hidden="true"><i></i><i></i><i></i></span><span class="label"></span>';
button.addEventListener('click', toggle);
document.body.append(button);
render();
}
/* ---------- in-place navigation while the music plays ---------- */
let navToken = 0;
let moduleGeneration = 0;
function softNavigable(url) {
if (url.origin !== location.origin) return false;
if (url.pathname === location.pathname && url.search === location.search && url.hash) return false; // same-page anchor
if (HARD_NAV.test(url.pathname)) return false;
return url.pathname === '/' || url.pathname === '/index.html' || url.pathname.startsWith('/members/');
}
function onClick(event) {
if (!(music.playing || starting) || event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const link = event.target.closest?.('a[href]');
if (!link || (link.target && link.target !== '_self') || link.hasAttribute('download')) return;
const url = new URL(link.href, location.href);
if (!softNavigable(url)) return;
event.preventDefault();
navigate(url, { push: true });
}
async function adoptStylesheets(doc) {
const wanted = [...doc.querySelectorAll('link[rel="stylesheet"]')].map(l => new URL(l.getAttribute('href'), location.href).href);
const current = [...document.querySelectorAll('link[rel="stylesheet"]')];
// Load the new sheets first (no unstyled flash), then put every sheet in the new
// page's order; cascade order matters (pure.css must come before home.css).
const fresh = new Map();
for (const href of wanted) {
if (current.some(l => l.href === href)) continue;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
fresh.set(href, new Promise(resolve => { link.onload = link.onerror = () => resolve(link); }));
document.head.append(link);
}
const added = new Map(await Promise.all([...fresh].map(async ([href, p]) => [href, await p])));
for (const link of current) if (!wanted.includes(link.href)) link.remove();
for (const href of wanted) document.head.append(added.get(href) ?? current.find(l => l.href === href));
}
function runScripts(container) {
for (const old of container.querySelectorAll('script')) {
const type = old.getAttribute('type') || '';
if (type && type !== 'module' && type !== 'text/javascript') continue; // e.g. JSON-LD
if (old.src && new URL(old.src, location.href).pathname === new URL(import.meta.url).pathname) { old.remove(); continue; }
const fresh = document.createElement('script');
for (const { name, value } of old.attributes) fresh.setAttribute(name, value);
if (old.src && type === 'module') {
// A module URL only runs once per document; a new query makes it run again.
const src = new URL(old.src, location.href);
src.searchParams.set('nav', String(++moduleGeneration));
fresh.src = src.href;
}
fresh.textContent = old.textContent;
old.replaceWith(fresh);
}
}
async function navigate(url, { push }) {
const token = ++navToken;
let res;
try {
res = await fetch(url.href, { credentials: 'same-origin', headers: { Accept: 'text/html' } });
} catch { location.assign(url.href); return; } // e.g. redirected to another origin
if (token !== navToken) return;
const finalURL = new URL(res.url || url.href);
if (!res.ok || !(res.headers.get('content-type') || '').includes('text/html') || finalURL.origin !== location.origin) {
location.assign(url.href);
return;
}
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
if (token !== navToken) return;
finalURL.hash = url.hash;
if (push) history.pushState({ eomTheme: true }, '', finalURL.href);
renderedPath = finalURL.pathname + finalURL.search;
await adoptStylesheets(doc);
document.title = doc.title;
for (const name of ['description', 'robots']) {
const next = doc.querySelector(`meta[name="${name}"]`);
const cur = document.querySelector(`meta[name="${name}"]`);
if (next && cur) cur.content = next.content;
else if (next) document.head.append(next.cloneNode());
else cur?.remove();
}
document.documentElement.lang = doc.documentElement.lang || document.documentElement.lang;
const keep = document.getElementById(PLAYER_ID);
document.body.replaceChildren(...doc.body.childNodes);
for (const { name, value } of doc.body.attributes) document.body.setAttribute(name, value);
if (keep) document.body.append(keep);
runScripts(document.body);
const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
if (target) target.scrollIntoView();
else window.scrollTo(0, 0);
(document.querySelector('main') || document.body).focus?.({ preventScroll: true });
}
function onPopState() {
if (location.pathname + location.search === renderedPath) return; // only the #hash changed
if (!music.playing) { location.reload(); return; }
navigate(new URL(location.href), { push: false });
}
/* ---------- boot ---------- */
function boot() {
if (window.__eomThemeMusic) return; // already running in this document
window.__eomThemeMusic = music;
mountButton();
document.addEventListener('click', onClick);
window.addEventListener('popstate', onPopState);
if (!history.state) history.replaceState({ eomTheme: true }, '');
// Keep the loop position across a real reload of the homepage.
window.addEventListener('pagehide', () => {
if (music.playing) storage.set(RESUME_KEY, JSON.stringify({ at: music.position(), t: Date.now() }), sessionStorage);
else try { sessionStorage.removeItem(RESUME_KEY); } catch { /* private mode */ }
});
try {
const saved = JSON.parse(storage.get(RESUME_KEY, sessionStorage) || 'null');
if (saved && Date.now() - saved.t < 30_000) music.offset = saved.at + (Date.now() - saved.t) / 1000;
} catch { /* ignore */ }
if (storage.get(PREF_KEY) === 'off') return;
music.load().catch(() => {}); // fetch/decode early so the first click starts instantly
start(); // succeeds only where autoplay is allowed
const onGesture = event => {
if (event.target.closest?.(`#${PLAYER_ID}`)) return; // the toggle handles its own click
for (const type of ['pointerdown', 'keydown']) removeEventListener(type, onGesture, true);
if (!music.playing && storage.get(PREF_KEY) !== 'off') start();
};
for (const type of ['pointerdown', 'keydown']) addEventListener(type, onGesture, true);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
else boot();
+1 -1
View File
@@ -24,7 +24,7 @@ const MIME = new Map([
['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.gif', 'image/gif'],
['.webp', 'image/webp'], ['.avif', 'image/avif'], ['.woff', 'font/woff'],
['.woff2', 'font/woff2'], ['.ttf', 'font/ttf'], ['.otf', 'font/otf'],
['.mp3', 'audio/mpeg'], ['.ogg', 'audio/ogg'], ['.wav', 'audio/wav'],
['.mp3', 'audio/mpeg'], ['.ogg', 'audio/ogg'], ['.m4a', 'audio/mp4'], ['.wav', 'audio/wav'],
['.mp4', 'video/mp4'], ['.webm', 'video/webm'], ['.pdf', 'application/pdf'],
['.wasm', 'application/wasm'], ['.pub', 'text/plain; charset=utf-8'],
]);
+451 -431
View File
@@ -1,431 +1,451 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startPreview, parsePort } from '../scripts/preview.mjs';
const repository = new URL('../', import.meta.url);
// Small scanner for this site's static markup, not a general-purpose HTML validator.
function attributes(tag) {
return new Map([...tag.matchAll(/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/gu)]
.map(([, name, double, single]) => [name.toLowerCase(), double ?? single]));
}
function text(html) {
return html.replace(/<[^>]*>/gu, ' ').replace(/&amp;/gu, '&')
.replace(/&(?:nbsp|#160);/gu, ' ').replace(/\s+/gu, ' ').trim();
}
function links(html) {
return [...html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/giu)]
.map(([, tag, inner]) => ({ attrs: attributes(tag), inner }));
}
function get(server, path = '/', method = 'GET', headers = {}) {
// Raw request paths preserve traversal probes that fetch/URL would normalize away.
return new Promise((accept, reject) => {
const req = request({ host: '127.0.0.1', port: server.address().port, path,
method, headers, agent: false }, res => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('error', reject);
res.on('end', () => accept({ status: res.statusCode, headers: res.headers,
body: Buffer.concat(chunks) }));
});
req.on('error', reject);
req.setTimeout(3_000, () => req.destroy(new Error('Preview request timed out')));
req.end();
});
}
async function preview(t, options) {
const server = await startPreview({ ...options, port: 0 });
t.after(() => new Promise((accept, reject) => {
server.close(error => error ? reject(error) : accept());
server.closeAllConnections();
}));
assert.equal(server.address().address, '127.0.0.1');
return server;
}
async function homepage(t) {
const response = await get(await preview(t));
assert.equal(response.status, 200);
return response.body.toString('utf8');
}
test('homepage assembles only the three common includes, with complete metadata and JSON-LD', async t => {
const html = await homepage(t);
assert.doesNotMatch(html, /<!--\s*#/u);
assert.match(html, /<!doctype html>/iu);
assert.match(html, /<html\s+lang="en"/u);
assert.equal([...html.matchAll(/<h1\b/giu)].length, 1);
assert.equal([...html.matchAll(/<head>/giu)].length, 1);
assert.equal([...html.matchAll(/<footer>/giu)].length, 1);
assert.match(text(html.match(/<title>([\s\S]*?)<\/title>/iu)?.[1] ?? ''), /Eric.*Meehan/iu);
const metas = [...html.matchAll(/<meta\b[^>]*>/giu)].map(([tag]) => attributes(tag));
assert.ok(metas.some(meta => meta.get('charset')?.toLowerCase() === 'utf-8'));
assert.ok(metas.some(meta => meta.get('name') === 'viewport' && meta.get('content')));
for (const name of ['description', 'og:title', 'og:description', 'twitter:title', 'twitter:description']) {
const meta = metas.find(item => (item.get('name') ?? item.get('property')) === name);
assert.ok(meta?.get('content'), `Missing ${name}`);
for (const topic of [/music/iu, /films?/iu, /engineering/iu]) assert.match(meta.get('content'), topic);
}
const canonical = [...html.matchAll(/<link\b[^>]*>/giu)].map(([tag]) => attributes(tag))
.find(attrs => attrs.get('rel') === 'canonical');
assert.equal(canonical?.get('href'), 'https://www.eom.dev/');
for (const [name, expected] of [['og:url', canonical.get('href')], ['og:type', 'website'],
['og:site_name', 'eom.dev'], ['twitter:card', 'summary']]) {
assert.equal(metas.find(meta => (meta.get('name') ?? meta.get('property')) === name)?.get('content'), expected);
}
assert.equal(new URL(metas.find(meta => meta.get('property') === 'og:image')?.get('content')).protocol, 'https:');
assert.ok(metas.find(meta => meta.get('property') === 'og:image:alt')?.get('content'));
const schemas = [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/giu)]
.filter(([, tag]) => attributes(tag).get('type') === 'application/ld+json')
.map(([, , json]) => JSON.parse(json));
assert.ok(schemas.length > 0, 'Missing JSON-LD');
for (const schema of schemas) {
assert.equal(schema['@context'], 'https://schema.org');
assert.equal(schema['@type'], 'Person');
assert.match(schema.name, /Meehan/u);
assert.equal(schema.url, canonical.get('href'));
assert.match(schema.jobTitle, /musician/iu);
assert.ok(Array.isArray(schema.sameAs) && schema.sameAs.length > 0);
for (const url of [schema.url, schema.image, ...schema.sameAs]) {
assert.equal(new URL(url).protocol, 'https:');
}
}
});
test('unique anchors, valid local fragments, and four creative paths ahead of services', async t => {
const html = await homepage(t);
const ids = [...html.matchAll(/<[a-z][^>]*>/giu)].map(([tag]) => attributes(tag).get('id'))
.filter(id => id !== undefined);
assert.equal(new Set(ids).size, ids.length, 'Duplicate IDs');
assert.ok(ids.every(id => id && !/\s/u.test(id)), 'IDs must be nonempty and whitespace-free');
const destinations = new Set(links(html).map(link => link.attrs.get('href')));
for (const href of destinations) {
assert.ok(href?.trim(), 'Every link needs a destination');
const url = new URL(href.replaceAll('&amp;', '&'), 'https://www.eom.dev/');
assert.ok(['https:', 'mailto:'].includes(url.protocol), `Unsafe link: ${href}`);
if (url.origin === 'https://www.eom.dev' && url.pathname === '/' && url.hash) {
assert.ok(ids.includes(decodeURIComponent(url.hash.slice(1))), `Broken fragment: ${href}`);
}
}
for (const id of ['films', 'music', 'projects', 'gaming']) {
assert.ok(destinations.has(`#${id}`), `Missing creative path ${id}`);
assert.ok(ids.indexOf(id) >= 0 && ids.indexOf(id) < ids.indexOf('services'), `${id} must precede services`);
}
const cards = [...html.matchAll(/<article\b([^>]*)>([\s\S]*?)<\/article>/giu)]
.filter(([, tag]) => attributes(tag).get('class')?.split(/\s/u).includes('path-card'));
assert.equal(cards.length, 4);
for (const id of ['films', 'music', 'projects', 'gaming']) {
assert.ok(cards.some(([, , card]) => links(card).some(link => link.attrs.get('href') === `#${id}`)));
}
for (const [, tag] of html.matchAll(/<[a-z]\w*\b([^>]*)>/giu)) {
for (const id of (attributes(tag).get('aria-labelledby') ?? '').split(/\s/u).filter(Boolean)) {
assert.ok(ids.includes(id), `Broken aria-labelledby: ${id}`);
}
}
});
test('featured films and music retain actual video destinations and labeled image links', async t => {
const html = await homepage(t);
const anchors = links(html);
for (const [id, label] of [
['b16e17cc-2545-40ad-b9fa-b9200400aff5', /Great Smokey Mountains/iu],
['678e7a68-9beb-48b2-b4cc-8cfede773a81', /La Banda Maguey/iu],
['f9613754-234e-4053-b194-341154f007c5', /Tidal Waltz/iu],
]) {
const href = `https://peertube.eom.dev/videos/watch/${id}`;
const matching = anchors.filter(link => link.attrs.get('href') === href);
assert.ok(matching.length > 0, `Missing featured video ${id}`);
const imageLink = matching.find(link => /<img\b/iu.test(link.inner));
assert.ok(imageLink, `Video ${id} needs an image link`);
assert.match(imageLink.attrs.get('aria-label') ?? text(imageLink.inner), label);
}
for (const [tag] of html.matchAll(/<img\b[^>]*>/giu)) {
const attrs = attributes(tag);
assert.ok(attrs.has('alt'), 'Images need alt text, even if decorative');
assert.ok(Number(attrs.get('width')) > 0 && Number(attrs.get('height')) > 0);
assert.ok(attrs.get('src'));
}
for (const { attrs, inner } of anchors) {
const visible = text(inner.replace(/<[^>]*\baria-hidden="true"[^>]*>[\s\S]*?<\/[^>]+>/giu, ''));
const imageLabels = [...inner.matchAll(/<img\b[^>]*>/giu)]
.map(([tag]) => attributes(tag).get('alt') ?? '').join('');
assert.ok(attrs.get('aria-label')?.trim() || visible || imageLabels.trim(), `Unlabeled link ${attrs.get('href')}`);
}
});
test('prior game, service, account and donation destinations remain intact', async t => {
const html = await homepage(t);
const destinations = new Set(links(html).map(link => link.attrs.get('href')));
for (const href of [
'https://gitea.eom.dev/WikiDeck/wikideck', 'https://button.eom.dev/',
'https://gitea.eom.dev/Gaming/the-button', 'https://discourse.eom.dev/t/luanti-season-v/690',
'https://gitea.eom.dev/explore/repos', 'https://discourse.eom.dev/',
'https://element.eom.dev/', 'https://gitea.eom.dev/', 'https://mastodon.eom.dev/',
'https://peertube.eom.dev/', 'https://owncast.eom.dev/', 'https://discourse.eom.dev/latest',
'https://peertube.eom.dev/videos/browse', 'https://mastodon.eom.dev/@eric/',
'mailto:eric@eom.dev', '/eric@eom.dev.pub', '/members/', '/members/register/',
'https://donate.stripe.com/dRm28qczOcDx05Nfd9cQU00', '/xmrig.json',
]) assert.ok(destinations.has(href), `Missing preserved destination: ${href}`);
assert.match(html, /luanti\.eom\.dev:30000/u);
assert.match(html, /48mFvxYDSCzMy5a3Zaj7ewApbX4GwXVXX9T1JC8mqBC19s9UcrNq8eaKkQ6GKdhHGQGmhdpvMd3mcbjf4eQbP8Yv7KdAREw/u);
});
test('live publication regions and automatic collection browser are wired to the API module', async t => {
const html = await homepage(t);
const regions = [...html.matchAll(/<(?:section|div)\b[^>]*\bdata-peertube-feed\b[^>]*>/giu)]
.map(([tag]) => attributes(tag));
assert.deepEqual(regions.map(region => region.get('data-channels')),
['eric_channel', 'eric_music', 'eric_tech', 'eric_gaming']);
assert.equal([...html.matchAll(/\bdata-feed-items\b/gu)].length, 4);
assert.equal([...html.matchAll(/\bdata-feed-status\b/gu)].length, 4);
assert.match(html, /<script type="module" src="\/static\/peertube-feeds\.mjs"><\/script>/u);
assert.match(html, /<label for="peertube-collection">/u);
assert.match(html, /<select id="peertube-collection" disabled>/u);
assert.match(html, /data-library-items/u);
assert.match(html, /data-library-status role="status" aria-live="polite"/u);
assert.match(html, /<noscript>/u);
const response = await get(await preview(t), '/static/peertube-feeds.mjs');
assert.equal(response.status, 200);
assert.equal(response.headers['content-type'], 'text/javascript; charset=utf-8');
});
test('gaming is separate from engineering and writing links to Discourse', async t => {
const html = await homepage(t);
const sections = [...html.matchAll(/<section\b([^>]*)>([\s\S]*?)<\/section>/giu)]
.map(([, tag, content]) => ({ id: attributes(tag).get('id'), content }));
const engineering = sections.find(section => section.id === 'projects').content;
const gaming = sections.find(section => section.id === 'gaming').content;
assert.match(engineering, /data-channels="eric_tech"/u);
assert.doesNotMatch(engineering, /eric_gaming|luanti\.eom\.dev:30000/u);
assert.match(gaming, /data-channels="eric_gaming"/u);
assert.match(gaming, /luanti\.eom\.dev:30000/u);
assert.match(gaming, /https:\/\/button\.eom\.dev\//u);
assert.doesNotMatch(engineering, /<h3>Wikideck<\/h3>/u);
assert.match(gaming, /<h3>Wikideck<\/h3>/u);
assert.ok(gaming.indexOf('<h3>Luanti</h3>') < gaming.indexOf('<h3>The Button</h3>'));
assert.ok(gaming.indexOf('<h3>Luanti</h3>') < gaming.indexOf('<h3>Wikideck</h3>'));
assert.ok(links(html).some(link => link.attrs.get('href') === 'https://discourse.eom.dev/' && /writing/iu.test(text(link.inner))));
assert.match(html, /href="\/#gaming">Gaming/u);
});
test('my projects follows public cross-organization Gitea activity', async t => {
const html = await homepage(t);
assert.match(html, /<h2 id="projects-title">My projects\.<\/h2>/u);
assert.doesNotMatch(html, /Things I wanted to build/u);
assert.match(html, /id="gitea-projects"/u);
assert.match(html, /data-project-items/u);
assert.match(html, /data-project-status role="status" aria-live="polite"/u);
assert.ok(links(html).some(link => link.attrs.get('href') === 'https://gitea.eom.dev/eric?tab=activity'));
assert.match(html, /<script type="module" src="\/static\/gitea-projects\.mjs"><\/script>/u);
const response = await get(await preview(t), '/static/gitea-projects.mjs');
assert.equal(response.status, 200);
assert.equal(response.headers['content-type'], 'text/javascript; charset=utf-8');
});
test('membership templates and routes preserve account and payment actions without emulation', async () => {
// Inspect only: no Django rendering, database, authentication, or network calls.
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
for (const name of ['home', 'register', 'login', 'profile', 'portal', 'logout', 'checkout', 'crypto_checkout']) {
assert.match(template, new RegExp(`\\{% url 'members:${name}'`), `Missing portal action ${name}`);
}
const routes = await readFile(new URL('membership/members/urls.py', repository), 'utf8');
for (const route of ['register/', 'login/', 'logout/', 'profile/', 'billing/', 'checkout/<slug:tier>/']) {
assert.ok(routes.includes(`path("${route}"`), `Missing account route ${route}`);
}
});
test('homepage groups creative sections before the library and membership distinguishes tier benefits', async t => {
const html = await homepage(t);
const order = ['films', 'music', 'projects', 'gaming', 'peertube-library'];
const positions = order.map(id => html.indexOf(`id="${id}"`));
assert.ok(positions.every((position, index) => position >= 0 && (!index || position > positions[index - 1])));
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
assert.match(template, /services included at your membership level/u);
assert.match(template, /Bronze adds exclusive Discourse and Matrix content; Silver adds previews when available; Gold adds project-topic polls/u);
assert.ok(template.indexOf('class="member-grid"') < template.indexOf('Community services by level'));
assert.match(template, /No paid-exclusive game servers are currently included/u);
assert.match(template, /There is no fixed release schedule/u);
assert.match(template, /personal consulting, managed hosting, guaranteed support/u);
assert.doesNotMatch(template, /One account for all of eom\.dev/u);
});
test('creative positioning has no appointments or consulting sales calls', async t => {
const html = await homepage(t);
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
for (const page of [html, template]) {
assert.doesNotMatch(page, /apps\/appointments|calendly\.com|cal\.com\//iu);
assert.doesNotMatch(text(page), /(?:book|schedule|buy|request)\s+(?:a\s+)?(?:free\s+)?(?:introductory\s+|sales\s+|consulting\s+)?(?:call|consultation)|hire me (?:for|to provide)/iu);
}
assert.match(text(html), /music/iu);
assert.match(text(html), /films/iu);
assert.match(text(html), /creative engineering/iu);
});
test('preview serves static MIME, no-store/nosniff and HEAD without a body', async t => {
const server = await preview(t);
for (const [path, mime] of [
['/', 'text/html; charset=utf-8'], ['/index.html', 'text/html; charset=utf-8'],
['/static/home.css', 'text/css; charset=utf-8'], ['/static/favicon.svg', 'image/svg+xml'],
['/static/profile_2026.jpg', 'image/jpeg'], ['/favicon.ico', 'image/x-icon'],
['/xmrig.json', 'application/json; charset=utf-8'], ['/robots.txt', 'text/plain; charset=utf-8'],
['/sitemap.xml', 'application/xml; charset=utf-8'],
]) {
const response = await get(server, path);
assert.equal(response.status, 200, path);
assert.equal(response.headers['content-type'], mime, path);
assert.equal(response.headers['cache-control'], 'no-store');
assert.equal(response.headers['x-content-type-options'], 'nosniff');
assert.equal(Number(response.headers['content-length']), response.body.length);
const head = await get(server, path, 'HEAD');
assert.equal(head.status, response.status);
assert.equal(head.headers['content-type'], mime);
assert.equal(head.headers['content-length'], response.headers['content-length']);
assert.equal(head.body.length, 0);
}
assert.deepEqual((await get(server, '/?preview=true')).body, (await get(server, '/')).body);
});
test('preview rejects traversal, dotfiles, malformed encodings, private files and unsafe methods', async t => {
const server = await preview(t);
for (const path of [
'/../README.md', '/static/../../membership/config/settings.py', '/%2e%2e/README.md',
'/static/%2E%2E/index.html', '/%252e%252e/README.md', '/%2f../README.md',
'/static%5c..%5cindex.html', '/static\\..\\index.html', '/.env', '/%2eenv',
'/.git/config', '/static/.secret', '/%', '/%GG', '/%C0%AF', '/%00',
'/index.html:secret', '/index.html.', '/index.html%20', '/CON', '//README.md',
]) {
const response = await get(server, path);
assert.ok([400, 403].includes(response.status), `${path}: ${response.status}`);
assert.equal(response.headers['x-content-type-options'], 'nosniff');
assert.equal(response.headers['cache-control'], 'no-store');
assert.doesNotMatch(response.body.toString(), /Z:\\|Repositories|Traceback/u);
}
for (const path of ['/README.md', '/membership/config/settings.py', '/conf/httpd.conf',
'/scripts/preview.mjs', '/tests/site.test.mjs', '/static/', '/missing.html']) {
assert.equal((await get(server, path)).status, 404, path);
assert.equal((await get(server, path, 'HEAD')).body.length, 0);
}
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE']) {
const response = await get(server, '/', method);
assert.equal(response.status, 405, method);
assert.equal(response.headers.allow, 'GET, HEAD');
}
assert.equal((await get(server, '/', 'GET', { Host: 'attacker.example' })).status, 403);
});
test('members routes are explicitly preview-only, not authentication or billing emulation', async t => {
const server = await preview(t);
for (const path of ['/members', '/members/', '/members/register/', '/members/login/',
'/members/profile/', '/members/billing/', '/members/checkout/gold/']) {
const response = await get(server, path);
assert.equal(response.status, 503, path);
assert.match(response.body.toString(), /local preview only/iu);
assert.match(response.body.toString(), /dynamic membership portal is unavailable/iu);
assert.doesNotMatch(response.body.toString(), /<form|<input|csrf|\{%/iu);
assert.equal(response.headers['set-cookie'], undefined);
const head = await get(server, path, 'HEAD');
assert.equal(head.status, 503);
assert.equal(head.body.length, 0);
assert.equal(head.headers['content-length'], response.headers['content-length']);
assert.equal((await get(server, path, 'POST')).status, 405);
}
});
async function fixture(t) {
const directory = await mkdtemp(join(tmpdir(), 'www-preview-'));
t.after(() => rm(directory, { recursive: true, force: true }));
const root = join(directory, 'htdocs');
await mkdir(join(root, 'common'), { recursive: true });
await writeFile(join(directory, 'private.txt'), 'outside-public-root');
return { directory, root };
}
test('SSI is literal, nonrecursive and cannot include arbitrary files or execute directives', async t => {
const { root } = await fixture(t);
for (const name of ['header', 'topnav', 'footer']) {
await writeFile(join(root, 'common', `${name}.html`), `<p>${name} $&</p>`);
}
const server = await preview(t, { publicRoot: root });
await writeFile(join(root, 'index.html'), ['header', 'topnav', 'footer']
.map(name => `<!--#include virtual="/common/${name}.html" -->`).join('\n'));
const response = await get(server);
assert.equal(response.status, 200);
assert.doesNotMatch(response.body.toString(), /<!--#/u);
assert.match(response.body.toString(), /header \$&/u);
for (const directive of [
'<!--#include virtual="/../private.txt" -->', '<!--#include file="../private.txt" -->',
'<!--#exec cmd="echo should-never-execute" -->', '<!--#include virtual="/common/other.html" -->',
]) {
await writeFile(join(root, 'index.html'), directive);
const rejected = await get(server);
assert.equal(rejected.status, 500);
assert.doesNotMatch(rejected.body.toString(), /outside-public-root|should-never-execute/u);
}
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
await writeFile(join(root, 'common', 'header.html'), '<!--#include virtual="/common/footer.html" -->');
assert.equal((await get(server)).status, 500);
});
test('realpath containment blocks directory symlinks/junctions, including common SSI directories', async t => {
const { directory, root } = await fixture(t);
const outside = join(directory, 'outside');
await mkdir(outside);
await writeFile(join(outside, 'header.html'), 'outside-public-root');
try {
await symlink(outside, join(root, 'escape-dir'), process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) {
t.skip(`Directory symlinks unavailable on this host (${error.code}).`);
return;
}
throw error;
}
const server = await preview(t, { publicRoot: root });
assert.equal((await get(server, '/escape-dir/header.html')).status, 403);
await rm(join(root, 'common'), { recursive: true });
await symlink(outside, join(root, 'common'), process.platform === 'win32' ? 'junction' : 'dir');
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
const response = await get(server);
assert.equal(response.status, 403);
assert.doesNotMatch(response.body.toString(), /outside-public-root/u);
});
test('realpath containment blocks file symlinks and hidden targets, including SSI', async t => {
const { directory, root } = await fixture(t);
await writeFile(join(root, '.hidden.txt'), 'hidden-target');
try {
await symlink(join(directory, 'private.txt'), join(root, 'escape.txt'), 'file');
await symlink(join(root, '.hidden.txt'), join(root, 'alias.txt'), 'file');
await symlink(join(directory, 'private.txt'), join(root, 'common', 'header.html'), 'file');
} catch (error) {
if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) {
t.skip(`File symlinks unavailable on this host (${error.code}); run on a symlink-capable host too.`);
return;
}
throw error;
}
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
const server = await preview(t, { publicRoot: root });
for (const path of ['/escape.txt', '/alias.txt', '/']) {
const response = await get(server, path);
assert.equal(response.status, 403, path);
assert.doesNotMatch(response.body.toString(), /outside-public-root|hidden-target/u);
}
});
test('port parsing defaults to 8082 and accepts only numeric ports in range', () => {
assert.equal(parsePort(), 8082);
assert.equal(parsePort('0'), 0);
assert.equal(parsePort('65535'), 65535);
for (const value of ['', '-1', '65536', '1.5', '8082x', ' 8082', 'Infinity', '127.0.0.1:8082']) {
assert.throws(() => parsePort(value), /Port must be numeric/u);
}
});
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startPreview, parsePort } from '../scripts/preview.mjs';
const repository = new URL('../', import.meta.url);
// Small scanner for this site's static markup, not a general-purpose HTML validator.
function attributes(tag) {
return new Map([...tag.matchAll(/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/gu)]
.map(([, name, double, single]) => [name.toLowerCase(), double ?? single]));
}
function text(html) {
return html.replace(/<[^>]*>/gu, ' ').replace(/&amp;/gu, '&')
.replace(/&(?:nbsp|#160);/gu, ' ').replace(/\s+/gu, ' ').trim();
}
function links(html) {
return [...html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/giu)]
.map(([, tag, inner]) => ({ attrs: attributes(tag), inner }));
}
function get(server, path = '/', method = 'GET', headers = {}) {
// Raw request paths preserve traversal probes that fetch/URL would normalize away.
return new Promise((accept, reject) => {
const req = request({ host: '127.0.0.1', port: server.address().port, path,
method, headers, agent: false }, res => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('error', reject);
res.on('end', () => accept({ status: res.statusCode, headers: res.headers,
body: Buffer.concat(chunks) }));
});
req.on('error', reject);
req.setTimeout(3_000, () => req.destroy(new Error('Preview request timed out')));
req.end();
});
}
async function preview(t, options) {
const server = await startPreview({ ...options, port: 0 });
t.after(() => new Promise((accept, reject) => {
server.close(error => error ? reject(error) : accept());
server.closeAllConnections();
}));
assert.equal(server.address().address, '127.0.0.1');
return server;
}
async function homepage(t) {
const response = await get(await preview(t));
assert.equal(response.status, 200);
return response.body.toString('utf8');
}
test('homepage assembles only the three common includes, with complete metadata and JSON-LD', async t => {
const html = await homepage(t);
assert.doesNotMatch(html, /<!--\s*#/u);
assert.match(html, /<!doctype html>/iu);
assert.match(html, /<html\s+lang="en"/u);
assert.equal([...html.matchAll(/<h1\b/giu)].length, 1);
assert.equal([...html.matchAll(/<head>/giu)].length, 1);
assert.equal([...html.matchAll(/<footer>/giu)].length, 1);
assert.match(text(html.match(/<title>([\s\S]*?)<\/title>/iu)?.[1] ?? ''), /Eric.*Meehan/iu);
const metas = [...html.matchAll(/<meta\b[^>]*>/giu)].map(([tag]) => attributes(tag));
assert.ok(metas.some(meta => meta.get('charset')?.toLowerCase() === 'utf-8'));
assert.ok(metas.some(meta => meta.get('name') === 'viewport' && meta.get('content')));
for (const name of ['description', 'og:title', 'og:description', 'twitter:title', 'twitter:description']) {
const meta = metas.find(item => (item.get('name') ?? item.get('property')) === name);
assert.ok(meta?.get('content'), `Missing ${name}`);
for (const topic of [/music/iu, /films?/iu, /engineering/iu]) assert.match(meta.get('content'), topic);
}
const canonical = [...html.matchAll(/<link\b[^>]*>/giu)].map(([tag]) => attributes(tag))
.find(attrs => attrs.get('rel') === 'canonical');
assert.equal(canonical?.get('href'), 'https://www.eom.dev/');
for (const [name, expected] of [['og:url', canonical.get('href')], ['og:type', 'website'],
['og:site_name', 'eom.dev'], ['twitter:card', 'summary']]) {
assert.equal(metas.find(meta => (meta.get('name') ?? meta.get('property')) === name)?.get('content'), expected);
}
assert.equal(new URL(metas.find(meta => meta.get('property') === 'og:image')?.get('content')).protocol, 'https:');
assert.ok(metas.find(meta => meta.get('property') === 'og:image:alt')?.get('content'));
const schemas = [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/giu)]
.filter(([, tag]) => attributes(tag).get('type') === 'application/ld+json')
.map(([, , json]) => JSON.parse(json));
assert.ok(schemas.length > 0, 'Missing JSON-LD');
for (const schema of schemas) {
assert.equal(schema['@context'], 'https://schema.org');
assert.equal(schema['@type'], 'Person');
assert.match(schema.name, /Meehan/u);
assert.equal(schema.url, canonical.get('href'));
assert.match(schema.jobTitle, /musician/iu);
assert.ok(Array.isArray(schema.sameAs) && schema.sameAs.length > 0);
for (const url of [schema.url, schema.image, ...schema.sameAs]) {
assert.equal(new URL(url).protocol, 'https:');
}
}
});
test('unique anchors, valid local fragments, and four creative paths ahead of services', async t => {
const html = await homepage(t);
const ids = [...html.matchAll(/<[a-z][^>]*>/giu)].map(([tag]) => attributes(tag).get('id'))
.filter(id => id !== undefined);
assert.equal(new Set(ids).size, ids.length, 'Duplicate IDs');
assert.ok(ids.every(id => id && !/\s/u.test(id)), 'IDs must be nonempty and whitespace-free');
const destinations = new Set(links(html).map(link => link.attrs.get('href')));
for (const href of destinations) {
assert.ok(href?.trim(), 'Every link needs a destination');
const url = new URL(href.replaceAll('&amp;', '&'), 'https://www.eom.dev/');
assert.ok(['https:', 'mailto:'].includes(url.protocol), `Unsafe link: ${href}`);
if (url.origin === 'https://www.eom.dev' && url.pathname === '/' && url.hash) {
assert.ok(ids.includes(decodeURIComponent(url.hash.slice(1))), `Broken fragment: ${href}`);
}
}
for (const id of ['films', 'music', 'projects', 'gaming']) {
assert.ok(destinations.has(`#${id}`), `Missing creative path ${id}`);
assert.ok(ids.indexOf(id) >= 0 && ids.indexOf(id) < ids.indexOf('services'), `${id} must precede services`);
}
const cards = [...html.matchAll(/<article\b([^>]*)>([\s\S]*?)<\/article>/giu)]
.filter(([, tag]) => attributes(tag).get('class')?.split(/\s/u).includes('path-card'));
assert.equal(cards.length, 4);
for (const id of ['films', 'music', 'projects', 'gaming']) {
assert.ok(cards.some(([, , card]) => links(card).some(link => link.attrs.get('href') === `#${id}`)));
}
for (const [, tag] of html.matchAll(/<[a-z]\w*\b([^>]*)>/giu)) {
for (const id of (attributes(tag).get('aria-labelledby') ?? '').split(/\s/u).filter(Boolean)) {
assert.ok(ids.includes(id), `Broken aria-labelledby: ${id}`);
}
}
});
test('featured films and music retain actual video destinations and labeled image links', async t => {
const html = await homepage(t);
const anchors = links(html);
for (const [id, label] of [
['b16e17cc-2545-40ad-b9fa-b9200400aff5', /Great Smokey Mountains/iu],
['678e7a68-9beb-48b2-b4cc-8cfede773a81', /La Banda Maguey/iu],
['f9613754-234e-4053-b194-341154f007c5', /Tidal Waltz/iu],
]) {
const href = `https://peertube.eom.dev/videos/watch/${id}`;
const matching = anchors.filter(link => link.attrs.get('href') === href);
assert.ok(matching.length > 0, `Missing featured video ${id}`);
const imageLink = matching.find(link => /<img\b/iu.test(link.inner));
assert.ok(imageLink, `Video ${id} needs an image link`);
assert.match(imageLink.attrs.get('aria-label') ?? text(imageLink.inner), label);
}
for (const [tag] of html.matchAll(/<img\b[^>]*>/giu)) {
const attrs = attributes(tag);
assert.ok(attrs.has('alt'), 'Images need alt text, even if decorative');
assert.ok(Number(attrs.get('width')) > 0 && Number(attrs.get('height')) > 0);
assert.ok(attrs.get('src'));
}
for (const { attrs, inner } of anchors) {
const visible = text(inner.replace(/<[^>]*\baria-hidden="true"[^>]*>[\s\S]*?<\/[^>]+>/giu, ''));
const imageLabels = [...inner.matchAll(/<img\b[^>]*>/giu)]
.map(([tag]) => attributes(tag).get('alt') ?? '').join('');
assert.ok(attrs.get('aria-label')?.trim() || visible || imageLabels.trim(), `Unlabeled link ${attrs.get('href')}`);
}
});
test('prior game, service, account and donation destinations remain intact', async t => {
const html = await homepage(t);
const destinations = new Set(links(html).map(link => link.attrs.get('href')));
for (const href of [
'https://gitea.eom.dev/WikiDeck/wikideck', 'https://button.eom.dev/',
'https://gitea.eom.dev/Gaming/the-button', 'https://discourse.eom.dev/t/luanti-season-v/690',
'https://gitea.eom.dev/explore/repos', 'https://discourse.eom.dev/',
'https://element.eom.dev/', 'https://gitea.eom.dev/', 'https://mastodon.eom.dev/',
'https://peertube.eom.dev/', 'https://owncast.eom.dev/', 'https://discourse.eom.dev/latest',
'https://peertube.eom.dev/videos/browse', 'https://mastodon.eom.dev/@eric/',
'mailto:eric@eom.dev', '/eric@eom.dev.pub', '/members/', '/members/register/',
'https://donate.stripe.com/dRm28qczOcDx05Nfd9cQU00', '/xmrig.json',
]) assert.ok(destinations.has(href), `Missing preserved destination: ${href}`);
assert.match(html, /luanti\.eom\.dev:30000/u);
assert.match(html, /48mFvxYDSCzMy5a3Zaj7ewApbX4GwXVXX9T1JC8mqBC19s9UcrNq8eaKkQ6GKdhHGQGmhdpvMd3mcbjf4eQbP8Yv7KdAREw/u);
});
test('live publication regions and automatic collection browser are wired to the API module', async t => {
const html = await homepage(t);
const regions = [...html.matchAll(/<(?:section|div)\b[^>]*\bdata-peertube-feed\b[^>]*>/giu)]
.map(([tag]) => attributes(tag));
assert.deepEqual(regions.map(region => region.get('data-channels')),
['eric_channel', 'eric_music', 'eric_tech', 'eric_gaming']);
assert.equal([...html.matchAll(/\bdata-feed-items\b/gu)].length, 4);
assert.equal([...html.matchAll(/\bdata-feed-status\b/gu)].length, 4);
assert.match(html, /<script type="module" src="\/static\/peertube-feeds\.mjs"><\/script>/u);
assert.match(html, /<label for="peertube-collection">/u);
assert.match(html, /<select id="peertube-collection" disabled>/u);
assert.match(html, /data-library-items/u);
assert.match(html, /data-library-status role="status" aria-live="polite"/u);
assert.match(html, /<noscript>/u);
const response = await get(await preview(t), '/static/peertube-feeds.mjs');
assert.equal(response.status, 200);
assert.equal(response.headers['content-type'], 'text/javascript; charset=utf-8');
});
test('homepage theme music loads its module and seamless loop assets', async t => {
const html = await homepage(t);
assert.match(html, /<script type="module" src="\/static\/theme-player\.mjs"><\/script>/u);
const server = await preview(t);
for (const [path, type] of [
['/static/theme-player.mjs', 'text/javascript; charset=utf-8'],
['/static/audio/eom-theme-loop.ogg', 'audio/ogg'],
['/static/audio/eom-theme-loop.m4a', 'audio/mp4'],
['/static/audio/eom-theme.mp3', 'audio/mpeg'],
]) {
const response = await get(server, path);
assert.equal(response.status, 200, path);
assert.equal(response.headers['content-type'], type, path);
}
const player = await readFile(new URL('htdocs/static/theme-player.mjs', repository), 'utf8');
// Sign-in, checkout and billing must stay real navigations, never fetched in place.
for (const route of ['login', 'logout', 'oidc', 'checkout', 'billing', 'stripe', 'shkeeper'])
assert.match(player, new RegExp(`HARD_NAV = .*\\b${route}\\b`, 'u'), route);
});
test('gaming is separate from engineering and writing links to Discourse', async t => {
const html = await homepage(t);
const sections = [...html.matchAll(/<section\b([^>]*)>([\s\S]*?)<\/section>/giu)]
.map(([, tag, content]) => ({ id: attributes(tag).get('id'), content }));
const engineering = sections.find(section => section.id === 'projects').content;
const gaming = sections.find(section => section.id === 'gaming').content;
assert.match(engineering, /data-channels="eric_tech"/u);
assert.doesNotMatch(engineering, /eric_gaming|luanti\.eom\.dev:30000/u);
assert.match(gaming, /data-channels="eric_gaming"/u);
assert.match(gaming, /luanti\.eom\.dev:30000/u);
assert.match(gaming, /https:\/\/button\.eom\.dev\//u);
assert.doesNotMatch(engineering, /<h3>Wikideck<\/h3>/u);
assert.match(gaming, /<h3>Wikideck<\/h3>/u);
assert.ok(gaming.indexOf('<h3>Luanti</h3>') < gaming.indexOf('<h3>The Button</h3>'));
assert.ok(gaming.indexOf('<h3>Luanti</h3>') < gaming.indexOf('<h3>Wikideck</h3>'));
assert.ok(links(html).some(link => link.attrs.get('href') === 'https://discourse.eom.dev/' && /writing/iu.test(text(link.inner))));
assert.match(html, /href="\/#gaming">Gaming/u);
});
test('my projects follows public cross-organization Gitea activity', async t => {
const html = await homepage(t);
assert.match(html, /<h2 id="projects-title">My projects\.<\/h2>/u);
assert.doesNotMatch(html, /Things I wanted to build/u);
assert.match(html, /id="gitea-projects"/u);
assert.match(html, /data-project-items/u);
assert.match(html, /data-project-status role="status" aria-live="polite"/u);
assert.ok(links(html).some(link => link.attrs.get('href') === 'https://gitea.eom.dev/eric?tab=activity'));
assert.match(html, /<script type="module" src="\/static\/gitea-projects\.mjs"><\/script>/u);
const response = await get(await preview(t), '/static/gitea-projects.mjs');
assert.equal(response.status, 200);
assert.equal(response.headers['content-type'], 'text/javascript; charset=utf-8');
});
test('membership templates and routes preserve account and payment actions without emulation', async () => {
// Inspect only: no Django rendering, database, authentication, or network calls.
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
for (const name of ['home', 'register', 'login', 'profile', 'portal', 'logout', 'checkout', 'crypto_checkout']) {
assert.match(template, new RegExp(`\\{% url 'members:${name}'`), `Missing portal action ${name}`);
}
const routes = await readFile(new URL('membership/members/urls.py', repository), 'utf8');
for (const route of ['register/', 'login/', 'logout/', 'profile/', 'billing/', 'checkout/<slug:tier>/']) {
assert.ok(routes.includes(`path("${route}"`), `Missing account route ${route}`);
}
});
test('homepage groups creative sections before the library and membership distinguishes tier benefits', async t => {
const html = await homepage(t);
const order = ['films', 'music', 'projects', 'gaming', 'peertube-library'];
const positions = order.map(id => html.indexOf(`id="${id}"`));
assert.ok(positions.every((position, index) => position >= 0 && (!index || position > positions[index - 1])));
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
assert.match(template, /services included at your membership level/u);
assert.match(template, /Bronze adds exclusive Discourse and Matrix content; Silver adds previews when available; Gold adds project-topic polls/u);
assert.ok(template.indexOf('class="member-grid"') < template.indexOf('Community services by level'));
assert.match(template, /No paid-exclusive game servers are currently included/u);
assert.match(template, /There is no fixed release schedule/u);
assert.match(template, /personal consulting, managed hosting, guaranteed support/u);
assert.doesNotMatch(template, /One account for all of eom\.dev/u);
});
test('creative positioning has no appointments or consulting sales calls', async t => {
const html = await homepage(t);
const template = await readFile(new URL('membership/templates/members.html', repository), 'utf8');
for (const page of [html, template]) {
assert.doesNotMatch(page, /apps\/appointments|calendly\.com|cal\.com\//iu);
assert.doesNotMatch(text(page), /(?:book|schedule|buy|request)\s+(?:a\s+)?(?:free\s+)?(?:introductory\s+|sales\s+|consulting\s+)?(?:call|consultation)|hire me (?:for|to provide)/iu);
}
assert.match(text(html), /music/iu);
assert.match(text(html), /films/iu);
assert.match(text(html), /creative engineering/iu);
});
test('preview serves static MIME, no-store/nosniff and HEAD without a body', async t => {
const server = await preview(t);
for (const [path, mime] of [
['/', 'text/html; charset=utf-8'], ['/index.html', 'text/html; charset=utf-8'],
['/static/home.css', 'text/css; charset=utf-8'], ['/static/favicon.svg', 'image/svg+xml'],
['/static/profile_2026.jpg', 'image/jpeg'], ['/favicon.ico', 'image/x-icon'],
['/xmrig.json', 'application/json; charset=utf-8'], ['/robots.txt', 'text/plain; charset=utf-8'],
['/sitemap.xml', 'application/xml; charset=utf-8'],
]) {
const response = await get(server, path);
assert.equal(response.status, 200, path);
assert.equal(response.headers['content-type'], mime, path);
assert.equal(response.headers['cache-control'], 'no-store');
assert.equal(response.headers['x-content-type-options'], 'nosniff');
assert.equal(Number(response.headers['content-length']), response.body.length);
const head = await get(server, path, 'HEAD');
assert.equal(head.status, response.status);
assert.equal(head.headers['content-type'], mime);
assert.equal(head.headers['content-length'], response.headers['content-length']);
assert.equal(head.body.length, 0);
}
assert.deepEqual((await get(server, '/?preview=true')).body, (await get(server, '/')).body);
});
test('preview rejects traversal, dotfiles, malformed encodings, private files and unsafe methods', async t => {
const server = await preview(t);
for (const path of [
'/../README.md', '/static/../../membership/config/settings.py', '/%2e%2e/README.md',
'/static/%2E%2E/index.html', '/%252e%252e/README.md', '/%2f../README.md',
'/static%5c..%5cindex.html', '/static\\..\\index.html', '/.env', '/%2eenv',
'/.git/config', '/static/.secret', '/%', '/%GG', '/%C0%AF', '/%00',
'/index.html:secret', '/index.html.', '/index.html%20', '/CON', '//README.md',
]) {
const response = await get(server, path);
assert.ok([400, 403].includes(response.status), `${path}: ${response.status}`);
assert.equal(response.headers['x-content-type-options'], 'nosniff');
assert.equal(response.headers['cache-control'], 'no-store');
assert.doesNotMatch(response.body.toString(), /Z:\\|Repositories|Traceback/u);
}
for (const path of ['/README.md', '/membership/config/settings.py', '/conf/httpd.conf',
'/scripts/preview.mjs', '/tests/site.test.mjs', '/static/', '/missing.html']) {
assert.equal((await get(server, path)).status, 404, path);
assert.equal((await get(server, path, 'HEAD')).body.length, 0);
}
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE']) {
const response = await get(server, '/', method);
assert.equal(response.status, 405, method);
assert.equal(response.headers.allow, 'GET, HEAD');
}
assert.equal((await get(server, '/', 'GET', { Host: 'attacker.example' })).status, 403);
});
test('members routes are explicitly preview-only, not authentication or billing emulation', async t => {
const server = await preview(t);
for (const path of ['/members', '/members/', '/members/register/', '/members/login/',
'/members/profile/', '/members/billing/', '/members/checkout/gold/']) {
const response = await get(server, path);
assert.equal(response.status, 503, path);
assert.match(response.body.toString(), /local preview only/iu);
assert.match(response.body.toString(), /dynamic membership portal is unavailable/iu);
assert.doesNotMatch(response.body.toString(), /<form|<input|csrf|\{%/iu);
assert.equal(response.headers['set-cookie'], undefined);
const head = await get(server, path, 'HEAD');
assert.equal(head.status, 503);
assert.equal(head.body.length, 0);
assert.equal(head.headers['content-length'], response.headers['content-length']);
assert.equal((await get(server, path, 'POST')).status, 405);
}
});
async function fixture(t) {
const directory = await mkdtemp(join(tmpdir(), 'www-preview-'));
t.after(() => rm(directory, { recursive: true, force: true }));
const root = join(directory, 'htdocs');
await mkdir(join(root, 'common'), { recursive: true });
await writeFile(join(directory, 'private.txt'), 'outside-public-root');
return { directory, root };
}
test('SSI is literal, nonrecursive and cannot include arbitrary files or execute directives', async t => {
const { root } = await fixture(t);
for (const name of ['header', 'topnav', 'footer']) {
await writeFile(join(root, 'common', `${name}.html`), `<p>${name} $&</p>`);
}
const server = await preview(t, { publicRoot: root });
await writeFile(join(root, 'index.html'), ['header', 'topnav', 'footer']
.map(name => `<!--#include virtual="/common/${name}.html" -->`).join('\n'));
const response = await get(server);
assert.equal(response.status, 200);
assert.doesNotMatch(response.body.toString(), /<!--#/u);
assert.match(response.body.toString(), /header \$&/u);
for (const directive of [
'<!--#include virtual="/../private.txt" -->', '<!--#include file="../private.txt" -->',
'<!--#exec cmd="echo should-never-execute" -->', '<!--#include virtual="/common/other.html" -->',
]) {
await writeFile(join(root, 'index.html'), directive);
const rejected = await get(server);
assert.equal(rejected.status, 500);
assert.doesNotMatch(rejected.body.toString(), /outside-public-root|should-never-execute/u);
}
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
await writeFile(join(root, 'common', 'header.html'), '<!--#include virtual="/common/footer.html" -->');
assert.equal((await get(server)).status, 500);
});
test('realpath containment blocks directory symlinks/junctions, including common SSI directories', async t => {
const { directory, root } = await fixture(t);
const outside = join(directory, 'outside');
await mkdir(outside);
await writeFile(join(outside, 'header.html'), 'outside-public-root');
try {
await symlink(outside, join(root, 'escape-dir'), process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) {
t.skip(`Directory symlinks unavailable on this host (${error.code}).`);
return;
}
throw error;
}
const server = await preview(t, { publicRoot: root });
assert.equal((await get(server, '/escape-dir/header.html')).status, 403);
await rm(join(root, 'common'), { recursive: true });
await symlink(outside, join(root, 'common'), process.platform === 'win32' ? 'junction' : 'dir');
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
const response = await get(server);
assert.equal(response.status, 403);
assert.doesNotMatch(response.body.toString(), /outside-public-root/u);
});
test('realpath containment blocks file symlinks and hidden targets, including SSI', async t => {
const { directory, root } = await fixture(t);
await writeFile(join(root, '.hidden.txt'), 'hidden-target');
try {
await symlink(join(directory, 'private.txt'), join(root, 'escape.txt'), 'file');
await symlink(join(root, '.hidden.txt'), join(root, 'alias.txt'), 'file');
await symlink(join(directory, 'private.txt'), join(root, 'common', 'header.html'), 'file');
} catch (error) {
if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) {
t.skip(`File symlinks unavailable on this host (${error.code}); run on a symlink-capable host too.`);
return;
}
throw error;
}
await writeFile(join(root, 'index.html'), '<!--#include virtual="/common/header.html" -->');
const server = await preview(t, { publicRoot: root });
for (const path of ['/escape.txt', '/alias.txt', '/']) {
const response = await get(server, path);
assert.equal(response.status, 403, path);
assert.doesNotMatch(response.body.toString(), /outside-public-root|hidden-target/u);
}
});
test('port parsing defaults to 8082 and accepts only numeric ports in range', () => {
assert.equal(parsePort(), 8082);
assert.equal(parsePort('0'), 0);
assert.equal(parsePort('65535'), 65535);
for (const value of ['', '-1', '65536', '1.5', '8082x', ' 8082', 'Infinity', '127.0.0.1:8082']) {
assert.throws(() => parsePort(value), /Port must be numeric/u);
}
});