@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,3 +1,14 @@
1
+ import {
2
+ escapeHtml,
3
+ escapeMarkdown,
4
+ escapeTemplateText,
5
+ isExternalUrl,
6
+ joinUrl,
7
+ toMarkdownUrl,
8
+ toPageLink,
9
+ toUrlPath
10
+ } from "./chunk-42Z6H7IN.js";
11
+
1
12
  // src/collect.ts
2
13
  function isPost(page) {
3
14
  return page.post !== null;
@@ -19,6 +30,11 @@ function hasAnyTag(post, tags) {
19
30
  const wanted = new Set(tags.map((tag) => tag.toLowerCase()));
20
31
  return post.post.tags.some((tag) => wanted.has(tag.toLowerCase()));
21
32
  }
33
+ function inAnyCategory(post, categories) {
34
+ if (categories.length === 0) return true;
35
+ const category = post.post.category.toLowerCase();
36
+ return !!category && categories.some((wanted) => wanted.toLowerCase() === category);
37
+ }
22
38
  function blogCoversPost(index, post, sections) {
23
39
  if (index.index.scope === "section") return isInFolders(post.relativePath, index.index.folders);
24
40
  if (!isInFolders(post.relativePath, index.index.folders)) return false;
@@ -32,22 +48,22 @@ function compareDates(a, b) {
32
48
  function compareByDate(a, b) {
33
49
  return compareDates(a.post.date, b.post.date) || a.post.title.localeCompare(b.post.title);
34
50
  }
35
- function pinStickyPosts(posts, limit) {
36
- const sticky = posts.filter((post) => post.post.sticky);
37
- const pinned = new Set(limit ? sticky.slice(0, limit) : sticky);
51
+ function pinPosts(posts, limit) {
52
+ const candidates = posts.filter((post) => post.post.pinned);
53
+ const pinned = new Set(limit ? candidates.slice(0, limit) : candidates);
38
54
  return { pinned: [...pinned], rest: posts.filter((post) => !pinned.has(post)) };
39
55
  }
40
56
  function listBlogPosts(pages, index, options) {
41
57
  const sections = pages.filter(isIndex).filter((page) => page.index.scope === "section");
42
- let posts = pages.filter(isPost).filter((post) => options.drafts || !post.post.draft).filter((post) => blogCoversPost(index, post, sections) && hasAnyTag(post, index.index.tags)).sort(compareByDate);
43
- if (options.stickyFirst) {
44
- const { pinned, rest } = pinStickyPosts(posts, index.index.stickyLimit);
58
+ let posts = pages.filter(isPost).filter((post) => options.drafts || !post.post.draft).filter((post) => blogCoversPost(index, post, sections) && hasAnyTag(post, index.index.tags)).filter((post) => inAnyCategory(post, index.index.categories)).sort(compareByDate);
59
+ if (options.pinnedFirst) {
60
+ const { pinned, rest } = pinPosts(posts, index.index.pinLimit);
45
61
  posts = [...pinned, ...rest];
46
62
  }
47
63
  return options.applyLimit && index.index.limit ? posts.slice(0, index.index.limit) : posts;
48
64
  }
49
65
  function findHomeBlog(pages, post, drafts) {
50
- const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false }).includes(post));
66
+ const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, pinnedFirst: false, applyLimit: false }).includes(post));
51
67
  const sections = listing.filter((index) => index.index.scope === "section");
52
68
  if (sections.length) return deepestFirst(sections, post.relativePath)[0];
53
69
  return listing[0] ?? null;
@@ -66,43 +82,19 @@ function findBlogForPage(pages, relativePath) {
66
82
  }
67
83
  function findBlogsListing(pages, post) {
68
84
  return pages.filter(isIndex).filter(
69
- (index) => listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).includes(post)
85
+ (index) => listBlogPosts(pages, index, { drafts: false, pinnedFirst: false, applyLimit: false }).includes(post)
70
86
  );
