@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.
package/dist/index.cjs CHANGED
@@ -63,6 +63,11 @@ function hasAnyTag(post, tags) {
63
63
  const wanted = new Set(tags.map((tag) => tag.toLowerCase()));
64
64
  return post.post.tags.some((tag) => wanted.has(tag.toLowerCase()));
65
65
  }
66
+ function inAnyCategory(post, categories) {
67
+ if (categories.length === 0) return true;
68
+ const category = post.post.category.toLowerCase();
69
+ return !!category && categories.some((wanted) => wanted.toLowerCase() === category);
70
+ }
66
71
  function blogCoversPost(index, post, sections) {
67
72
  if (index.index.scope === "section") return isInFolders(post.relativePath, index.index.folders);
68
73
  if (!isInFolders(post.relativePath, index.index.folders)) return false;
@@ -76,22 +81,22 @@ function compareDates(a, b) {
76
81
  function compareByDate(a, b) {
77
82
  return compareDates(a.post.date, b.post.date) || a.post.title.localeCompare(b.post.title);
78
83
  }
79
- function pinStickyPosts(posts, limit) {
80
- const sticky = posts.filter((post) => post.post.sticky);
81
- const pinned = new Set(limit ? sticky.slice(0, limit) : sticky);
84
+ function pinPosts(posts, limit) {
85
+ const candidates = posts.filter((post) => post.post.pinned);
86
+ const pinned = new Set(limit ? candidates.slice(0, limit) : candidates);
82
87
  return { pinned: [...pinned], rest: posts.filter((post) => !pinned.has(post)) };
83
88
  }
84
89
  function listBlogPosts(pages, index, options) {
85
90
  const sections = pages.filter(isIndex).filter((page) => page.index.scope === "section");
86
- 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);
87
- if (options.stickyFirst) {
88
- const { pinned, rest } = pinStickyPosts(posts, index.index.stickyLimit);
91
+ 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);
92
+ if (options.pinnedFirst) {
93
+ const { pinned, rest } = pinPosts(posts, index.index.pinLimit);
89
94
  posts = [...pinned, ...rest];
90
95
  }
91
96
  return options.applyLimit && index.index.limit ? posts.slice(0, index.index.limit) : posts;
92
97
  }
93
98
  function findHomeBlog(pages, post, drafts) {
94
- const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false }).includes(post));
99
+ const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, pinnedFirst: false, applyLimit: false }).includes(post));
95
100
  const sections = listing.filter((index) => index.index.scope === "section");
96
101
  if (sections.length) return deepestFirst(sections, post.relativePath)[0];
97
102
  return listing[0] ?? null;
