Refactor site generation into a modular content pipeline
Deploy website / deploy (push) Successful in 24s
Deploy website / deploy (push) Successful in 24s
This commit is contained in:
1 parent
0f69028d4a
commit
146d516f24
20 files changed
+826
-427
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user