feat: post build script for opengraph
This commit is contained in:
+1
-1
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
+13
-4
@@ -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)),
|
||||||
@@ -18,4 +26,5 @@ export default defineConfig({
|
|||||||
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
|
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user