@@ -207,6 +212,13 @@ function toDateTimeAttribute({ date, hasTime }, timeZone) {
207
212
  function yearIn(date, timeZone) {
208
213
  return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
209
214
  }
215
+ function monthIn(date, timeZone) {
216
+ return toZonedIsoDate(date, timeZone).slice(0, 7);
217
+ }
218
+ function formatMonth(month, locale, format) {
219
+ const [year, number] = month.split("-").map(Number);
220
+ return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(Date.UTC(year, number - 1, 1, 12));
221
+ }
210
222
 
211
223
  // src/feed.ts
212
224
  var import_node_crypto = require("crypto");
@@ -258,18 +270,24 @@ var DEFAULT_LABELS = {
258
270
  older: "Older post",
259
271
  allPosts: "All posts in {blog}",
260
272
  draft: "Draft",
261
- sticky: "Pinned",
273
+ pinned: "Pinned",
262
274
  announcement: "Announcement",
263
275
  announcements: "Announcements",
264
- noAnnouncements: "No announcements.",
265
276
  announcementCount: "{n} announcements",
266
277
  dismiss: "Dismiss",
267
278
  noPosts: "No posts yet.",
268
279
  undated: "Undated",
280
+ browse: "Browse posts",
281
+ categories: "Categories",
269
282
  tags: "Keywords",
270
- tagged: "Posts tagged {tag}",
283
+ archive: "Archive",
284
+ category: "Category",
285
+ tag: "Keyword",
286
+ postCount: "{n} posts",
287
+ postCountOne: "{n} post",
288
+ noMatches: "No posts match these filters.",
289
+ removeFilter: "Remove filter: {filter}",
271
290
  showAll: "Show all posts",
272
- noTagged: "No posts tagged {tag}.",
273
291
  previous: "Previous",
274
292
  next: "Next",
275
293
  pages: "Pages",
@@ -331,27 +349,33 @@ function renderAuthors(authors, context) {
331
349
  function renderBadge(kind, context) {
332
350
  return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
333
351
  }
334
- function toTagUrl(blogRoutePath, tag) {
335
- const value = encodeURIComponent(tag.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
352
+ function toFilterUrl(blogRoutePath, param, value) {
353
+ const encoded = encodeURIComponent(value.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
336
354
  const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
337
- return `${toMarkdownUrl(`/${page}`)}?tag=${value}`;
355
+ return `${toMarkdownUrl(`/${page}`)}?${param}=${encoded}`;
356
+ }
357
+ function renderFilterLink(text, param, blogRoutePath, className) {
358
+ return blogRoutePath ? `[${escapeMarkdown(text)}](${toFilterUrl(blogRoutePath, param, text)})` : `<span class="${className}">${escapeTemplateText(text)}</span>`;
338
359
  }
339
360
  function renderTags(tags, blogRoutePath) {
340
- return tags.map(
341
- (tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
342
- ).join(" ");
361
+ return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
343
362
  }
344
363
  function toTagsAttribute(post) {
345
364
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
346
365
  }
347
- function toItemAttributes(post) {
348
- return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
366
+ function toItemAttributes(post, context) {
367
+ const { category, date, announcement } = post.post;
368
+ const attributes = [`data-vp-blog-item data-tags="${toTagsAttribute(post)}"`];
369
+ if (category) attributes.push(`data-category="${escapeHtml(category.toLowerCase())}"`);
370
+ if (date) attributes.push(`data-month="${monthIn(date, context.dates.timeZone)}"`);
371
+ if (announcement) attributes.push("data-announcement");
372
+ return attributes.join(" ");
349
373
  }
350
374
  function renderBadges(post, context) {
351
- const { draft, sticky, announcement } = post.post;
375
+ const { draft, pinned, announcement } = post.post;
352
376
  return [
353
377
  draft && renderBadge("draft", context),
354
- sticky && renderBadge("sticky", context),
378
+ pinned && renderBadge("pinned", context),
355
379
  announcement && renderBadge("announcement", context)
356
380
  ].filter(Boolean).join(" ");
357
381
  }
@@ -386,7 +410,7 @@ function renderCard(post, context, index) {
386
410
  const link = `/${post.routePath}`;
387
411
  const image = toImageSource(post, context.pageDirectory);
388
412
  const modifiers = [
389
- post.post.sticky && "sticky",
413
+ post.post.pinned && "pinned",
390
414
  post.post.draft && "draft",
391
415
  post.post.announcement && "announcement",
392
416
  image && "with-image",
@@ -394,9 +418,10 @@ function renderCard(post, context, index) {
394
418
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
395
419
  const alt = escapeMarkdown(post.post.imageAlt);
396
420
  return [
397
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
421
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
398
422
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
399
423
  '<div class="vp-blog-card__body">',
424
+ ...index.index.showCategory && post.post.category ? block("vp-blog-card__category", renderFilterLink(post.post.category, "category", index.routePath, "")) : [],
400
425
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
401
426
  ...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
402
427
  ...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
@@ -408,7 +433,7 @@ function renderCard(post, context, index) {
408
433
  function renderCompactItem(post, context) {
409
434
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
410
435
  const badges = renderBadges(post, context);
411
- const marker = `<span hidden ${toItemAttributes(post)}></span>`;
436
+ const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
412
437
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
413
438
  }
414
439
  function renderGroup(posts, index, context) {
@@ -428,18 +453,23 @@ function renderGroup(posts, index, context) {
428
453
  "</div>"
429
454
  ];
430
455
  }
431
- function renderYears(posts, index, context) {
432
- const years = /* @__PURE__ */ new Map();
456
+ function renderGroups(posts, index, context) {
457
+ const { groupBy } = index.index;
458
+ if (!groupBy) return renderGroup(posts, index, context);
459
+ const { timeZone, locale } = context.dates;
460
+ const groups = /* @__PURE__ */ new Map();
433
461
  for (const post of posts) {
434
- const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
435
- years.set(year, [...years.get(year) ?? [], post]);
462
+ const { date } = post.post;
463
+ const key = !date ? "" : groupBy === "year" ? String(yearIn(date, timeZone)) : monthIn(date, timeZone);
464
+ groups.set(key, [...groups.get(key) ?? [], post]);
436
465
  }
437
466
  const lines = [];
438
- for (const [year, group] of years) {
467
+ for (const [key, group] of groups) {
468
+ const heading = !key ? context.labels.undated : groupBy === "year" ? key : formatMonth(key, locale, { month: "long", year: "numeric" });
439
469
  lines.push(
440
- '<section class="vp-blog-year">',
470
+ `<section class="vp-blog-group vp-blog-${groupBy}">`,
441
471
  "",
442
- `## ${escapeMarkdown(year)}`,
472
+ `## ${escapeMarkdown(heading)}`,
443
473
  "",
444
474
  ...renderGroup(group, index, context),
445
475
  "",
@@ -448,44 +478,114 @@ function renderYears(posts, index, context) {
448
478
  }
449
479
  return lines;
450
480
  }
451
- function collectTags(posts) {
452
- const tags = /* @__PURE__ */ new Map();
481
+ function collectValues(posts, pick) {
482
+ const values = /* @__PURE__ */ new Map();
453
483
  for (const post of posts) {
454
- for (const tag of new Set(post.post.tags)) {
455
- const entry = tags.get(tag.toLowerCase()) ?? { tag, count: 0 };
484
+ for (const value of new Set(pick(post))) {
485
+ const entry = values.get(value.toLowerCase()) ?? { value, count: 0 };
456
486
  entry.count++;
457
- tags.set(tag.toLowerCase(), entry);
487
+ values.set(value.toLowerCase(), entry);
458
488
  }
459
489
  }
460
- return [...tags.values()].sort((a, b) => a.tag.localeCompare(b.tag, void 0, { sensitivity: "base" }));
490
+ return [...values.values()].sort((a, b) => a.value.localeCompare(b.value, void 0, { sensitivity: "base" }));
461
491
  }
462
- function renderTagList(posts, index, context) {
463
- const tags = collectTags(posts);
464
- if (!index.index.tagList || tags.length === 0) return [];
465
- const links = tags.map(
466
- ({ tag, count }) => `[${escapeMarkdown(tag)} <span class="vp-blog-tags__count">${count}</span>](${toTagUrl(index.routePath, tag)})`
467
- );
468
- const label = `<span class="vp-blog-tags__label">${escapeTemplateText(context.labels.tags)}</span>`;
492
+ function collectTags(posts) {
493
+ return collectValues(posts, (post) => post.post.tags).map(({ value, count }) => ({ tag: value, count }));
494
+ }
495
+ function collectCategories(posts) {
496
+ return collectValues(posts, (post) => post.post.category ? [post.post.category] : []);
497
+ }
498
+ function collectArchive(posts, timeZone) {
499
+ const years = /* @__PURE__ */ new Map();
500
+ for (const post of posts) {
501
+ if (!post.post.date) continue;
502
+ const month = monthIn(post.post.date, timeZone);
503
+ const year = years.get(month.slice(0, 4)) ?? { value: month.slice(0, 4), count: 0, months: [] };
504
+ year.count++;
505
+ const entry = year.months.find((candidate) => candidate.value === month);
506
+ if (entry) entry.count++;
507
+ else year.months.push({ value: month, count: 1 });
508
+ years.set(year.value, year);
509
+ }
510
+ const newestFirst = (a, b) => b.value.localeCompare(a.value);
511
+ return [...years.values()].sort(newestFirst).map((year) => ({ ...year, months: [...year.months].sort(newestFirst) }));
512
+ }
513
+ function renderChip(name, count, url, title = "") {
514
+ const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
515
+ return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
516
+ }
517
+ function renderFacet(facet, label, chips, ariaLabel = label, attributes = "") {
469
518
  return [
470
- `<nav class="vp-blog-tags" aria-label="${escapeHtml(context.labels.tags)}">`,
519
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${attributes}>`,
520
+ `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
521
+ '<div class="vp-blog-facet__chips">',
471
522
  "",
472
- `${label} ${links.join(" ")}`,
523
+ chips.join(" "),
473
524
  "",
474
- "</nav>"
525
+ "</div>",
526
+ "</div>"
475
527
  ];
476
528
  }
529
+ function renderBrowse(posts, index, context) {
530
+ const { labels } = context;
531
+ const { locale, timeZone } = context.dates;
532
+ const blog = index.routePath;
533
+ const rows = [];
534
+ const categories = index.index.categoryList ? collectCategories(posts) : [];
535
+ if (categories.length) {
536
+ const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
537
+ rows.push(...renderFacet("category", labels.categories, chips));
538
+ }
539
+ const tags = index.index.tagList ? collectTags(posts) : [];
540
+ if (tags.length) {
541
+ const chips = tags.map(({ tag, count }) => renderChip(tag, count, toFilterUrl(blog, "tag", tag)));
542
+ rows.push(...renderFacet("tag", labels.tags, chips));
543
+ }
544
+ const archive = index.index.archive ? collectArchive(posts, timeZone) : [];
545
+ if (archive.reduce((months, year) => months + year.months.length, 0) > 1) {
546
+ const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
547
+ rows.push(...renderFacet("year", labels.archive, years));
548
+ for (const year of archive) {
549
+ const months = year.months.map(
550
+ ({ value, count }) => renderChip(
551
+ formatMonth(value, locale, { month: "short" }),
552
+ count,
553
+ toFilterUrl(blog, "month", value),
554
+ formatMonth(value, locale, { month: "long", year: "numeric" })
555
+ )
556
+ );
557
+ const shown = archive.length === 1 ? "" : " hidden";
558
+ rows.push(
559
+ ...renderFacet(
560
+ "month",
561
+ year.value,
562
+ months,
563
+ `${labels.archive} ${year.value}`,
564
+ ` data-year="${year.value}"${shown}`
565
+ )
566
+ );
567
+ }
568
+ }
569
+ if (rows.length === 0) return [];
570
+ return [`<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`, ...rows, "</nav>"];
571
+ }
477
572
  function renderClientAttributes(index, context) {
478
573
  const { labels } = context;
479
574
  const attributes = {
480
575
  "data-page-size": String(index.index.pageSize),
481
- "data-label-tagged": labels.tagged,
576
+ // For writing a `?month=` filter's name: "October 2026".
577
+ "data-locale": context.locale,
482
578
  "data-label-show-all": labels.showAll,
483
- "data-label-no-tagged": labels.noTagged,
484
579
  "data-label-previous": labels.previous,
485
580
  "data-label-next": labels.next,
486
581
  "data-label-pages": labels.pages,
487
582
  "data-label-announcements": labels.announcements,
488
- "data-label-no-announcements": labels.noAnnouncements
583
+ "data-label-category": labels.category,
584
+ "data-label-tag": labels.tag,
585
+ "data-label-post-count": labels.postCount,
586
+ "data-label-post-count-one": labels.postCountOne,
587
+ "data-label-no-matches": labels.noMatches,
588
+ "data-label-remove-filter": labels.removeFilter
489
589
  };
490
590
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
491
591
  }
@@ -496,17 +596,16 @@ function renderPostList(pinned, rest, index, context) {
496
596
  const lines = [
497
597
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
498
598
  ...block("vp-blog-subscribe", renderRssLink(index, context)),
499
- ...renderTagList(all, index, context),
500
- '<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
599
+ ...renderBrowse(all, index, context),
600
+ '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
501
601
  ];
502
602
  if (pinned.length)
503
603
  lines.push(
504
- '<section class="vp-blog-sticky" data-vp-blog-sticky>',
604
+ '<section class="vp-blog-pinned" data-vp-blog-pinned>',
505
605
  ...renderGroup(pinned, index, context),
506
606
  "</section>"
507
607
  );
508
- if (rest.length)
509
- lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
608
+ if (rest.length) lines.push(...renderGroups(rest, index, context));
510
609
  lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
511
610
  return `${lines.join("\n")}
512
611
  `;
@@ -515,8 +614,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
515
614
  const lines = [];
516
615
  if (show.byline) {
517
616
  const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
518
- const tags = post.post.tags.length ? `<span class="vp-blog-byline__tags">${renderTags(post.post.tags, blogRoutePath)}</span>` : "";
519
- lines.push(...block("vp-blog-byline", [meta, tags].filter(Boolean).join("\n\n")));
617
+ const { category, tags } = post.post;
618
+ const chips = [
619
+ category && `<span class="vp-blog-byline__category">${renderFilterLink(category, "category", blogRoutePath, "vp-blog-category")}</span>`,
620
+ tags.length && `<span class="vp-blog-byline__tags">${renderTags(tags, blogRoutePath)}</span>`
621
+ ].filter(Boolean).join(" ");
622
+ lines.push(...block("vp-blog-byline", [meta, chips].filter(Boolean).join("\n\n")));
520
623
  }
521
624
  if (show.cover && post.post.image) {
522
625
  const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
@@ -672,7 +775,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
672
775
  tags: toStringList(pick("tags")),
673
776
  category: toText(pick("category")),
674
777
  scope: toScope(post.scope),
675
- sticky: post.sticky === true,
778
+ // `sticky` is this setting's old name.
779
+ pinned: (post.pinned ?? post.sticky) === true,
676
780
  draft: post.draft === true,
677
781
  announcement: post.announcement === true || !!announcementText,
678
782
  announcementText,
@@ -685,6 +789,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
685
789
  function toLayout(value) {
686
790
  return value === "grid" || value === "compact" ? value : "list";
687
791
  }
792
+ function toGroupBy(value) {
793
+ return value === "year" || value === "month" ? value : "";
794
+ }
688
795
  function defaultFeedPath(relativePath) {
689
796
  const name = import_node_path3.default.posix.basename(relativePath, ".md");
690
797
  return name === "index" ? "feed.xml" : `${name}-feed.xml`;
@@ -718,13 +825,17 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
718
825
  scope,
719
826
  folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
720
827
  tags: toStringList(options.tags),
828
+ categories: toStringList(options.categories),
721
829
  limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
722
830
  layout: toLayout(options.layout),
723
- groupByYear: options.groupBy === "year",
831
+ groupBy: toGroupBy(options.groupBy),
724
832
  showTags: options.showTags !== false,
833
+ showCategory: options.showCategory !== false,
834
+ categoryList: options.categoryList !== false,
725
835
  tagList: options.tagList !== false,
836
+ archive: options.archive !== false,
726
837
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
727
- stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
838
+ pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
728
839
  rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
729
840
  feed: toFeed(options.feed, relativePath)
730
841
  };
@@ -791,7 +902,7 @@ function toOptionsKey(options) {
791
902
  options.wordsPerMinute,
792
903
  options.stripTags,
793
904
  options.pageSize,
794
- options.stickyLimit,
905
+ options.pinLimit ?? options.stickyLimit,
795
906
  options.rssLink,
796
907
  options.timeZone
797
908
  ]);
@@ -871,10 +982,13 @@ function resolveWidgetConfig(body) {
871
982
  config: {
872
983
  blog: typeof data.blog === "string" ? data.blog.trim() : "",
873
984
  limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
874
- stickyLimit: toCount2(data.stickyLimit),
985
+ // `stickyLimit` is this setting's old name.
986
+ pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
875
987
  layout,
876
988
  showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
989
+ showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
877
990
  tags: toStringList(data.tags),
991
+ categories: toStringList(data.categories),
878
992
  more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
879
993
  rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
880
994
  },
@@ -890,19 +1004,19 @@ function renderWidget(body, environment) {
890
1004
  warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
891
1005
  return "";
892
1006
  }
893
- const stickyLimit = config.stickyLimit ?? blog.index.stickyLimit;
894
- const wanted = new Set(config.tags.map((tag) => tag.toLowerCase()));
895
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false }).filter(
896
- (post) => wanted.size === 0 || post.post.tags.some((tag) => wanted.has(tag.toLowerCase()))
1007
+ const pinLimit = config.pinLimit ?? blog.index.pinLimit;
1008
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false }).filter(
1009
+ (post) => hasAnyTag(post, config.tags) && inAnyCategory(post, config.categories)
897
1010
  );
898
- const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
1011
+ const { pinned, rest } = pinPosts(posts, pinLimit);
899
1012
  const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
900
1013
  const view = {
901
1014
  ...blog,
902
1015
  index: {
903
1016
  ...blog.index,
904
1017
  layout: config.layout ?? blog.index.layout,
905
- showTags: config.showTags ?? blog.index.showTags
1018
+ showTags: config.showTags ?? blog.index.showTags,
1019
+ showCategory: config.showCategory ?? blog.index.showCategory
906
1020
  }
907
1021
  };
908
1022
  const lines = [`<div class="vp-blog-widget">`];
@@ -975,8 +1089,8 @@ function hasFeeds(options) {
975
1089
  }
976
1090
  function withPostList(source, page, pages, options) {
977
1091
  if (!isIndex(page)) return source;
978
- const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, stickyFirst: true, applyLimit: true });
979
- const { pinned, rest } = pinStickyPosts(posts, page.index.stickyLimit);
1092
+ const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, pinnedFirst: true, applyLimit: true });
1093
+ const { pinned, rest } = pinPosts(posts, page.index.pinLimit);
980
1094
  const context = createRenderContext(options, import_node_path6.default.dirname(page.absolutePath), hasFeeds(options));
981
1095
  const generated = renderPostList(pinned, rest, page, context);
982
1096
  return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
@@ -999,7 +1113,7 @@ function withPostDecorations(source, page, pages, options) {
999
1113
  let result = header ? insertAfterTitle(source, header) : source;
1000
1114
  if (page.post.postNav ?? options.postNav ?? true) {
1001
1115
  if (blog) {
1002
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
1116
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
1003
1117
  result = `${result.replace(/\s*$/, "")}
1004
1118
 
1005
1119
  ${renderPostNav(page, posts, blog, context)}