Synergetics Dictionary Loader (SP3) — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Bring the ~21,188 Synergetics Dictionary cards onto the static Astro reader as 24 grouped letter pages with deep-linkable per-card anchors, per-card search, and R2-served images — under the Cloudflare 20k-file limit.
Architecture: A build-time Node script bundles the dict repo's already-generated per-card markdown into 24 per-letter JSON shards (committed into buckyverse — the "import sink"). A new Astro content-layer dictionaryLoader reads the shards and emits one collection entry per letter-group. /dictionary/[letter] renders every card as a <section id="C#####"> with content-visibility:auto; markdown→HTML happens at build via markdown-it, then cheerio rewrites intra-dictionary links to /dictionary/<letter>#<id> and card-image refs to R2 URLs. Card images bulk-upload to R2 once, out of the deploy.
Tech Stack: Astro 5 · TypeScript · Vitest · markdown-it (new dep) · cheerio (existing) · Pagefind · Cloudflare R2 + wrangler.
Spec: docs/superpowers/specs/2026-07-06-synergetics-dictionary-loader-design.md. This plan refines spec §2 toward the spec's documented fallback (consume the generated markdown), bundled into per-letter shards for the file-count win — the body is the proven markdown projection; cross-refs/nav/image ride embedded in it and are rewritten by the reader.
File structure
New (apps/reader):
scripts/build-dictionary-shards.mjs— bundles dict-repo generated markdown → per-letter shards.src/lib/dictionary/types.ts—DictCard,LetterGroupShard.src/lib/dictionary/parseShard.ts(+.test.ts) — parse+validate one shard JSON.src/lib/dictionary/rewriteCardHtml.ts(+.test.ts) — rewrite intra-dictionary links + image refs.src/lib/dictionary/renderCard.ts(+.test.ts) — markdown→HTML → rewrite →<section>wrap.src/loaders/dictionaryLoader.ts— content-layer loader over shards.src/pages/dictionary/index.astro— A–Z landing.src/pages/dictionary/[letter].astro— one route per letter-group.scripts/upload-dictionary-images-to-r2.sh— one-time R2 bulk upload.dictionary.config.ts— shard dir + R2 image base URL.
Modified (apps/reader):
src/content.config.ts— add thedictionarycollection.package.json— addmarkdown-it(+@types/markdown-it); add ashardsscript.public/_redirects— dictionary redirect rules (created if absent).
New (buckyverse root):
synergetics_dictionary/shards/<letter>.json× 24 — committed shard artifacts.
Config/env:
DICT_CARDS_DIR— path to the dict repo'soutput/generated/markdown/cards(default~/Projects/elib-synergetics-dictionary/output/generated/markdown/cards).PUBLIC_DICT_IMAGE_BASE— R2 base for card images (defaulthttps://downloads.buckyverse.org/dictionary/images).
Task 1: Shard builder script (generated markdown → per-letter JSON)
Files:
-
Create:
apps/reader/scripts/build-dictionary-shards.mjs -
Output:
synergetics_dictionary/shards/<letter>.json(buckyverse root) -
Step 1: Write the script
// apps/reader/scripts/build-dictionary-shards.mjs
// Bundle the dict repo's generated per-card markdown into one JSON shard per
// letter-group. Run when the dictionary content changes. Pure file processing —
// the card body stays as its proven markdown projection; the reader renders it.
import { readdir, readFile, mkdir, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
const CARDS_DIR =
process.env.DICT_CARDS_DIR ||
path.join(os.homedir(), 'Projects/elib-synergetics-dictionary/output/generated/markdown/cards');
// Shards live in the buckyverse repo root (two dirs up from apps/reader).
const OUT_DIR = path.resolve(process.cwd(), '../../synergetics_dictionary/shards');
/** Split YAML-ish frontmatter from a card .md; return { fm, body }. */
function splitFrontmatter(md) {
const m = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!m) return { fm: {}, body: md };
const fm = {};
for (const line of m[1].split('\n')) {
const kv = line.match(/^(\w+):\s*(.*)$/);
if (kv) fm[kv[1]] = kv[2].replace(/^["']|["']$/g, '');
}
return { fm, body: m[2] };
}
/** First `# Heading` line → term title. */
function extractTerm(body) {
const h = body.match(/^#\s+(.+?)\s*$/m);
return h ? h[1].trim() : '';
}
async function main() {
if (!existsSync(CARDS_DIR)) {
console.error(`Cards dir not found: ${CARDS_DIR}. Set DICT_CARDS_DIR.`);
process.exit(1);
}
await mkdir(OUT_DIR, { recursive: true });
const letters = (await readdir(CARDS_DIR, { withFileTypes: true }))
.filter((d) => d.isDirectory())
.map((d) => d.name)
.sort();
let total = 0;
for (const letter of letters) {
const dir = path.join(CARDS_DIR, letter);
const files = (await readdir(dir)).filter((f) => /^C\d+\.md$/.test(f)).sort();
const cards = [];
for (const f of files) {
const raw = await readFile(path.join(dir, f), 'utf8');
const { fm, body } = splitFrontmatter(raw);
const id = fm.id || f.replace(/\.md$/, '');
cards.push({ id, term: extractTerm(body), letter, bodyMarkdown: body.trim() });
}
await writeFile(path.join(OUT_DIR, `${letter}.json`), JSON.stringify({ letter, cards }, null, 0));
console.log(`${letter}: ${cards.length} cards`);
total += cards.length;
}
console.log(`Wrote ${letters.length} shards, ${total} cards → ${OUT_DIR}`);
}
main();
- Step 2: Add the
shardsnpm script
In apps/reader/package.json "scripts", add:
"shards": "node scripts/build-dictionary-shards.mjs"
- Step 3: Run it and verify shard count + totals
Run: cd apps/reader && pnpm shards
Expected: prints per-letter counts and Wrote 24 shards, 21188 cards. Then:
Run: ls ../../synergetics_dictionary/shards | wc -l → 24
Run: node -e "const s=require('../../synergetics_dictionary/shards/xyz.json');console.log(s.letter,s.cards.length,s.cards[0].id,JSON.stringify(s.cards[0].term))"
Expected: xyz 188 C21001 "XYZ" (or the real first term).
- Step 4: Commit
cd /Volumes/Storage/belmendo/Projects/buckyverse.org
git add apps/reader/scripts/build-dictionary-shards.mjs apps/reader/package.json synergetics_dictionary/shards
git commit -m "feat(dictionary): build per-letter card shards from generated markdown"
Task 2: Dictionary types + shard parser
Files:
-
Create:
apps/reader/src/lib/dictionary/types.ts -
Create:
apps/reader/src/lib/dictionary/parseShard.ts -
Test:
apps/reader/src/lib/dictionary/parseShard.test.ts -
Step 1: Write the types
// apps/reader/src/lib/dictionary/types.ts
export interface DictCard {
id: string; // 'C18234'
term: string; // card title
letter: string; // letter-group, e.g. 's'
bodyMarkdown: string;
}
export interface LetterGroupShard {
letter: string;
cards: DictCard[];
}
- Step 2: Write the failing test
// apps/reader/src/lib/dictionary/parseShard.test.ts
import { describe, it, expect } from 'vitest';
import { parseShard } from './parseShard';
describe('parseShard', () => {
it('parses a valid shard and preserves card order', () => {
const json = JSON.stringify({
letter: 's',
cards: [
{ id: 'C1', term: 'Synergy', letter: 's', bodyMarkdown: '# Synergy\n\ntext' },
{ id: 'C2', term: 'System', letter: 's', bodyMarkdown: '# System' },
],
});
const g = parseShard(json);
expect(g.letter).toBe('s');
expect(g.cards.map((c) => c.id)).toEqual(['C1', 'C2']);
expect(g.cards[0].term).toBe('Synergy');
});
it('throws on a malformed shard (missing cards array)', () => {
expect(() => parseShard(JSON.stringify({ letter: 's' }))).toThrow(/cards/);
});
});
- Step 3: Run test to verify it fails
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/parseShard.test.ts
Expected: FAIL — cannot find module ./parseShard.
- Step 4: Write the implementation
// apps/reader/src/lib/dictionary/parseShard.ts
import type { LetterGroupShard, DictCard } from './types';
export function parseShard(jsonText: string): LetterGroupShard {
const raw = JSON.parse(jsonText) as unknown;
if (!raw || typeof raw !== 'object') throw new Error('shard: not an object');
const obj = raw as Record<string, unknown>;
if (typeof obj.letter !== 'string') throw new Error('shard: missing letter');
if (!Array.isArray(obj.cards)) throw new Error('shard: missing cards array');
const cards: DictCard[] = obj.cards.map((c, i) => {
const card = c as Record<string, unknown>;
if (typeof card.id !== 'string') throw new Error(`shard ${obj.letter}: card ${i} missing id`);
return {
id: card.id,
term: typeof card.term === 'string' ? card.term : '',
letter: typeof card.letter === 'string' ? card.letter : (obj.letter as string),
bodyMarkdown: typeof card.bodyMarkdown === 'string' ? card.bodyMarkdown : '',
};
});
return { letter: obj.letter, cards };
}
- Step 5: Run test to verify it passes
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/parseShard.test.ts
Expected: PASS (2 tests).
- Step 6: Commit
git add apps/reader/src/lib/dictionary/types.ts apps/reader/src/lib/dictionary/parseShard.ts apps/reader/src/lib/dictionary/parseShard.test.ts
git commit -m "feat(dictionary): shard types + parser"
Task 3: Rewrite intra-dictionary links + image refs
Files:
- Create:
apps/reader/src/lib/dictionary/rewriteCardHtml.ts - Test:
apps/reader/src/lib/dictionary/rewriteCardHtml.test.ts
The generated markdown links to other cards as /synergetics-dictionary/<letter>/<id>.md and images as /synergetics_dictionary/images/cards/<letter>/<id>.... After markdown→HTML these are <a href>/<img src>; rewrite them.
- Step 1: Write the failing test
// apps/reader/src/lib/dictionary/rewriteCardHtml.test.ts
import { describe, it, expect } from 'vitest';
import { rewriteCardHtml } from './rewriteCardHtml';
const IMG_BASE = 'https://downloads.buckyverse.org/dictionary/images';
describe('rewriteCardHtml', () => {
it('rewrites a card link to a /dictionary anchor', () => {
const html = '<p>See <a href="/synergetics-dictionary/xyz/C21115.md">Zero</a></p>';
const out = rewriteCardHtml(html, IMG_BASE);
expect(out).toContain('href="/dictionary/xyz#C21115"');
expect(out).not.toContain('.md');
});
it('rewrites a card image ref to the R2 base and lazy-loads it', () => {
const html = '<p><img src="/synergetics_dictionary/images/cards/xyz/C21147.jpg" alt="C21147"></p>';
const out = rewriteCardHtml(html, IMG_BASE);
expect(out).toContain(`src="${IMG_BASE}/xyz/C21147.jpg"`);
expect(out).toContain('loading="lazy"');
});
it('leaves external links untouched', () => {
const html = '<a href="https://example.com">x</a>';
expect(rewriteCardHtml(html, IMG_BASE)).toContain('href="https://example.com"');
});
});
- Step 2: Run test to verify it fails
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/rewriteCardHtml.test.ts
Expected: FAIL — cannot find module.
- Step 3: Write the implementation
// apps/reader/src/lib/dictionary/rewriteCardHtml.ts
import * as cheerio from 'cheerio';
const CARD_LINK = /^\/synergetics-dictionary\/([a-z]+)\/(C\d+)\.md$/;
const CARD_IMG = /^\/synergetics_dictionary\/images\/cards\/(.+)$/;
/** Rewrite generated-markdown card links → /dictionary/<letter>#<id> and card
* image refs → the R2 base (lazy-loaded). imageBase has no trailing slash. */
export function rewriteCardHtml(html: string, imageBase: string): string {
const $ = cheerio.load(html, { xml: { xmlMode: false, decodeEntities: false } });
$('a[href]').each((_, el) => {
const m = ($(el).attr('href') || '').match(CARD_LINK);
if (m) $(el).attr('href', `/dictionary/${m[1]}#${m[2]}`);
});
$('img[src]').each((_, el) => {
const m = ($(el).attr('src') || '').match(CARD_IMG);
if (m) {
$(el).attr('src', `${imageBase}/${m[1]}`);
$(el).attr('loading', 'lazy');
}
});
const body = $('body');
return (body.length ? body.html() : $.root().html()) ?? '';
}
- Step 4: Run test to verify it passes
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/rewriteCardHtml.test.ts
Expected: PASS (3 tests).
- Step 5: Commit
git add apps/reader/src/lib/dictionary/rewriteCardHtml.ts apps/reader/src/lib/dictionary/rewriteCardHtml.test.ts
git commit -m "feat(dictionary): rewrite card links + image refs"
Task 4: Render a card to a Pagefind-indexed section
Files:
-
Modify:
apps/reader/package.json(addmarkdown-it,@types/markdown-it) -
Create:
apps/reader/src/lib/dictionary/renderCard.ts -
Test:
apps/reader/src/lib/dictionary/renderCard.test.ts -
Step 1: Add the markdown dependency
Run: cd apps/reader && pnpm add markdown-it && pnpm add -D @types/markdown-it
Expected: both added to package.json.
- Step 2: Write the failing test
// apps/reader/src/lib/dictionary/renderCard.test.ts
import { describe, it, expect } from 'vitest';
import { renderCard } from './renderCard';
const IMG_BASE = 'https://downloads.buckyverse.org/dictionary/images';
const card = {
id: 'C21147',
term: 'Zero = Normal',
letter: 'xyz',
bodyMarkdown:
'# Zero = Normal\n\nSee [Zero](/synergetics-dictionary/xyz/C21115.md)\n\n',
};
describe('renderCard', () => {
it('wraps the card as a Pagefind sub-result section with an id anchor', () => {
const html = renderCard(card, IMG_BASE);
expect(html).toContain('<section id="C21147"');
expect(html).toContain('data-pagefind-body');
expect(html).toContain('data-pagefind-meta="title:Zero = Normal"');
});
it('renders markdown, rewrites links + images', () => {
const html = renderCard(card, IMG_BASE);
expect(html).toContain('href="/dictionary/xyz#C21115"');
expect(html).toContain(`src="${IMG_BASE}/xyz/C21147.jpg"`);
});
});
- Step 3: Run test to verify it fails
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/renderCard.test.ts
Expected: FAIL — cannot find module ./renderCard.
- Step 4: Write the implementation
// apps/reader/src/lib/dictionary/renderCard.ts
import MarkdownIt from 'markdown-it';
import type { DictCard } from './types';
import { rewriteCardHtml } from './rewriteCardHtml';
// Preserve the RBF ASCII diagrams (indented code) and inline HTML the generator
// may emit; no auto-linkification (card links are explicit).
const md = new MarkdownIt({ html: true, linkify: false, breaks: false });
/** One card → a self-contained, Pagefind-indexable <section> with a #<id> anchor. */
export function renderCard(card: DictCard, imageBase: string): string {
const inner = rewriteCardHtml(md.render(card.bodyMarkdown), imageBase);
const term = card.term.replace(/"/g, '"');
return (
`<section id="${card.id}" class="dict-card" data-pagefind-body ` +
`data-pagefind-meta="title:${term}">` +
inner +
`</section>`
);
}
- Step 5: Run test to verify it passes
Run: cd apps/reader && pnpm vitest run src/lib/dictionary/renderCard.test.ts
Expected: PASS (2 tests).
- Step 6: Commit
git add apps/reader/package.json apps/reader/pnpm-lock.yaml apps/reader/src/lib/dictionary/renderCard.ts apps/reader/src/lib/dictionary/renderCard.test.ts
git commit -m "feat(dictionary): render card to a Pagefind section"
Task 5: The dictionary content-layer loader
Files:
-
Create:
apps/reader/dictionary.config.ts -
Create:
apps/reader/src/loaders/dictionaryLoader.ts -
Modify:
apps/reader/src/content.config.ts -
Test:
apps/reader/src/loaders/dictionaryLoader.test.ts -
Step 1: Write the config
// apps/reader/dictionary.config.ts
import path from 'node:path';
// Shards live in the buckyverse repo root; build cwd is apps/reader.
export const SHARD_DIR = path.resolve(process.cwd(), '../../synergetics_dictionary/shards');
export const IMAGE_BASE = (
(import.meta.env?.PUBLIC_DICT_IMAGE_BASE as string | undefined) ??
'https://downloads.buckyverse.org/dictionary/images'
).replace(/\/$/, '');
- Step 2: Write the failing test
// apps/reader/src/loaders/dictionaryLoader.test.ts
import { describe, it, expect } from 'vitest';
import { buildLetterEntry } from './dictionaryLoader';
describe('buildLetterEntry', () => {
it('renders a letter shard into one entry with cardCount and joined html', () => {
const shard = {
letter: 's',
cards: [
{ id: 'C1', term: 'Synergy', letter: 's', bodyMarkdown: '# Synergy\n\nA' },
{ id: 'C2', term: 'System', letter: 's', bodyMarkdown: '# System\n\nB' },
],
};
const entry = buildLetterEntry(shard, 'https://img');
expect(entry.letter).toBe('s');
expect(entry.cardCount).toBe(2);
expect(entry.cards).toHaveLength(2);
expect(entry.cards[0]).toEqual({ id: 'C1', term: 'Synergy' });
expect(entry.html).toContain('<section id="C1"');
expect(entry.html).toContain('<section id="C2"');
});
});
- Step 3: Run test to verify it fails
Run: cd apps/reader && pnpm vitest run src/loaders/dictionaryLoader.test.ts
Expected: FAIL — cannot find module.
- Step 4: Write the loader (with an exported pure
buildLetterEntry)
// apps/reader/src/loaders/dictionaryLoader.ts
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import type { Loader } from 'astro/loaders';
import { parseShard } from '../lib/dictionary/parseShard';
import { renderCard } from '../lib/dictionary/renderCard';
import type { LetterGroupShard } from '../lib/dictionary/types';
import { SHARD_DIR, IMAGE_BASE } from '../../dictionary.config';
export interface LetterEntry {
letter: string;
cardCount: number;
cards: Array<{ id: string; term: string }>; // lightweight TOC for the page
html: string; // all card <section>s concatenated
}
/** Pure: shard → one rendered letter entry. Unit-tested. */
export function buildLetterEntry(shard: LetterGroupShard, imageBase: string): LetterEntry {
return {
letter: shard.letter,
cardCount: shard.cards.length,
cards: shard.cards.map((c) => ({ id: c.id, term: c.term })),
html: shard.cards.map((c) => renderCard(c, imageBase)).join('\n'),
};
}
export function dictionaryLoader(): Loader {
return {
name: 'buckyverse-dictionary-loader',
load: async ({ store, logger }) => {
store.clear();
const files = (await readdir(SHARD_DIR)).filter((f) => f.endsWith('.json')).sort();
let total = 0;
for (const f of files) {
const shard = parseShard(await readFile(path.join(SHARD_DIR, f), 'utf8'));
const entry = buildLetterEntry(shard, IMAGE_BASE);
store.set({ id: entry.letter, data: entry });
total += entry.cardCount;
}
logger.info(`dictionary: ${files.length} letter groups, ${total} cards`);
},
};
}
- Step 5: Run test to verify it passes
Run: cd apps/reader && pnpm vitest run src/loaders/dictionaryLoader.test.ts
Expected: PASS.
- Step 6: Register the collection
In apps/reader/src/content.config.ts, add the import and collection (mirrors catalog):
import { dictionaryLoader } from './loaders/dictionaryLoader';
const dictionary = defineCollection({
loader: dictionaryLoader(),
schema: z.object({
letter: z.string(),
cardCount: z.number(),
cards: z.array(z.object({ id: z.string(), term: z.string() })),
html: z.string(),
}),
});
Then add dictionary to the exported collections:
export const collections = { chapters, catalog, dictionary };
- Step 7: Commit
git add apps/reader/dictionary.config.ts apps/reader/src/loaders/dictionaryLoader.ts apps/reader/src/loaders/dictionaryLoader.test.ts apps/reader/src/content.config.ts
git commit -m "feat(dictionary): content-layer loader + collection"
Task 6: The letter-group route
Files:
-
Create:
apps/reader/src/pages/dictionary/[letter].astro -
Step 1: Write the route
---
// apps/reader/src/pages/dictionary/[letter].astro
import { getCollection } from 'astro:content';
import SiteLayout from '../../layouts/SiteLayout.astro';
export async function getStaticPaths() {
const groups = await getCollection('dictionary');
return groups.map((g) => ({ params: { letter: g.data.letter }, props: { g: g.data } }));
}
const { g } = Astro.props;
---
<SiteLayout title={`Dictionary — ${g.letter.toUpperCase()}`}>
<main class="dict-letter">
<h1>Synergetics Dictionary — {g.letter.toUpperCase()}</h1>
<p class="dict-count">{g.cardCount} cards</p>
<div class="dict-cards" set:html={g.html} />
</main>
</SiteLayout>
<style>
.dict-letter { max-width: 72ch; margin: 0 auto; padding: 1rem; }
/* Offscreen cards cost nothing to render — key to the big letters (s = 2293). */
:global(.dict-card) {
content-visibility: auto;
contain-intrinsic-size: auto 320px;
padding-block: 1.25rem;
border-top: 1px solid var(--rule, #e4e0d8);
scroll-margin-top: 4rem; /* so #id anchors clear the top bar */
}
:global(.dict-card img) { max-width: 100%; height: auto; }
</style>
- Step 2: Build and verify the routes + a deep-link anchor exist
Run: cd apps/reader && pnpm shards && pnpm build
Expected: build succeeds; dist/dictionary/xyz/index.html exists.
Run: grep -c 'class="dict-card"' dist/dictionary/xyz/index.html
Expected: 188 (matches the xyz card count).
Run: grep -o 'id="C21002"' dist/dictionary/xyz/index.html | head -1
Expected: id="C21002".
- Step 3: Commit
git add apps/reader/src/pages/dictionary/'[letter].astro'
git commit -m "feat(dictionary): letter-group route with content-visibility cards"
Task 7: The dictionary landing page
Files:
-
Create:
apps/reader/src/pages/dictionary/index.astro -
Step 1: Write the landing page
---
// apps/reader/src/pages/dictionary/index.astro
import { getCollection } from 'astro:content';
import SiteLayout from '../../layouts/SiteLayout.astro';
const groups = (await getCollection('dictionary')).sort((a, b) =>
a.data.letter.localeCompare(b.data.letter),
);
const totalCards = groups.reduce((n, g) => n + g.data.cardCount, 0);
---
<SiteLayout title="Synergetics Dictionary">
<main class="dict-index">
<h1>Synergetics Dictionary</h1>
<p>{totalCards.toLocaleString()} cards across {groups.length} letter groups.</p>
<nav class="dict-az" aria-label="Letter groups">
<ul>
{groups.map((g) => (
<li>
<a href={`/dictionary/${g.data.letter}`}>
<span class="dict-az-letter">{g.data.letter.toUpperCase()}</span>
<span class="dict-az-count">{g.data.cardCount}</span>
</a>
</li>
))}
</ul>
</nav>
</main>
</SiteLayout>
<style>
.dict-index { max-width: 72ch; margin: 0 auto; padding: 1rem; }
.dict-az ul { list-style: none; padding: 0; display: grid;
grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr)); gap: .5rem; }
.dict-az a { display: flex; justify-content: space-between; padding: .6rem .8rem;
border: 1px solid var(--rule, #e4e0d8); border-radius: .4rem; text-decoration: none; }
.dict-az-count { color: var(--muted, #666); font-variant-numeric: tabular-nums; }
</style>
- Step 2: Build and verify the landing lists all 24 groups
Run: cd apps/reader && pnpm build
Run: grep -c 'dict-az-letter' dist/dictionary/index.html
Expected: 24.
- Step 3: Commit
git add apps/reader/src/pages/dictionary/index.astro
git commit -m "feat(dictionary): A–Z landing page"
Task 8: Re-home the front-matter pages
The dictionary's front-matter (How-to-Use, Sources-Cited, Preamble, Contents-of-the-Set, Some-Perspectives, Annotated-Bibliography, …) currently lives as markdown at buckyverse root synergetics-dictionary/*.md and synergetics_dictionary/*.md. Surface them under /dictionary/.
Files:
-
Create:
apps/reader/src/pages/dictionary/about/[page].astro -
Create:
apps/reader/dictionary-frontmatter.config.ts -
Step 1: Config listing the front-matter source files
// apps/reader/dictionary-frontmatter.config.ts
import path from 'node:path';
const ROOT = path.resolve(process.cwd(), '../../synergetics-dictionary');
// slug (URL) → repo markdown file
export const FRONT_MATTER: Array<{ slug: string; file: string; title: string }> = [
{ slug: 'how-to-use', file: path.join(ROOT, 'How-to-Use-the-Synergetics-Dictionary.md'), title: 'How to Use the Synergetics Dictionary' },
{ slug: 'preamble', file: path.join(ROOT, 'Preamble-with-a-Few-Explanations.md'), title: 'Preamble' },
{ slug: 'contents-of-the-set', file: path.join(ROOT, 'Contents-of-the-Set.md'), title: 'Contents of the Set' },
{ slug: 'sources-cited', file: path.join(ROOT, 'Sources-Cited-List-of-Titles.md'), title: 'Sources Cited' },
{ slug: 'some-perspectives', file: path.join(ROOT, 'Some-Perspectives-of-Fullers-Mathematics.md'), title: "Some Perspectives of Fuller's Mathematics" },
{ slug: 'annotated-bibliography', file: path.join(ROOT, 'An-Annotated-Bibliography-of-Books-by-R-Buckminster-Fuller.md'), title: 'An Annotated Bibliography of Books by R. Buckminster Fuller' },
];
- Step 2: Write the route (strips WikiJS frontmatter, renders markdown, reuses rewrite)
---
// apps/reader/src/pages/dictionary/about/[page].astro
import { readFile } from 'node:fs/promises';
import MarkdownIt from 'markdown-it';
import SiteLayout from '../../../layouts/SiteLayout.astro';
import { rewriteCardHtml } from '../../../lib/dictionary/rewriteCardHtml';
import { IMAGE_BASE } from '../../../../dictionary.config';
import { FRONT_MATTER } from '../../../../dictionary-frontmatter.config';
const md = new MarkdownIt({ html: true, linkify: false });
export async function getStaticPaths() {
return Promise.all(
FRONT_MATTER.map(async (fm) => {
const raw = await readFile(fm.file, 'utf8');
const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, ''); // drop WikiJS frontmatter
const html = rewriteCardHtml(md.render(body), IMAGE_BASE);
return { params: { page: fm.slug }, props: { title: fm.title, html } };
}),
);
}
const { title, html } = Astro.props;
---
<SiteLayout title={title}>
<main class="dict-about" data-pagefind-body>
<article set:html={html} />
</main>
</SiteLayout>
<style>.dict-about { max-width: 72ch; margin: 0 auto; padding: 1rem; }</style>
- Step 3: Build and verify a front-matter page renders
Run: cd apps/reader && pnpm build
Run: test -f dist/dictionary/about/how-to-use/index.html && echo OK
Expected: OK.
- Step 4: Commit
git add apps/reader/dictionary-frontmatter.config.ts apps/reader/src/pages/dictionary/about/'[page].astro'
git commit -m "feat(dictionary): re-home front-matter pages under /dictionary/about"
Task 9: Upload card images to R2
Files:
- Create:
apps/reader/scripts/upload-dictionary-images-to-r2.sh
The ~21,193 card JPGs live in the dict repo at content/images/cards/** (or buckyverse synergetics_dictionary/images/cards/**). Upload once to R2 under dictionary/images/<letter>/<id>.jpg so IMAGE_BASE resolves.
- Step 1: Write the upload script
#!/usr/bin/env bash
# apps/reader/scripts/upload-dictionary-images-to-r2.sh
# One-time bulk upload of dictionary card images to R2. Requires wrangler auth.
set -euo pipefail
SRC="${DICT_IMAGES_DIR:-$HOME/Projects/elib-synergetics-dictionary/content/images/cards}"
BUCKET="${R2_BUCKET:-buckyverse-downloads}"
[ -d "$SRC" ] || { echo "Images dir not found: $SRC (set DICT_IMAGES_DIR)"; exit 1; }
count=0
while IFS= read -r -d '' f; do
rel="${f#"$SRC"/}" # e.g. xyz/C21147.jpg
key="dictionary/images/${rel}"
npx --no-install wrangler r2 object put "${BUCKET}/${key}" --file "$f" --content-type image/jpeg >/dev/null
count=$((count+1))
[ $((count % 500)) -eq 0 ] && echo "uploaded $count…"
done < <(find "$SRC" -type f -name '*.jpg' -print0)
echo "Done: uploaded $count images to r2://${BUCKET}/dictionary/images/"
- Step 2: Make it executable and dry-check one object
Run: chmod +x apps/reader/scripts/upload-dictionary-images-to-r2.sh
Run (single-file smoke): upload one known image, then:
curl -s -o /dev/null -w '%{http_code}\n' https://downloads.buckyverse.org/dictionary/images/xyz/C21147.jpg
Expected: 200 after the real run. (Full run is long; run it in the background.)
- Step 3: Commit the script
git add apps/reader/scripts/upload-dictionary-images-to-r2.sh
git commit -m "feat(dictionary): R2 bulk-upload script for card images"
Task 10: Dictionary redirect rules (for SP6)
Files:
- Modify/Create:
apps/reader/public/_redirects
Cloudflare Pages _redirects supports :splat/placeholders but cannot map a path to a #fragment per-card with a wildcard. So: redirect the per-card and index URLs to the letter page; the card anchor is best-effort via a param. Redirect the letter-index and front-matter exactly.
- Step 1: Append the dictionary rules
Create apps/reader/public/_redirects (or append) with:
# Synergetics Dictionary (Wiki.js → Astro). Activated at SP6 cutover.
/synergetics-dictionary/:letter/index /dictionary/:letter 301
/synergetics-dictionary/:letter/:card /dictionary/:letter#:card 301
/en/synergetics-dictionary/:letter/index /dictionary/:letter 301
/en/synergetics-dictionary/:letter/:card /dictionary/:letter#:card 301
/synergetics-dictionary/How-to-Use-the-Synergetics-Dictionary /dictionary/about/how-to-use 301
- Step 2: Verify Astro copies
_redirectsinto the build
Run: cd apps/reader && pnpm build
Run: head -3 dist/_redirects
Expected: the dictionary comment + first rule appear (Astro copies public/** verbatim).
- Step 3: Commit
git add apps/reader/public/_redirects
git commit -m "feat(dictionary): Wiki.js→Astro redirect rules (SP6)"
Task 11: Full build smoke + Pagefind per-card results
Files: none (verification + one deploy)
- Step 1: Full build with Pagefind
Run: cd apps/reader && pnpm shards && pnpm build
Expected: succeeds. File-budget check:
Run: find dist -type f | wc -l
Expected: previous count (~8,545) + ~40, still well under 20,000.
- Step 2: Confirm Pagefind indexed cards as sub-results
Run: grep -rl 'data-pagefind-body' dist/dictionary/ | head
Expected: letter pages listed.
Run: ls dist/pagefind/ | head
Expected: pagefind.js and index fragments present (cards indexed).
- Step 3: Run the whole unit suite
Run: cd apps/reader && pnpm test
Expected: all prior tests + the 4 new dictionary test files PASS.
- Step 4: Deploy to future.buckyverse.org and verify live
Run: cd apps/reader && node_modules/.bin/wrangler pages deploy dist --project-name buckyverse-future --branch main --commit-dirty=true
Then:
Run: curl -s -o /dev/null -w '%{http_code}\n' https://future.buckyverse.org/dictionary/
Run: curl -s -o /dev/null -w '%{http_code}\n' https://future.buckyverse.org/dictionary/xyz
Run: curl -s https://future.buckyverse.org/dictionary/xyz | grep -o 'id="C21002"' | head -1
Expected: 200, 200, id="C21002".
- Step 5: Commit any final tweaks
git add -A apps/reader
git commit -m "chore(dictionary): SP3 build + deploy verification"
Self-review notes (coverage against the spec)
- §1 goals → Tasks 6/7 (browsable), 6 (deep link
#id), 3 (cross-refs), 4+11 (per-card Pagefind), 9 (R2 images), 11 (file budget), 10 (redirect map). ✅ - §2 source/import artifact → Task 1 (per-letter shards from generated markdown; documented refinement of §2). ✅
- §4 architecture (loader, grouped pages, content-visibility, ~40 files) → Tasks 5, 6, 11. ✅
- §5 URL model + cross-links + redirects → Tasks 3, 6, 10. ✅
- §6 images→R2 → Task 9. ✅
- §7 per-card Pagefind → Tasks 4, 11. ✅
- §8 scope (front-matter pages) → Task 8. ✅
- §9 testing → Tasks 2–7, 11. ✅
Deferred to SP5 (not this plan): cross-references from cards into book passages / .wiki/ concepts. Parallel track: re-running Task 1 (shards) + Task 9 (images) when dictionary content changes.