@binarynoir/vitepress-blog 0.3.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.
@@ -30,6 +30,11 @@ function hasAnyTag(post, tags) {
30
30
  const wanted = new Set(tags.map((tag) => tag.toLowerCase()));
31
31
  return post.post.tags.some((tag) => wanted.has(tag.toLowerCase()));
32
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
+ }
33
38
  function blogCoversPost(index, post, sections) {
34
39
  if (index.index.scope === "section") return isInFolders(post.relativePath, index.index.folders);
35
40
  if (!isInFolders(post.relativePath, index.index.folders)) return false;
@@ -43,22 +48,22 @@ function compareDates(a, b) {
43
48
  function compareByDate(a, b) {
44
49
  return compareDates(a.post.date, b.post.date) || a.post.title.localeCompare(b.post.title);
45
50
  }
46
- function pinStickyPosts(posts, limit) {
47
- const sticky = posts.filter((post) => post.post.sticky);
48
- 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);
49
54
  return { pinned: [...pinned], rest: posts.filter((post) => !pinned.has(post)) };
50
55
  }
51
56
  function listBlogPosts(pages, index, options) {
52
57
  const sections = pages.filter(isIndex).filter((page) => page.index.scope === "section");
53
- 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);
54
- if (options.stickyFirst) {
55
- 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);
56
61
  posts = [...pinned, ...rest];
57
62
  }
58
63
  return options.applyLimit && index.index.limit ? posts.slice(0, index.index.limit) : posts;
59
64
  }
60
65
  function findHomeBlog(pages, post, drafts) {
61
- 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));
62
67
  const sections = listing.filter((index) => index.index.scope === "section");
63
68
  if (sections.length) return deepestFirst(sections, post.relativePath)[0];
64
69
  return listing[0] ?? null;
@@ -77,7 +82,7 @@ function findBlogForPage(pages, relativePath) {
77
82
  }
78
83
  function findBlogsListing(pages, post) {
79
84
  return pages.filter(isIndex).filter(
80
- (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)
81
86
  );
82
87
  }
