✨feature: First working version with dummy content
Some checks failed
build-site / build (push) Failing after 6m4s
Some checks failed
build-site / build (push) Failing after 6m4s
This commit is contained in:
268
bin/build.js
Normal file
268
bin/build.js
Normal file
@@ -0,0 +1,268 @@
|
||||
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 `<pre><code class="hljs ${lang}">${hljs.highlight(str, { language: lang }).value}</code></pre>`;
|
||||
}
|
||||
return `<pre><code class="hljs">${md.utils.escapeHtml(str)}</code></pre>`;
|
||||
},
|
||||
});
|
||||
|
||||
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 `<a class="${isActive}" href="/${slug}/">${name}</a>`;
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
const buildSidebarLinks = (notes, currentSlug) =>
|
||||
notes
|
||||
.map((note) => {
|
||||
const isActive = currentSlug === note.slug ? 'is-active' : '';
|
||||
return `<li><a class="${isActive}" href="/${note.sectionSlug}/${note.slug}/">${note.title}</a></li>`;
|
||||
})
|
||||
.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) => `
|
||||
<a class="card" href="/${note.sectionSlug}/${note.slug}/">
|
||||
<div class="badge">${section.name}</div>
|
||||
<h3>${note.title}</h3>
|
||||
<p class="muted">${note.summary}</p>
|
||||
<div class="tag-list">${note.tags.map((tag) => `<span class="tag">#${tag}</span>`).join('')}</div>
|
||||
</a>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
const sectionContent = `
|
||||
<div class="hero">
|
||||
<p class="eyebrow">Section</p>
|
||||
<h1>${section.name}</h1>
|
||||
<p class="muted">Notes grouped by ${section.name}. Use the sidebar or search to jump in.</p>
|
||||
</div>
|
||||
<div class="card-grid">${sectionList}</div>
|
||||
`;
|
||||
|
||||
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) => `<span class="tag">#${tag}</span>`).join('');
|
||||
const noteContent = `
|
||||
<article>
|
||||
<p class="eyebrow">${section.name}</p>
|
||||
<h1>${note.title}</h1>
|
||||
<div class="note-meta">
|
||||
<span>${note.summary}</span>
|
||||
${tags ? `<span class="tag-list">${tags}</span>` : ''}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
${note.html}
|
||||
</article>
|
||||
`;
|
||||
|
||||
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) => `
|
||||
<a class="card" href="/${section.slug}/">
|
||||
<div class="badge">${section.notes.length} note${section.notes.length === 1 ? '' : 's'}</div>
|
||||
<h3>${section.name}</h3>
|
||||
<p class="muted">${section.notes[0]?.summary || 'Section overview'}
|
||||
</p>
|
||||
</a>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
const homeContent = `
|
||||
<div class="hero">
|
||||
<p class="eyebrow">Workspace</p>
|
||||
<h1>Developer Notes Hub</h1>
|
||||
<p class="muted">Markdown-first notes rendered into a fast static site. Use search or browse by section.</p>
|
||||
</div>
|
||||
<div class="card-grid">${summaryCards}</div>
|
||||
`;
|
||||
|
||||
const homeSidebar = render(templates.sidebar, {
|
||||
sectionTitle: 'Sections',
|
||||
sidebarLinks: sections
|
||||
.map((section) => `<li><a href="/${section.slug}/">${section.name}</a></li>`)
|
||||
.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();
|
||||
Reference in New Issue
Block a user