@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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { B as BlogAnnouncementData, a as BlogAnnouncementItem, b as BlogAuthor, c as BlogFeedConfig, d as BlogGroupBy, e as BlogIndexConfig, f as BlogLabels, g as BlogLayout, h as BlogOptions, i as BlogPostData, j as BlogScope, D as DEFAULT_LABELS, k as blogMarkdown } from './plugin-DUTUA_t4.cjs';
1
+ export { B as BlogAnnouncementData, a as BlogAnnouncementItem, b as BlogAuthor, c as BlogFeedConfig, d as BlogGroupBy, e as BlogIndexConfig, f as BlogLabels, g as BlogLayout, h as BlogOptions, i as BlogPostData, j as BlogScope, D as DEFAULT_LABELS, k as blogMarkdown } from './plugin-kzO68Abz.cjs';
2
2
  import 'markdown-it';
3
3
 
4
4
  /** Marker a blog page can use to say where its list of posts goes. Without it the list is appended. */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { B as BlogAnnouncementData, a as BlogAnnouncementItem, b as BlogAuthor, c as BlogFeedConfig, d as BlogGroupBy, e as BlogIndexConfig, f as BlogLabels, g as BlogLayout, h as BlogOptions, i as BlogPostData, j as BlogScope, D as DEFAULT_LABELS, k as blogMarkdown } from './plugin-DUTUA_t4.js';
1
+ export { B as BlogAnnouncementData, a as BlogAnnouncementItem, b as BlogAuthor, c as BlogFeedConfig, d as BlogGroupBy, e as BlogIndexConfig, f as BlogLabels, g as BlogLayout, h as BlogOptions, i as BlogPostData, j as BlogScope, D as DEFAULT_LABELS, k as blogMarkdown } from './plugin-kzO68Abz.js';
2
2
  import 'markdown-it';
3
3
 
4
4
  /** Marker a blog page can use to say where its list of posts goes. Without it the list is appended. */
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  POST_FRONTMATTER_KEY,
6
6
  WIDGET_NAME,
7
7
  blogMarkdown
8
- } from "./chunk-ZM5FOUT4.js";
8
+ } from "./chunk-FPFH7ORO.js";
9
9
  import "./chunk-42Z6H7IN.js";
