${section.name}
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 SECTIONS_FILE = path.join(TEMPLATE_DIR, 'sections.json'); 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 loadSectionMeta = () => {
if (!fs.existsSync(SECTIONS_FILE)) return [];
const raw = fs.readFileSync(SECTIONS_FILE, 'utf8');
try {
const data = JSON.parse(raw);
if (Array.isArray(data)) return data;
if (data && typeof data === 'object') {
return Object.entries(data).map(([slug, meta]) => ({
slug,
name: meta?.name || slug,
description: meta?.description || '',
}));
}
} catch (err) {
console.warn('Invalid sections.json, skipping section descriptions.', err);
}
return [];
};
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 `Section
${sectionDescription}
${section.name}
${section.description || 'Section overview'}
`) .join(''); const homeContent = `Workspace
Notes related to various aspects of my projects and homelab.