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, `<title>${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)), + }, }, - }, -}) + }) +}