- glob content loader over the vault's Digital_Garden with verbatim-path ids - remark plugin: wikilinks, image embeds (wiki + relative md), callouts - URL parity with live Quartz sitemap verified 102/102 (built set is a strict superset: +30 synthetic folder listings) - KaTeX, lazy mermaid, shiki dual themes, quoted-string draft handling - copy-assets resolves embeds vault-wide (fixes live-site 404s for Blog Scratch/assets images) into flat /assets/ - homepage odometer widget: fetches api.c0smere.net baseline, ticks at lifetime average rate, fail-closed hidden on error - deploy/: nginx try_files config + compose (port 18100) + containerized build script for cyrion Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.8 KiB
JavaScript
86 lines
2.8 KiB
JavaScript
// Collect every image referenced by an Obsidian embed (![[...]]) in the
|
|
// garden content and copy it to public/assets/ flat by basename — matching
|
|
// the /assets/<name> URLs the remark plugin emits. Resolution order:
|
|
// Digital_Garden first, then the wider vault (some embeds point at files
|
|
// that live outside the published folder; the live Quartz site 404s those —
|
|
// we fix them, but loudly, so stray references are visible).
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
CONTENT_DIR,
|
|
VAULT_ROOT,
|
|
walkMarkdown,
|
|
} from '../src/lib/quartz-compat.mjs';
|
|
|
|
const OUT_DIR = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'public',
|
|
'assets',
|
|
);
|
|
const EMBED_RE = /!\[\[([^\]|]+?\.(?:png|jpe?g|gif|svg|webp))(?:\|[^\]]*)?\]\]/gi;
|
|
const MD_IMG_RE = /!\[[^\]]*\]\(([^)\s]+?\.(?:png|jpe?g|gif|svg|webp))\)/gi;
|
|
|
|
function walkFiles(dir) {
|
|
const out = [];
|
|
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (ent.name.startsWith('.')) continue;
|
|
const full = path.join(dir, ent.name);
|
|
if (ent.isDirectory()) out.push(...walkFiles(full));
|
|
else out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// referenced image names (basename, as Obsidian resolves vault-wide)
|
|
const wanted = new Map(); // lowercase basename -> name as referenced
|
|
for (const rel of walkMarkdown(CONTENT_DIR)) {
|
|
const text = fs.readFileSync(path.join(CONTENT_DIR, rel), 'utf8');
|
|
for (const m of text.matchAll(EMBED_RE)) {
|
|
const name = m[1].split('/').pop().trim();
|
|
wanted.set(name.toLowerCase(), name);
|
|
}
|
|
for (const m of text.matchAll(MD_IMG_RE)) {
|
|
if (/^(?:[a-z]+:)?\/\//i.test(m[1])) continue;
|
|
const name = decodeURIComponent(m[1]).split('/').pop().trim();
|
|
wanted.set(name.toLowerCase(), name);
|
|
}
|
|
}
|
|
|
|
// index every file in the vault by lowercase basename
|
|
const inDG = new Map();
|
|
const inVault = new Map();
|
|
for (const f of walkFiles(VAULT_ROOT)) {
|
|
const key = path.basename(f).toLowerCase();
|
|
if (f.startsWith(CONTENT_DIR + path.sep)) {
|
|
if (!inDG.has(key)) inDG.set(key, f);
|
|
} else if (!inVault.has(key)) {
|
|
inVault.set(key, f);
|
|
}
|
|
}
|
|
|
|
fs.rmSync(OUT_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
let copied = 0;
|
|
let missing = 0;
|
|
for (const [key, name] of wanted) {
|
|
const src = inDG.get(key) ?? inVault.get(key);
|
|
if (!src) {
|
|
console.warn(`copy-assets: UNRESOLVED image embed: ${name}`);
|
|
missing++;
|
|
continue;
|
|
}
|
|
if (!inDG.has(key)) {
|
|
console.warn(
|
|
`copy-assets: ${name} lives outside Digital_Garden (${path.relative(VAULT_ROOT, src)})`,
|
|
);
|
|
}
|
|
fs.copyFileSync(src, path.join(OUT_DIR, name));
|
|
copied++;
|
|
}
|
|
|
|
console.log(`copy-assets: ${copied} copied, ${missing} unresolved -> ${OUT_DIR}`);
|
|
if (missing) process.exitCode = 0; // warn, don't fail the build
|