Files
md-blog/post-build.ts
T
2026-04-26 15:10:30 +02:00

130 lines
4.2 KiB
TypeScript

import fs from 'fs'
import process from 'process'
import { Feed } from 'feed'
import articlesConfig from './articles/config.json'
function getFiles(dir: string): string[] {
return fs.readdirSync(dir).flatMap((name) => {
const path = `${dir}/${name}`
if (fs.statSync(path).isDirectory()) {
return getFiles(path)
} else {
return [path]
}
})
}
const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m
const METADATA_REGEX = /^(\w+):(.*)$/gm
function readArticleMetadata(path: string): Record<string, string> | null {
const content = fs.readFileSync(path, { encoding: 'utf8' })
const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content)
if (!match || !match[1]) {
console.warn(`No metadata for: ${path}`)
return null
}
let subMatch: RegExpExecArray | null = null
const metadata: Record<string, string> = {
path: path.replaceAll('/index.md', ''),
}
do {
subMatch = METADATA_REGEX.exec(match[1])
if (subMatch && subMatch[1] && subMatch[2]) {
metadata[subMatch[1]] = subMatch[2].trim()
}
} while (subMatch)
return metadata
}
function formatArticlePage(metadata: Record<string, string>, baseHtml: string): string {
let outHtml = baseHtml
outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, '')
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:url" content="${articlesConfig['base_url']}${metadata.path}/">\n</head>`,
)
const blog_title = articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim()
if (metadata.title) {
const title = metadata.title.replace(/(<([^>]+)>)/gi, '').trim()
outHtml = outHtml.replace(/<title>.*?<\/title>/gm, `<title>${blog_title}${title}</title>`)
outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, '')
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:title" content="${title}">\n</head>`,
)
outHtml = outHtml.replace(/<.*?property="og:description".*?>/gm, '')
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:description" content="${blog_title}">\n</head>`,
)
}
if (metadata.thumbnail) {
outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, '')
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:image" content="${metadata.thumbnail.replace('./', articlesConfig['base_url'] + metadata.path + '/')}">\n</head>`,
)
}
return outHtml
}
function addFeedArticle(metadata: Record<string, string>, feed: Feed) {
if (metadata.draft !== 'true') {
feed.addItem({
title: metadata.title.replace(/(<([^>]+)>)/gi, '').trim(),
id: metadata.path,
link: `${articlesConfig['base_url']}${metadata.path}/`,
date: new Date(Date.parse(metadata.date)),
image: metadata.thumbnail.replace('./', articlesConfig['base_url'] + metadata.path + '/'),
})
}
}
const indexContent = fs.readFileSync('dist/index.html', { encoding: 'utf8' })
if (!indexContent) {
console.error('Could not read dist/index.html')
process.exit(1)
}
const metadatas = getFiles('articles')
.filter((path) => path.match(/\/index.md$/))
.map((path) => readArticleMetadata(path))
.filter((metadata) => !!metadata)
const feed = new Feed({
title: articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim() ?? '',
id: articlesConfig['base_url'],
link: articlesConfig['base_url'],
language: articlesConfig['lang'],
favicon: articlesConfig['base_url'] + 'articles/favicon.ico',
generator: 'md-blog',
feedLinks: {
json: articlesConfig['base_url'] + 'feed.json',
atom: articlesConfig['base_url'] + 'atom.xml',
rss: articlesConfig['base_url'] + 'rss',
},
updated: new Date(
Math.max(
0,
...metadatas
.filter((metadata) => metadata.draft !== 'true')
.map((metadata) => Date.parse(metadata.date)),
),
),
})
metadatas.forEach((metadata) => {
fs.writeFileSync(`dist/${metadata.path}/index.html`, formatArticlePage(metadata, indexContent))
console.info(`Wrote dist/${metadata.path}/index.html`)
addFeedArticle(metadata, feed)
})
fs.writeFileSync('dist/feed.json', feed.json1())
console.info(`Wrote dist/feed.json`)
fs.writeFileSync('dist/atom.xml', feed.atom1())
console.info(`Wrote dist/atom.xml`)
fs.writeFileSync('dist/rss.xml', feed.rss2())
console.info(`Wrote dist/rss.xml`)