@binarynoir/vitepress-blog 0.4.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.
package/README.md CHANGED
@@ -267,24 +267,27 @@ Notes from Team A.
267
267
 
268
268
  The list goes where `<!-- blog-posts -->` is, or after the page's content.
269
269
 
270
- | Key | Default | What it does |
271
- | -------------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
272
- | `scope` | `section` | `site` makes this the main blog. See [Scopes](#scopes). |
273
- | `folders` | the page's folder | Folders to list posts from, relative to the source directory; `./` and `../` are page-relative. |
274
- | `tags` | all | Only list posts with at least one of these tags. |
275
- | `categories` | all | Only list posts in one of these categories. |
276
- | `limit` | all | Most posts to list. The feed has its own limit. |
277
- | `layout` | `list` | `list` (image beside text), `grid` (cards) or `compact` (dates and titles). |
278
- | `groupBy` | none | `year` or `month` puts a heading ("2026", "October 2026") above each year's or month's posts. |
279
- | `showTags` | `true` | `false` hides tag chips on cards. |
280
- | `showCategory` | `true` | `false` hides the category above each card's title. |
281
- | `categoryList` | `true` | A row of categories in the browse panel, each with its count. `false` leaves it out. |
282
- | `tagList` | `true` | A row of keywords in the browse panel, each with its count. `false` leaves it out. |
283
- | `archive` | `true` | Rows of years, and each year's months, in the browse panel. `false` leaves them out. |
284
- | `pageSize` | `10` (the `pageSize` option) | Posts per page. `0` shows them all on one page. |
285
- | `pinLimit` | all (the `pinLimit` option) | Most pinned posts kept at the top. Extra ones take their place by date. |
286
- | `rssLink` | `true` (the `rssLink` option) | A "Subscribe" link with the RSS icon to this blog's feed. `false` leaves it out. |
287
- | `feed` | on | `false` turns the feed off, or `{ title, description, limit, path }` to set it up. |
270
+ | Key | Default | What it does |
271
+ | --------------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
272
+ | `scope` | `section` | `site` makes this the main blog. See [Scopes](#scopes). |
273
+ | `folders` | the page's folder | Folders to list posts from, relative to the source directory; `./` and `../` are page-relative. |
274
+ | `tags` | all | Only list posts with at least one of these tags. |
275
+ | `categories` | all | Only list posts in one of these categories. |
276
+ | `limit` | all | Most posts to list. The feed has its own limit. |
277
+ | `layout` | `list` | `list` (image beside text), `grid` (cards) or `compact` (dates and titles). |
278
+ | `groupBy` | none | `year` or `month` puts a heading ("2026", "October 2026") above each year's or month's posts. |
279
+ | `showTags` | `true` | `false` hides tag chips on cards. |
280
+ | `showCategory` | `true` | `false` hides the category above each card's title. |
281
+ | `categoryList` | `true` | A row of categories in the browse panel, each with its count. `false` leaves it out. |
282
+ | `categoryLimit` | `8` | Categories shown before "+N more", most used first. `0` shows them all. |
283
+ | `tagList` | `true` | A row of keywords in the browse panel, each with its count. `false` leaves it out. |
284
+ | `tagLimit` | `12` | Keywords shown before "+N more", most used first. `0` shows them all. |
285
+ | `tagMinCount` | `1` | Leave keywords used by fewer posts out of the browse panel. They still show on cards. |
286
+ | `archive` | `true` | Rows of years, and each year's months, in the browse panel. `false` leaves them out. |
287
+ | `pageSize` | `10` (the `pageSize` option) | Posts per page. `0` shows them all on one page. |
288
+ | `pinLimit` | all (the `pinLimit` option) | Most pinned posts kept at the top. Extra ones take their place by date. |
289
+ | `rssLink` | `true` (the `rssLink` option) | A "Subscribe" link with the RSS icon to this blog's feed. `false` leaves it out. |
290
+ | `feed` | on | `false` turns the feed off, or `{ title, description, limit, path }` to set it up. |
288
291
 
289
292
  ### Pinned posts
290
293
 
@@ -328,7 +331,8 @@ Filters combine: `?category=engineering&tag=api&year=2026` shows the
328
331
  engineering posts tagged `api` from 2026. Pages count the matching posts only.
329
332
 
330
333
  **The browse panel** above the posts has a row each for categories, keywords
331
- and the archive, each chip with its post count:
334
+ and the archive, each chip with its post count. It is a flat toolbar between
335
+ two rules, with the blog's "Subscribe" link at the end of its first row.
332
336
 
333
337
  - **Chips combine.** A chip adds its filter to the ones in view; another chip
334
338
  in the same row replaces it, and clicking the active chip takes it off again.
@@ -338,8 +342,18 @@ and the archive, each chip with its post count:
338
342
  - **Years open into months.** Choosing a year shows a row of its months below
339
343
  the years (with only one year, its months always show). A month keeps its
340
344
  year active, and clicking it again steps back out to the whole year.
341
- - Rows only appear when they have something to choose: no categories means no
342
- category row, and the archive needs posts from more than one month.
345
+ - **It stays short with many values.** Categories and keywords are ranked by
346
+ how many posts use them, and a row shows its first `categoryLimit` (8) or
347
+ `tagLimit` (12), then "+N more". That opens the full list alphabetically,
348
+ and a row of more than 30 values also gets a field to narrow it. An active
349
+ chip always shows. `tagMinCount: 2` leaves out keywords used only once.
350
+ - **One line per row on phones.** On screens narrower than 640px, each row is
351
+ a single line that scrolls sideways, its end fading while there is more.
352
+ Chips are 32px high on touch screens.
353
+ - **Rows only appear when they can narrow the posts.** No categories, or one
354
+ category that every post is in, means no category row. The archive needs
355
+ posts from more than one month, and with only one year, it is that year's
356
+ months alone.
343
357
 
344
358
  **The active filters** show above the posts while any filter is on: how many
345
359
  posts match ("4 posts", or "No posts match these filters."), a chip for each
@@ -552,6 +566,10 @@ withBlog(config, {
552
566
  noMatches: 'Keine Beiträge für diese Filter.',
553
567
  removeFilter: 'Filter entfernen: {filter}',
554
568
  showAll: 'Alle Beiträge zeigen',
569
+ more: '+{n} weitere',
570
+ less: 'Weniger zeigen',
571
+ filter: '{row} filtern',
572
+ noFilterMatches: 'Keine Treffer.',
555
573
  previous: 'Zurück',
556
574
  next: 'Weiter',
557
575
  pages: 'Seiten',
@@ -575,7 +593,7 @@ mode. Override these to restyle it without new rules:
575
593
  | `--vp-blog-tag-bg` | `var(--vp-c-default-soft)` |
576
594
  | `--vp-blog-tag-color` | `var(--vp-c-text-2)` |
577
595
  | `--vp-blog-accent` | `var(--vp-c-brand-1)` |
578
- | `--vp-blog-browse-bg` | `var(--vp-c-bg-soft)` |
596
+ | `--vp-blog-browse-bg` | `transparent` |
579
597
  | `--vp-blog-facet-label-width` | `6.5rem` |
580
598
  | `--vp-blog-announcement-height` | `40px` |
581
599
  | `--vp-blog-announcement-bg` | `var(--vp-button-brand-bg)` |
@@ -220,6 +220,10 @@ var DEFAULT_LABELS = {
220
220
  noMatches: "No posts match these filters.",
221
221
  removeFilter: "Remove filter: {filter}",
222
222
  showAll: "Show all posts",
223
+ more: "+{n} more",
224
+ less: "Show fewer",
225
+ filter: "Filter {row}",
226
+ noFilterMatches: "Nothing matches.",
223
227
  previous: "Previous",
224
228
  next: "Next",
225
229
  pages: "Pages",
@@ -357,7 +361,8 @@ function renderRssLink(index, context, show = index.index.rssLink) {
357
361
  if (!show || !context.feeds || !index.index.feed) return "";
358
362
  const title = fill(context.labels.rssFeed, { blog: index.title }).replace(/"/g, "&quot;");
359
363
  const url = toMarkdownUrl(`/${toFeedPath(index)}`);
360
- 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>`;
361
366
  }
362
367
  function fill(template, values) {
363
368
  return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
@@ -552,60 +557,73 @@ function renderChip(name, count, url, title = "") {
552
557
  const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
553
558
  return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
554
559
  }
555
- function renderFacet(facet, label, chips, ariaLabel = label, attributes = "") {
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}"` : "";
556
563
  return [
557
- `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${attributes}>`,
564
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${limited}${attributes}>`,
558
565
  `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
559
566
  '<div class="vp-blog-facet__chips">',
560
567
  "",
561
568
  chips.join(" "),
562
569
  "",
563
570
  "</div>",
571
+ ...block("vp-blog-facet__aside", aside),
564
572
  "</div>"
565
573
  ];
566
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
+ }
567
581
  function renderBrowse(posts, index, context) {
568
582
  const { labels } = context;
569
583
  const { locale, timeZone } = context.dates;
584
+ const { categoryList, categoryLimit, tagList, tagLimit, tagMinCount, archive: showArchive } = index.index;
570
585
  const blog = index.routePath;
571
586
  const rows = [];
572
- const categories = index.index.categoryList ? collectCategories(posts) : [];
573
- if (categories.length) {
587
+ const categories = categoryList ? collectCategories(posts).sort(byUse) : [];
588
+ if (canNarrow(categories, posts.length)) {
574
589
  const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
575
- rows.push(...renderFacet("category", labels.categories, chips));
590
+ rows.push({ facet: "category", label: labels.categories, chips, limit: categoryLimit });
576
591
  }
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));
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 });
581
596
  }
582
- const archive = index.index.archive ? collectArchive(posts, timeZone) : [];
583
- if (archive.reduce((months, year) => months + year.months.length, 0) > 1) {
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) {
584
612
  const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
585
- rows.push(...renderFacet("year", labels.archive, years));
613
+ rows.push({ facet: "year", label: labels.archive, chips: years });
586
614
  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
- );
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 });
605
618
  }
