Files
garden-astro/scripts/copy-assets.mjs
T
wesandClaude Opus 4.8 04c9aa2fb2 Astro garden skeleton: Quartz-compatible URLs, Obsidian markdown, odometer homepage
- 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>
2026-07-15 14:18:59 -04:00

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