diff --git a/.env.example b/.env.example
index 32d632e..5c9dc08 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
+VITE_BASE_URL=http://localhost:8080/
VITE_APP_TITLE= My Blog
-VITE_APP_TITLE_NO_HTML=My Blog
VITE_APP_SIGNATURE=By me
VITE_APP_LANG=en
\ No newline at end of file
diff --git a/README.md b/README.md
index ea189ba..6759280 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ This template should help get you started developing with Vue 3 in Vite.
- [x] link to home
- [ ] link to previous/next article
- [x] set page title
-- [ ] SPA and opengraph
+- [x] SPA and opengraph
## Recommended IDE Setup
diff --git a/index.html b/index.html
index a749e0d..41a0b04 100644
--- a/index.html
+++ b/index.html
@@ -2,12 +2,14 @@
-
+
%VITE_APP_TITLE_NO_HTML%
+
+
diff --git a/package.json b/package.json
index cf24ac1..2642fa8 100644
--- a/package.json
+++ b/package.json
@@ -6,14 +6,15 @@
"repository": "https://github.com/klemek/md-blog",
"scripts": {
"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",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
- "format": "prettier --write src/"
+ "format": "prettier --write src/ *.ts *.json"
},
"dependencies": {
"highlight.js": "^11.11.1",
diff --git a/post-build.ts b/post-build.ts
new file mode 100644
index 0000000..a76f64f
--- /dev/null
+++ b/post-build.ts
@@ -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 | 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 = {
+ 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, baseHtml: string): string {
+ let outHtml = baseHtml
+ outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, '')
+ outHtml = outHtml.replace(
+ /<\/head>/gm,
+ ` \n`,
+ )
+ 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>/gm, `${blog_title} — ${title} `)
+ outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, '')
+ outHtml = outHtml.replace(
+ /<\/head>/gm,
+ ` \n`,
+ )
+ outHtml = outHtml.replace(/<.*?property="og:description".*?>/gm, '')
+ outHtml = outHtml.replace(
+ /<\/head>/gm,
+ ` \n`,
+ )
+ }
+ if (metadata.thumbnail) {
+ outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, '')
+ outHtml = outHtml.replace(
+ /<\/head>/gm,
+ ` \n`,
+ )
+ }
+ 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`)
+ }
+ })
diff --git a/src/lib/articles.ts b/src/lib/articles.ts
index 5876feb..fa68503 100644
--- a/src/lib/articles.ts
+++ b/src/lib/articles.ts
@@ -84,7 +84,7 @@ export async function listArticles(): Promise {
return null
}
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)
diff --git a/tsconfig.node.json b/tsconfig.node.json
index c9b2bad..f7d5da2 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -6,7 +6,8 @@
"vitest.config.*",
"cypress.config.*",
"playwright.config.*",
- "eslint.config.*"
+ "eslint.config.*",
+ "post-build.ts"
],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
diff --git a/vite.config.ts b/vite.config.ts
index 61b784d..b9e1fa0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,21 +1,30 @@
import { fileURLToPath, URL } from 'node:url'
-
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import { plugin as mdPlugin, Mode } from 'vite-plugin-markdown'
// https://vite.dev/config/
-export default defineConfig({
- plugins: [vue(), vueDevTools(), mdPlugin({ mode: [Mode.HTML] })],
- resolve: {
- alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url)),
- '@views': fileURLToPath(new URL('./src/views', import.meta.url)),
- '@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
- '@articles': fileURLToPath(new URL('./articles', import.meta.url)),
- '@interfaces': fileURLToPath(new URL('./src/interfaces.ts', import.meta.url)),
- '@components': fileURLToPath(new URL('./src/components', import.meta.url)),
+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] })],
+ base: process.env.VITE_BASE_URL,
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ '@views': fileURLToPath(new URL('./src/views', import.meta.url)),
+ '@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
+ '@articles': fileURLToPath(new URL('./articles', import.meta.url)),
+ '@interfaces': fileURLToPath(new URL('./src/interfaces.ts', import.meta.url)),
+ '@components': fileURLToPath(new URL('./src/components', import.meta.url)),
+ },
},
- },
-})
+ })
+}