71
87
  }
72
-
73
- // src/util.ts
74
- var HTML_ESCAPES = {
75
- "&": "&",
76
- "<": "&lt;",
77
- ">": "&gt;",
78
- '"': "&quot;",
79
- "'": "&#39;"
80
- };
81
- function escapeHtml(value) {
82
- return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
83
- }
84
- function escapeMarkdown(text) {
85
- const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
86
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
87
- }
88
- function escapeTemplateText(text) {
89
- const escaped = escapeHtml(text);
90
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
91
- }
92
- function isExternalUrl(url) {
93
- return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
94
- }
95
- function toMarkdownUrl(url) {
96
- return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
97
- }
98
- function toPageLink(text, routePath) {
99
- return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
88
+ function listAnnouncements(pages, drafts) {
89
+ return pages.filter(isPost).filter((post) => post.post.announcement && (drafts || !post.post.draft)).sort(compareByDate);
100
90
  }
101
- function toUrlPath(routePath, cleanUrls) {
102
- return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
103
- }
104
- function joinUrl(base, urlPath) {
105
- return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
91
+ function findAnnouncementBlog(pages, announcements, drafts) {
92
+ if (announcements.length === 0) return null;
93
+ const listingAll = pages.filter(isIndex).filter((index) => {
94
+ const listed = listBlogPosts(pages, index, { drafts, pinnedFirst: false, applyLimit: false });
95
+ return announcements.every((post) => listed.includes(post));
96
+ });
97
+ return listingAll.find((index) => index.index.scope === "site") ?? listingAll[0] ?? findHomeBlog(pages, announcements[0], drafts);
106
98
  }
107
99
 
108
100
  // src/feed.ts
@@ -210,13 +202,24 @@ var DEFAULT_LABELS = {
210
202
  older: "Older post",
211
203
  allPosts: "All posts in {blog}",
212
204
  draft: "Draft",
213
- sticky: "Pinned",
205
+ pinned: "Pinned",
206
+ announcement: "Announcement",
207
+ announcements: "Announcements",
208
+ announcementCount: "{n} announcements",
209
+ dismiss: "Dismiss",
214
210
  noPosts: "No posts yet.",
215
211
  undated: "Undated",
212
+ browse: "Browse posts",
213
+ categories: "Categories",
216
214
  tags: "Keywords",
217
- tagged: "Posts tagged {tag}",
215
+ archive: "Archive",
216
+ category: "Category",
217
+ tag: "Keyword",
218
+ postCount: "{n} posts",
219
+ postCountOne: "{n} post",
220
+ noMatches: "No posts match these filters.",
221
+ removeFilter: "Remove filter: {filter}",
218
222
  showAll: "Show all posts",
219
- noTagged: "No posts tagged {tag}.",
220
223
  previous: "Previous",
221
224
  next: "Next",
222
225
  pages: "Pages",
@@ -322,6 +325,13 @@ function toDateTimeAttribute({ date, hasTime }, timeZone) {
322
325
  function yearIn(date, timeZone) {
323
326
  return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
324
327
  }
328
+ function monthIn(date, timeZone) {
329
+ return toZonedIsoDate(date, timeZone).slice(0, 7);
330
+ }
331
+ function formatMonth(month, locale, format) {
332
+ const [year, number] = month.split("-").map(Number);
333
+ return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(Date.UTC(year, number - 1, 1, 12));
334
+ }
325
335
 
326
336
  // src/render.ts
327
337
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
@@ -377,25 +387,41 @@ function renderAuthors(authors, context) {
377
387
  function renderBadge(kind, context) {
378
388
  return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
379
389
  }
380
- function toTagUrl(blogRoutePath, tag) {
381
- const value = encodeURIComponent(tag.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
390
+ function toFilterUrl(blogRoutePath, param, value) {
391
+ const encoded = encodeURIComponent(value.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
382
392
  const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
383
- return `${toMarkdownUrl(`/${page}`)}?tag=${value}`;
393
+ return `${toMarkdownUrl(`/${page}`)}?${param}=${encoded}`;
394
+ }
395
+ function renderFilterLink(text, param, blogRoutePath, className) {
396
+ return blogRoutePath ? `[${escapeMarkdown(text)}](${toFilterUrl(blogRoutePath, param, text)})` : `<span class="${className}">${escapeTemplateText(text)}</span>`;
384
397
  }
385
398
  function renderTags(tags, blogRoutePath) {
386
- return tags.map(
387
- (tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
388
- ).join(" ");
399
+ return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
389
400
  }
390
401
  function toTagsAttribute(post) {
391
402
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
392
403
  }
404
+ function toItemAttributes(post, context) {
405
+ const { category, date, announcement } = post.post;
406
+ const attributes = [`data-vp-blog-item data-tags="${toTagsAttribute(post)}"`];
407
+ if (category) attributes.push(`data-category="${escapeHtml(category.toLowerCase())}"`);
408
+ if (date) attributes.push(`data-month="${monthIn(date, context.dates.timeZone)}"`);
409
+ if (announcement) attributes.push("data-announcement");
410
+ return attributes.join(" ");
411
+ }
412
+ function renderBadges(post, context) {
413
+ const { draft, pinned, announcement } = post.post;
414
+ return [
415
+ draft && renderBadge("draft", context),
416
+ pinned && renderBadge("pinned", context),
417
+ announcement && renderBadge("announcement", context)
418
+ ].filter(Boolean).join(" ");
419
+ }
393
420
  function renderMeta(post, context, include) {
394
- const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors, draft, sticky } = post.post;
421
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
395
422
  const parts = [];
396
423
  if (include.badges) {
397
- const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
398
- const shown = badges.filter(Boolean).join(" ");
424
+ const shown = renderBadges(post, context);
399
425
  if (shown) parts.push(shown);
400
426
  }
401
427
  if (date) {
@@ -422,16 +448,18 @@ function renderCard(post, context, index) {
422
448
  const link = `/${post.routePath}`;
423
449
  const image = toImageSource(post, context.pageDirectory);
424
450
  const modifiers = [
425
- post.post.sticky && "sticky",
451
+ post.post.pinned && "pinned",
426
452
  post.post.draft && "draft",
453
+ post.post.announcement && "announcement",
427
454
  image && "with-image",
428
455
  image && post.post.imageFit === "contain" && "contain"
429
456
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
430
457
  const alt = escapeMarkdown(post.post.imageAlt);
431
458
  return [
432
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
459
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
433
460
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
434
461
  '<div class="vp-blog-card__body">',
462
+ ...index.index.showCategory && post.post.category ? block("vp-blog-card__category", renderFilterLink(post.post.category, "category", index.routePath, "")) : [],
435
463
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
436
464
  ...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
437
465
  ...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
@@ -442,8 +470,8 @@ function renderCard(post, context, index) {
442
470
  }
443
471
  function renderCompactItem(post, context) {
444
472
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
445
- const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
446
- const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
473
+ const badges = renderBadges(post, context);
474
+ const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
447
475
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
448
476
  }
449
477
  function renderGroup(posts, index, context) {
@@ -463,18 +491,23 @@ function renderGroup(posts, index, context) {
463
491
  "</div>"
464
492
  ];
465
493
  }
466
- function renderYears(posts, index, context) {
467
- const years = /* @__PURE__ */ new Map();
494
+ function renderGroups(posts, index, context) {
495
+ const { groupBy } = index.index;
496
+ if (!groupBy) return renderGroup(posts, index, context);
497
+ const { timeZone, locale } = context.dates;
498
+ const groups = /* @__PURE__ */ new Map();
468
499
  for (const post of posts) {
469
- const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
470
- years.set(year, [...years.get(year) ?? [], post]);
500
+ const { date } = post.post;
501
+ const key = !date ? "" : groupBy === "year" ? String(yearIn(date, timeZone)) : monthIn(date, timeZone);
502
+ groups.set(key, [...groups.get(key) ?? [], post]);
471
503
  }
472
504
  const lines = [];
473
- for (const [year, group] of years) {
505
+ for (const [key, group] of groups) {
506
+ const heading = !key ? context.labels.undated : groupBy === "year" ? key : formatMonth(key, locale, { month: "long", year: "numeric" });
474
507
  lines.push(
475
- '<section class="vp-blog-year">',
508
+ `<section class="vp-blog-group vp-blog-${groupBy}">`,
476
509
  "",
477
- `## ${escapeMarkdown(year)}`,
510
+ `## ${escapeMarkdown(heading)}`,
478
511
  "",
479
512
  ...renderGroup(group, index, context),
480
513
  "",
@@ -483,42 +516,114 @@ function renderYears(posts, index, context) {
483
516
  }
484
517
  return lines;
485
518
  }
486
- function collectTags(posts) {
487
- const tags = /* @__PURE__ */ new Map();
519
+ function collectValues(posts, pick) {
520
+ const values = /* @__PURE__ */ new Map();
488
521
  for (const post of posts) {
489
- for (const tag of new Set(post.post.tags)) {
490
- const entry = tags.get(tag.toLowerCase()) ?? { tag, count: 0 };
522
+ for (const value of new Set(pick(post))) {
523
+ const entry = values.get(value.toLowerCase()) ?? { value, count: 0 };
491
524
  entry.count++;
492
- tags.set(tag.toLowerCase(), entry);
525
+ values.set(value.toLowerCase(), entry);
493
526
  }
494
527
  }
495
- return [...tags.values()].sort((a, b) => a.tag.localeCompare(b.tag, void 0, { sensitivity: "base" }));
528
+ return [...values.values()].sort((a, b) => a.value.localeCompare(b.value, void 0, { sensitivity: "base" }));
496
529
  }
497
- function renderTagList(posts, index, context) {
498
- const tags = collectTags(posts);
499
- if (!index.index.tagList || tags.length === 0) return [];
500
- const links = tags.map(
501
- ({ tag, count }) => `[${escapeMarkdown(tag)} <span class="vp-blog-tags__count">${count}</span>](${toTagUrl(index.routePath, tag)})`
502
- );
503
- const label = `<span class="vp-blog-tags__label">${escapeTemplateText(context.labels.tags)}</span>`;
530
+ function collectTags(posts) {
531
+ return collectValues(posts, (post) => post.post.tags).map(({ value, count }) => ({ tag: value, count }));
532
+ }
533
+ function collectCategories(posts) {
534
+ return collectValues(posts, (post) => post.post.category ? [post.post.category] : []);
535
+ }
536
+ function collectArchive(posts, timeZone) {
537
+ const years = /* @__PURE__ */ new Map();
538
+ for (const post of posts) {
539
+ if (!post.post.date) continue;
540
+ const month = monthIn(post.post.date, timeZone);
541
+ const year = years.get(month.slice(0, 4)) ?? { value: month.slice(0, 4), count: 0, months: [] };
542
+ year.count++;
543
+ const entry = year.months.find((candidate) => candidate.value === month);
544
+ if (entry) entry.count++;
545
+ else year.months.push({ value: month, count: 1 });
546
+ years.set(year.value, year);
547
+ }
548
+ const newestFirst = (a, b) => b.value.localeCompare(a.value);
549
+ return [...years.values()].sort(newestFirst).map((year) => ({ ...year, months: [...year.months].sort(newestFirst) }));
550
+ }
551
+ function renderChip(name, count, url, title = "") {
552
+ const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
553
+ return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
554
+ }
555
+ function renderFacet(facet, label, chips, ariaLabel = label, attributes = "") {
504
556
  return [
505
- `<nav class="vp-blog-tags" aria-label="${escapeHtml(context.labels.tags)}">`,
557
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${attributes}>`,
558
+ `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
559
+ '<div class="vp-blog-facet__chips">',
506
560
  "",
507
- `${label} ${links.join(" ")}`,
561
+ chips.join(" "),
508
562
  "",
509
- "</nav>"
563
+ "</div>",
564
+ "</div>"
510
565
  ];
511
566
  }
567
+ function renderBrowse(posts, index, context) {
568
+ const { labels } = context;
569
+ const { locale, timeZone } = context.dates;
570
+ const blog = index.routePath;
571
+ const rows = [];
572
+ const categories = index.index.categoryList ? collectCategories(posts) : [];
573
+ if (categories.length) {
574
+ const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
575
+ rows.push(...renderFacet("category", labels.categories, chips));
576
+ }
577
+ const tags = index.index.tagList ? collectTags(posts) : [];
578
+ if (tags.length) {
579
+ const chips = tags.map(({ tag, count }) => renderChip(tag, count, toFilterUrl(blog, "tag", tag)));
580
+ rows.push(...renderFacet("tag", labels.tags, chips));
581
+ }
582
+ const archive = index.index.archive ? collectArchive(posts, timeZone) : [];
583
+ if (archive.reduce((months, year) => months + year.months.length, 0) > 1) {
584
+ const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
585
+ rows.push(...renderFacet("year", labels.archive, years));
586
+ for (const year of archive) {
587
+ const months = year.months.map(
588
+ ({ value, count }) => renderChip(
589
+ formatMonth(value, locale, { month: "short" }),
590
+ count,
591
+ toFilterUrl(blog, "month", value),
592
+ formatMonth(value, locale, { month: "long", year: "numeric" })
593
+ )
594
+ );
595
+ const shown = archive.length === 1 ? "" : " hidden";
596
+ rows.push(
597
+ ...renderFacet(
598
+ "month",
599
+ year.value,
600
+ months,
601
+ `${labels.archive} ${year.value}`,
602
+ ` data-year="${year.value}"${shown}`
603
+ )
604
+ );
605
+ }
606
+ }
607
+ if (rows.length === 0) return [];
608
+ return [`<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`, ...rows, "</nav>"];
609
+ }
512
610
  function renderClientAttributes(index, context) {
513
611
  const { labels } = context;
514
612
  const attributes = {
515
613
  "data-page-size": String(index.index.pageSize),
516
- "data-label-tagged": labels.tagged,
614
+ // For writing a `?month=` filter's name: "October 2026".
615
+ "data-locale": context.locale,
517
616
  "data-label-show-all": labels.showAll,
518
- "data-label-no-tagged": labels.noTagged,
519
617
  "data-label-previous": labels.previous,
520
618
  "data-label-next": labels.next,
521
- "data-label-pages": labels.pages
619
+ "data-label-pages": labels.pages,
620
+ "data-label-announcements": labels.announcements,
621
+ "data-label-category": labels.category,
622
+ "data-label-tag": labels.tag,
623
+ "data-label-post-count": labels.postCount,
624
+ "data-label-post-count-one": labels.postCountOne,
625
+ "data-label-no-matches": labels.noMatches,
626
+ "data-label-remove-filter": labels.removeFilter
522
627
  };
523
628
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
524
629
  }
@@ -529,17 +634,16 @@ function renderPostList(pinned, rest, index, context) {
529
634
  const lines = [
530
635
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
531
636
  ...block("vp-blog-subscribe", renderRssLink(index, context)),
532
- ...renderTagList(all, index, context),
533
- '<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
637
+ ...renderBrowse(all, index, context),
638
+ '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
534
639
  ];
535
640
  if (pinned.length)
536
641
  lines.push(
537
- '<section class="vp-blog-sticky" data-vp-blog-sticky>',
642
+ '<section class="vp-blog-pinned" data-vp-blog-pinned>',
538
643
  ...renderGroup(pinned, index, context),
539
644
  "</section>"
540
645
  );
541
- if (rest.length)
542
- lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
646
+ if (rest.length) lines.push(...renderGroups(rest, index, context));
543
647
  lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
544
648
  return `${lines.join("\n")}
545
649
  `;
@@ -548,8 +652,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
548
652
  const lines = [];
549
653
  if (show.byline) {
550
654
  const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
551
- const tags = post.post.tags.length ? `<span class="vp-blog-byline__tags">${renderTags(post.post.tags, blogRoutePath)}</span>` : "";
552
- lines.push(...block("vp-blog-byline", [meta, tags].filter(Boolean).join("\n\n")));
655
+ const { category, tags } = post.post;
656
+ const chips = [
657
+ category && `<span class="vp-blog-byline__category">${renderFilterLink(category, "category", blogRoutePath, "vp-blog-category")}</span>`,
658
+ tags.length && `<span class="vp-blog-byline__tags">${renderTags(tags, blogRoutePath)}</span>`
659
+ ].filter(Boolean).join(" ");
660
+ lines.push(...block("vp-blog-byline", [meta, chips].filter(Boolean).join("\n\n")));
553
661
  }
554
662
  if (show.cover && post.post.image) {
555
663
  const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
@@ -684,6 +792,7 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
684
792
  const timeZone = resolveTimeZone(options.timeZone);
685
793
  const date = parsePostDate(pick("date"), timeZone);
686
794
  const updated = parsePostDate(pick("updated"), timeZone);
795
+ const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
687
796
  return {
688
797
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
689
798
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
@@ -699,8 +808,11 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
699
808
  tags: toStringList(pick("tags")),
700
809
  category: toText(pick("category")),
701
810
  scope: toScope(post.scope),
702
- sticky: post.sticky === true,
811
+ // `sticky` is this setting's old name.
812
+ pinned: (post.pinned ?? post.sticky) === true,
703
813
  draft: post.draft === true,
814
+ announcement: post.announcement === true || !!announcementText,
815
+ announcementText,
704
816
  byline: toBoolean(post.byline),
705
817
  cover: toBoolean(post.cover),
706
818
  postNav: toBoolean(post.postNav),
@@ -710,6 +822,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
710
822
  function toLayout(value) {
711
823
  return value === "grid" || value === "compact" ? value : "list";
712
824
  }
825
+ function toGroupBy(value) {
826
+ return value === "year" || value === "month" ? value : "";
827
+ }
713
828
  function defaultFeedPath(relativePath) {
714
829
  const name = path3.posix.basename(relativePath, ".md");
715
830
  return name === "index" ? "feed.xml" : `${name}-feed.xml`;
@@ -743,13 +858,17 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
743
858
  scope,
744
859
  folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
745
860
  tags: toStringList(options.tags),
861
+ categories: toStringList(options.categories),
746
862
  limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
747
863
  layout: toLayout(options.layout),
748
- groupByYear: options.groupBy === "year",
864
+ groupBy: toGroupBy(options.groupBy),
749
865
  showTags: options.showTags !== false,
866
+ showCategory: options.showCategory !== false,
867
+ categoryList: options.categoryList !== false,
750
868
  tagList: options.tagList !== false,
869
+ archive: options.archive !== false,
751
870
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
752
- stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
871
+ pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
753
872
  rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
754
873
  feed: toFeed(options.feed, relativePath)
755
874
  };
@@ -819,7 +938,7 @@ function toOptionsKey(options) {
819
938
  options.wordsPerMinute,
820
939
  options.stripTags,
821
940
  options.pageSize,
822
- options.stickyLimit,
941
+ options.pinLimit ?? options.stickyLimit,
823
942
  options.rssLink,
824
943
  options.timeZone
825
944
  ]);
@@ -908,10 +1027,13 @@ function resolveWidgetConfig(body) {
908
1027
  config: {
909
1028
  blog: typeof data.blog === "string" ? data.blog.trim() : "",
910
1029
  limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
911
- stickyLimit: toCount2(data.stickyLimit),
1030
+ // `stickyLimit` is this setting's old name.
1031
+ pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
912
1032
  layout,
913
1033
  showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
1034
+ showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
914
1035
  tags: toStringList(data.tags),
1036
+ categories: toStringList(data.categories),
915
1037
  more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
916
1038
  rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
917
1039
  },
@@ -927,19 +1049,19 @@ function renderWidget(body, environment) {
927
1049
  warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
928
1050
  return "";
929
1051
  }
930
- const stickyLimit = config.stickyLimit ?? blog.index.stickyLimit;
931
- const wanted = new Set(config.tags.map((tag) => tag.toLowerCase()));
932
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false }).filter(
933
- (post) => wanted.size === 0 || post.post.tags.some((tag) => wanted.has(tag.toLowerCase()))
1052
+ const pinLimit = config.pinLimit ?? blog.index.pinLimit;
1053
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false }).filter(
1054
+ (post) => hasAnyTag(post, config.tags) && inAnyCategory(post, config.categories)
934
1055
  );
935
- const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
1056
+ const { pinned, rest } = pinPosts(posts, pinLimit);
936
1057
  const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
937
1058
  const view = {
938
1059
  ...blog,
939
1060
  index: {
940
1061
  ...blog.index,
941
1062
  layout: config.layout ?? blog.index.layout,
942
- showTags: config.showTags ?? blog.index.showTags
1063
+ showTags: config.showTags ?? blog.index.showTags,
1064
+ showCategory: config.showCategory ?? blog.index.showCategory
943
1065
  }
944
1066
  };
945
1067
  const lines = [`<div class="vp-blog-widget">`];
@@ -1013,8 +1135,8 @@ function hasFeeds(options) {
1013
1135
  }
1014
1136
  function withPostList(source, page, pages, options) {
1015
1137
  if (!isIndex(page)) return source;
1016
- const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, stickyFirst: true, applyLimit: true });
1017
- const { pinned, rest } = pinStickyPosts(posts, page.index.stickyLimit);
1138
+ const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, pinnedFirst: true, applyLimit: true });
1139
+ const { pinned, rest } = pinPosts(posts, page.index.pinLimit);
1018
1140
  const context = createRenderContext(options, path6.dirname(page.absolutePath), hasFeeds(options));
1019
1141
  const generated = renderPostList(pinned, rest, page, context);
1020
1142
  return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
@@ -1037,7 +1159,7 @@ function withPostDecorations(source, page, pages, options) {
1037
1159
  let result = header ? insertAfterTitle(source, header) : source;
1038
1160
  if (page.post.postNav ?? options.postNav ?? true) {
1039
1161
  if (blog) {
1040
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
1162
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
1041
1163
  result = `${result.replace(/\s*$/, "")}
1042
1164
 
1043
1165
  ${renderPostNav(page, posts, blog, context)}
@@ -1088,7 +1210,8 @@ export {
1088
1210
  isIndex,
1089
1211
  listBlogPosts,
1090
1212
  findBlogsListing,
1091
- joinUrl,
1213
+ listAnnouncements,
1214
+ findAnnouncementBlog,
1092
1215
  toPageUrl,
1093
1216
  toFeedPath,
1094
1217
  resolvePostImage,
@@ -1105,4 +1228,4 @@ export {
1105
1228
  hasWidget,
1106
1229
  blogMarkdown
1107
1230
  };
1108
- //# sourceMappingURL=chunk-PWNWSYI4.js.map
1231
+ //# sourceMappingURL=chunk-ZM5FOUT4.js.map