606
619
  }
607
620
  if (rows.length === 0) return [];
608
- return [`<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`, ...rows, "</nav>"];
621
+ rows[0].aside = renderRssLink(index, context);
622
+ return [
623
+ `<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`,
624
+ ...rows.flatMap(renderFacet),
625
+ "</nav>"
626
+ ];
609
627
  }
610
628
  function renderClientAttributes(index, context) {
611
629
  const { labels } = context;
@@ -623,7 +641,11 @@ function renderClientAttributes(index, context) {
623
641
  "data-label-post-count": labels.postCount,
624
642
  "data-label-post-count-one": labels.postCountOne,
625
643
  "data-label-no-matches": labels.noMatches,
626
- "data-label-remove-filter": labels.removeFilter
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
627
649
  };
628
650
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
629
651
  }
@@ -631,10 +653,10 @@ function renderPostList(pinned, rest, index, context) {
631
653
  const all = [...pinned, ...rest];
632
654
  if (all.length === 0) return `<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>
633
655
  `;
656
+ const browse = renderBrowse(all, index, context);
634
657
  const lines = [
635
658
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
636
- ...block("vp-blog-subscribe", renderRssLink(index, context)),
637
- ...renderBrowse(all, index, context),
659
+ ...browse.length ? browse : block("vp-blog-subscribe", renderRssLink(index, context)),
638
660
  '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
639
661
  ];
640
662
  if (pinned.length)
@@ -695,6 +717,8 @@ var FENCED_CODE = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[`~]*[ \t]*$|
695
717
  var HEADING_1 = /^ {0,3}#[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/m;
696
718
  var TAG_SYNTAX = /\(\(<?tag([|/])[^|/)\r\n]*(?:\1[^|/)\r\n]*){0,2}\)\)/g;
697
719
  var MAX_SUMMARY_LENGTH = 200;
720
+ var DEFAULT_CATEGORY_LIMIT = 8;
721
+ var DEFAULT_TAG_LIMIT = 12;
698
722
  function readFrontmatter(raw) {
699
723
  const source = raw.replace(/^/, "");
700
724
  const match = FRONTMATTER.exec(source);
@@ -865,7 +889,10 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
865
889
  showTags: options.showTags !== false,
866
890
  showCategory: options.showCategory !== false,
867
891
  categoryList: options.categoryList !== false,
892
+ categoryLimit: toCount(options.categoryLimit, DEFAULT_CATEGORY_LIMIT),
868
893
  tagList: options.tagList !== false,
894
+ tagLimit: toCount(options.tagLimit, DEFAULT_TAG_LIMIT),
895
+ tagMinCount: toCount(options.tagMinCount, 1),
869
896
  archive: options.archive !== false,
870
897
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
871
898
  pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
@@ -1228,4 +1255,4 @@ export {
1228
1255
  hasWidget,
1229
1256
  blogMarkdown
1230
1257
  };
1231
- //# sourceMappingURL=chunk-ZM5FOUT4.js.map
1258
+ //# sourceMappingURL=chunk-FPFH7ORO.js.map