83
88
  function listAnnouncements(pages, drafts) {
@@ -86,7 +91,7 @@ function listAnnouncements(pages, drafts) {
86
91
  function findAnnouncementBlog(pages, announcements, drafts) {
87
92
  if (announcements.length === 0) return null;
88
93
  const listingAll = pages.filter(isIndex).filter((index) => {
89
- const listed = listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false });
94
+ const listed = listBlogPosts(pages, index, { drafts, pinnedFirst: false, applyLimit: false });
90
95
  return announcements.every((post) => listed.includes(post));
91
96
  });
92
97
  return listingAll.find((index) => index.index.scope === "site") ?? listingAll[0] ?? findHomeBlog(pages, announcements[0], drafts);
@@ -197,18 +202,24 @@ var DEFAULT_LABELS = {
197
202
  older: "Older post",
198
203
  allPosts: "All posts in {blog}",
199
204
  draft: "Draft",
200
- sticky: "Pinned",
205
+ pinned: "Pinned",
201
206
  announcement: "Announcement",
202
207
  announcements: "Announcements",
203
- noAnnouncements: "No announcements.",
204
208
  announcementCount: "{n} announcements",
205
209
  dismiss: "Dismiss",
206
210
  noPosts: "No posts yet.",
207
211
  undated: "Undated",
212
+ browse: "Browse posts",
213
+ categories: "Categories",
208
214
  tags: "Keywords",
209
- 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}",
210
222
  showAll: "Show all posts",
211
- noTagged: "No posts tagged {tag}.",
212
223
  previous: "Previous",
213
224
  next: "Next",
214
225
  pages: "Pages",
@@ -314,6 +325,13 @@ function toDateTimeAttribute({ date, hasTime }, timeZone) {
314
325
  function yearIn(date, timeZone) {
315
326
  return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
316
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
+ }
317
335
 
318
336
  // src/render.ts
319
337
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
@@ -369,27 +387,33 @@ function renderAuthors(authors, context) {
369
387
  function renderBadge(kind, context) {
370
388
  return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
371
389
  }
372
- function toTagUrl(blogRoutePath, tag) {
373
- 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");
374
392
  const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
375
- 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>`;
376
397
  }
377
398
  function renderTags(tags, blogRoutePath) {
378
- return tags.map(
379
- (tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
380
- ).join(" ");
399
+ return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
381
400
  }
382
401
  function toTagsAttribute(post) {
383
402
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
384
403
  }
385
- function toItemAttributes(post) {
386
- return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
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(" ");
387
411
  }
388
412
  function renderBadges(post, context) {
389
- const { draft, sticky, announcement } = post.post;
413
+ const { draft, pinned, announcement } = post.post;
390
414
  return [
391
415
  draft && renderBadge("draft", context),
392
- sticky && renderBadge("sticky", context),
416
+ pinned && renderBadge("pinned", context),
393
417
  announcement && renderBadge("announcement", context)
394
418
  ].filter(Boolean).join(" ");
395
419
  }
@@ -424,7 +448,7 @@ function renderCard(post, context, index) {
424
448
  const link = `/${post.routePath}`;
425
449
  const image = toImageSource(post, context.pageDirectory);
426
450
  const modifiers = [
427
- post.post.sticky && "sticky",
451
+ post.post.pinned && "pinned",
428
452
  post.post.draft && "draft",
429
453
  post.post.announcement && "announcement",
430
454
  image && "with-image",
@@ -432,9 +456,10 @@ function renderCard(post, context, index) {
432
456
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
433
457
  const alt = escapeMarkdown(post.post.imageAlt);
434
458
  return [
435
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
459
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
436
460
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
437
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, "")) : [],
438
463
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
439
464
  ...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
440
465
  ...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
@@ -446,7 +471,7 @@ function renderCard(post, context, index) {
446
471
  function renderCompactItem(post, context) {
447
472
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
448
473
  const badges = renderBadges(post, context);
449
- const marker = `<span hidden ${toItemAttributes(post)}></span>`;
474
+ const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
450
475
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
451
476
  }
452
477
  function renderGroup(posts, index, context) {
@@ -466,18 +491,23 @@ function renderGroup(posts, index, context) {
466
491
  "</div>"
467
492
  ];
468
493
  }
469
- function renderYears(posts, index, context) {
470
- 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();
471
499
  for (const post of posts) {
472
- const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
473
- 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]);
474
503
  }
475
504
  const lines = [];
476
- 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" });
477
507
  lines.push(
478
- '<section class="vp-blog-year">',
508
+ `<section class="vp-blog-group vp-blog-${groupBy}">`,
479
509
  "",
480
- `## ${escapeMarkdown(year)}`,
510
+ `## ${escapeMarkdown(heading)}`,
481
511
  "",
482
512
  ...renderGroup(group, index, context),
483
513
  "",
@@ -486,44 +516,114 @@ function renderYears(posts, index, context) {
486
516
  }
487
517
  return lines;
488
518
  }
489
- function collectTags(posts) {
490
- const tags = /* @__PURE__ */ new Map();
519
+ function collectValues(posts, pick) {
520
+ const values = /* @__PURE__ */ new Map();
491
521
  for (const post of posts) {
492
- for (const tag of new Set(post.post.tags)) {
493
- 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 };
494
524
  entry.count++;
495
- tags.set(tag.toLowerCase(), entry);
525
+ values.set(value.toLowerCase(), entry);
496
526
  }
497
527
  }
498
- 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" }));
499
529
  }
500
- function renderTagList(posts, index, context) {
501
- const tags = collectTags(posts);
502
- if (!index.index.tagList || tags.length === 0) return [];
503
- const links = tags.map(
504
- ({ tag, count }) => `[${escapeMarkdown(tag)} <span class="vp-blog-tags__count">${count}</span>](${toTagUrl(index.routePath, tag)})`
505
- );
506
- 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 = "") {
507
556
  return [
508
- `<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">',
509
560
  "",
510
- `${label} ${links.join(" ")}`,
561
+ chips.join(" "),
511
562
  "",
512
- "</nav>"
563
+ "</div>",
564
+ "</div>"
513
565
  ];
514
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
+ }
515
610
  function renderClientAttributes(index, context) {
516
611
  const { labels } = context;
517
612
  const attributes = {
518
613
  "data-page-size": String(index.index.pageSize),
519
- "data-label-tagged": labels.tagged,
614
+ // For writing a `?month=` filter's name: "October 2026".
615
+ "data-locale": context.locale,
520
616
  "data-label-show-all": labels.showAll,
521
- "data-label-no-tagged": labels.noTagged,
522
617
  "data-label-previous": labels.previous,
523
618
  "data-label-next": labels.next,
524
619
  "data-label-pages": labels.pages,
525
620
  "data-label-announcements": labels.announcements,
526
- "data-label-no-announcements": labels.noAnnouncements
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
527
627
  };
528
628
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
529
629
  }
@@ -534,17 +634,16 @@ function renderPostList(pinned, rest, index, context) {
534
634
  const lines = [
535
635
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
536
636
  ...block("vp-blog-subscribe", renderRssLink(index, context)),
537
- ...renderTagList(all, index, context),
538
- '<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>'
539
639
  ];
540
640
  if (pinned.length)
541
641
  lines.push(
542
- '<section class="vp-blog-sticky" data-vp-blog-sticky>',
642
+ '<section class="vp-blog-pinned" data-vp-blog-pinned>',
543
643
  ...renderGroup(pinned, index, context),
544
644
  "</section>"
545
645
  );
546
- if (rest.length)
547
- lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
646
+ if (rest.length) lines.push(...renderGroups(rest, index, context));
548
647
  lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
549
648
  return `${lines.join("\n")}
550
649
  `;
@@ -553,8 +652,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
553
652
  const lines = [];
554
653
  if (show.byline) {
555
654
  const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
556
- const tags = post.post.tags.length ? `<span class="vp-blog-byline__tags">${renderTags(post.post.tags, blogRoutePath)}</span>` : "";
557
- 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")));
558
661
  }
559
662
  if (show.cover && post.post.image) {
560
663
  const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
@@ -705,7 +808,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
705
808
  tags: toStringList(pick("tags")),
706
809
  category: toText(pick("category")),
707
810
  scope: toScope(post.scope),
708
- sticky: post.sticky === true,
811
+ // `sticky` is this setting's old name.
812
+ pinned: (post.pinned ?? post.sticky) === true,
709
813
  draft: post.draft === true,
710
814
  announcement: post.announcement === true || !!announcementText,
711
815
  announcementText,
@@ -718,6 +822,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
718
822
  function toLayout(value) {
719
823
  return value === "grid" || value === "compact" ? value : "list";
720
824
  }
825
+ function toGroupBy(value) {
826
+ return value === "year" || value === "month" ? value : "";
827
+ }
721
828
  function defaultFeedPath(relativePath) {
722
829
  const name = path3.posix.basename(relativePath, ".md");
723
830
  return name === "index" ? "feed.xml" : `${name}-feed.xml`;
@@ -751,13 +858,17 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
751
858
  scope,
752
859
  folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
753
860
  tags: toStringList(options.tags),
861
+ categories: toStringList(options.categories),
754
862
  limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
755
863
  layout: toLayout(options.layout),
756
- groupByYear: options.groupBy === "year",
864
+ groupBy: toGroupBy(options.groupBy),
757
865
  showTags: options.showTags !== false,
866
+ showCategory: options.showCategory !== false,
867
+ categoryList: options.categoryList !== false,
758
868
  tagList: options.tagList !== false,
869
+ archive: options.archive !== false,
759
870
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
760
- stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
871
+ pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
761
872
  rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
762
873
  feed: toFeed(options.feed, relativePath)
763
874
  };
@@ -827,7 +938,7 @@ function toOptionsKey(options) {
827
938
  options.wordsPerMinute,
828
939
  options.stripTags,
829
940
  options.pageSize,
830
- options.stickyLimit,
941
+ options.pinLimit ?? options.stickyLimit,
831
942
  options.rssLink,
832
943
  options.timeZone
833
944
  ]);
@@ -916,10 +1027,13 @@ function resolveWidgetConfig(body) {
916
1027
  config: {
917
1028
  blog: typeof data.blog === "string" ? data.blog.trim() : "",
918
1029
  limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
919
- stickyLimit: toCount2(data.stickyLimit),
1030
+ // `stickyLimit` is this setting's old name.
1031
+ pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
920
1032
  layout,
921
1033
  showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
1034
+ showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
922
1035
  tags: toStringList(data.tags),
1036
+ categories: toStringList(data.categories),
923
1037
  more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
924
1038
  rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
925
1039
  },
@@ -935,19 +1049,19 @@ function renderWidget(body, environment) {
935
1049
  warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
936
1050
  return "";
937
1051
  }
938
- const stickyLimit = config.stickyLimit ?? blog.index.stickyLimit;
939
- const wanted = new Set(config.tags.map((tag) => tag.toLowerCase()));
940
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false }).filter(
941
- (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)
942
1055
  );
943
- const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
1056
+ const { pinned, rest } = pinPosts(posts, pinLimit);
944
1057
  const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
945
1058
  const view = {
946
1059
  ...blog,
947
1060
  index: {
948
1061
  ...blog.index,
949
1062
  layout: config.layout ?? blog.index.layout,
950
- showTags: config.showTags ?? blog.index.showTags
1063
+ showTags: config.showTags ?? blog.index.showTags,
1064
+ showCategory: config.showCategory ?? blog.index.showCategory
951
1065
  }
952
1066
  };
953
1067
  const lines = [`<div class="vp-blog-widget">`];
@@ -1021,8 +1135,8 @@ function hasFeeds(options) {
1021
1135
  }
1022
1136
  function withPostList(source, page, pages, options) {
1023
1137
  if (!isIndex(page)) return source;
1024
- const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, stickyFirst: true, applyLimit: true });
1025
- 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);
1026
1140
  const context = createRenderContext(options, path6.dirname(page.absolutePath), hasFeeds(options));
1027
1141
  const generated = renderPostList(pinned, rest, page, context);
1028
1142
  return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
@@ -1045,7 +1159,7 @@ function withPostDecorations(source, page, pages, options) {
1045
1159
  let result = header ? insertAfterTitle(source, header) : source;
1046
1160
  if (page.post.postNav ?? options.postNav ?? true) {
1047
1161
  if (blog) {
1048
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
1162
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
1049
1163
  result = `${result.replace(/\s*$/, "")}
1050
1164
 
1051
1165
  ${renderPostNav(page, posts, blog, context)}
@@ -1114,4 +1228,4 @@ export {
1114
1228
  hasWidget,
1115
1229
  blogMarkdown
1116
1230
  };
1117
- //# sourceMappingURL=chunk-LYTJBMVK.js.map
1231
+ //# sourceMappingURL=chunk-ZM5FOUT4.js.map