feat: post build script for opengraph

This commit is contained in:
2026-04-25 20:45:20 +02:00
parent 9162c149c4
commit 7885d22194
8 changed files with 124 additions and 21 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
VITE_BASE_URL=http://localhost:8080/
VITE_APP_TITLE=<i icon=rss></i> My Blog VITE_APP_TITLE=<i icon=rss></i> My Blog
VITE_APP_TITLE_NO_HTML=My Blog
VITE_APP_SIGNATURE=By <b>me</b> VITE_APP_SIGNATURE=By <b>me</b>
VITE_APP_LANG=en VITE_APP_LANG=en
+1 -1
View File
@@ -13,7 +13,7 @@ This template should help get you started developing with Vue 3 in Vite.
- [x] link to home - [x] link to home
- [ ] link to previous/next article - [ ] link to previous/next article
- [x] set page title - [x] set page title
- [ ] SPA and opengraph - [x] SPA and opengraph
## Recommended IDE Setup ## Recommended IDE Setup
+3 -1
View File
@@ -2,12 +2,14 @@
<html lang="%VITE_APP_LANG%"> <html lang="%VITE_APP_LANG%">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="@articles/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE_NO_HTML%</title> <title>%VITE_APP_TITLE_NO_HTML%</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<link href="https://fonts.googleapis.com/css?family=Lato&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Lato&display=swap" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/src/style.scss" /> <link rel="stylesheet" type="text/css" href="/src/style.scss" />
<meta property="og:url" content="%VITE_BASE_URL%" />
<meta property="og:title" content="%VITE_APP_TITLE_NO_HTML%" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+3 -2
View File
@@ -6,14 +6,15 @@
"repository": "https://github.com/klemek/md-blog", "repository": "https://github.com/klemek/md-blog",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" -- && run-p post-build",
"post-build": "node ./post-build.ts",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint": "run-s lint:*", "lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix", "lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache", "lint:eslint": "eslint . --fix --cache",
"format": "prettier --write src/" "format": "prettier --write src/ *.ts *.json"
}, },
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
+90
View File
@@ -0,0 +1,90 @@
import fs from 'fs'
import process from 'process'
process.loadEnvFile()
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="${process.env.VITE_BASE_URL}${metadata.path}/">\n</head>`,
)
const blog_title = process.env.VITE_APP_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('./', process.env.VITE_BASE_URL + metadata.path + '/')}">\n</head>`,
)
}
return outHtml
}
const indexContent = fs.readFileSync('dist/index.html', { encoding: 'utf8' })
if (!indexContent) {
console.error('Could not read dist/index.html')
process.exit(1)
}
getFiles('articles')
.filter((path) => path.match(/index.md$/))
.forEach((path) => {
const metadata = readArticleMetadata(path)
if (metadata) {
fs.writeFileSync(
`dist/${metadata.path}/index.html`,
formatArticlePage(metadata, indexContent),
)
console.info(`Wrote dist/${metadata.path}/index.html`)
}
})
+1 -1
View File
@@ -84,7 +84,7 @@ export async function listArticles(): Promise<ArticleMetadata[]> {
return null return null
} }
const date = dateFromParts(match[1], match[2], match[3]) const date = dateFromParts(match[1], match[2], match[3])
return parseMetadata(data.attributes, key.replace('/index.md', ''), date) return parseMetadata(data.attributes, key.replace('index.md', ''), date)
}), }),
) )
).filter((item) => item !== null) ).filter((item) => item !== null)
+2 -1
View File
@@ -6,7 +6,8 @@
"vitest.config.*", "vitest.config.*",
"cypress.config.*", "cypress.config.*",
"playwright.config.*", "playwright.config.*",
"eslint.config.*" "eslint.config.*",
"post-build.ts"
], ],
"compilerOptions": { "compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping. // Most tools use transpilation instead of Node.js's native type-stripping.
+12 -3
View File
@@ -1,13 +1,21 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import { plugin as mdPlugin, Mode } from 'vite-plugin-markdown' import { plugin as mdPlugin, Mode } from 'vite-plugin-markdown'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default ({ mode }: { mode: string }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
process.env.VITE_APP_TITLE_NO_HTML = process.env.VITE_APP_TITLE?.replace(
/(<([^>]+)>)/gi,
'',
).trim()
return defineConfig({
plugins: [vue(), vueDevTools(), mdPlugin({ mode: [Mode.HTML] })], plugins: [vue(), vueDevTools(), mdPlugin({ mode: [Mode.HTML] })],
base: process.env.VITE_BASE_URL,
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
@@ -19,3 +27,4 @@ export default defineConfig({
}, },
}, },
}) })
}