@binarynoir/vitepress-blog 0.1.1 → 0.3.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.
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/vitepress.ts
31
31
  var vitepress_exports = {};
32
32
  __export(vitepress_exports, {
33
+ attachAnnouncements: () => attachAnnouncements,
33
34
  blogHead: () => blogHead,
34
35
  blogMarkdown: () => blogMarkdown,
35
36
  blogPageData: () => blogPageData,
@@ -113,10 +114,213 @@ function findBlogsListing(pages, post) {
113
114
  (index) => listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).includes(post)
114
115
  );
115
116
  }
117
+ function listAnnouncements(pages, drafts) {
118
+ return pages.filter(isPost).filter((post) => post.post.announcement && (drafts || !post.post.draft)).sort(compareByDate);
119
+ }
120
+ function findAnnouncementBlog(pages, announcements, drafts) {
121
+ if (announcements.length === 0) return null;
122
+ const listingAll = pages.filter(isIndex).filter((index) => {
123
+ const listed = listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false });
124
+ return announcements.every((post) => listed.includes(post));
125
+ });
126
+ return listingAll.find((index) => index.index.scope === "site") ?? listingAll[0] ?? findHomeBlog(pages, announcements[0], drafts);
127
+ }
128
+
129
+ // src/types.ts
130
+ var DEFAULT_LABELS = {
131
+ by: "By",
132
+ published: "Published",
133
+ updated: "Updated",
134
+ dateTime: "{date} at {time}",
135
+ readingTime: "{n} min read",
136
+ newer: "Newer post",
137
+ older: "Older post",
138
+ allPosts: "All posts in {blog}",
139
+ draft: "Draft",
140
+ sticky: "Pinned",
141
+ announcement: "Announcement",
142
+ announcements: "Announcements",
143
+ noAnnouncements: "No announcements.",
144
+ announcementCount: "{n} announcements",
145
+ dismiss: "Dismiss",
146
+ noPosts: "No posts yet.",
147
+ undated: "Undated",
148
+ tags: "Keywords",
149
+ tagged: "Posts tagged {tag}",
150
+ showAll: "Show all posts",
151
+ noTagged: "No posts tagged {tag}.",
152
+ previous: "Previous",
153
+ next: "Next",
154
+ pages: "Pages",
155
+ subscribe: "Subscribe",
156
+ rssFeed: "RSS feed for {blog}"
157
+ };
158
+
159
+ // src/util.ts
160
+ var HTML_ESCAPES = {
161
+ "&": "&",
162
+ "<": "&lt;",
163
+ ">": "&gt;",
164
+ '"': "&quot;",
165
+ "'": "&#39;"
166
+ };
167
+ function escapeHtml(value) {
168
+ return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
169
+ }
170
+ function escapeMarkdown(text) {
171
+ const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
172
+ return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
173
+ }
174
+ function escapeTemplateText(text) {
175
+ const escaped = escapeHtml(text);
176
+ return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
177
+ }
178
+ function isExternalUrl(url) {
179
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
180
+ }
181
+ function toMarkdownUrl(url) {
182
+ return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
183
+ }
184
+ function toPageLink(text, routePath) {
185
+ return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
186
+ }
187
+ function toUrlPath(routePath, cleanUrls) {
188
+ return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
189
+ }
190
+ function joinUrl(base, urlPath) {
191
+ return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
192
+ }
193
+ var ANNOUNCEMENTS_KEY = "blogAnnouncements";
194
+ var DISMISSED_STORAGE_KEY = "vp-blog-announcements-dismissed";
195
+ var DISMISSED_CLASS = "vp-blog-announcement-dismissed";
196
+
197
+ // src/announcements.ts
198
+ function resolveAnnouncements(pages, options, cleanUrls) {
199
+ const drafts = options.drafts ?? false;
200
+ const announcements = listAnnouncements(pages, drafts);
201
+ if (announcements.length === 0) return null;
202
+ const blog = findAnnouncementBlog(pages, announcements, drafts);
203
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
204
+ return {
205
+ items: announcements.map((post) => ({
206
+ id: `${post.routePath}@${post.post.date?.toISOString() ?? ""}`,
207
+ text: post.post.announcementText || post.post.title,
208
+ link: toUrlPath(post.routePath, cleanUrls)
209
+ })),
210
+ listLink: blog ? `${toUrlPath(blog.routePath, cleanUrls)}?announcements` : "",
211
+ labels: {
212
+ announcement: labels.announcement,
213
+ announcementCount: labels.announcementCount,
214
+ dismiss: labels.dismiss
215
+ }
216
+ };
217
+ }
218
+ function renderDismissedScript(data) {
219
+ const id = JSON.stringify(data.items[0].id).replace(/</g, "\\u003c");
220
+ 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){}`;
221
+ }
116
222
 
117
223
  // src/frontmatter.ts
118
224
  var import_node_path = __toESM(require("path"), 1);
119
225
  var import_yaml = require("yaml");
226
+
227
+ // src/dates.ts
228
+ var DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3})\d*)?)?)?\s*(Z|[+-]\d{2}:?\d{2})?$/i;
229
+ function isTimeZone(timeZone) {
230
+ try {
231
+ new Intl.DateTimeFormat("en-US", { timeZone });
232
+ return true;
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+ var warnedZones = /* @__PURE__ */ new Set();
238
+ function resolveTimeZone(timeZone) {
239
+ if (!timeZone) return "UTC";
240
+ if (isTimeZone(timeZone)) return timeZone;
241
+ if (!warnedZones.has(timeZone)) {
242
+ warnedZones.add(timeZone);
243
+ console.warn(
244
+ `[vitepress-blog] "${timeZone}" is not a time zone; using UTC. Use an IANA name such as "America/New_York".`
245
+ );
246
+ }
247
+ return "UTC";
248
+ }
249
+ var offsetFormats = /* @__PURE__ */ new Map();
250
+ function zoneOffset(instant, timeZone) {
251
+ let format = offsetFormats.get(timeZone);
252
+ if (!format) {
253
+ format = new Intl.DateTimeFormat("en-US", {
254
+ timeZone,
255
+ hourCycle: "h23",
256
+ year: "numeric",
257
+ month: "2-digit",
258
+ day: "2-digit",
259
+ hour: "2-digit",
260
+ minute: "2-digit",
261
+ second: "2-digit"
262
+ });
263
+ offsetFormats.set(timeZone, format);
264
+ }
265
+ const parts = Object.fromEntries(format.formatToParts(instant).map(({ type, value }) => [type, Number(value)]));
266
+ const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
267
+ return wall - Math.floor(instant / 1e3) * 1e3;
268
+ }
269
+ function fromZonedTime(wall, timeZone) {
270
+ const first = wall - zoneOffset(wall, timeZone);
271
+ const second = wall - zoneOffset(first, timeZone);
272
+ return second;
273
+ }
274
+ function parsePostDate(value, timeZone = "UTC") {
275
+ if (value instanceof Date) {
276
+ if (Number.isNaN(value.getTime())) return null;
277
+ const midnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0;
278
+ return { date: value, hasTime: !midnight };
279
+ }
280
+ if (typeof value !== "string") return null;
281
+ const match = DATE_TIME.exec(value.trim());
282
+ if (!match) return null;
283
+ const [, year, month, day, hour, minute, second = "0", millisecond = "0", offset] = match;
284
+ const hasTime = hour !== void 0;
285
+ const wall = Date.UTC(
286
+ Number(year),
287
+ Number(month) - 1,
288
+ Number(day),
289
+ hasTime ? Number(hour) : 0,
290
+ hasTime ? Number(minute) : 0,
291
+ Number(second),
292
+ Number(millisecond.padEnd(3, "0"))
293
+ );
294
+ const check = new Date(wall);
295
+ if (check.getUTCMonth() !== Number(month) - 1 || check.getUTCDate() !== Number(day)) return null;
296
+ if (!offset) return { date: new Date(fromZonedTime(wall, timeZone)), hasTime };
297
+ if (offset.toUpperCase() === "Z") return { date: new Date(wall), hasTime };
298
+ const sign = offset.startsWith("-") ? -1 : 1;
299
+ const digits = offset.slice(1).replace(":", "");
300
+ const minutes = Number(digits.slice(0, 2)) * 60 + Number(digits.slice(2));
301
+ return { date: new Date(wall - sign * minutes * 6e4), hasTime };
302
+ }
303
+ function formatPostDate({ date, hasTime }, display) {
304
+ const { locale, timeZone } = display;
305
+ const day = new Intl.DateTimeFormat(locale, { timeZone, ...display.dateFormat }).format(date);
306
+ if (!hasTime) return day;
307
+ const time = new Intl.DateTimeFormat(locale, { timeZone, ...display.timeFormat }).format(date);
308
+ return display.dateTime.replace("{date}", day).replace("{time}", time);
309
+ }
310
+ function toZonedIsoDate(date, timeZone) {
311
+ const parts = Object.fromEntries(
312
+ new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date).map(({ type, value }) => [type, value])
313
+ );
314
+ return `${parts.year}-${parts.month}-${parts.day}`;
315
+ }
316
+ function toDateTimeAttribute({ date, hasTime }, timeZone) {
317
+ return hasTime ? date.toISOString() : toZonedIsoDate(date, timeZone);
318
+ }
319
+ function yearIn(date, timeZone) {
320
+ return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
321
+ }
322
+
323
+ // src/frontmatter.ts
120
324
  var POST_FRONTMATTER_KEY = "blog";
121
325
  var INDEX_FRONTMATTER_KEY = "blog-index";
122
326
  var FRONTMATTER = /^---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)/;
@@ -155,12 +359,6 @@ function toStringList(value) {
155
359
  const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
156
360
  return values.map(toText).filter(Boolean);
157
361
  }
158
- function toDate(value) {
159
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
160
- if (typeof value !== "string" && typeof value !== "number") return null;
161
- const date = new Date(value);
162
- return Number.isNaN(date.getTime()) ? null : date;
163
- }
164
362
  function toAuthor(value, options) {
165
363
  if (typeof value === "string") {
166
364
  const name2 = value.trim();
@@ -225,11 +423,17 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
225
423
  const stripTags = options.stripTags ?? true;
226
424
  const clean = (text) => stripTags ? stripTagSyntax(text) : text;
227
425
  const image = toRecord(pick("image"));
426
+ const timeZone = resolveTimeZone(options.timeZone);
427
+ const date = parsePostDate(pick("date"), timeZone);
428
+ const updated = parsePostDate(pick("updated"), timeZone);
429
+ const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
228
430
  return {
229
431
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
230
432
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
231
- date: toDate(pick("date")),
232
- updated: toDate(pick("updated")),
433
+ date: date?.date ?? null,
434
+ dateHasTime: date?.hasTime ?? false,
435
+ updated: updated?.date ?? null,
436
+ updatedHasTime: updated?.hasTime ?? false,
233
437
  image: toText(image.src) || toText(pick("image")),
234
438
  imageAlt: toText(image.alt) || toText(post.imageAlt) || toText(data.imageAlt),
235
439
  imageFit: (toText(image.fit) || toText(post.imageFit)) === "contain" ? "contain" : "cover",
@@ -240,6 +444,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
240
444
  scope: toScope(post.scope),
241
445
  sticky: post.sticky === true,
242
446
  draft: post.draft === true,
447
+ announcement: post.announcement === true || !!announcementText,
448
+ announcementText,
243
449
  byline: toBoolean(post.byline),
244
450
  cover: toBoolean(post.cover),
245
451
  postNav: toBoolean(post.postNav),
@@ -298,49 +504,6 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
298
504
  var import_node_crypto = require("crypto");
299
505
  var import_node_fs = require("fs");
300
506
  var import_node_path2 = __toESM(require("path"), 1);
301
-
302
- // src/util.ts
303
- var HTML_ESCAPES = {
304
- "&": "&amp;",
305
- "<": "&lt;",
306
- ">": "&gt;",
307
- '"': "&quot;",
308
- "'": "&#39;"
309
- };
310
- function escapeHtml(value) {
311
- return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
312
- }
313
- function escapeMarkdown(text) {
314
- const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
315
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
316
- }
317
- function escapeTemplateText(text) {
318
- const escaped = escapeHtml(text);
319
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
320
- }
321
- function isExternalUrl(url) {
322
- return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
323
- }
324
- function toMarkdownUrl(url) {
325
- return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
326
- }
327
- function toPageLink(text, routePath) {
328
- return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
329
- }
330
- function toUrlPath(routePath, cleanUrls) {
331
- return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
332
- }
333
- function joinUrl(base, urlPath) {
334
- return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
335
- }
336
- function toIsoDate(date) {
337
- return date.toISOString().slice(0, 10);
338
- }
339
- function formatDate(date, locale, format) {
340
- return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(date);
341
- }
342
-
343
- // src/feed.ts
344
507
  function toAbsoluteUrl(urlPath, site) {
345
508
  return joinUrl(site.siteUrl, joinUrl(site.base, urlPath));
346
509
  }
@@ -436,37 +599,20 @@ var import_node_path6 = __toESM(require("path"), 1);
436
599
 
437
600
  // src/render.ts
438
601
  var import_node_path3 = __toESM(require("path"), 1);
439
-
440
- // src/types.ts
441
- var DEFAULT_LABELS = {
442
- by: "By",
443
- updated: "Updated",
444
- readingTime: "{n} min read",
445
- newer: "Newer post",
446
- older: "Older post",
447
- allPosts: "All posts in {blog}",
448
- draft: "Draft",
449
- sticky: "Pinned",
450
- noPosts: "No posts yet.",
451
- undated: "Undated",
452
- tags: "Keywords",
453
- tagged: "Posts tagged {tag}",
454
- showAll: "Show all posts",
455
- noTagged: "No posts tagged {tag}.",
456
- previous: "Previous",
457
- next: "Next",
458
- pages: "Pages",
459
- subscribe: "Subscribe",
460
- rssFeed: "RSS feed for {blog}"
461
- };
462
-
463
- // src/render.ts
464
602
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
465
603
  function createRenderContext(options, pageDirectory, feeds = !!options.siteUrl) {
604
+ const locale = options.locale ?? "en-US";
605
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
466
606
  return {
467
- locale: options.locale ?? "en-US",
468
- dateFormat: options.dateFormat ?? { dateStyle: "long" },
469
- labels: { ...DEFAULT_LABELS, ...options.labels },
607
+ locale,
608
+ dates: {
609
+ locale,
610
+ timeZone: resolveTimeZone(options.timeZone),
611
+ dateFormat: options.dateFormat ?? { dateStyle: "long" },
612
+ timeFormat: options.timeFormat ?? { hour: "numeric", minute: "2-digit", timeZoneName: "short" },
613
+ dateTime: labels.dateTime
614
+ },
615
+ labels,
470
616
  pageDirectory,
471
617
  feeds
472
618
  };
@@ -488,8 +634,10 @@ function toImageSource(post, pageDirectory) {
488
634
  const relative = import_node_path3.default.relative(pageDirectory, absolute).split(import_node_path3.default.sep).join("/");
489
635
  return relative.startsWith(".") ? relative : `./${relative}`;
490
636
  }
491
- function renderTime(date, context) {
492
- return `<time datetime="${toIsoDate(date)}">${escapeTemplateText(formatDate(date, context.locale, context.dateFormat))}</time>`;
637
+ function renderTime(date, hasTime, context) {
638
+ const value = { date, hasTime };
639
+ const attribute = toDateTimeAttribute(value, context.dates.timeZone);
640
+ return `<time datetime="${attribute}">${escapeTemplateText(formatPostDate(value, context.dates))}</time>`;
493
641
  }
494
642
  function renderAuthor(author) {
495
643
  return author.url ? `[${escapeMarkdown(author.name)}](${toMarkdownUrl(author.url)})` : escapeMarkdown(author.name);
@@ -517,17 +665,30 @@ function renderTags(tags, blogRoutePath) {
517
665
  function toTagsAttribute(post) {
518
666
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
519
667
  }
668
+ function toItemAttributes(post) {
669
+ return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
670
+ }
671
+ function renderBadges(post, context) {
672
+ const { draft, sticky, announcement } = post.post;
673
+ return [
674
+ draft && renderBadge("draft", context),
675
+ sticky && renderBadge("sticky", context),
676
+ announcement && renderBadge("announcement", context)
677
+ ].filter(Boolean).join(" ");
678
+ }
520
679
  function renderMeta(post, context, include) {
521
- const { date, updated, readingMinutes, authors, draft, sticky } = post.post;
680
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
522
681
  const parts = [];
523
682
  if (include.badges) {
524
- const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
525
- const shown = badges.filter(Boolean).join(" ");
683
+ const shown = renderBadges(post, context);
526
684
  if (shown) parts.push(shown);
527
685
  }
528
- if (date) parts.push(renderTime(date, context));
686
+ if (date) {
687
+ const time = renderTime(date, dateHasTime, context);
688
+ parts.push(include.updated ? `${escapeMarkdown(context.labels.published)} ${time}` : time);
689
+ }
529
690
  if (include.updated && updated && (!date || updated.getTime() !== date.getTime())) {
530
- parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, context)}`);
691
+ parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, updatedHasTime, context)}`);
531
692
  }
532
693
  if (readingMinutes) parts.push(escapeMarkdown(fill(context.labels.readingTime, { n: String(readingMinutes) })));
533
694
  if (include.authors) {
@@ -548,12 +709,13 @@ function renderCard(post, context, index) {
548
709
  const modifiers = [
549
710
  post.post.sticky && "sticky",
550
711
  post.post.draft && "draft",
712
+ post.post.announcement && "announcement",
551
713
  image && "with-image",
552
714
  image && post.post.imageFit === "contain" && "contain"
553
715
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
554
716
  const alt = escapeMarkdown(post.post.imageAlt);
555
717
  return [
556
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
718
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
557
719
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
558
720
  '<div class="vp-blog-card__body">',
559
721
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
@@ -565,9 +727,9 @@ function renderCard(post, context, index) {
565
727
  ];
566
728
  }
567
729
  function renderCompactItem(post, context) {
568
- const date = post.post.date ? `${renderTime(post.post.date, context)} ` : "";
569
- const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
570
- const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
730
+ const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
731
+ const badges = renderBadges(post, context);
732
+ const marker = `<span hidden ${toItemAttributes(post)}></span>`;
571
733
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
572
734
  }
573
735
  function renderGroup(posts, index, context) {
@@ -590,7 +752,7 @@ function renderGroup(posts, index, context) {
590
752
  function renderYears(posts, index, context) {
591
753
  const years = /* @__PURE__ */ new Map();
592
754
  for (const post of posts) {
593
- const year = post.post.date ? String(post.post.date.getUTCFullYear()) : context.labels.undated;
755
+ const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
594
756
  years.set(year, [...years.get(year) ?? [], post]);
595
757
  }
596
758
  const lines = [];
@@ -642,7 +804,9 @@ function renderClientAttributes(index, context) {
642
804
  "data-label-no-tagged": labels.noTagged,
643
805
  "data-label-previous": labels.previous,
644
806
  "data-label-next": labels.next,
645
- "data-label-pages": labels.pages
807
+ "data-label-pages": labels.pages,
808
+ "data-label-announcements": labels.announcements,
809
+ "data-label-no-announcements": labels.noAnnouncements
646
810
  };
647
811
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
648
812
  }
@@ -765,7 +929,8 @@ function toOptionsKey(options) {
765
929
  options.stripTags,
766
930
  options.pageSize,
767
931
  options.stickyLimit,
768
- options.rssLink
932
+ options.rssLink,
933
+ options.timeZone
769
934
  ]);
770
935
  }
771
936
  function indexBlogSource(sourceDirectory, options = {}, rewrites = {}) {
@@ -1028,6 +1193,9 @@ function blogMarkdown(md, options = {}) {
1028
1193
 
1029
1194
  // src/vitepress.ts
1030
1195
  var DEFAULT_FEED_LIMIT = 20;
1196
+ var PACKAGE_NAME = "@binarynoir/vitepress-blog";
1197
+ var SITE_DATA_MODULE = "/@siteData";
1198
+ var SCRIPT_MARKER = "data-vp-blog-announcement";
1031
1199
  function resolveSiteLocation(siteConfig, options) {
1032
1200
  const siteUrl = options.siteUrl ?? siteConfig.userConfig?.sitemap?.hostname ?? siteConfig.sitemap?.hostname;
1033
1201
  if (!siteUrl) return null;
@@ -1042,6 +1210,16 @@ function resolveSiteLocation(siteConfig, options) {
1042
1210
  function loadPages(siteConfig, options) {
1043
1211
  return loadBlogPages(siteConfig.srcDir, options, siteConfig);
1044
1212
  }
1213
+ function attachAnnouncements(siteConfig, options = {}) {
1214
+ const site = siteConfig.site;
1215
+ const data = resolveAnnouncements(loadPages(siteConfig, options), options, !!siteConfig.cleanUrls);
1216
+ const changed = JSON.stringify(site[ANNOUNCEMENTS_KEY] ?? null) !== JSON.stringify(data);
1217
+ if (data) site[ANNOUNCEMENTS_KEY] = data;
1218
+ else delete site[ANNOUNCEMENTS_KEY];
1219
+ site.head = (site.head ?? []).filter(([, attributes]) => !(SCRIPT_MARKER in (attributes ?? {})));
1220
+ if (data) site.head.push(["script", { [SCRIPT_MARKER]: "" }, renderDismissedScript(data)]);
1221
+ return changed;
1222
+ }
1045
1223
  function listFeedPosts(pages, index, options) {
1046
1224
  const limit = index.index.feed?.limit ?? options.feedLimit ?? DEFAULT_FEED_LIMIT;
1047
1225
  return listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).slice(0, limit);
@@ -1146,11 +1324,26 @@ function blogVitePlugin(options = {}) {
1146
1324
  let siteConfig;
1147
1325
  const currentPages = () => siteConfig ? loadPages(siteConfig, options) : [];
1148
1326
  const dependentFiles = () => [...currentPages().map((page) => page.absolutePath), ...widgetPages];
1327
+ const invalidate = (server, files) => {
1328
+ for (const file of files) {
1329
+ for (const module2 of server.moduleGraph.getModulesByFile(file) ?? []) server.moduleGraph.invalidateModule(module2);
1330
+ }
1331
+ };
1332
+ const refreshAnnouncements = (server) => {
1333
+ if (!siteConfig || !attachAnnouncements(siteConfig, options)) return false;
1334
+ const module2 = server.moduleGraph.getModuleById(SITE_DATA_MODULE);
1335
+ if (module2) server.moduleGraph.invalidateModule(module2);
1336
+ return true;
1337
+ };
1149
1338
  return {
1150
1339
  name: "vitepress-blog",
1151
1340
  enforce: "pre",
1341
+ config() {
1342
+ return { ssr: { noExternal: [PACKAGE_NAME] } };
1343
+ },
1152
1344
  configResolved(config) {
1153
1345
  siteConfig = config.vitepress;
1346
+ if (siteConfig) attachAnnouncements(siteConfig, options);
1154
1347
  },
1155
1348
  transform(code, id) {
1156
1349
  if (!siteConfig || !id.endsWith(".md") || !(mayHaveBlogFrontmatter(code) || hasWidget(code))) return null;
@@ -1161,11 +1354,8 @@ function blogVitePlugin(options = {}) {
1161
1354
  configureServer(server) {
1162
1355
  const refresh = (file) => {
1163
1356
  if (!siteConfig || !file.endsWith(".md")) return;
1164
- for (const dependent of dependentFiles()) {
1165
- for (const module2 of server.moduleGraph.getModulesByFile(dependent) ?? []) {
1166
- server.moduleGraph.invalidateModule(module2);
1167
- }
1168
- }
1357
+ invalidate(server, dependentFiles());
1358
+ refreshAnnouncements(server);
1169
1359
  server.ws.send({ type: "full-reload" });
1170
1360
  };
1171
1361
  server.watcher.on("add", refresh).on("unlink", refresh);
@@ -1196,6 +1386,11 @@ function blogVitePlugin(options = {}) {
1196
1386
  },
1197
1387
  handleHotUpdate({ file, modules, server }) {
1198
1388
  if (!siteConfig || !file.endsWith(".md")) return;
1389
+ if (refreshAnnouncements(server)) {
1390
+ invalidate(server, dependentFiles());
1391
+ server.ws.send({ type: "full-reload" });
1392
+ return [];
1393
+ }
1199
1394
  const pages = currentPages();
1200
1395
  if (!pages.some((page) => page.absolutePath === file)) return;
1201
1396
  const affected = dependentFiles().filter((dependent) => dependent !== file).flatMap((dependent) => [...server.moduleGraph.getModulesByFile(dependent) ?? []]);
@@ -1232,6 +1427,7 @@ function withBlog(config, options = {}) {
1232
1427
  }
1233
1428
  // Annotate the CommonJS export names for ESM import in node:
1234
1429
  0 && (module.exports = {
1430
+ attachAnnouncements,
1235
1431
  blogHead,
1236
1432
  blogMarkdown,
1237
1433
  blogPageData,