const fs = require('fs'); const path = require('path'); const matter = require('gray-matter'); const MarkdownIt = require('markdown-it'); const hljs = require('highlight.js'); const ROOT = path.resolve(__dirname, '..'); const CONTENT_DIR = path.join(ROOT, 'content'); const DIST_DIR = path.join(ROOT, 'dist'); const TEMPLATE_DIR = path.join(ROOT, 'templates'); const ASSETS_DIR = path.join(ROOT, 'assets'); const md = new MarkdownIt({ html: true, linkify: true, highlight: (str, lang) => { if (lang && hljs.getLanguage(lang)) { return `
${hljs.highlight(str, { language: lang }).value}
`; } return `
${md.utils.escapeHtml(str)}
`; }, }); const readTemplate = (name) => fs.readFileSync(path.join(TEMPLATE_DIR, name), 'utf8'); const templates = { base: readTemplate('base.html'), header: readTemplate('header.html'), sidebar: readTemplate('sidebar.html'), footer: readTemplate('footer.html'), }; const render = (template, data) => template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => data[key] ?? ''); const slugify = (value) => value .toString() .trim() .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); const walkMarkdownFiles = (dir) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); return entries.flatMap((entry) => { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) return walkMarkdownFiles(fullPath); if (entry.isFile() && entry.name.endsWith('.md')) return [fullPath]; return []; }); }; const cleanDir = (dir) => { if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); }; const stripMarkdown = (text) => text.replace(/[`*_>#\-]/g, '').replace(/\s+/g, ' ').trim(); const loadNotes = () => { const files = walkMarkdownFiles(CONTENT_DIR); return files.map((filePath) => { const raw = fs.readFileSync(filePath, 'utf8'); const { data, content } = matter(raw); const relative = path.relative(CONTENT_DIR, filePath); const fileName = path.basename(filePath, '.md'); const section = data.section || path.dirname(relative) || 'notes'; const sectionSlug = slugify(section); const slug = slugify(data.slug || fileName); const title = data.title || fileName.replace(/-/g, ' '); const html = md.render(content); const summary = (data.summary || stripMarkdown(content).slice(0, 180)) + '...'; return { title, section, sectionSlug, slug, summary, tags: data.tags || [], nav: data.nav ?? Number.MAX_SAFE_INTEGER, html, sourcePath: relative, }; }); }; const buildNavLinks = (sections, activeSection) => { return sections .map(({ name, slug }) => { const isActive = activeSection === slug ? 'is-active' : ''; return `${name}`; }) .join(''); }; const buildSidebarLinks = (notes, currentSlug) => notes .map((note) => { const isActive = currentSlug === note.slug ? 'is-active' : ''; return `
  • ${note.title}
  • `; }) .join(''); const assemblePage = ({ pageTitle, header, sidebar, content, extraHead = '' }) => render(templates.base, { pageTitle, header, sidebar, content, footer: templates.footer, extraHead }); const writePage = (targetPath, html) => { fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.writeFileSync(targetPath, html, 'utf8'); }; const copyAssets = () => { const target = path.join(DIST_DIR, 'assets'); fs.cpSync(ASSETS_DIR, target, { recursive: true }); }; const buildSearchIndex = (notes) => { const index = notes.map((note) => ({ title: note.title, section: note.section, summary: note.summary, tags: note.tags, url: `/${note.sectionSlug}/${note.slug}/`, })); fs.writeFileSync(path.join(DIST_DIR, 'search-index.json'), JSON.stringify(index, null, 2)); }; const buildPages = () => { cleanDir(DIST_DIR); const notes = loadNotes(); const sectionsMap = new Map(); notes.forEach((note) => { if (!sectionsMap.has(note.sectionSlug)) { sectionsMap.set(note.sectionSlug, { name: note.section, slug: note.sectionSlug, notes: [], }); } sectionsMap.get(note.sectionSlug).notes.push(note); }); const sections = Array.from(sectionsMap.values()).sort((a, b) => a.name.localeCompare(b.name)); sections.forEach((section) => section.notes.sort((a, b) => (a.nav - b.nav) || a.title.localeCompare(b.title))); // Build header once per page. const renderHeader = (activeSectionSlug) => render(templates.header, { navLinks: buildNavLinks(sections, activeSectionSlug), }); // Section pages and notes sections.forEach((section) => { const sidebar = render(templates.sidebar, { sectionTitle: section.name, sidebarLinks: buildSidebarLinks(section.notes, null), }); // Section index const sectionList = section.notes .map((note) => `
    ${section.name}

    ${note.title}

    ${note.summary}

    ${note.tags.map((tag) => `#${tag}`).join('')}
    `) .join(''); const sectionContent = `

    Section

    ${section.name}

    Notes grouped by ${section.name}. Use the sidebar or search to jump in.

    ${sectionList}
    `; const sectionPage = assemblePage({ pageTitle: section.name, header: renderHeader(section.slug), sidebar, content: sectionContent, }); writePage(path.join(DIST_DIR, section.slug, 'index.html'), sectionPage); // Individual notes section.notes.forEach((note) => { const sidebarWithActive = render(templates.sidebar, { sectionTitle: section.name, sidebarLinks: buildSidebarLinks(section.notes, note.slug), }); const tags = note.tags.map((tag) => `#${tag}`).join(''); const noteContent = `

    ${section.name}

    ${note.title}

    ${note.summary} ${tags ? `${tags}` : ''}
    ${note.html}
    `; const page = assemblePage({ pageTitle: note.title, header: renderHeader(section.slug), sidebar: sidebarWithActive, content: noteContent, }); writePage(path.join(DIST_DIR, section.slug, note.slug, 'index.html'), page); }); }); // Home page const summaryCards = sections .map((section) => `
    ${section.notes.length} note${section.notes.length === 1 ? '' : 's'}

    ${section.name}

    ${section.notes[0]?.summary || 'Section overview'}

    `) .join(''); const homeContent = `

    Workspace

    Developer Notes Hub

    Markdown-first notes rendered into a fast static site. Use search or browse by section.

    ${summaryCards}
    `; const homeSidebar = render(templates.sidebar, { sectionTitle: 'Sections', sidebarLinks: sections .map((section) => `
  • ${section.name}
  • `) .join(''), }); const homePage = assemblePage({ pageTitle: 'Home', header: renderHeader(null), sidebar: homeSidebar, content: homeContent, }); writePage(path.join(DIST_DIR, 'index.html'), homePage); // Assets and search index copyAssets(); buildSearchIndex(notes); console.log(`Built ${notes.length} notes across ${sections.length} sections → ${DIST_DIR}`); }; buildPages();