@binarynoir/vitepress-blog 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
- import { TransformContext, HeadConfig, PageData, TransformPageContext, SiteConfig, UserConfig } from 'vitepress';
3
- import { e as BlogOptions } from './plugin-BlfpqyX4.cjs';
4
- export { h as blogMarkdown } from './plugin-BlfpqyX4.cjs';
2
+ import { SiteConfig, TransformContext, HeadConfig, PageData, TransformPageContext, UserConfig } from 'vitepress';
3
+ import { h as BlogOptions } from './plugin-DUTUA_t4.cjs';
4
+ export { k as blogMarkdown } from './plugin-DUTUA_t4.cjs';
5
5
  import 'markdown-it';
6
6
 
7
7
  /** Where the site is published, for the absolute links feeds and social cards need. */
@@ -19,6 +19,13 @@ interface SiteLocation {
19
19
 
20
20
  /** Where the site is published, or `null` without a site URL: feeds need absolute links. */
21
21
  declare function resolveSiteLocation(siteConfig: SiteConfig, options: BlogOptions): SiteLocation | null;
22
+ /**
23
+ * Puts the announcement banner's data on VitePress's site data, where
24
+ * `useData().site` reads it on every page, and adds the `<head>` script that
25
+ * hides a dismissed banner before the page hydrates. Returns whether the
26
+ * data changed.
27
+ */
28
+ declare function attachAnnouncements(siteConfig: SiteConfig, options?: BlogOptions): boolean;
22
29
  /**
23
30
  * `<head>` tags for blog pages: a feed link on each blog page, and on each
24
31
  * post Open Graph and Twitter card tags (title, summary, image, dates) plus
@@ -57,12 +64,15 @@ declare function writeBlogFeeds(siteConfig: SiteConfig, options?: BlogOptions):
57
64
  * cache) and reloads them when any of them changes.
58
65
  * - Serves each blog's feed and the copied post images, which the build
59
66
  * writes in `buildEnd`, so feed links work in development too.
67
+ * - Gives every page the announcement banner's data (see `attachAnnouncements`),
68
+ * kept current while posts change, and has Vite bundle this package for the
69
+ * server-side build, so the banner can read it there too.
60
70
  */
61
71
  declare function blogVitePlugin(options?: BlogOptions): Plugin;
62
72
  /**
63
- * Wraps a VitePress config with the whole blog: the Markdown plugin, the dev
64
- * server plugin, `<head>` tags, descriptions from summaries, and feeds at the
65
- * end of the build.
73
+ * Wraps a VitePress config with the whole blog: the Markdown plugin, the Vite
74
+ * plugin (dev server and announcements), `<head>` tags, descriptions from
75
+ * summaries, and feeds at the end of the build.
66
76
  *
67
77
  * ```ts
68
78
  * // .vitepress/config.mts
@@ -78,4 +88,4 @@ declare function blogVitePlugin(options?: BlogOptions): Plugin;
78
88
  */
79
89
  declare function withBlog(config: UserConfig, options?: BlogOptions): UserConfig;
80
90
 
81
- export { BlogOptions, blogHead, blogPageData, blogVitePlugin, resolveSiteLocation, withBlog, writeBlogFeeds };
91
+ export { BlogOptions, attachAnnouncements, blogHead, blogPageData, blogVitePlugin, resolveSiteLocation, withBlog, writeBlogFeeds };
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from 'vite';
2
- import { TransformContext, HeadConfig, PageData, TransformPageContext, SiteConfig, UserConfig } from 'vitepress';
3
- import { e as BlogOptions } from './plugin-BlfpqyX4.js';
4
- export { h as blogMarkdown } from './plugin-BlfpqyX4.js';
2
+ import { SiteConfig, TransformContext, HeadConfig, PageData, TransformPageContext, UserConfig } from 'vitepress';
3
+ import { h as BlogOptions } from './plugin-DUTUA_t4.js';
4
+ export { k as blogMarkdown } from './plugin-DUTUA_t4.js';
5
5
  import 'markdown-it';
6
6
 
7
7
  /** Where the site is published, for the absolute links feeds and social cards need. */
@@ -19,6 +19,13 @@ interface SiteLocation {
19
19
 
20
20
  /** Where the site is published, or `null` without a site URL: feeds need absolute links. */
21
21
  declare function resolveSiteLocation(siteConfig: SiteConfig, options: BlogOptions): SiteLocation | null;
22
+ /**
23
+ * Puts the announcement banner's data on VitePress's site data, where
24
+ * `useData().site` reads it on every page, and adds the `<head>` script that
25
+ * hides a dismissed banner before the page hydrates. Returns whether the
26
+ * data changed.
27
+ */
28
+ declare function attachAnnouncements(siteConfig: SiteConfig, options?: BlogOptions): boolean;
22
29
  /**
23
30
  * `<head>` tags for blog pages: a feed link on each blog page, and on each
24
31
  * post Open Graph and Twitter card tags (title, summary, image, dates) plus
@@ -57,12 +64,15 @@ declare function writeBlogFeeds(siteConfig: SiteConfig, options?: BlogOptions):
57
64
  * cache) and reloads them when any of them changes.
58
65
  * - Serves each blog's feed and the copied post images, which the build
59
66
  * writes in `buildEnd`, so feed links work in development too.
67
+ * - Gives every page the announcement banner's data (see `attachAnnouncements`),
68
+ * kept current while posts change, and has Vite bundle this package for the
69
+ * server-side build, so the banner can read it there too.
60
70
  */
61
71
  declare function blogVitePlugin(options?: BlogOptions): Plugin;
62
72
  /**
63
- * Wraps a VitePress config with the whole blog: the Markdown plugin, the dev
64
- * server plugin, `<head>` tags, descriptions from summaries, and feeds at the
65
- * end of the build.
73
+ * Wraps a VitePress config with the whole blog: the Markdown plugin, the Vite
74
+ * plugin (dev server and announcements), `<head>` tags, descriptions from
75
+ * summaries, and feeds at the end of the build.
66
76
  *
67
77
  * ```ts
68
78
  * // .vitepress/config.mts
@@ -78,4 +88,4 @@ declare function blogVitePlugin(options?: BlogOptions): Plugin;
78
88
  */
79
89
  declare function withBlog(config: UserConfig, options?: BlogOptions): UserConfig;
80
90
 
81
- export { BlogOptions, blogHead, blogPageData, blogVitePlugin, resolveSiteLocation, withBlog, writeBlogFeeds };
91
+ export { BlogOptions, attachAnnouncements, blogHead, blogPageData, blogVitePlugin, resolveSiteLocation, withBlog, writeBlogFeeds };
package/dist/vitepress.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import {
2
+ DEFAULT_LABELS,
2
3
  blogMarkdown,
4
+ findAnnouncementBlog,
3
5
  findBlogsListing,
4
6
  fingerprintBlogSource,
5
7
  hasWidget,
6
8
  isIndex,
7
9
  isPost,
8
- joinUrl,
10
+ listAnnouncements,
9
11
  listBlogPosts,
10
12
  loadBlogPages,
11
13
  mayHaveBlogFrontmatter,
@@ -14,12 +16,50 @@ import {
14
16
  toFeedPath,
15
17
  toPageUrl,
16
18
  widgetPages
17
- } from "./chunk-PWNWSYI4.js";
19
+ } from "./chunk-ZM5FOUT4.js";
20
+ import {
21
+ ANNOUNCEMENTS_KEY,
22
+ DISMISSED_CLASS,
23
+ DISMISSED_STORAGE_KEY,
24
+ joinUrl,
25
+ toUrlPath
26
+ } from "./chunk-42Z6H7IN.js";
18
27
 
19
28
  // src/vitepress.ts
20
29
  import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
21
30
  import path from "path";
31
+
32
+ // src/announcements.ts
33
+ function resolveAnnouncements(pages, options, cleanUrls) {
34
+ const drafts = options.drafts ?? false;
35
+ const announcements = listAnnouncements(pages, drafts);
36
+ if (announcements.length === 0) return null;
37
+ const blog = findAnnouncementBlog(pages, announcements, drafts);
38
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
39
+ return {
40
+ items: announcements.map((post) => ({
41
+ id: `${post.routePath}@${post.post.date?.toISOString() ?? ""}`,
42
+ text: post.post.announcementText || post.post.title,
43
+ link: toUrlPath(post.routePath, cleanUrls)
44
+ })),
45
+ listLink: blog ? `${toUrlPath(blog.routePath, cleanUrls)}?announcements` : "",
46
+ labels: {
47
+ announcement: labels.announcement,
48
+ announcementCount: labels.announcementCount,
49
+ dismiss: labels.dismiss
50
+ }
51
+ };
52
+ }
53
+ function renderDismissedScript(data) {
54
+ const id = JSON.stringify(data.items[0].id).replace(/</g, "\\u003c");
55
+ return `try{var d=JSON.parse(localStorage.getItem(${JSON.stringify(DISMISSED_STORAGE_KEY)})||'[]');if(Array.isArray(d)&&d.indexOf(${id})>-1)document.documentElement.classList.add(${JSON.stringify(DISMISSED_CLASS)})}catch(e){}`;
56
+ }
57
+
58
+ // src/vitepress.ts
22
59
  var DEFAULT_FEED_LIMIT = 20;
60
+ var PACKAGE_NAME = "@binarynoir/vitepress-blog";
61
+ var SITE_DATA_MODULE = "/@siteData";
62
+ var SCRIPT_MARKER = "data-vp-blog-announcement";
23
63
  function resolveSiteLocation(siteConfig, options) {
24
64
  const siteUrl = options.siteUrl ?? siteConfig.userConfig?.sitemap?.hostname ?? siteConfig.sitemap?.hostname;
25
65
  if (!siteUrl) return null;
@@ -34,9 +74,19 @@ function resolveSiteLocation(siteConfig, options) {
34
74
  function loadPages(siteConfig, options) {
35
75
  return loadBlogPages(siteConfig.srcDir, options, siteConfig);
36
76
  }
77
+ function attachAnnouncements(siteConfig, options = {}) {
78
+ const site = siteConfig.site;
79
+ const data = resolveAnnouncements(loadPages(siteConfig, options), options, !!siteConfig.cleanUrls);
80
+ const changed = JSON.stringify(site[ANNOUNCEMENTS_KEY] ?? null) !== JSON.stringify(data);
81
+ if (data) site[ANNOUNCEMENTS_KEY] = data;
82
+ else delete site[ANNOUNCEMENTS_KEY];
83
+ site.head = (site.head ?? []).filter(([, attributes]) => !(SCRIPT_MARKER in (attributes ?? {})));
84
+ if (data) site.head.push(["script", { [SCRIPT_MARKER]: "" }, renderDismissedScript(data)]);
85
+ return changed;
86
+ }
37
87
  function listFeedPosts(pages, index, options) {
38
88
  const limit = index.index.feed?.limit ?? options.feedLimit ?? DEFAULT_FEED_LIMIT;
39
- return listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).slice(0, limit);
89
+ return listBlogPosts(pages, index, { drafts: false, pinnedFirst: false, applyLimit: false }).slice(0, limit);
40
90
  }
41
91
  function renderIndexFeed(pages, index, site, siteConfig, options) {
42
92
  return renderFeed({
@@ -138,11 +188,26 @@ function blogVitePlugin(options = {}) {
138
188
  let siteConfig;
139
189
  const currentPages = () => siteConfig ? loadPages(siteConfig, options) : [];
140
190
  const dependentFiles = () => [...currentPages().map((page) => page.absolutePath), ...widgetPages];
191
+ const invalidate = (server, files) => {
192
+ for (const file of files) {
193
+ for (const module of server.moduleGraph.getModulesByFile(file) ?? []) server.moduleGraph.invalidateModule(module);
194
+ }
195
+ };
196
+ const refreshAnnouncements = (server) => {
197
+ if (!siteConfig || !attachAnnouncements(siteConfig, options)) return false;
198
+ const module = server.moduleGraph.getModuleById(SITE_DATA_MODULE);
199
+ if (module) server.moduleGraph.invalidateModule(module);
200
+ return true;
201
+ };
141
202
  return {
142
203
  name: "vitepress-blog",
143
204
  enforce: "pre",
205
+ config() {
206
+ return { ssr: { noExternal: [PACKAGE_NAME] } };
207
+ },
144
208
  configResolved(config) {
145
209
  siteConfig = config.vitepress;
210
+ if (siteConfig) attachAnnouncements(siteConfig, options);
146
211
  },
147
212
  transform(code, id) {
148
213
  if (!siteConfig || !id.endsWith(".md") || !(mayHaveBlogFrontmatter(code) || hasWidget(code))) return null;
@@ -153,11 +218,8 @@ function blogVitePlugin(options = {}) {
153
218
  configureServer(server) {
154
219
  const refresh = (file) => {
155
220
  if (!siteConfig || !file.endsWith(".md")) return;
156
- for (const dependent of dependentFiles()) {
157
- for (const module of server.moduleGraph.getModulesByFile(dependent) ?? []) {
158
- server.moduleGraph.invalidateModule(module);
159
- }
160
- }
221
+ invalidate(server, dependentFiles());
222
+ refreshAnnouncements(server);
161
223
  server.ws.send({ type: "full-reload" });
162
224
  };
163
225
  server.watcher.on("add", refresh).on("unlink", refresh);
@@ -188,6 +250,11 @@ function blogVitePlugin(options = {}) {
188
250
  },
189
251
  handleHotUpdate({ file, modules, server }) {
190
252
  if (!siteConfig || !file.endsWith(".md")) return;
253
+ if (refreshAnnouncements(server)) {
254
+ invalidate(server, dependentFiles());
255
+ server.ws.send({ type: "full-reload" });
256
+ return [];
257
+ }
191
258
  const pages = currentPages();
192
259
  if (!pages.some((page) => page.absolutePath === file)) return;
193
260
  const affected = dependentFiles().filter((dependent) => dependent !== file).flatMap((dependent) => [...server.moduleGraph.getModulesByFile(dependent) ?? []]);
@@ -223,6 +290,7 @@ function withBlog(config, options = {}) {
223
290
  return config;
224
291
  }
225
292
  export {
293
+ attachAnnouncements,
226
294
  blogHead,
227
295
  blogMarkdown,
228
296
  blogPageData,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vitepress.ts"],"sourcesContent":["import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { MarkdownIt } from 'markdown-it';\nimport type { Plugin } from 'vite';\nimport type { HeadConfig, PageData, SiteConfig, TransformContext, TransformPageContext, UserConfig } from 'vitepress';\nimport { findBlogsListing, isIndex, isPost, listBlogPosts, type IndexPage, type PostPage } from './collect.js';\nimport { mayHaveBlogFrontmatter } from './frontmatter.js';\nimport { renderFeed, resolvePostImage, toFeedPath, toPageUrl, type SiteLocation } from './feed.js';\nimport { blogMarkdown } from './plugin.js';\nimport { hasWidget, widgetPages } from './widget.js';\nimport { fingerprintBlogSource, loadBlogPages, type BlogPage } from './scan.js';\nimport type { BlogOptions } from './types.js';\nimport { joinUrl } from './util.js';\n\nconst DEFAULT_FEED_LIMIT = 20;\n\n/** Where the site is published, or `null` without a site URL: feeds need absolute links. */\nexport function resolveSiteLocation(siteConfig: SiteConfig, options: BlogOptions): SiteLocation | null {\n const siteUrl = options.siteUrl ?? siteConfig.userConfig?.sitemap?.hostname ?? siteConfig.sitemap?.hostname;\n if (!siteUrl) return null;\n return {\n siteUrl: siteUrl.replace(/\\/+$/, ''),\n base: siteConfig.site?.base ?? '/',\n cleanUrls: !!siteConfig.cleanUrls,\n publicDir: siteConfig.publicDir ?? path.join(siteConfig.srcDir, 'public'),\n imagesDir: (options.imagesDir ?? 'blog-images').replace(/^\\/+|\\/+$/g, ''),\n };\n}\n\nfunction loadPages(siteConfig: SiteConfig, options: BlogOptions): BlogPage[] {\n return loadBlogPages(siteConfig.srcDir, options, siteConfig);\n}\n\n/** The posts in a blog's feed: no drafts, newest first, up to the feed's limit. */\nfunction listFeedPosts(pages: BlogPage[], index: IndexPage, options: BlogOptions): PostPage[] {\n const limit = index.index.feed?.limit ?? options.feedLimit ?? DEFAULT_FEED_LIMIT;\n return listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).slice(0, limit);\n}\n\nfunction renderIndexFeed(\n pages: BlogPage[],\n index: IndexPage,\n site: SiteLocation,\n siteConfig: SiteConfig,\n options: BlogOptions,\n) {\n return renderFeed({\n index,\n posts: listFeedPosts(pages, index, options),\n site,\n language: siteConfig.site?.lang,\n siteDescription: siteConfig.site?.description,\n });\n}\n\nfunction feedLink(index: IndexPage, base: string): HeadConfig {\n const href = joinUrl(base, toFeedPath(index));\n return [\n 'link',\n { rel: 'alternate', type: 'application/rss+xml', title: index.index.feed?.title ?? index.title, href },\n ];\n}\n\n/**\n * `<head>` tags for blog pages: a feed link on each blog page, and on each\n * post Open Graph and Twitter card tags (title, summary, image, dates) plus\n * links to the feeds that carry it. Use it from VitePress's `transformHead`:\n *\n * ```ts\n * transformHead: (context) => blogHead(context, options),\n * ```\n *\n * Social tags need absolute URLs, so `og:url` and `og:image` are left out\n * without a site URL (the `siteUrl` option or `sitemap.hostname`).\n */\nexport function blogHead(context: TransformContext, options: BlogOptions = {}): HeadConfig[] {\n const { pageData, siteConfig } = context;\n const frontmatter = pageData.frontmatter ?? {};\n if (!pageData.filePath || (!frontmatter.blog && !frontmatter['blog-index'])) return [];\n const pages = loadPages(siteConfig, options);\n const page = pages.find((candidate) => candidate.relativePath === pageData.filePath);\n if (!page) return [];\n const site = resolveSiteLocation(siteConfig, options);\n const base = siteConfig.site?.base ?? '/';\n const head: HeadConfig[] = [];\n\n if (isIndex(page)) {\n if (page.index.feed) head.push(feedLink(page, base));\n if (site) head.push(['meta', { property: 'og:url', content: toPageUrl(page.routePath, site) }]);\n return head;\n }\n if (!isPost(page)) return head;\n\n const { post } = page;\n head.push(\n ['meta', { property: 'og:type', content: 'article' }],\n ['meta', { property: 'og:title', content: post.title }],\n );\n if (post.summary) head.push(['meta', { property: 'og:description', content: post.summary }]);\n if (site) head.push(['meta', { property: 'og:url', content: toPageUrl(page.routePath, site) }]);\n\n const image = site ? resolvePostImage(page, site) : null;\n if (image) {\n head.push(['meta', { property: 'og:image', content: image.url }]);\n if (post.imageAlt) head.push(['meta', { property: 'og:image:alt', content: post.imageAlt }]);\n }\n head.push(['meta', { name: 'twitter:card', content: image ? 'summary_large_image' : 'summary' }]);\n if (post.date) head.push(['meta', { property: 'article:published_time', content: post.date.toISOString() }]);\n if (post.updated) head.push(['meta', { property: 'article:modified_time', content: post.updated.toISOString() }]);\n if (post.authors.length) {\n head.push(['meta', { property: 'article:author', content: post.authors.map((author) => author.name).join(', ') }]);\n }\n if (post.category) head.push(['meta', { property: 'article:section', content: post.category }]);\n if (!post.draft) {\n for (const blog of findBlogsListing(pages, page)) if (blog.index.feed) head.push(feedLink(blog, base));\n }\n return head;\n}\n\n/**\n * Gives a post without a `description` its summary (or the first paragraph,\n * when it has no summary) as one, so VitePress writes it into the page's\n * `<meta name=\"description\">`. Use it from `transformPageData`:\n *\n * ```ts\n * transformPageData: (pageData, context) => blogPageData(pageData, context, options),\n * ```\n */\nexport function blogPageData(pageData: PageData, context: TransformPageContext, options: BlogOptions = {}): void {\n if (!pageData.frontmatter?.blog || pageData.description || !pageData.filePath) return;\n const page = loadPages(context.siteConfig, options).find((candidate) => candidate.relativePath === pageData.filePath);\n if (page?.post?.summary) pageData.description = page.post.summary;\n}\n\n/**\n * Writes an RSS feed for every blog page whose feed isn't turned off, and\n * copies post images to `imagesDir` for the feeds and social cards to link.\n * Use it from VitePress's `buildEnd`.\n */\nexport function writeBlogFeeds(siteConfig: SiteConfig, options: BlogOptions = {}): void {\n const pages = loadPages(siteConfig, options);\n const indexes = pages.filter(isIndex).filter((index) => index.index.feed);\n const site = resolveSiteLocation(siteConfig, options);\n if (!site) {\n if (indexes.length) {\n siteConfig.logger?.warn(\n '[vitepress-blog] no feeds written: set the siteUrl option or sitemap.hostname so feeds can use absolute links.',\n );\n }\n return;\n }\n\n const written = new Map<string, string>();\n for (const index of indexes) {\n const feedPath = toFeedPath(index);\n if (written.has(feedPath)) {\n siteConfig.logger?.warn(\n `[vitepress-blog] ${index.relativePath} and ${written.get(feedPath)} both write ${feedPath}; set feed.path on one of them.`,\n );\n continue;\n }\n written.set(feedPath, index.relativePath);\n const file = path.join(siteConfig.outDir, feedPath);\n mkdirSync(path.dirname(file), { recursive: true });\n writeFileSync(file, renderIndexFeed(pages, index, site, siteConfig, options));\n }\n\n for (const post of pages.filter(isPost)) {\n if (post.post.draft) continue;\n const image = resolvePostImage(post, site);\n if (!image?.file || !image.outputPath) continue;\n const target = path.join(siteConfig.outDir, image.outputPath);\n mkdirSync(path.dirname(target), { recursive: true });\n copyFileSync(image.file, target);\n }\n}\n\n/**\n * The Vite side of the blog, for `vitepress dev`:\n *\n * - VitePress caches a page's rendered output by its own source, so a new or\n * edited post would leave blog lists and \"newer / older\" links stale. This\n * adds a fingerprint of every post to blog pages and posts (busting the\n * cache) and reloads them when any of them changes.\n * - Serves each blog's feed and the copied post images, which the build\n * writes in `buildEnd`, so feed links work in development too.\n */\nexport function blogVitePlugin(options: BlogOptions = {}): Plugin {\n let siteConfig: SiteConfig | undefined;\n const currentPages = () => (siteConfig ? loadPages(siteConfig, options) : []);\n // Every file whose output shows posts: blog pages, posts, and pages with a `::: blog-posts` block.\n const dependentFiles = () => [...currentPages().map((page) => page.absolutePath), ...widgetPages];\n\n return {\n name: 'vitepress-blog',\n enforce: 'pre',\n configResolved(config) {\n siteConfig = (config as { vitepress?: SiteConfig }).vitepress;\n },\n transform(code, id) {\n if (!siteConfig || !id.endsWith('.md') || !(mayHaveBlogFrontmatter(code) || hasWidget(code))) return null;\n return { code: `${code}\\n<!-- vitepress-blog:${fingerprintBlogSource(currentPages())} -->\\n`, map: null };\n },\n configureServer(server) {\n // Vite only calls handleHotUpdate for edits; a post added or deleted changes the lists too.\n const refresh = (file: string) => {\n if (!siteConfig || !file.endsWith('.md')) return;\n for (const dependent of dependentFiles()) {\n for (const module of server.moduleGraph.getModulesByFile(dependent) ?? []) {\n server.moduleGraph.invalidateModule(module);\n }\n }\n server.ws.send({ type: 'full-reload' });\n };\n server.watcher.on('add', refresh).on('unlink', refresh);\n\n server.middlewares.use((request, response, next) => {\n if (!siteConfig || !request.url) return next();\n const base = siteConfig.site?.base ?? '/';\n const pathname = decodeURIComponent(request.url.split('?')[0]);\n if (!pathname.startsWith(base)) return next();\n const relative = pathname.slice(base.length);\n const pages = currentPages();\n // Feeds link to the live site, or to the dev server without a site URL.\n const site =\n resolveSiteLocation(siteConfig, options) ??\n resolveSiteLocation(siteConfig, { ...options, siteUrl: `http://${request.headers.host ?? 'localhost'}` })!;\n\n const index = pages.filter(isIndex).find((page) => page.index.feed && toFeedPath(page) === relative);\n if (index) {\n response.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');\n response.end(renderIndexFeed(pages, index, site, siteConfig, options));\n return;\n }\n for (const post of pages.filter(isPost)) {\n const image = resolvePostImage(post, site);\n if (image?.outputPath === relative && image.file) {\n if (image.type) response.setHeader('Content-Type', image.type);\n response.end(readFileSync(image.file));\n return;\n }\n }\n next();\n });\n },\n handleHotUpdate({ file, modules, server }) {\n if (!siteConfig || !file.endsWith('.md')) return;\n const pages = currentPages();\n if (!pages.some((page) => page.absolutePath === file)) return;\n const affected = dependentFiles()\n .filter((dependent) => dependent !== file)\n .flatMap((dependent) => [...(server.moduleGraph.getModulesByFile(dependent) ?? [])]);\n return affected.length ? [...modules, ...affected] : undefined;\n },\n };\n}\n\n/**\n * Wraps a VitePress config with the whole blog: the Markdown plugin, the dev\n * server plugin, `<head>` tags, descriptions from summaries, and feeds at the\n * end of the build.\n *\n * ```ts\n * // .vitepress/config.mts\n * import { defineConfig } from 'vitepress';\n * import { withBlog } from '@binarynoir/vitepress-blog/vitepress';\n *\n * export default withBlog(defineConfig({ /* ...your config... *\\/ }), { siteUrl: 'https://example.com' });\n * ```\n *\n * Hooks you already have (`markdown.config`, `transformHead`,\n * `transformPageData`, `buildEnd`) are not replaced: the blog's runs and\n * then yours, so it composes with other `withX()` wrappers in any order.\n */\nexport function withBlog(config: UserConfig, options: BlogOptions = {}): UserConfig {\n config.markdown ??= {};\n const existingMarkdownConfig = config.markdown.config ?? (() => {});\n config.markdown.config = (md) => {\n // VitePress 2 types this callback against `markdown-it-async`'s MarkdownIt,\n // a structural superset of plain markdown-it's; the cast bridges the two.\n blogMarkdown(md as unknown as MarkdownIt, options);\n existingMarkdownConfig(md);\n };\n\n config.vite ??= {};\n config.vite.plugins = [...(config.vite.plugins ?? []), blogVitePlugin(options)];\n\n const existingTransformHead = config.transformHead;\n config.transformHead = async (context) => [\n ...blogHead(context, options),\n ...((await existingTransformHead?.(context)) ?? []),\n ];\n\n const existingTransformPageData = config.transformPageData;\n config.transformPageData = (pageData, context) => {\n blogPageData(pageData, context, options);\n return existingTransformPageData?.(pageData, context);\n };\n\n const existingBuildEnd = config.buildEnd;\n config.buildEnd = async (siteConfig) => {\n writeBlogFeeds(siteConfig, options);\n await existingBuildEnd?.(siteConfig);\n };\n\n return config;\n}\n\nexport { blogMarkdown };\nexport type { BlogOptions };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/vitepress.ts","../src/announcements.ts"],"sourcesContent":["import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { MarkdownIt } from 'markdown-it';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport type { HeadConfig, PageData, SiteConfig, TransformContext, TransformPageContext, UserConfig } from 'vitepress';\nimport { renderDismissedScript, resolveAnnouncements } from './announcements.js';\nimport { findBlogsListing, isIndex, isPost, listBlogPosts, type IndexPage, type PostPage } from './collect.js';\nimport { mayHaveBlogFrontmatter } from './frontmatter.js';\nimport { renderFeed, resolvePostImage, toFeedPath, toPageUrl, type SiteLocation } from './feed.js';\nimport { blogMarkdown } from './plugin.js';\nimport { hasWidget, widgetPages } from './widget.js';\nimport { fingerprintBlogSource, loadBlogPages, type BlogPage } from './scan.js';\nimport type { BlogOptions } from './types.js';\nimport { ANNOUNCEMENTS_KEY, joinUrl } from './util.js';\n\nconst DEFAULT_FEED_LIMIT = 20;\nconst PACKAGE_NAME = '@binarynoir/vitepress-blog';\n// VitePress's id for the module that serves the site data to the browser.\nconst SITE_DATA_MODULE = '/@siteData';\n// Marks the `<head>` script this adds, so it is replaced rather than added twice.\nconst SCRIPT_MARKER = 'data-vp-blog-announcement';\n\n/** Where the site is published, or `null` without a site URL: feeds need absolute links. */\nexport function resolveSiteLocation(siteConfig: SiteConfig, options: BlogOptions): SiteLocation | null {\n const siteUrl = options.siteUrl ?? siteConfig.userConfig?.sitemap?.hostname ?? siteConfig.sitemap?.hostname;\n if (!siteUrl) return null;\n return {\n siteUrl: siteUrl.replace(/\\/+$/, ''),\n base: siteConfig.site?.base ?? '/',\n cleanUrls: !!siteConfig.cleanUrls,\n publicDir: siteConfig.publicDir ?? path.join(siteConfig.srcDir, 'public'),\n imagesDir: (options.imagesDir ?? 'blog-images').replace(/^\\/+|\\/+$/g, ''),\n };\n}\n\nfunction loadPages(siteConfig: SiteConfig, options: BlogOptions): BlogPage[] {\n return loadBlogPages(siteConfig.srcDir, options, siteConfig);\n}\n\n/**\n * Puts the announcement banner's data on VitePress's site data, where\n * `useData().site` reads it on every page, and adds the `<head>` script that\n * hides a dismissed banner before the page hydrates. Returns whether the\n * data changed.\n */\nexport function attachAnnouncements(siteConfig: SiteConfig, options: BlogOptions = {}): boolean {\n const site = siteConfig.site as SiteConfig['site'] & Record<string, unknown>;\n const data = resolveAnnouncements(loadPages(siteConfig, options), options, !!siteConfig.cleanUrls);\n const changed = JSON.stringify(site[ANNOUNCEMENTS_KEY] ?? null) !== JSON.stringify(data);\n if (data) site[ANNOUNCEMENTS_KEY] = data;\n else delete site[ANNOUNCEMENTS_KEY];\n site.head = (site.head ?? []).filter(([, attributes]) => !(SCRIPT_MARKER in (attributes ?? {})));\n if (data) site.head.push(['script', { [SCRIPT_MARKER]: '' }, renderDismissedScript(data)]);\n return changed;\n}\n\n/** The posts in a blog's feed: no drafts, newest first, up to the feed's limit. */\nfunction listFeedPosts(pages: BlogPage[], index: IndexPage, options: BlogOptions): PostPage[] {\n const limit = index.index.feed?.limit ?? options.feedLimit ?? DEFAULT_FEED_LIMIT;\n return listBlogPosts(pages, index, { drafts: false, pinnedFirst: false, applyLimit: false }).slice(0, limit);\n}\n\nfunction renderIndexFeed(\n pages: BlogPage[],\n index: IndexPage,\n site: SiteLocation,\n siteConfig: SiteConfig,\n options: BlogOptions,\n) {\n return renderFeed({\n index,\n posts: listFeedPosts(pages, index, options),\n site,\n language: siteConfig.site?.lang,\n siteDescription: siteConfig.site?.description,\n });\n}\n\nfunction feedLink(index: IndexPage, base: string): HeadConfig {\n const href = joinUrl(base, toFeedPath(index));\n return [\n 'link',\n { rel: 'alternate', type: 'application/rss+xml', title: index.index.feed?.title ?? index.title, href },\n ];\n}\n\n/**\n * `<head>` tags for blog pages: a feed link on each blog page, and on each\n * post Open Graph and Twitter card tags (title, summary, image, dates) plus\n * links to the feeds that carry it. Use it from VitePress's `transformHead`:\n *\n * ```ts\n * transformHead: (context) => blogHead(context, options),\n * ```\n *\n * Social tags need absolute URLs, so `og:url` and `og:image` are left out\n * without a site URL (the `siteUrl` option or `sitemap.hostname`).\n */\nexport function blogHead(context: TransformContext, options: BlogOptions = {}): HeadConfig[] {\n const { pageData, siteConfig } = context;\n const frontmatter = pageData.frontmatter ?? {};\n if (!pageData.filePath || (!frontmatter.blog && !frontmatter['blog-index'])) return [];\n const pages = loadPages(siteConfig, options);\n const page = pages.find((candidate) => candidate.relativePath === pageData.filePath);\n if (!page) return [];\n const site = resolveSiteLocation(siteConfig, options);\n const base = siteConfig.site?.base ?? '/';\n const head: HeadConfig[] = [];\n\n if (isIndex(page)) {\n if (page.index.feed) head.push(feedLink(page, base));\n if (site) head.push(['meta', { property: 'og:url', content: toPageUrl(page.routePath, site) }]);\n return head;\n }\n if (!isPost(page)) return head;\n\n const { post } = page;\n head.push(\n ['meta', { property: 'og:type', content: 'article' }],\n ['meta', { property: 'og:title', content: post.title }],\n );\n if (post.summary) head.push(['meta', { property: 'og:description', content: post.summary }]);\n if (site) head.push(['meta', { property: 'og:url', content: toPageUrl(page.routePath, site) }]);\n\n const image = site ? resolvePostImage(page, site) : null;\n if (image) {\n head.push(['meta', { property: 'og:image', content: image.url }]);\n if (post.imageAlt) head.push(['meta', { property: 'og:image:alt', content: post.imageAlt }]);\n }\n head.push(['meta', { name: 'twitter:card', content: image ? 'summary_large_image' : 'summary' }]);\n if (post.date) head.push(['meta', { property: 'article:published_time', content: post.date.toISOString() }]);\n if (post.updated) head.push(['meta', { property: 'article:modified_time', content: post.updated.toISOString() }]);\n if (post.authors.length) {\n head.push(['meta', { property: 'article:author', content: post.authors.map((author) => author.name).join(', ') }]);\n }\n if (post.category) head.push(['meta', { property: 'article:section', content: post.category }]);\n if (!post.draft) {\n for (const blog of findBlogsListing(pages, page)) if (blog.index.feed) head.push(feedLink(blog, base));\n }\n return head;\n}\n\n/**\n * Gives a post without a `description` its summary (or the first paragraph,\n * when it has no summary) as one, so VitePress writes it into the page's\n * `<meta name=\"description\">`. Use it from `transformPageData`:\n *\n * ```ts\n * transformPageData: (pageData, context) => blogPageData(pageData, context, options),\n * ```\n */\nexport function blogPageData(pageData: PageData, context: TransformPageContext, options: BlogOptions = {}): void {\n if (!pageData.frontmatter?.blog || pageData.description || !pageData.filePath) return;\n const page = loadPages(context.siteConfig, options).find((candidate) => candidate.relativePath === pageData.filePath);\n if (page?.post?.summary) pageData.description = page.post.summary;\n}\n\n/**\n * Writes an RSS feed for every blog page whose feed isn't turned off, and\n * copies post images to `imagesDir` for the feeds and social cards to link.\n * Use it from VitePress's `buildEnd`.\n */\nexport function writeBlogFeeds(siteConfig: SiteConfig, options: BlogOptions = {}): void {\n const pages = loadPages(siteConfig, options);\n const indexes = pages.filter(isIndex).filter((index) => index.index.feed);\n const site = resolveSiteLocation(siteConfig, options);\n if (!site) {\n if (indexes.length) {\n siteConfig.logger?.warn(\n '[vitepress-blog] no feeds written: set the siteUrl option or sitemap.hostname so feeds can use absolute links.',\n );\n }\n return;\n }\n\n const written = new Map<string, string>();\n for (const index of indexes) {\n const feedPath = toFeedPath(index);\n if (written.has(feedPath)) {\n siteConfig.logger?.warn(\n `[vitepress-blog] ${index.relativePath} and ${written.get(feedPath)} both write ${feedPath}; set feed.path on one of them.`,\n );\n continue;\n }\n written.set(feedPath, index.relativePath);\n const file = path.join(siteConfig.outDir, feedPath);\n mkdirSync(path.dirname(file), { recursive: true });\n writeFileSync(file, renderIndexFeed(pages, index, site, siteConfig, options));\n }\n\n for (const post of pages.filter(isPost)) {\n if (post.post.draft) continue;\n const image = resolvePostImage(post, site);\n if (!image?.file || !image.outputPath) continue;\n const target = path.join(siteConfig.outDir, image.outputPath);\n mkdirSync(path.dirname(target), { recursive: true });\n copyFileSync(image.file, target);\n }\n}\n\n/**\n * The Vite side of the blog, for `vitepress dev`:\n *\n * - VitePress caches a page's rendered output by its own source, so a new or\n * edited post would leave blog lists and \"newer / older\" links stale. This\n * adds a fingerprint of every post to blog pages and posts (busting the\n * cache) and reloads them when any of them changes.\n * - Serves each blog's feed and the copied post images, which the build\n * writes in `buildEnd`, so feed links work in development too.\n * - Gives every page the announcement banner's data (see `attachAnnouncements`),\n * kept current while posts change, and has Vite bundle this package for the\n * server-side build, so the banner can read it there too.\n */\nexport function blogVitePlugin(options: BlogOptions = {}): Plugin {\n let siteConfig: SiteConfig | undefined;\n const currentPages = () => (siteConfig ? loadPages(siteConfig, options) : []);\n // Every file whose output shows posts: blog pages, posts, and pages with a `::: blog-posts` block.\n const dependentFiles = () => [...currentPages().map((page) => page.absolutePath), ...widgetPages];\n const invalidate = (server: ViteDevServer, files: string[]) => {\n for (const file of files) {\n for (const module of server.moduleGraph.getModulesByFile(file) ?? []) server.moduleGraph.invalidateModule(module);\n }\n };\n /** Re-reads the announcements; when they changed, the site data is served afresh. */\n const refreshAnnouncements = (server: ViteDevServer): boolean => {\n if (!siteConfig || !attachAnnouncements(siteConfig, options)) return false;\n const module = server.moduleGraph.getModuleById(SITE_DATA_MODULE);\n if (module) server.moduleGraph.invalidateModule(module);\n return true;\n };\n\n return {\n name: 'vitepress-blog',\n enforce: 'pre',\n config() {\n // Bundled rather than loaded by Node, the client entry gets VitePress's\n // client (`useData`) on the server too, so the banner renders there.\n return { ssr: { noExternal: [PACKAGE_NAME] } };\n },\n configResolved(config) {\n siteConfig = (config as { vitepress?: SiteConfig }).vitepress;\n if (siteConfig) attachAnnouncements(siteConfig, options);\n },\n transform(code, id) {\n if (!siteConfig || !id.endsWith('.md') || !(mayHaveBlogFrontmatter(code) || hasWidget(code))) return null;\n return { code: `${code}\\n<!-- vitepress-blog:${fingerprintBlogSource(currentPages())} -->\\n`, map: null };\n },\n configureServer(server) {\n // Vite only calls handleHotUpdate for edits; a post added or deleted changes the lists too.\n const refresh = (file: string) => {\n if (!siteConfig || !file.endsWith('.md')) return;\n invalidate(server, dependentFiles());\n refreshAnnouncements(server);\n server.ws.send({ type: 'full-reload' });\n };\n server.watcher.on('add', refresh).on('unlink', refresh);\n\n server.middlewares.use((request, response, next) => {\n if (!siteConfig || !request.url) return next();\n const base = siteConfig.site?.base ?? '/';\n const pathname = decodeURIComponent(request.url.split('?')[0]);\n if (!pathname.startsWith(base)) return next();\n const relative = pathname.slice(base.length);\n const pages = currentPages();\n // Feeds link to the live site, or to the dev server without a site URL.\n const site =\n resolveSiteLocation(siteConfig, options) ??\n resolveSiteLocation(siteConfig, { ...options, siteUrl: `http://${request.headers.host ?? 'localhost'}` })!;\n\n const index = pages.filter(isIndex).find((page) => page.index.feed && toFeedPath(page) === relative);\n if (index) {\n response.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');\n response.end(renderIndexFeed(pages, index, site, siteConfig, options));\n return;\n }\n for (const post of pages.filter(isPost)) {\n const image = resolvePostImage(post, site);\n if (image?.outputPath === relative && image.file) {\n if (image.type) response.setHeader('Content-Type', image.type);\n response.end(readFileSync(image.file));\n return;\n }\n }\n next();\n });\n },\n handleHotUpdate({ file, modules, server }) {\n if (!siteConfig || !file.endsWith('.md')) return;\n // Every page shows the banner, so a change to it reloads them all. Checked\n // first: a page that stops being an announcement is no longer a post.\n if (refreshAnnouncements(server)) {\n invalidate(server, dependentFiles());\n server.ws.send({ type: 'full-reload' });\n return [];\n }\n const pages = currentPages();\n if (!pages.some((page) => page.absolutePath === file)) return;\n const affected = dependentFiles()\n .filter((dependent) => dependent !== file)\n .flatMap((dependent) => [...(server.moduleGraph.getModulesByFile(dependent) ?? [])]);\n return affected.length ? [...modules, ...affected] : undefined;\n },\n };\n}\n\n/**\n * Wraps a VitePress config with the whole blog: the Markdown plugin, the Vite\n * plugin (dev server and announcements), `<head>` tags, descriptions from\n * summaries, and feeds at the end of the build.\n *\n * ```ts\n * // .vitepress/config.mts\n * import { defineConfig } from 'vitepress';\n * import { withBlog } from '@binarynoir/vitepress-blog/vitepress';\n *\n * export default withBlog(defineConfig({ /* ...your config... *\\/ }), { siteUrl: 'https://example.com' });\n * ```\n *\n * Hooks you already have (`markdown.config`, `transformHead`,\n * `transformPageData`, `buildEnd`) are not replaced: the blog's runs and\n * then yours, so it composes with other `withX()` wrappers in any order.\n */\nexport function withBlog(config: UserConfig, options: BlogOptions = {}): UserConfig {\n config.markdown ??= {};\n const existingMarkdownConfig = config.markdown.config ?? (() => {});\n config.markdown.config = (md) => {\n // VitePress 2 types this callback against `markdown-it-async`'s MarkdownIt,\n // a structural superset of plain markdown-it's; the cast bridges the two.\n blogMarkdown(md as unknown as MarkdownIt, options);\n existingMarkdownConfig(md);\n };\n\n config.vite ??= {};\n config.vite.plugins = [...(config.vite.plugins ?? []), blogVitePlugin(options)];\n\n const existingTransformHead = config.transformHead;\n config.transformHead = async (context) => [\n ...blogHead(context, options),\n ...((await existingTransformHead?.(context)) ?? []),\n ];\n\n const existingTransformPageData = config.transformPageData;\n config.transformPageData = (pageData, context) => {\n blogPageData(pageData, context, options);\n return existingTransformPageData?.(pageData, context);\n };\n\n const existingBuildEnd = config.buildEnd;\n config.buildEnd = async (siteConfig) => {\n writeBlogFeeds(siteConfig, options);\n await existingBuildEnd?.(siteConfig);\n };\n\n return config;\n}\n\nexport { blogMarkdown };\nexport type { BlogOptions };\n","import { findAnnouncementBlog, listAnnouncements } from './collect.js';\nimport type { BlogPage } from './scan.js';\nimport { DEFAULT_LABELS, type BlogAnnouncementData, type BlogOptions } from './types.js';\nimport { DISMISSED_CLASS, DISMISSED_STORAGE_KEY, toUrlPath } from './util.js';\n\n/** The banner's data: every announcement, newest first, and where the list of them is. `null` with none. */\nexport function resolveAnnouncements(\n pages: BlogPage[],\n options: BlogOptions,\n cleanUrls: boolean,\n): BlogAnnouncementData | null {\n const drafts = options.drafts ?? false;\n const announcements = listAnnouncements(pages, drafts);\n if (announcements.length === 0) return null;\n const blog = findAnnouncementBlog(pages, announcements, drafts);\n const labels = { ...DEFAULT_LABELS, ...options.labels };\n return {\n items: announcements.map((post) => ({\n id: `${post.routePath}@${post.post.date?.toISOString() ?? ''}`,\n text: post.post.announcementText || post.post.title,\n link: toUrlPath(post.routePath, cleanUrls),\n })),\n listLink: blog ? `${toUrlPath(blog.routePath, cleanUrls)}?announcements` : '',\n labels: {\n announcement: labels.announcement,\n announcementCount: labels.announcementCount,\n dismiss: labels.dismiss,\n },\n };\n}\n\n/**\n * A `<head>` script that marks `<html>` with `DISMISSED_CLASS` when the visitor\n * dismissed the newest announcement, so the server-rendered banner never\n * flashes before the page hydrates.\n */\nexport function renderDismissedScript(data: BlogAnnouncementData): string {\n // `<` escaped so an id can't close the script element.\n const id = JSON.stringify(data.items[0].id).replace(/</g, '\\\\u003c');\n return (\n `try{var d=JSON.parse(localStorage.getItem(${JSON.stringify(DISMISSED_STORAGE_KEY)})||'[]');` +\n `if(Array.isArray(d)&&d.indexOf(${id})>-1)document.documentElement.classList.add(${JSON.stringify(DISMISSED_CLASS)})}catch(e){}`\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binarynoir/vitepress-blog",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Turn any VitePress 2 page into a blog post with frontmatter. A main blog, folder-scoped section blogs, sticky posts, pagination, keyword filters and an RSS feed for each.",
5
5
  "keywords": [
6
6
  "vitepress",