@binarynoir/vitepress-blog 0.3.0 → 0.5.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,28 @@ 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}.",
223
+ more: "+{n} more",
224
+ less: "Show fewer",
225
+ filter: "Filter {row}",
226
+ noFilterMatches: "Nothing matches.",
212
227
  previous: "Previous",
213
228
  next: "Next",
214
229
  pages: "Pages",
@@ -314,6 +329,13 @@ function toDateTimeAttribute({ date, hasTime }, timeZone) {
314
329
  function yearIn(date, timeZone) {
315
330
  return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
316
331
  }
332
+ function monthIn(date, timeZone) {
333
+ return toZonedIsoDate(date, timeZone).slice(0, 7);
334
+ }
335
+ function formatMonth(month, locale, format) {
336
+ const [year, number] = month.split("-").map(Number);
337
+ return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(Date.UTC(year, number - 1, 1, 12));
338
+ }
317
339
 
318
340
  // src/render.ts
319
341
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
@@ -339,7 +361,8 @@ function renderRssLink(index, context, show = index.index.rssLink) {
339
361
  if (!show || !context.feeds || !index.index.feed) return "";
340
362
  const title = fill(context.labels.rssFeed, { blog: index.title }).replace(/"/g, "&quot;");
341
363
  const url = toMarkdownUrl(`/${toFeedPath(index)}`);
342
- return `<span class="vp-blog-rss">[${RSS_ICON} ${escapeMarkdown(context.labels.subscribe)}](${url} "${title}")</span>`;
364
+ const text = `<span class="vp-blog-rss__text">${escapeMarkdown(context.labels.subscribe)}</span>`;
365
+ return `<span class="vp-blog-rss">[${RSS_ICON} ${text}](${url} "${title}")</span>`;
343
366
  }
344
367
  function fill(template, values) {
345
368
  return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
@@ -369,27 +392,33 @@ function renderAuthors(authors, context) {
369
392
  function renderBadge(kind, context) {
370
393
  return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
371
394
  }
372
- function toTagUrl(blogRoutePath, tag) {
373
- const value = encodeURIComponent(tag.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
395
+ function toFilterUrl(blogRoutePath, param, value) {
396
+ const encoded = encodeURIComponent(value.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
374
397
  const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
375
- return `${toMarkdownUrl(`/${page}`)}?tag=${value}`;
398
+ return `${toMarkdownUrl(`/${page}`)}?${param}=${encoded}`;
399
+ }
400
+ function renderFilterLink(text, param, blogRoutePath, className) {
401
+ return blogRoutePath ? `[${escapeMarkdown(text)}](${toFilterUrl(blogRoutePath, param, text)})` : `<span class="${className}">${escapeTemplateText(text)}</span>`;
376
402
  }
377
403
  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(" ");
404
+ return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
381
405
  }
382
406
  function toTagsAttribute(post) {
383
407
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
384
408
  }
385
- function toItemAttributes(post) {
386
- return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
409
+ function toItemAttributes(post, context) {
410
+ const { category, date, announcement } = post.post;
411
+ const attributes = [`data-vp-blog-item data-tags="${toTagsAttribute(post)}"`];
412
+ if (category) attributes.push(`data-category="${escapeHtml(category.toLowerCase())}"`);
413
+ if (date) attributes.push(`data-month="${monthIn(date, context.dates.timeZone)}"`);
414
+ if (announcement) attributes.push("data-announcement");
415
+ return attributes.join(" ");
387
416
  }
388
417
  function renderBadges(post, context) {
389
- const { draft, sticky, announcement } = post.post;
418
+ const { draft, pinned, announcement } = post.post;
390
419
  return [
391
420
  draft && renderBadge("draft", context),
392
- sticky && renderBadge("sticky", context),
421
+ pinned && renderBadge("pinned", context),
393
422
  announcement && renderBadge("announcement", context)
394
423
  ].filter(Boolean).join(" ");
395
424
  }
@@ -424,7 +453,7 @@ function renderCard(post, context, index) {
424
453
  const link = `/${post.routePath}`;
425
454
  const image = toImageSource(post, context.pageDirectory);
426
455
  const modifiers = [
427
- post.post.sticky && "sticky",
456
+ post.post.pinned && "pinned",
428
457
  post.post.draft && "draft",
429
458
  post.post.announcement && "announcement",
430
459
  image && "with-image",
@@ -432,9 +461,10 @@ function renderCard(post, context, index) {
432
461
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
433
462
  const alt = escapeMarkdown(post.post.imageAlt);
434
463
  return [
435
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
464
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
436
465
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
437
466
  '<div class="vp-blog-card__body">',
467
+ ...index.index.showCategory && post.post.category ? block("vp-blog-card__category", renderFilterLink(post.post.category, "category", index.routePath, "")) : [],
438
468
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
439
469
  ...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
440
470
  ...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
@@ -446,7 +476,7 @@ function renderCard(post, context, index) {
446
476
  function renderCompactItem(post, context) {
447
477
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
448
478
  const badges = renderBadges(post, context);
449
- const marker = `<span hidden ${toItemAttributes(post)}></span>`;
479
+ const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
450
480
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
451
481
  }
452
482
  function renderGroup(posts, index, context) {
@@ -466,18 +496,23 @@ function renderGroup(posts, index, context) {
466
496
  "</div>"
467
497
  ];
468
498
  }
469
- function renderYears(posts, index, context) {
470
- const years = /* @__PURE__ */ new Map();
499
+ function renderGroups(posts, index, context) {
500
+ const { groupBy } = index.index;
501
+ if (!groupBy) return renderGroup(posts, index, context);
502
+ const { timeZone, locale } = context.dates;
503
+ const groups = /* @__PURE__ */ new Map();
471
504
  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]);
505
+ const { date } = post.post;
506
+ const key = !date ? "" : groupBy === "year" ? String(yearIn(date, timeZone)) : monthIn(date, timeZone);
507
+ groups.set(key, [...groups.get(key) ?? [], post]);
474
508
  }
475
509
  const lines = [];
476
- for (const [year, group] of years) {
510
+ for (const [key, group] of groups) {
511
+ const heading = !key ? context.labels.undated : groupBy === "year" ? key : formatMonth(key, locale, { month: "long", year: "numeric" });
477
512
  lines.push(
478
- '<section class="vp-blog-year">',
513
+ `<section class="vp-blog-group vp-blog-${groupBy}">`,
479
514
  "",
480
- `## ${escapeMarkdown(year)}`,
515
+ `## ${escapeMarkdown(heading)}`,
481
516
  "",
482
517
  ...renderGroup(group, index, context),
483
518
  "",
@@ -486,29 +521,107 @@ function renderYears(posts, index, context) {
486
521
  }
487
522
  return lines;
488
523
  }
489
- function collectTags(posts) {
490
- const tags = /* @__PURE__ */ new Map();
524
+ function collectValues(posts, pick) {
525
+ const values = /* @__PURE__ */ new Map();
491
526
  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 };
527
+ for (const value of new Set(pick(post))) {
528
+ const entry = values.get(value.toLowerCase()) ?? { value, count: 0 };
494
529
  entry.count++;
495
- tags.set(tag.toLowerCase(), entry);
530
+ values.set(value.toLowerCase(), entry);
496
531
  }
497
532
  }
498
- return [...tags.values()].sort((a, b) => a.tag.localeCompare(b.tag, void 0, { sensitivity: "base" }));
533
+ return [...values.values()].sort((a, b) => a.value.localeCompare(b.value, void 0, { sensitivity: "base" }));
499
534
  }
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>`;
535
+ function collectTags(posts) {
536
+ return collectValues(posts, (post) => post.post.tags).map(({ value, count }) => ({ tag: value, count }));
537
+ }
538
+ function collectCategories(posts) {
539
+ return collectValues(posts, (post) => post.post.category ? [post.post.category] : []);
540
+ }
541
+ function collectArchive(posts, timeZone) {
542
+ const years = /* @__PURE__ */ new Map();
543
+ for (const post of posts) {
544
+ if (!post.post.date) continue;
545
+ const month = monthIn(post.post.date, timeZone);
546
+ const year = years.get(month.slice(0, 4)) ?? { value: month.slice(0, 4), count: 0, months: [] };
547
+ year.count++;
548
+ const entry = year.months.find((candidate) => candidate.value === month);
549
+ if (entry) entry.count++;
550
+ else year.months.push({ value: month, count: 1 });
551
+ years.set(year.value, year);
552
+ }
553
+ const newestFirst = (a, b) => b.value.localeCompare(a.value);
554
+ return [...years.values()].sort(newestFirst).map((year) => ({ ...year, months: [...year.months].sort(newestFirst) }));
555
+ }
556
+ function renderChip(name, count, url, title = "") {
557
+ const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
558
+ return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
559
+ }
560
+ function renderFacet(row) {
561
+ const { facet, label, chips, ariaLabel = label, limit = 0, attributes = "", aside = "" } = row;
562
+ const limited = limit && chips.length > limit ? ` data-limit="${limit}"` : "";
507
563
  return [
508
- `<nav class="vp-blog-tags" aria-label="${escapeHtml(context.labels.tags)}">`,
564
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${limited}${attributes}>`,
565
+ `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
566
+ '<div class="vp-blog-facet__chips">',
509
567
  "",
510
- `${label} ${links.join(" ")}`,
568
+ chips.join(" "),
511
569
  "",
570
+ "</div>",
571
+ ...block("vp-blog-facet__aside", aside),
572
+ "</div>"
573
+ ];
574
+ }
575
+ function canNarrow(values, total) {
576
+ return values.length > 1 || values.length === 1 && values[0].count < total;
577
+ }
578
+ function byUse(a, b) {
579
+ return b.count - a.count || a.value.localeCompare(b.value, void 0, { sensitivity: "base" });
580
+ }
581
+ function renderBrowse(posts, index, context) {
582
+ const { labels } = context;
583
+ const { locale, timeZone } = context.dates;
584
+ const { categoryList, categoryLimit, tagList, tagLimit, tagMinCount, archive: showArchive } = index.index;
585
+ const blog = index.routePath;
586
+ const rows = [];
587
+ const categories = categoryList ? collectCategories(posts).sort(byUse) : [];
588
+ if (canNarrow(categories, posts.length)) {
589
+ const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
590
+ rows.push({ facet: "category", label: labels.categories, chips, limit: categoryLimit });
591
+ }
592
+ const tags = tagList ? collectTags(posts).map(({ tag, count }) => ({ value: tag, count })).filter(({ count }) => count >= tagMinCount).sort(byUse) : [];
593
+ if (canNarrow(tags, posts.length)) {
594
+ const chips = tags.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "tag", value)));
595
+ rows.push({ facet: "tag", label: labels.tags, chips, limit: tagLimit });
596
+ }
597
+ const archive = showArchive ? collectArchive(posts, timeZone) : [];
598
+ const monthChips = (months) => months.map(
599
+ ({ value, count }) => renderChip(
600
+ formatMonth(value, locale, { month: "short" }),
601
+ count,
602
+ toFilterUrl(blog, "month", value),
603
+ formatMonth(value, locale, { month: "long", year: "numeric" })
604
+ )
605
+ );
606
+ if (archive.length === 1 && canNarrow(archive[0].months, posts.length)) {
607
+ const [year] = archive;
608
+ const ariaLabel = `${labels.archive} ${year.value}`;
609
+ const attributes = ` data-year="${year.value}"`;
610
+ rows.push({ facet: "month", label: labels.archive, chips: monthChips(year.months), ariaLabel, attributes });
611
+ } else if (archive.length > 1) {
612
+ const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
613
+ rows.push({ facet: "year", label: labels.archive, chips: years });
614
+ for (const year of archive) {
615
+ const ariaLabel = `${labels.archive} ${year.value}`;
616
+ const attributes = ` data-year="${year.value}" hidden`;
617
+ rows.push({ facet: "month", label: year.value, chips: monthChips(year.months), ariaLabel, attributes });
618
+ }
619
+ }
620
+ if (rows.length === 0) return [];
621
+ rows[0].aside = renderRssLink(index, context);
622
+ return [
623
+ `<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`,
624
+ ...rows.flatMap(renderFacet),
512
625
  "</nav>"
513
626
  ];
514
627
  }
@@ -516,14 +629,23 @@ function renderClientAttributes(index, context) {
516
629
  const { labels } = context;
517
630
  const attributes = {
518
631
  "data-page-size": String(index.index.pageSize),
519
- "data-label-tagged": labels.tagged,
632
+ // For writing a `?month=` filter's name: "October 2026".
633
+ "data-locale": context.locale,
520
634
  "data-label-show-all": labels.showAll,
521
- "data-label-no-tagged": labels.noTagged,
522
635
  "data-label-previous": labels.previous,
523
636
  "data-label-next": labels.next,
524
637
  "data-label-pages": labels.pages,
525
638
  "data-label-announcements": labels.announcements,
526
- "data-label-no-announcements": labels.noAnnouncements
639
+ "data-label-category": labels.category,
640
+ "data-label-tag": labels.tag,
641
+ "data-label-post-count": labels.postCount,
642
+ "data-label-post-count-one": labels.postCountOne,
643
+ "data-label-no-matches": labels.noMatches,
644
+ "data-label-remove-filter": labels.removeFilter,
645
+ "data-label-more": labels.more,
646
+ "data-label-less": labels.less,
647
+ "data-label-filter": labels.filter,
648
+ "data-label-no-filter-matches": labels.noFilterMatches
527
649
  };
528
650
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
529
651
  }
@@ -531,20 +653,19 @@ function renderPostList(pinned, rest, index, context) {
531
653
  const all = [...pinned, ...rest];
532
654
  if (all.length === 0) return `<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>
533
655
  `;
656
+ const browse = renderBrowse(all, index, context);
534
657
  const lines = [
535
658
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
536
- ...block("vp-blog-subscribe", renderRssLink(index, context)),
537
- ...renderTagList(all, index, context),
538
- '<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
659
+ ...browse.length ? browse : block("vp-blog-subscribe", renderRssLink(index, context)),
660
+ '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
539
661
  ];
540
662
  if (pinned.length)
541
663
  lines.push(
542
- '<section class="vp-blog-sticky" data-vp-blog-sticky>',
664
+ '<section class="vp-blog-pinned" data-vp-blog-pinned>',
543
665
  ...renderGroup(pinned, index, context),
544
666
  "</section>"
545
667
  );
546
- if (rest.length)
547
- lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
668
+ if (rest.length) lines.push(...renderGroups(rest, index, context));
548
669
  lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
549
670
  return `${lines.join("\n")}
550
671
  `;
@@ -553,8 +674,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
553
674
  const lines = [];
554
675
  if (show.byline) {
555
676
  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")));
677
+ const { category, tags } = post.post;
678
+ const chips = [
679
+ category && `<span class="vp-blog-byline__category">${renderFilterLink(category, "category", blogRoutePath, "vp-blog-category")}</span>`,
680
+ tags.length && `<span class="vp-blog-byline__tags">${renderTags(tags, blogRoutePath)}</span>`
681
+ ].filter(Boolean).join(" ");
682
+ lines.push(...block("vp-blog-byline", [meta, chips].filter(Boolean).join("\n\n")));
558
683
  }
559
684
  if (show.cover && post.post.image) {
560
685
  const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
@@ -592,6 +717,8 @@ var FENCED_CODE = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[`~]*[ \t]*$|
592
717
  var HEADING_1 = /^ {0,3}#[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/m;
593
718
  var TAG_SYNTAX = /\(\(<?tag([|/])[^|/)\r\n]*(?:\1[^|/)\r\n]*){0,2}\)\)/g;
594
719
  var MAX_SUMMARY_LENGTH = 200;
720
+ var DEFAULT_CATEGORY_LIMIT = 8;
721
+ var DEFAULT_TAG_LIMIT = 12;
595
722
  function readFrontmatter(raw) {
596
723
  const source = raw.replace(/^/, "");
597
724
  const match = FRONTMATTER.exec(source);
@@ -705,7 +832,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
705
832
  tags: toStringList(pick("tags")),
706
833
  category: toText(pick("category")),
707
834
  scope: toScope(post.scope),
708
- sticky: post.sticky === true,
835
+ // `sticky` is this setting's old name.
836
+ pinned: (post.pinned ?? post.sticky) === true,
709
837
  draft: post.draft === true,
710
838
  announcement: post.announcement === true || !!announcementText,
711
839
  announcementText,
@@ -718,6 +846,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
718
846
  function toLayout(value) {
719
847
  return value === "grid" || value === "compact" ? value : "list";
720
848
  }
849
+ function toGroupBy(value) {
850
+ return value === "year" || value === "month" ? value : "";
851
+ }
721
852
  function defaultFeedPath(relativePath) {
722
853
  const name = path3.posix.basename(relativePath, ".md");
723
854
  return name === "index" ? "feed.xml" : `${name}-feed.xml`;
@@ -751,13 +882,20 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
751
882
  scope,
752
883
  folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
753
884
  tags: toStringList(options.tags),
885
+ categories: toStringList(options.categories),
754
886
  limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
755
887
  layout: toLayout(options.layout),
756
- groupByYear: options.groupBy === "year",
888
+ groupBy: toGroupBy(options.groupBy),
757
889
  showTags: options.showTags !== false,
890
+ showCategory: options.showCategory !== false,
891
+ categoryList: options.categoryList !== false,
892
+ categoryLimit: toCount(options.categoryLimit, DEFAULT_CATEGORY_LIMIT),
758
893
  tagList: options.tagList !== false,
894
+ tagLimit: toCount(options.tagLimit, DEFAULT_TAG_LIMIT),
895
+ tagMinCount: toCount(options.tagMinCount, 1),
896
+ archive: options.archive !== false,
759
897
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
760
- stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
898
+ pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
761
899
  rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
762
900
  feed: toFeed(options.feed, relativePath)
763
901
  };
@@ -827,7 +965,7 @@ function toOptionsKey(options) {
827
965
  options.wordsPerMinute,
828
966
  options.stripTags,
829
967
  options.pageSize,
830
- options.stickyLimit,
968
+ options.pinLimit ?? options.stickyLimit,
831
969
  options.rssLink,
832
970
  options.timeZone
833
971
  ]);
@@ -916,10 +1054,13 @@ function resolveWidgetConfig(body) {
916
1054
  config: {
917
1055
  blog: typeof data.blog === "string" ? data.blog.trim() : "",
918
1056
  limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
919
- stickyLimit: toCount2(data.stickyLimit),
1057
+ // `stickyLimit` is this setting's old name.
1058
+ pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
920
1059
  layout,
921
1060
  showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
1061
+ showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
922
1062
  tags: toStringList(data.tags),
1063
+ categories: toStringList(data.categories),
923
1064
  more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
924
1065
  rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
925
1066
  },
@@ -935,19 +1076,19 @@ function renderWidget(body, environment) {
935
1076
  warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
936
1077
  return "";
937
1078
  }
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()))
1079
+ const pinLimit = config.pinLimit ?? blog.index.pinLimit;
1080
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false }).filter(
1081
+ (post) => hasAnyTag(post, config.tags) && inAnyCategory(post, config.categories)
942
1082
  );
943
- const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
1083
+ const { pinned, rest } = pinPosts(posts, pinLimit);
944
1084
  const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
945
1085
  const view = {
946
1086
  ...blog,
947
1087
  index: {
948
1088
  ...blog.index,
949
1089
  layout: config.layout ?? blog.index.layout,
950
- showTags: config.showTags ?? blog.index.showTags
1090
+ showTags: config.showTags ?? blog.index.showTags,
1091
+ showCategory: config.showCategory ?? blog.index.showCategory
951
1092
  }
952
1093
  };
953
1094
  const lines = [`<div class="vp-blog-widget">`];
@@ -1021,8 +1162,8 @@ function hasFeeds(options) {
1021
1162
  }
1022
1163
  function withPostList(source, page, pages, options) {
1023
1164
  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);
1165
+ const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, pinnedFirst: true, applyLimit: true });
1166
+ const { pinned, rest } = pinPosts(posts, page.index.pinLimit);
1026
1167
  const context = createRenderContext(options, path6.dirname(page.absolutePath), hasFeeds(options));
1027
1168
  const generated = renderPostList(pinned, rest, page, context);
1028
1169
  return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
@@ -1045,7 +1186,7 @@ function withPostDecorations(source, page, pages, options) {
1045
1186
  let result = header ? insertAfterTitle(source, header) : source;
1046
1187
  if (page.post.postNav ?? options.postNav ?? true) {
1047
1188
  if (blog) {
1048
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
1189
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
1049
1190
  result = `${result.replace(/\s*$/, "")}
1050
1191
 
1051
1192
  ${renderPostNav(page, posts, blog, context)}
@@ -1114,4 +1255,4 @@ export {
1114
1255
  hasWidget,
1115
1256
  blogMarkdown
1116
1257
  };
1117
- //# sourceMappingURL=chunk-LYTJBMVK.js.map
1258
+ //# sourceMappingURL=chunk-FPFH7ORO.js.map