Refactor site generation into a modular content pipeline
Deploy website / deploy (push) Successful in 24s

This commit is contained in:
paul committed 2026-09-22 20:51:13 -04:00
1 parent 0f69028d4a
commit 146d516f24
20 files changed
+826 -427

No files matched your search

+10 -9
View File
@@ -1,8 +1,9 @@
import { execFileSync } from 'node:child_process'
import { readdir, readFile, stat } from 'node:fs/promises'
import { join } from 'node:path'
import { generatedRoot, siteUrl } from './site.config.js'
import { htmlFacts } from './html-checks.js'
const generatedRoot = '.generated'
const walk = async (directory) => {
const files = []
@@ -45,21 +46,22 @@ const prismLanguageAliases = { sh: 'bash', shell: 'bash' }
for (const file of htmlFiles) {
const html = await readFile(file, 'utf8')
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1])
const { ids, hrefs, languages, hasViewport, hasHeadingElement, isDraft } = htmlFacts(html)
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index)
if (duplicateIds.length > 0) errors.push(`${file}: duplicate IDs: ${[...new Set(duplicateIds)].join(', ')}`)
if (!html.includes('<meta name="viewport"')) errors.push(`${file}: missing viewport metadata`)
if (html.includes('<heading')) errors.push(`${file}: contains a nonstandard <heading> element`)
if (html.includes('name="robots"') && html.includes('noindex')) {
if (!hasViewport) errors.push(`${file}: missing viewport metadata`)
if (hasHeadingElement) errors.push(`${file}: contains a nonstandard <heading> element`)
if (isDraft) {
noindexFiles.add(file)
errors.push(`${file}: draft page must not be in production output`)
}
for (const [, fragment] of html.matchAll(/href="#([^"]+)"/g)) {
for (const href of hrefs.filter((value) => value.startsWith('#') && value.length > 1)) {
const fragment = decodeURIComponent(href.slice(1))
if (!ids.includes(fragment)) errors.push(`${file}: missing fragment target #${fragment}`)
}
for (const [, href] of html.matchAll(/href="(\/[^"]*)"/g)) {
for (const href of hrefs.filter((value) => value.startsWith('/'))) {
if (href === '/') continue
const pathname = href.split(/[?#]/, 1)[0]
const relative = pathname.replace(/^\//, '')
@@ -70,7 +72,7 @@ for (const file of htmlFiles) {
if (!(await exists(target))) errors.push(`${file}: broken internal link ${href}`)
}
for (const [, language] of html.matchAll(/class="language-([\w-]+)"/g)) {
for (const language of languages) {
if (builtInPrismLanguages.has(language)) continue
const component = prismLanguageAliases[language] ?? language
if (!clientSource.includes(`prism-${component}`))
@@ -78,7 +80,6 @@ for (const file of htmlFiles) {
}
}
const siteUrl = 'https://paulw.xyz'
const generatedFile = (pathname) => {
const relative = pathname.replace(/^\//, '')
return pathname.endsWith('/') ? join(generatedRoot, relative, 'index.html') : join(generatedRoot, relative)