10
10
  export {
11
11
  BLOG_POSTS_PLACEHOLDER,
@@ -77,7 +77,7 @@ interface BlogOptions {
77
77
  * on a site with a site URL (without one, no feeds are written). Default: `true`.
78
78
  */
79
79
  rssLink?: boolean;
80
- /** Words the plugin writes on pages, for translating. `{n}`, `{blog}` and `{filter}` are filled in. */
80
+ /** Words the plugin writes on pages, for translating. `{n}`, `{blog}`, `{filter}` and `{row}` are filled in. */
81
81
  labels?: Partial<BlogLabels>;
82
82
  }
83
83
  interface BlogLabels {
@@ -131,6 +131,14 @@ interface BlogLabels {
131
131
  removeFilter: string;
132
132
  /** Link that clears every filter. */
133
133
  showAll: string;
134
+ /** Button that shows the rest of a browse row: "+{n} more". */
135
+ more: string;
136
+ /** Button that shows a browse row's first values again. */
137
+ less: string;
138
+ /** Accessible name of the field that narrows a long browse row: "Filter {row}". */
139
+ filter: string;
140
+ /** When nothing in a browse row matches the filter field. */
141
+ noFilterMatches: string;
134
142
  previous: string;
135
143
  next: string;
136
144
  /** Accessible name of the page links. */
@@ -173,8 +181,14 @@ interface BlogIndexConfig {
173
181
  showCategory: boolean;
174
182
  /** List every category above the posts, each one a filter. */
175
183
  categoryList: boolean;
184
+ /** Categories shown before "+N more", most used first. `0` shows them all. */
185
+ categoryLimit: number;
176
186
  /** List every keyword above the posts, each one a filter. */
177
187
  tagList: boolean;
188
+ /** Keywords shown before "+N more", most used first. `0` shows them all. */
189
+ tagLimit: number;
190
+ /** Leave keywords used by fewer posts than this out of the browse panel. They still show on cards. */
191
+ tagMinCount: number;
178
192
  /** List the years and months that have posts above the posts, each one a filter. */
179
193
  archive: boolean;
180
194
  /** Posts per page. `0` shows them all on one page. */
@@ -77,7 +77,7 @@ interface BlogOptions {
77
77
  * on a site with a site URL (without one, no feeds are written). Default: `true`.
78
78
  */
79
79
  rssLink?: boolean;
80
- /** Words the plugin writes on pages, for translating. `{n}`, `{blog}` and `{filter}` are filled in. */
80
+ /** Words the plugin writes on pages, for translating. `{n}`, `{blog}`, `{filter}` and `{row}` are filled in. */
81
81
  labels?: Partial<BlogLabels>;
82
82
  }
83
83
  interface BlogLabels {
@@ -131,6 +131,14 @@ interface BlogLabels {
131
131
  removeFilter: string;
132
132
  /** Link that clears every filter. */
133
133
  showAll: string;
134
+ /** Button that shows the rest of a browse row: "+{n} more". */
135
+ more: string;
136
+ /** Button that shows a browse row's first values again. */
137
+ less: string;
138
+ /** Accessible name of the field that narrows a long browse row: "Filter {row}". */
139
+ filter: string;
140
+ /** When nothing in a browse row matches the filter field. */
141
+ noFilterMatches: string;
134
142
  previous: string;
135
143
  next: string;
136
144
  /** Accessible name of the page links. */
@@ -173,8 +181,14 @@ interface BlogIndexConfig {
173
181
  showCategory: boolean;
174
182
  /** List every category above the posts, each one a filter. */
175
183
  categoryList: boolean;
184
+ /** Categories shown before "+N more", most used first. `0` shows them all. */
185
+ categoryLimit: number;
176
186
  /** List every keyword above the posts, each one a filter. */
177
187
  tagList: boolean;
188
+ /** Keywords shown before "+N more", most used first. `0` shows them all. */
189
+ tagLimit: number;
190
+ /** Leave keywords used by fewer posts than this out of the browse panel. They still show on cards. */
191
+ tagMinCount: number;
178
192
  /** List the years and months that have posts above the posts, each one a filter. */
179
193
  archive: boolean;
180
194
  /** Posts per page. `0` shows them all on one page. */
package/dist/style.css CHANGED
@@ -10,7 +10,7 @@
10
10
  --vp-blog-tag-bg: var(--vp-c-default-soft);
11
11
  --vp-blog-tag-color: var(--vp-c-text-2);
12
12
  --vp-blog-accent: var(--vp-c-brand-1);
13
- --vp-blog-browse-bg: var(--vp-c-bg-soft);
13
+ --vp-blog-browse-bg: transparent;
14
14
  /* Width of the label column in the browse panel, so every row's chips line up. */
15
15
  --vp-blog-facet-label-width: 6.5rem;
16
16
  --vp-blog-announcement-height: 40px;
@@ -192,7 +192,7 @@
192
192
  .vp-blog-tag,
193
193
  .vp-blog-category,
194
194
  .vp-blog-badge,
195
- .vp-blog-facet a,
195
+ .vp-blog-facet__chips a,
196
196
  .vp-blog-card__tags a,
197
197
  .vp-blog-byline__category a,
198
198
  .vp-blog-byline__tags a {
@@ -204,11 +204,22 @@
204
204
  color: var(--vp-blog-tag-color);
205
205
  font-size: 12px;
206
206
  font-weight: 500;
207
- line-height: 22px;
207
+ /* 24px: the smallest target WCAG 2.2 AA allows. Touch screens get more below. */
208
+ line-height: 24px;
209
+ }
210
+
211
+ /* Chips you tap are 32px high on touch screens. */
212
+ @media (pointer: coarse) {
213
+ .vp-blog-facet__chips a,
214
+ .vp-blog-card__tags a,
215
+ .vp-blog-byline__category a,
216
+ .vp-blog-byline__tags a {
217
+ line-height: 32px;
218
+ }
208
219
  }
209
220
 
210
221
  /* Chips are links that filter their blog. */
211
- .vp-blog .vp-blog-facet a,
222
+ .vp-blog .vp-blog-facet__chips a,
212
223
  .vp-blog .vp-blog-card__tags a,
213
224
  .vp-blog-byline__category a,
214
225
  .vp-blog-byline__tags a {
@@ -219,11 +230,11 @@
219
230
  opacity 0.25s;
220
231
  }
221
232
 
222
- .vp-blog .vp-blog-facet a:hover,
233
+ .vp-blog .vp-blog-facet__chips a:hover,
223
234
  .vp-blog .vp-blog-card__tags a:hover,
224
235
  .vp-blog-byline__category a:hover,
225
236
  .vp-blog-byline__tags a:hover,
226
- .vp-blog .vp-blog-facet a.is-active,
237
+ .vp-blog .vp-blog-facet__chips a.is-active,
227
238
  .vp-blog .vp-blog-card__tags a.is-active {
228
239
  background: var(--vp-c-brand-soft);
229
240
  color: var(--vp-blog-accent);
@@ -311,24 +322,27 @@
311
322
 
312
323
  /*
313
324
  * The browse panel: one row each for categories, keywords and years, and the
314
- * months of the year in view below the years. Labels sit in a column of their
315
- * own on wide screens, above their chips on narrow ones.
325
+ * months of the year in view below the years. A flat toolbar between two
326
+ * rules, so it reads as a tool rather than another post. On wide screens the
327
+ * labels sit in a column of their own and chips wrap; on narrow ones each row
328
+ * is a single line that scrolls sideways.
316
329
  */
317
330
  .vp-blog-browse {
318
331
  display: flex;
319
332
  flex-direction: column;
320
- gap: 10px;
333
+ gap: 8px;
321
334
  margin: 16px 0;
322
- padding: 14px 16px;
323
- border: 1px solid var(--vp-blog-card-border);
324
- border-radius: var(--vp-blog-card-radius);
335
+ padding: 12px 0;
336
+ border-top: 1px solid var(--vp-c-divider);
337
+ border-bottom: 1px solid var(--vp-c-divider);
325
338
  background: var(--vp-blog-browse-bg);
326
339
  }
327
340
 
328
341
  .vp-blog-facet {
329
342
  display: grid;
330
- grid-template-columns: var(--vp-blog-facet-label-width) minmax(0, 1fr);
331
- gap: 2px 12px;
343
+ /* Label, chips, then "+N more" and the "Subscribe" link when the row has them. */
344
+ grid-template-columns: var(--vp-blog-facet-label-width) minmax(0, 1fr) auto auto;
345
+ gap: 4px 12px;
332
346
  align-items: baseline;
333
347
  }
334
348
 
@@ -336,19 +350,33 @@
336
350
  color: var(--vp-blog-meta-color);
337
351
  font-size: 13px;
338
352
  font-weight: 600;
339
- line-height: 26px;
353
+ line-height: 28px;
340
354
  }
341
355
 
342
- /* A year's months: tucked under the years, their label the year. */
356
+ /* A year's months, under the years: their label is the year. */
343
357
  .vp-blog-facet--month .vp-blog-facet__label {
344
358
  font-variant-numeric: tabular-nums;
345
359
  font-weight: 500;
346
360
  }
347
361
 
362
+ .vp-blog-facet__chips {
363
+ min-width: 0;
364
+ }
365
+
348
366
  .vp-blog-facet__chips p {
367
+ display: flex;
368
+ flex-wrap: wrap;
369
+ gap: 4px 6px;
370
+ align-items: center;
349
371
  margin: 0;
350
372
  }
351
373
 
374
+ .vp-blog .vp-blog-facet__chips a {
375
+ flex: none;
376
+ margin: 0;
377
+ white-space: nowrap;
378
+ }
379
+
352
380
  /* The post count in a chip: a small counter badge inside it. */
353
381
  .vp-blog-facet__count {
354
382
  display: inline-block;
@@ -369,29 +397,125 @@
369
397
  color 0.25s;
370
398
  }
371
399
 
372
- .vp-blog-facet a:hover .vp-blog-facet__count,
373
- .vp-blog-facet a.is-active .vp-blog-facet__count {
400
+ .vp-blog-facet__chips a:hover .vp-blog-facet__count,
401
+ .vp-blog-facet__chips a.is-active .vp-blog-facet__count {
374
402
  background: var(--vp-blog-accent);
375
403
  /* The page background, so the number stays readable on the accent in light and dark mode. */
376
404
  color: var(--vp-c-bg);
377
405
  }
378
406
 
379
407
  /* A chip that would show no posts with the other filters kept. */
380
- .vp-blog .vp-blog-facet a.is-empty {
408
+ .vp-blog .vp-blog-facet__chips a.is-empty {
381
409
  opacity: 0.45;
382
410
  }
383
411
 
384
- .vp-blog .vp-blog-facet a.is-empty:hover {
412
+ .vp-blog .vp-blog-facet__chips a.is-empty:hover {
385
413
  opacity: 0.8;
386
414
  }
387
415
 
416
+ /* "+N more" and "Show fewer": a quiet text button at the end of the row's first line. */
417
+ .vp-blog-facet__more {
418
+ justify-self: start;
419
+ padding: 0 6px;
420
+ border-radius: 6px;
421
+ color: var(--vp-blog-accent);
422
+ font-size: 12px;
423
+ font-weight: 600;
424
+ line-height: 28px;
425
+ white-space: nowrap;
426
+ cursor: pointer;
427
+ }
428
+
429
+ .vp-blog-facet__more:hover {
430
+ background: var(--vp-c-brand-soft);
431
+ }
432
+
433
+ /* The field that narrows a long open row, above its chips. */
434
+ .vp-blog-facet__filter {
435
+ display: block;
436
+ box-sizing: border-box;
437
+ width: min(100%, 18rem);
438
+ margin: 0 0 8px;
439
+ padding: 4px 10px;
440
+ border: 1px solid var(--vp-c-divider);
441
+ border-radius: 8px;
442
+ background: var(--vp-c-bg);
443
+ color: var(--vp-c-text-1);
444
+ font-size: 13px;
445
+ line-height: 20px;
446
+ }
447
+
448
+ .vp-blog-facet__filter:focus {
449
+ border-color: var(--vp-blog-accent);
450
+ outline: none;
451
+ }
452
+
453
+ .vp-blog-facet__empty {
454
+ margin: 0;
455
+ color: var(--vp-blog-meta-color);
456
+ font-size: 13px;
457
+ }
458
+
459
+ /* The "Subscribe" link at the end of the first row. */
460
+ .vp-blog-facet__aside p {
461
+ margin: 0;
462
+ white-space: nowrap;
463
+ }
464
+
465
+ @media (pointer: coarse) {
466
+ .vp-blog-facet__more {
467
+ line-height: 32px;
468
+ }
469
+
470
+ .vp-blog-facet__label {
471
+ line-height: 32px;
472
+ }
473
+ }
474
+
388
475
  @media (max-width: 639px) {
389
476
  .vp-blog-facet {
390
- grid-template-columns: minmax(0, 1fr);
477
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
478
+ gap: 4px 8px;
479
+ align-items: center;
391
480
  }
392
481
 
393
482
  .vp-blog-facet__label {
394
- line-height: 20px;
483
+ font-size: 12px;
484
+ }
485
+
486
+ /* One line per row, scrolled sideways, its end fading while there is more. */
487
+ .vp-blog-facet:not(.is-open) .vp-blog-facet__chips {
488
+ overflow-x: auto;
489
+ overscroll-behavior-x: contain;
490
+ scrollbar-width: none;
491
+ }
492
+
493
+ .vp-blog-facet:not(.is-open) .vp-blog-facet__chips::-webkit-scrollbar {
494
+ display: none;
495
+ }
496
+
497
+ .vp-blog-facet:not(.is-open) .vp-blog-facet__chips p {
498
+ flex-wrap: nowrap;
499
+ }
500
+
501
+ .vp-blog-facet.is-scrollable:not(.is-open) .vp-blog-facet__chips {
502
+ mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
503
+ }
504
+
505
+ /* An open row wraps under its label, at full width. */
506
+ .vp-blog-facet.is-open .vp-blog-facet__chips {
507
+ grid-column: 1 / -1;
508
+ grid-row: 2;
509
+ }
510
+
511
+ /* The RSS icon alone, its words kept for screen readers. */
512
+ .vp-blog-facet__aside .vp-blog-rss__text {
513
+ position: absolute;
514
+ width: 1px;
515
+ height: 1px;
516
+ overflow: hidden;
517
+ clip-path: inset(50%);
518
+ white-space: nowrap;
395
519
  }
396
520
  }
397
521
 
@@ -160,6 +160,10 @@ var DEFAULT_LABELS = {
160
160
  noMatches: "No posts match these filters.",
161
161
  removeFilter: "Remove filter: {filter}",
162
162
  showAll: "Show all posts",
163
+ more: "+{n} more",
164
+ less: "Show fewer",
165
+ filter: "Filter {row}",
166
+ noFilterMatches: "Nothing matches.",
163
167
  previous: "Previous",
164
168
  next: "Next",
165
169
  pages: "Pages",
@@ -347,6 +351,8 @@ var FENCED_CODE = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[`~]*[ \t]*$|
347
351
  var HEADING_1 = /^ {0,3}#[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/m;
348
352
  var TAG_SYNTAX = /\(\(<?tag([|/])[^|/)\r\n]*(?:\1[^|/)\r\n]*){0,2}\)\)/g;
349
353
  var MAX_SUMMARY_LENGTH = 200;
354
+ var DEFAULT_CATEGORY_LIMIT = 8;
355
+ var DEFAULT_TAG_LIMIT = 12;
350
356
  function readFrontmatter(raw) {
351
357
  const source = raw.replace(/^/, "");
352
358
  const match = FRONTMATTER.exec(source);
@@ -517,7 +523,10 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
517
523
  showTags: options.showTags !== false,
518
524
  showCategory: options.showCategory !== false,
519
525
  categoryList: options.categoryList !== false,
526
+ categoryLimit: toCount(options.categoryLimit, DEFAULT_CATEGORY_LIMIT),
520
527
  tagList: options.tagList !== false,
528
+ tagLimit: toCount(options.tagLimit, DEFAULT_TAG_LIMIT),
529
+ tagMinCount: toCount(options.tagMinCount, 1),
521
530
  archive: options.archive !== false,
522
531
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
523
532
  pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
@@ -648,7 +657,8 @@ function renderRssLink(index, context, show = index.index.rssLink) {
648
657
  if (!show || !context.feeds || !index.index.feed) return "";
649
658
  const title = fill(context.labels.rssFeed, { blog: index.title }).replace(/"/g, "&quot;");
650
659
  const url = toMarkdownUrl(`/${toFeedPath(index)}`);
651
- return `<span class="vp-blog-rss">[${RSS_ICON} ${escapeMarkdown(context.labels.subscribe)}](${url} "${title}")</span>`;
660
+ const text = `<span class="vp-blog-rss__text">${escapeMarkdown(context.labels.subscribe)}</span>`;
661
+ return `<span class="vp-blog-rss">[${RSS_ICON} ${text}](${url} "${title}")</span>`;
652
662
  }
653
663
  function fill(template, values) {
654
664
  return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
@@ -843,60 +853,73 @@ function renderChip(name, count, url, title = "") {
843
853
  const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
844
854
  return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
845
855
  }
846
- function renderFacet(facet, label, chips, ariaLabel = label, attributes = "") {
856
+ function renderFacet(row) {
857
+ const { facet, label, chips, ariaLabel = label, limit = 0, attributes = "", aside = "" } = row;
858
+ const limited = limit && chips.length > limit ? ` data-limit="${limit}"` : "";
847
859
  return [
848
- `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${attributes}>`,
860
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${limited}${attributes}>`,
849
861
  `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
850
862
  '<div class="vp-blog-facet__chips">',
851
863
  "",
852
864
  chips.join(" "),
853
865
  "",
854
866
  "</div>",
867
+ ...block("vp-blog-facet__aside", aside),
855
868
  "</div>"
856
869
  ];
857
870
  }
871
+ function canNarrow(values, total) {
872
+ return values.length > 1 || values.length === 1 && values[0].count < total;
873
+ }
874
+ function byUse(a, b) {
875
+ return b.count - a.count || a.value.localeCompare(b.value, void 0, { sensitivity: "base" });
876
+ }
858
877
  function renderBrowse(posts, index, context) {
859
878
  const { labels } = context;
860
879
  const { locale, timeZone } = context.dates;
880
+ const { categoryList, categoryLimit, tagList, tagLimit, tagMinCount, archive: showArchive } = index.index;
861
881
  const blog = index.routePath;
862
882
  const rows = [];
863
- const categories = index.index.categoryList ? collectCategories(posts) : [];
864
- if (categories.length) {
883
+ const categories = categoryList ? collectCategories(posts).sort(byUse) : [];
884
+ if (canNarrow(categories, posts.length)) {
865
885
  const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
866
- rows.push(...renderFacet("category", labels.categories, chips));
886
+ rows.push({ facet: "category", label: labels.categories, chips, limit: categoryLimit });
867
887
  }
868
- const tags = index.index.tagList ? collectTags(posts) : [];
869
- if (tags.length) {
870
- const chips = tags.map(({ tag, count }) => renderChip(tag, count, toFilterUrl(blog, "tag", tag)));
871
- rows.push(...renderFacet("tag", labels.tags, chips));
888
+ const tags = tagList ? collectTags(posts).map(({ tag, count }) => ({ value: tag, count })).filter(({ count }) => count >= tagMinCount).sort(byUse) : [];
889
+ if (canNarrow(tags, posts.length)) {
890
+ const chips = tags.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "tag", value)));
891
+ rows.push({ facet: "tag", label: labels.tags, chips, limit: tagLimit });
872
892
  }
873
- const archive = index.index.archive ? collectArchive(posts, timeZone) : [];
874
- if (archive.reduce((months, year) => months + year.months.length, 0) > 1) {
893
+ const archive = showArchive ? collectArchive(posts, timeZone) : [];
894
+ const monthChips = (months) => months.map(
895
+ ({ value, count }) => renderChip(
896
+ formatMonth(value, locale, { month: "short" }),
897
+ count,
898
+ toFilterUrl(blog, "month", value),
899
+ formatMonth(value, locale, { month: "long", year: "numeric" })
900
+ )
901
+ );
902
+ if (archive.length === 1 && canNarrow(archive[0].months, posts.length)) {
903
+ const [year] = archive;
904
+ const ariaLabel = `${labels.archive} ${year.value}`;
905
+ const attributes = ` data-year="${year.value}"`;
906
+ rows.push({ facet: "month", label: labels.archive, chips: monthChips(year.months), ariaLabel, attributes });
907
+ } else if (archive.length > 1) {
875
908
  const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
876
- rows.push(...renderFacet("year", labels.archive, years));
909
+ rows.push({ facet: "year", label: labels.archive, chips: years });
877
910
  for (const year of archive) {
878
- const months = year.months.map(
879
- ({ value, count }) => renderChip(
880
- formatMonth(value, locale, { month: "short" }),
881
- count,
882
- toFilterUrl(blog, "month", value),
883
- formatMonth(value, locale, { month: "long", year: "numeric" })
884
- )
885
- );
886
- const shown = archive.length === 1 ? "" : " hidden";
887
- rows.push(
888
- ...renderFacet(
889
- "month",
890
- year.value,
891
- months,
892
- `${labels.archive} ${year.value}`,
893
- ` data-year="${year.value}"${shown}`
894
- )
895
- );
911
+ const ariaLabel = `${labels.archive} ${year.value}`;
912
+ const attributes = ` data-year="${year.value}" hidden`;
913
+ rows.push({ facet: "month", label: year.value, chips: monthChips(year.months), ariaLabel, attributes });
896
914
  }
897
915
  }
898
916
  if (rows.length === 0) return [];
899
- return [`<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`, ...rows, "</nav>"];
917
+ rows[0].aside = renderRssLink(index, context);
918
+ return [
919
+ `<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`,
920
+ ...rows.flatMap(renderFacet),
921
+ "</nav>"
922
+ ];
900
923
  }
901
924
  function renderClientAttributes(index, context) {
902
925
  const { labels } = context;
@@ -914,7 +937,11 @@ function renderClientAttributes(index, context) {
914
937
  "data-label-post-count": labels.postCount,
915
938
  "data-label-post-count-one": labels.postCountOne,
916
939
  "data-label-no-matches": labels.noMatches,
917
- "data-label-remove-filter": labels.removeFilter
940
+ "data-label-remove-filter": labels.removeFilter,
941
+ "data-label-more": labels.more,
942
+ "data-label-less": labels.less,
943
+ "data-label-filter": labels.filter,
944
+ "data-label-no-filter-matches": labels.noFilterMatches
918
945
  };
919
946
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
920
947
  }
@@ -922,10 +949,10 @@ function renderPostList(pinned, rest, index, context) {
922
949
  const all = [...pinned, ...rest];
923
950
  if (all.length === 0) return `<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>
924
951
  `;
952
+ const browse = renderBrowse(all, index, context);
925
953
  const lines = [
926
954
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
927
- ...block("vp-blog-subscribe", renderRssLink(index, context)),
928
- ...renderBrowse(all, index, context),
955
+ ...browse.length ? browse : block("vp-blog-subscribe", renderRssLink(index, context)),
929
956
  '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
930
957
  ];
931
958
  if (pinned.length)