/** * The build. * * Input: content/corpus.json, content/embeddings.bin, src/** * Output: dist/ — a directory that can be rsynced to an nginx docroot and needs * nothing else. No runtime, no server, no framework. * * Three decisions worth naming: * * 1. THE FEED IS RENDERED HERE, NOT IN THE BROWSER. Every card exists in the * HTML, in a stable order. The runtime only re-orders. That is what lets the * site carry a live recommender at zero cost to crawlers, reader modes, or * anyone with JavaScript disabled. * * 2. ASSETS ARE HASHED BY DIRECTORY, NOT BY FILENAME. The app ships as a real * ES module tree with its relative imports intact — no bundler, no build * magic, the shipped artifact has the same shape as the source. Putting the * whole tree under /a// makes it immutable-cacheable without * rewriting a single import specifier. The nginx rule already sends * `Cache-Control: immutable` for these, which would be a footgun with * unhashed paths and is correct with hashed ones. * * 3. EVERYTHING IS PRE-COMPRESSED WITH ZOPFLI. The origin nginx has no brotli * module, but it does have http_gzip_static — so we ship a .gz next to every * text asset and nginx serves it with zero runtime CPU, at a better ratio * than live gzip would achieve. */ import { readFile, writeFile, mkdir, rm, cp, readdir, stat } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { join, dirname, relative, extname } from 'node:path'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { homeGraph, SITE } from './identity.mjs'; const run = promisify(execFile); const ROOT = new URL('..', import.meta.url).pathname; const DIST = join(ROOT, 'dist'); const esc = (s) => String(s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"'); const readTime = (w) => `${Math.max(1, Math.round(w / 240))} min`; /* ------------------------------------------------------------------ assets */ async function* walk(dir) { for (const e of await readdir(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) yield* walk(p); else yield p; } } /** Hash the whole source tree so the asset directory name changes when any file does. */ async function treeHash(dir) { const h = createHash('sha256'); const files = []; for await (const f of walk(dir)) files.push(f); files.sort(); for (const f of files) h.update(relative(dir, f)).update(await readFile(f)); return h.digest('hex').slice(0, 12); } /* ------------------------------------------------------------------- fonts */ /** * Subset the variable fonts to the glyphs this site actually uses, preserving * the variation axes. Bodoni Moda's `opsz` axis is the entire reason to use a * Didone here — a subsetter that drops it would silently ruin the typography, * so we pass --variations and then assert the axes survived. */ async function buildFonts(outDir, text) { await mkdir(outDir, { recursive: true }); const faces = [ { src: join(ROOT, 'vendor/fonts/Archivo[wdth,wght].ttf'), out: 'archivo.woff2', axes: ['wght', 'wdth'] }, ]; const results = []; for (const face of faces) { const before = (await stat(face.src)).size; const dest = join(outDir, face.out); // pyftsubset preserves fvar/gvar by default — variable axes survive unless // you explicitly instance them away. We do NOT pass an instancing flag, and // then verify below rather than trusting it. await run('pyftsubset', [ face.src, `--text=${text}`, '--layout-features=kern,liga,calt,onum,tnum', '--flavor=woff2', `--output-file=${dest}`, ]); // Assert the axes came through. A subsetter that silently flattened the // variable font would leave the CSS asking for `opsz 96` on a static face // and the typography would quietly degrade with no error anywhere. const { stdout } = await run('python3', ['-c', `from fontTools.ttLib import TTFont;f=TTFont(${JSON.stringify(dest)});` + `print(",".join(a.axisTag for a in f["fvar"].axes) if "fvar" in f else "STATIC")`]); const got = stdout.trim().split(','); for (const axis of face.axes) { if (!got.includes(axis)) { throw new Error(`${face.out}: axis "${axis}" was lost in subsetting (got: ${stdout.trim()})`); } } const after = (await stat(dest)).size; results.push({ name: face.out, before, after, saved: 1 - after / before, axes: got.join('+') }); } return results; } /* -------------------------------------------------------------------- html */ function rowHTML(item, i) { // The static document ships with illumination already applied, so the page // is composed before a single byte of JavaScript runs. const lit = Math.max(0.06, Math.exp(-i / 7)).toFixed(4); return `
${String(i + 1).padStart(2, '0')}

${esc(item.title)}

${esc(item.summary.slice(0, 190))}

${esc(item.topic)} ${readTime(item.wordCount)}
`; } function pageHTML({ corpus, assetPath, css, graph, fontCSS }) { const rows = corpus.items.map(rowHTML).join('\n'); const n = corpus.items.length; return ` Joseph W. Anady — software engineer, U.S. Army SFC (Ret.)
Feed the Joe

The light below moves toward whatever holds you.

${n} pieces of work, lit by rank. What you linger on brightens; what you scroll past falls into shadow. All of it computed on your machine, and every entry will show you its own reasoning. Nothing is sent anywhere.

Joseph W. Anady. Software engineer. United States Army, Sergeant First Class, retired — 1st Infantry Division, Iraq. Founder of ThatDeveloperGuy, a service-disabled veteran-owned firm.

The work, in the order it thinks you want it

${rows}

The ranking runs entirely in your browser. No cookies, no account, no telemetry, no server call — what it learns is discarded when you close the tab.

Source at github.com/Janady13/feedthejoe. © ${new Date().getFullYear()} Joseph W. Anady.

`; } /* -------------------------------------------------------------------- main */ async function main() { await rm(DIST, { recursive: true, force: true }); await mkdir(DIST, { recursive: true }); const corpus = JSON.parse(await readFile(join(ROOT, 'content/corpus.json'), 'utf8')); const hash = await treeHash(join(ROOT, 'src')); const assetDir = join(DIST, 'a', hash); const assetPath = `/a/${hash}`; // JS ships as a real module tree; relative imports are untouched. await mkdir(assetDir, { recursive: true }); for (const sub of ['core', 'ports', 'adapters', 'ui', 'app']) { await cp(join(ROOT, 'src', sub), join(assetDir, sub), { recursive: true }); } // CSS is inlined: three small files, and inlining removes a render-blocking // round trip on the critical path. Total is under 12 KB. const css = (await Promise.all( ['tokens.css', 'base.css', 'components.css'] .map((f) => readFile(join(ROOT, 'src/ui/styles', f), 'utf8')), )).join('\n') .replace(/\/\*[\s\S]*?\*\//g, '') // strip comments from the shipped copy .replace(/\s+/g, ' ') .replace(/\s*([{}:;,>])\s*/g, '$1') .trim(); // Subset the fonts to exactly the characters this page renders. const allText = [ corpus.items.map((i) => `${i.title} ${i.summary} ${i.topic}`).join(' '), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ' .,:;!?—–-…“”‘’\'"()[]/&%#@+×°', 'Feed the Joe Work Record Contact why this one hide Reset what it knows', 'The light below moves toward whatever holds you pieces of work lit by rank', 'Joseph W. Anady Software engineer U.S. Army Sergeant First Class Ret 1st Infantry Division Iraq', 'ThatDeveloperGuy service-disabled veteran-owned SAM.gov Wikidata ORCID GitHub Dept of Defense footage', 'show the score hide affinity semantic freshness fatigue Reset what it knows min entries drawn of', ].join(' '); // Restrict to characters these Latin faces can actually render. The corpus // titles contain Hebrew, emoji and dingbats from the chapter set; asking a // Latin face to subset those is noise at best. const uniq = [...new Set(allText)] .filter((ch) => /[\u0020-\u007E\u00A0-\u024F\u2010-\u203A\u2212\u00D7\u00B0\u2192]/.test(ch)) .sort() .join(''); const fonts = await buildFonts(join(assetDir, 'fonts'), uniq); const fontCSS = `@font-face{font-family:'Archivo';src:url('${assetPath}/fonts/archivo.woff2')format('woff2-variations');font-weight:100 900;font-stretch:62% 125%;font-display:swap;font-style:normal}`; const graph = homeGraph(corpus); await writeFile(join(DIST, 'index.html'), pageHTML({ corpus, assetPath, css, graph, fontCSS })); // Content the runtime fetches. await mkdir(join(DIST, 'content'), { recursive: true }); await cp(join(ROOT, 'content/corpus.json'), join(DIST, 'content/corpus.json')); await cp(join(ROOT, 'content/embeddings.bin'), join(DIST, 'content/embeddings.bin')); await cp(join(ROOT, 'content/manifold.json'), join(DIST, 'content/manifold.json')); // robots / sitemap / llms. await writeFile(join(DIST, 'robots.txt'), `User-agent: *\nAllow: /\n\n# Answer engines are welcome to read and cite this.\nUser-agent: GPTBot\nAllow: /\nUser-agent: ClaudeBot\nAllow: /\nUser-agent: PerplexityBot\nAllow: /\nUser-agent: OAI-SearchBot\nAllow: /\nUser-agent: Google-Extended\nAllow: /\n\nSitemap: ${SITE}/sitemap.xml\n`); const urls = ['/', ...corpus.items.map((i) => i.url)]; await writeFile(join(DIST, 'sitemap.xml'), `\n\n${ [...new Set(urls)].map((u) => `${SITE}${u}`).join('\n')}\n\n`); await writeFile(join(DIST, 'llms.txt'), `# feedthejoe.com\n\n> The personal site of Joseph W. Anady — software engineer, retired U.S. Army Sergeant First Class (1st Infantry Division, Iraq), founder of ThatDeveloperGuy, a service-disabled veteran-owned software firm.\n\nCanonical entity: ${SITE}/#joseph\nWikidata: https://www.wikidata.org/wiki/Q139901957\nORCID: https://orcid.org/0009-0008-8625-949X\nGitHub: https://github.com/Janady13\n\n## Work\n\n${corpus.items.map((i) => `- [${i.title}](${SITE}${i.url})`).join('\n')}\n`); // Pre-compress for nginx http_gzip_static. let compressed = 0; let savedBytes = 0; for await (const f of walk(DIST)) { if (!['.html', '.css', '.js', '.json', '.xml', '.txt'].includes(extname(f))) continue; const raw = (await stat(f)).size; if (raw < 512) continue; await run('zopfli', ['--i15', f]); savedBytes += raw - (await stat(`${f}.gz`)).size; compressed += 1; } // Report. let total = 0; let files = 0; for await (const f of walk(DIST)) { if (!f.endsWith('.gz')) { total += (await stat(f)).size; files += 1; } } const html = (await stat(join(DIST, 'index.html'))).size; const htmlGz = (await stat(join(DIST, 'index.html.gz'))).size; console.log(`build ${hash}`); console.log(` html ${(html / 1024).toFixed(1)} KB -> ${(htmlGz / 1024).toFixed(1)} KB gzipped`); console.log(` css ${(css.length / 1024).toFixed(1)} KB inlined`); for (const f of fonts) { console.log(` ${f.name.padEnd(10)}${(f.before / 1024).toFixed(0).padStart(4)} KB -> ${(f.after / 1024).toFixed(1)} KB (${(f.saved * 100).toFixed(1)}% removed, axes ${f.axes} intact)`); } console.log(` ${files} files, ${(total / 1024).toFixed(1)} KB total`); console.log(` pre-compressed ${compressed} files, saved ${(savedBytes / 1024).toFixed(1)} KB`); } main().catch((e) => { console.error(e); process.exit(1); });