build-recent.mjs 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. #!/usr/bin/env node
  2. /**
  3. * build-recent.mjs — extract the latest 10 entries from each section index's
  4. * `## Log` (docs/notes/lists) into src/data/recent.json, for the "Recent"
  5. * column on the home page. URLs are normalized to real routes (verified against
  6. * graph.json) so the sidebar links resolve. Run via `npm run recent` (and the
  7. * prebuild hook). Reads graph.json, so run build-graph.mjs first.
  8. */
  9. import { readFileSync, writeFileSync, mkdirSync } from 'fs';
  10. import { join, dirname } from 'path';
  11. import { fileURLToPath } from 'url';
  12. const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
  13. const N = 10;
  14. const SECTIONS = [
  15. { key: 'docs', file: 'docs/index.md', base: '/docs' },
  16. { key: 'notes', file: 'notes/index.md', base: '/notes' },
  17. { key: 'lists', file: 'lists/index.md', base: '/lists' },
  18. ];
  19. const graph = JSON.parse(readFileSync(join(ROOT, 'src/data/graph.json'), 'utf8'));
  20. const routes = new Set(graph.nodes.map((n) => n.id));
  21. const routesLc = new Map(graph.nodes.map((n) => [n.id.toLowerCase(), n.id]));
  22. function normalizeUrl(url, base) {
  23. let u = url.split('#')[0].split('?')[0].replace(/\.mdx?$/i, '');
  24. if (!u.startsWith('/')) u = `${base}/${u}`;
  25. u = u.replace(/\/{2,}/g, '/');
  26. if (u.length > 1) u = u.replace(/\/$/, '');
  27. if (routes.has(u)) return u;
  28. const ci = routesLc.get(u.toLowerCase());
  29. if (ci) return ci;
  30. const seg = u.split('/'); // folder-index collapse: /x/foo/foo → /x/foo
  31. if (seg.length >= 2 && seg[seg.length - 1] === seg[seg.length - 2]) {
  32. const collapsed = seg.slice(0, -1).join('/');
  33. if (routes.has(collapsed)) return collapsed;
  34. }
  35. return u; // best effort
  36. }
  37. const LINK_RE = /\[([^\]]+)\]\(([^)\s]+)\)/;
  38. const out = {};
  39. for (const { key, file, base } of SECTIONS) {
  40. const text = readFileSync(join(ROOT, file), 'utf8');
  41. const logStart = text.indexOf('## Log');
  42. const body = logStart === -1 ? '' : text.slice(logStart);
  43. const items = [];
  44. for (const line of body.split('\n')) {
  45. const dm = line.match(/^\s*-\s*(\d{2}\/\d{2}(?:\/\d{2})?)\s*-\s*(.*)$/);
  46. if (!dm) continue;
  47. const link = dm[2].match(LINK_RE);
  48. if (!link) continue;
  49. items.push({ date: dm[1], title: link[1].replace(/[`*_]/g, '').trim(), url: normalizeUrl(link[2], base) });
  50. if (items.length >= N) break;
  51. }
  52. out[key] = items;
  53. }
  54. mkdirSync(join(ROOT, 'src', 'data'), { recursive: true });
  55. writeFileSync(join(ROOT, 'src', 'data', 'recent.json'), JSON.stringify(out, null, 0) + '\n');
  56. console.log('recent.json →', SECTIONS.map((s) => `${s.key}=${out[s.key].length}`).join(' '));
  57. for (const s of SECTIONS) console.log(` ${s.key}: ${out[s.key].slice(0, 3).map((i) => i.date + ' ' + i.url).join(' | ')} …`);