diff --git a/.env.tmpl b/.env.tmpl index 85bbdfd..c2a69d0 100644 --- a/.env.tmpl +++ b/.env.tmpl @@ -1,4 +1,4 @@ -BASE_URL=/blog/ +BASE_PATH=/blog/ # GISCUS Configs: generated from https://giscus.app/ GISCUS_REPO_ID= GISCUS_CATEGORY_ID= diff --git a/.github/contributing.md b/.github/contributing.md index da73206..25cfbbb 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -23,6 +23,15 @@ npm run dev npm run build ``` +### Atom Feed Preview + +Feed contents are built with VitePress's [`buildEnd`](https://vitepress.dev/reference/site-config#buildend) build hook, so you need to run `build` & `preview` to see the results: + +```sh +npm run build +npm run preview +``` + There are several other npm script commands in the `scripts` part of [`package.json`](../package.json) that you might want to use. ### Visual Studio Code diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3fd232e..62237c6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - BASE_URL: ${{ vars.BASE_URL }} + BASE_PATH: ${{ vars.BASE_PATH }} GISCUS_REPO_ID: ${{ vars.GISCUS_REPO_ID }} GISCUS_CATEGORY_ID: ${{ vars.GISCUS_CATEGORY_ID }} diff --git a/.vitepress/config.theme.ts b/.vitepress/config.theme.ts index a53eaea..09d1631 100644 --- a/.vitepress/config.theme.ts +++ b/.vitepress/config.theme.ts @@ -50,10 +50,11 @@ export default { text: "Acknowledgments", link: "/acknowledgments", }, - // { - // text: "Atom Feed", - // link: "/atom.xml", - // }, + { + text: "Atom Feed", + link: withBaseURL("/atom.xml"), + target: "_blank", + }, { text: "Repository", link: "https://github.com/Octobug/blog", diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 466d61f..725970b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1,14 +1,15 @@ import { defineConfigWithTheme } from "vitepress"; +import mdImageFigures from "markdown-it-image-figures"; import type { ThemeConfig } from "./theme/types/theme-config"; import themeConfig from "./config.theme"; -import { BASE_URL, withBaseURL } from "./config.utils"; +import { BASE_PATH, withBaseURL } from "./config.utils"; import gaConfig from "./theme/ganalytics"; -import mdImageFigures from "markdown-it-image-figures"; +import { buildFeed } from "./theme/feed"; export default defineConfigWithTheme({ title: "WhaleVocal", description: "Octobug's blog.", - base: BASE_URL, + base: BASE_PATH, cleanUrls: true, lastUpdated: true, head: [ @@ -46,6 +47,9 @@ export default defineConfigWithTheme({ "./README.md", ], themeConfig, + buildEnd: async ({ outDir }) => { + await buildFeed(outDir); + }, markdown: { config: (md) => { // usage: ![alt](https://link-to-image 'title'){.class} diff --git a/.vitepress/config.utils.ts b/.vitepress/config.utils.ts index 72c3e2f..f0980ae 100644 --- a/.vitepress/config.utils.ts +++ b/.vitepress/config.utils.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { env } from "process"; import * as path from "path"; -export const BASE_URL = env.BASE_URL || undefined; +export const BASE_PATH = env.BASE_PATH || undefined; export const giscus = { repo_id: env.GISCUS_REPO_ID || "", @@ -12,8 +12,13 @@ export const giscus = { export const gMeasurementID = env.G_MEASUREMENT_ID || ""; export function withBaseURL(urlPath: string) { - if (BASE_URL && urlPath.includes(BASE_URL)) { + if (BASE_PATH && urlPath.includes(BASE_PATH)) { return urlPath; } - return path.join(BASE_URL || "/", urlPath); + return path.join(BASE_PATH || "/", urlPath); +} + +export function joinURL(baseURL: string, ...paths: string[]) { + const urlPath = path.join(...paths); + return new URL(urlPath, baseURL).href; } diff --git a/.vitepress/theme/feed.ts b/.vitepress/theme/feed.ts new file mode 100644 index 0000000..9284c6f --- /dev/null +++ b/.vitepress/theme/feed.ts @@ -0,0 +1,77 @@ +import path from "path"; +import { promises as fs } from "fs"; +import { ContentData } from "vitepress"; +import type { FeedOptions, Item } from "feed"; +import { Feed } from "feed"; +import themeConfig from "../config.theme"; +import { BASE_PATH, withBaseURL, joinURL } from "../config.utils"; +import postLoader from "./posts.loader"; + +const DOMAIN_PRODUCTION = "blog.octobug.site"; +const DOMAIN_STAGING = "octobug.github.io"; + +const DOMAIN = BASE_PATH ? DOMAIN_STAGING : DOMAIN_PRODUCTION; +const BASE_URL = `https://${DOMAIN}${withBaseURL("/")}`; +const AUTHOR = { + name: "Octobug", + email: "whalevocal@gmail.com", + link: BASE_URL, +}; + +const FEED = { + ATOM: "atom.xml", + RSS: "rss.xml", +}; + +const OPTIONS: FeedOptions = { + title: "WhaleVocal", + description: "Octobug's Blog", + id: BASE_URL, + link: BASE_URL, + copyright: themeConfig.footer.copyright, + feedLinks: { + atom: joinURL(BASE_URL, FEED.ATOM), + rss: joinURL(BASE_URL, FEED.RSS), + }, + author: AUTHOR, + image: joinURL(BASE_URL, "avatar.png"), + favicon: joinURL(BASE_URL, "avatar.png"), +}; + +export async function buildFeed(outDir: string) { + const posts = await generateContents(); + + const feed = new Feed(OPTIONS); + posts.forEach(item => feed.addItem(item)); + + await fs.writeFile(path.join(outDir, FEED.ATOM), feed.atom1(), "utf8"); + await fs.writeFile(path.join(outDir, FEED.RSS), feed.rss2(), "utf8"); +} + +function withGitHubImages(post: ContentData) { + const baseURL = "https://raw.githubusercontent.com/"; + const imgBaseURL = joinURL(baseURL, "Octobug/blog/main", post.url); + return post.html?.replace(/ { + const loader = await postLoader({ + render: true, + excerpt: true, + }); + const allPosts = await loader.load(); + + return await Promise.all( + allPosts.map(async p => { + return { + title: p.frontmatter.title, + id: p.url, + link: joinURL(BASE_URL, p.url), + date: p.frontmatter.date, + content: withGitHubImages(p), + category: [p.frontmatter.sort].map(s => { return { name: s }; }), + author: [AUTHOR], + } satisfies Item; + }) + ); +} diff --git a/.vitepress/theme/posts.data.ts b/.vitepress/theme/posts.data.ts index 668f09c..1840baf 100644 --- a/.vitepress/theme/posts.data.ts +++ b/.vitepress/theme/posts.data.ts @@ -1,34 +1,12 @@ // https://vitepress.dev/guide/data-loading -import { createContentLoader, ContentData } from "vitepress"; -import readingTime from "reading-time"; -import extendedConfig from "../config.theme"; -import { withBaseURL } from "../config.utils"; -import getLocation from "./locations"; +import { ContentData } from "vitepress"; +import postLoader from "./posts.loader"; declare const data: ContentData[]; export { data }; -// Title Workaround -function extractTile(text: string) { - const titlePattern = /---\n\n# (?.*)\n/; - const match = text.match(titlePattern); - return match?.groups?.title || "NonTitled"; -} - -export default createContentLoader(extendedConfig.mdfilePatterns, { +export default await postLoader({ includeSrc: true, - transform(rawData) { - return rawData.map(p => { - const rt = readingTime(p.src || ""); - p.frontmatter.title = extractTile(p.src || ""); - p.frontmatter.datetime = new Date(p.frontmatter.date); - p.frontmatter.location = getLocation(p.frontmatter.spot); - p.frontmatter.readingTime = rt.text; - p.frontmatter.words = rt.words; - p.url = withBaseURL(p.url.replace("/README", "")); - return p; - }).sort((a, b) => { - return b.frontmatter.datetime - a.frontmatter.datetime; - }); - } + render: false, + excerpt: false, }); diff --git a/.vitepress/theme/posts.loader.ts b/.vitepress/theme/posts.loader.ts new file mode 100644 index 0000000..360fa55 --- /dev/null +++ b/.vitepress/theme/posts.loader.ts @@ -0,0 +1,41 @@ +// https://vitepress.dev/guide/data-loading +import { createContentLoader } from "vitepress"; +import readingTime from "reading-time"; +import extendedConfig from "../config.theme"; +import { withBaseURL } from "../config.utils"; +import getLocation from "./locations"; + +// Title Workaround +function extractTile(text: string) { + const titlePattern = /---\n\n# (?<title>.*)\n/; + const match = text.match(titlePattern); + return match?.groups?.title || "NonTitled"; +} + +export default async (options) => { + const { + includeSrc = true, + render = false, + excerpt = false, + } = options; + + return createContentLoader(extendedConfig.mdfilePatterns, { + includeSrc, + render, + excerpt, + transform(rawData) { + return rawData.map(p => { + const rt = readingTime(p.src || ""); + p.frontmatter.title = extractTile(p.src || ""); + p.frontmatter.datetime = new Date(p.frontmatter.date); + p.frontmatter.location = getLocation(p.frontmatter.spot); + p.frontmatter.readingTime = rt.text; + p.frontmatter.words = rt.words; + p.url = withBaseURL(p.url.replace("/README", "")); + return p; + }).sort((a, b) => { + return b.frontmatter.datetime - a.frontmatter.datetime; + }); + } + }); +}; diff --git a/README.md b/README.md index 6391a70..8912f07 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This blog is powered by [VitePress](https://vitepress.dev/) with a customized th - [x] Posts (Archives) - [x] Sorts (Categories) - [x] Tags -- [ ] Atom Feed +- [x] Atom Feed - [x] Others - [x] Local Search (supports Chinese) - [x] [Google Analytics](https://analytics.google.com/) diff --git a/package-lock.json b/package-lock.json index 78bc759..3fdf63e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "dotenv": "^16.3.1", "eslint": "^8.50.0", "eslint-plugin-vue": "^9.17.0", + "feed": "^4.2.2", "husky": "^8.0.3", "lint-staged": "^15.0.2", "markdown-it-image-figures": "^2.1.1", @@ -2262,6 +2263,18 @@ "reusify": "^1.0.4" } }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "dev": true, + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3646,6 +3659,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true + }, "node_modules/search-insights": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.8.2.tgz", @@ -4404,6 +4423,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 64729ef..90774f1 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dotenv": "^16.3.1", "eslint": "^8.50.0", "eslint-plugin-vue": "^9.17.0", + "feed": "^4.2.2", "husky": "^8.0.3", "lint-staged": "^15.0.2", "markdown-it-image-figures": "^2.1.1",