import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { escapeHtml, h, raw, render } from './html.js' import { dateFor, readPages } from './content.js' import { renderFeed, renderSitemap, toSitemapDate } from './feed.js' import { createOutput, discardOutput, publishOutput } from './output.js' import { renderMarkdownPage } from './markdown.js' import { contentDirectories, generatedRoot, navLabels, siteName, siteUrl, standalonePages, } from './site.config.js' const includeDrafts = process.argv.includes('--drafts') const output = await createOutput(generatedRoot) const pageShell = (title, body, path = '/', navTitle = '', { draft = false } = {}) => `${render( h('html', { lang: 'en-US' }, [ h('head', {}, [ h('meta', { charset: 'utf-8' }), h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1', }), draft ? h('meta', { name: 'robots', content: 'noindex, nofollow' }) : undefined, h('title', {}, [title ? `${title} | ${siteName}` : siteName]), h('link', { rel: 'icon', href: '/favicon.ico' }), h('link', { rel: 'alternate', type: 'application/rss+xml', title: `${siteName} feed`, href: '/feed.xml', }), h('link', { rel: 'stylesheet', href: '/src/global.css' }), h('script', { type: 'module', src: '/src/main.js' }), ]), h('body', {}, [raw(body), h('nav', { 'aria-label': 'Breadcrumb' }, [raw(nav(path, navTitle))])]), ]), )}` const nav = (path, currentTitle = '') => { if (path === '/') { if (!currentTitle) return render(h('span', {}, [siteName])) return render([h('a', { href: '/' }, [siteName]), ' / ', currentTitle]) } const parts = path.split('/').filter(Boolean) const children = [h('a', { href: '/' }, [siteName])] let href = '' for (const [index, part] of parts.entries()) { href += `/${part}` const isLast = index === parts.length - 1 const label = isLast && currentTitle ? currentTitle : (navLabels[part] ?? part) children.push(' / ', isLast ? label : h('a', { href: `${href}/` }, [label])) } return render(children) } const writePage = async (path, html) => { const directory = join(output, path) await mkdir(directory, { recursive: true }) await writeFile(join(directory, 'index.html'), html) } const titleHeading = (title, className = '') => `

${escapeHtml(title)}

` const renderHome = (externalLinks, sectionLinks, recentLinks) => pageShell( '', ` ${titleHeading(siteName)}
${sectionLinks}${externalLinks}

Recent

`, ) const renderNotesTable = (pages, directory) => { const rows = pages .map( (page) => ` ${escapeHtml(page.title)} ${page.date ? `` : ''} `, ) .join('') return `${rows}
${escapeHtml(directory)} pages
PageLast updated
` } const renderPostsList = (pages, directory) => { const items = pages .map( (page) => `
  • ${escapeHtml(page.title)} ${page.date ? `` : ''}
  • `, ) .join('') return `` } const renderEmptySection = () => `

    crickets

    Nothing to list...

    Go Home
    ` const renderNotFound = () => pageShell( 'Page not found', `${titleHeading('404: Page Not Found')}

    Uh oh! The page you are looking for does not exist...

    Go HomeMore on 404
    `, '/', ' ... ??? / 404: Page Not Found', ) const renderSection = (directory, pages) => { const title = directory[0].toUpperCase() + directory.slice(1) const listing = pages.length === 0 ? renderEmptySection() : directory === 'posts' ? renderPostsList(pages, directory) : renderNotesTable(pages, directory) return pageShell( title, `${titleHeading(title)}
    ${listing}
    `, `/${directory}`, title, ) } const renderArticle = (directory, page, navigationPath = `/${directory}/${page.name}`) => { const updated = page.date ? `` : '' const heading = titleHeading(page.title, `${directory}-heading`) const draftNotice = page.draft ? `

    Draft — rename .${escapeHtml(page.name)}.md to ${escapeHtml(page.name)}.md to publish.

    ` : '' const tableOfContents = page.markdown.toc const contents = tableOfContents ? `
    Contents${tableOfContents}

    ` : '' const article = `
    ${page.markdown.html}
    ` const body = `${heading}
    ${draftNotice}${updated}${contents}${article}
    ` return pageShell(page.title, body, navigationPath, page.title, { draft: Boolean(page.draft) }) } try { const sections = Object.fromEntries(await Promise.all(contentDirectories.map(async (dir) => [dir, await readPages(dir, { includeDrafts })]))) for (const pages of Object.values(sections)) { for (const page of pages) page.markdown = renderMarkdownPage(page.source) } const external = JSON.parse(await readFile('external.json', 'utf8')) const externalLinks = Object.entries(external) .map(([text, href]) => `${escapeHtml(text)}`) .join('') const sectionLinks = contentDirectories .map((dir) => `${dir[0].toUpperCase() + dir.slice(1)}`) .join('') const standaloneLinks = standalonePages .filter((page) => page.path !== 'licenses') .map((page) => `${page.title}`) .join('') const recentPages = contentDirectories .flatMap((dir) => sections[dir].filter((page) => !page.draft).map((page) => ({ ...page, dir }))) .sort((a, b) => b.date.localeCompare(a.date)) .slice(0, 5) const recentLinks = recentPages .map( (page) => `
  • ${page.date ? `` : ''}${page.dir}${escapeHtml(page.title)}
  • `, ) .join('') await writeFile(join(output, 'index.html'), renderHome(externalLinks, sectionLinks + standaloneLinks, recentLinks)) await writeFile(join(output, '404.html'), renderNotFound()) for (const dir of contentDirectories) { const pages = sections[dir] await writePage( dir, renderSection( dir, pages.filter((page) => !page.draft), ), ) for (const page of pages) { await writePage(join(dir, page.name), renderArticle(dir, page)) } } const generatedStandalone = [] for (const page of standalonePages) { const source = await readFile(page.source, 'utf8') const generatedPage = { name: page.path, title: page.title, source, markdown: renderMarkdownPage(source), date: await dateFor(page.source), } generatedStandalone.push(generatedPage) await writePage(page.path, renderArticle(page.path, generatedPage, '/')) } const feedItems = contentDirectories .flatMap((dir) => sections[dir].filter((page) => !page.draft).map((page) => ({ ...page, dir }))) .sort((a, b) => b.date.localeCompare(a.date)) for (const item of feedItems) item.description = item.markdown.excerpt await writeFile(join(output, 'feed.xml'), renderFeed(feedItems)) const sitemapUrls = [ { loc: `${siteUrl}/`, lastmod: toSitemapDate(feedItems[0]?.date), }, ...contentDirectories.map((dir) => ({ loc: `${siteUrl}/${dir}/`, lastmod: toSitemapDate( [...sections[dir]].filter((page) => !page.draft).sort((a, b) => b.date.localeCompare(a.date))[0]?.date, ), })), ...generatedStandalone.map((page) => ({ loc: `${siteUrl}/${page.name}/`, lastmod: toSitemapDate(page.date), })), ...feedItems.map((page) => ({ loc: `${siteUrl}/${page.dir}/${page.name}/`, lastmod: toSitemapDate(page.date), })), { loc: `${siteUrl}/feed.xml`, lastmod: toSitemapDate(feedItems[0]?.date) }, ] await writeFile(join(output, 'sitemap.xml'), renderSitemap(sitemapUrls)) await publishOutput(output, generatedRoot) } catch (error) { await discardOutput(output) throw error }