@binarynoir/vitepress-blog 0.2.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/README.md CHANGED
@@ -33,9 +33,9 @@ blog:
33
33
  ```
34
34
 
35
35
  - **Blog pages** (`blog-index` in the frontmatter) get a list of posts: cards
36
- with image, title, date, reading time, authors, summary and tags. Sticky
37
- posts are pinned to the top, the rest are split into pages, and a keyword
38
- list above them filters the posts by tag.
36
+ with image, title, date, reading time, authors, summary and tags. Pinned
37
+ posts stay at the top, the rest are split into pages, and a browse
38
+ panel above them filters the posts by category, keyword, year and month.
39
39
  - **Posts** (`blog` in the frontmatter) get a byline under their title, links to
40
40
  the newer and older posts, and Open Graph and Twitter card tags.
41
41
  - **Every blog page gets an RSS feed**, written beside it at build time and
@@ -43,8 +43,8 @@ blog:
43
43
  - Lists are plain Markdown generated at build time, with every post in the
44
44
  HTML, so they show up in search and work without JavaScript. Links go
45
45
  through VitePress, so they get your `base` and `cleanUrls` and are checked
46
- for dead links. A small composable, `useBlog()`, adds pages and keyword
47
- filtering in the browser.
46
+ for dead links. A small composable, `useBlog()`, adds pages and filtering
47
+ in the browser.
48
48
 
49
49
  ## Install
50
50
 
@@ -83,7 +83,7 @@ import '@binarynoir/vitepress-blog/style.css';
83
83
  export default {
84
84
  extends: DefaultTheme,
85
85
  setup() {
86
- // Pages (?page=2) and keyword filtering (?tag=release) on blog pages.
86
+ // Pages (?page=2) and filters (?category=, ?tag=, ?year=, ?month=) on blog pages.
87
87
  useBlog();
88
88
  },
89
89
  };
@@ -133,37 +133,57 @@ blog:
133
133
  image: ./launch.png
134
134
  imageAlt: The release dashboard
135
135
  author: [jane, Sam Lee]
136
- tags: [release, team-a]
137
- category: Engineering
136
+ tags: [team-a, dashboard]
137
+ category: Releases
138
138
  scope: site
139
- sticky: true
139
+ pinned: true
140
140
  ---
141
141
  ```
142
142
 
143
- | Key | Default | What it does |
144
- | --------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `title` | the page's `title`, then its `#` heading | Title in lists, feeds and social cards. |
146
- | `summary` | the page's `description`, then first prose | One or two sentences for cards, feeds and `<meta name="description">`. |
147
- | `date` | none | When the post was published: a date, or a date and time. See [Dates and times](#dates-and-times). Newest first; undated posts go last. |
148
- | `updated` | none | When the post was last changed, in the same forms. The byline shows both: "Published … · Updated …". It is the feed's last-updated time. |
149
- | `image` | none | Card, social and feed image: a path relative to the post, a public path (`/cover.png`) or a URL. `{ src, alt, fit, position }` also works. |
150
- | `imageAlt` | `''` | Alt text for the image. |
151
- | `imageFit` | `cover` | How cards fit the image in their frame: `cover` crops it to fill the frame, `contain` shows all of it, letterboxed. Use `contain` for logos and diagrams. |
152
- | `imagePosition` | the centre | Which part of a cropped image stays in view, as CSS `object-position`: `top`, `left`, `30% 20%`. |
153
- | `author` | the `defaultAuthor` option | A name, a key from the `authors` option, `{ name, url, email }`, or a list of these. `authors` works too. |
154
- | `tags` | none | A list or a comma-separated string. Shown as chips and written to the feed as categories. |
155
- | `category` | none | Written to the feed and `article:section`. |
156
- | `scope` | `section` | `site` (or `global`) also lists the post on the main blog. See [Scopes](#scopes). |
157
- | `sticky` | `false` | Pinned to the top of its blog pages, above the paginated posts, with a Pinned badge. See [Sticky posts](#sticky-posts). |
158
- | `draft` | `false` | Left out of lists and feeds. The page itself is still built: use `srcExclude` to hide it. |
159
- | `byline` | the `byline` option | `false` hides this post's byline. |
160
- | `cover` | the `cover` option | `true` shows this post's image under its byline. |
161
- | `postNav` | the `postNav` option | `false` hides this post's newer / older links. |
143
+ | Key | Default | What it does |
144
+ | --------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
+ | `title` | the page's `title`, then its `#` heading | Title in lists, feeds and social cards. |
146
+ | `summary` | the page's `description`, then first prose | One or two sentences for cards, feeds and `<meta name="description">`. |
147
+ | `date` | none | When the post was published: a date, or a date and time. See [Dates and times](#dates-and-times). Newest first; undated posts go last. |
148
+ | `updated` | none | When the post was last changed, in the same forms. The byline shows both: "Published … · Updated …". It is the feed's last-updated time. |
149
+ | `image` | none | Card, social and feed image: a path relative to the post, a public path (`/cover.png`) or a URL. `{ src, alt, fit, position }` also works. |
150
+ | `imageAlt` | `''` | Alt text for the image. |
151
+ | `imageFit` | `cover` | How cards fit the image in their frame: `cover` crops it to fill the frame, `contain` shows all of it, letterboxed. Use `contain` for logos and diagrams. |
152
+ | `imagePosition` | the centre | Which part of a cropped image stays in view, as CSS `object-position`: `top`, `left`, `30% 20%`. |
153
+ | `author` | the `defaultAuthor` option | A name, a key from the `authors` option, `{ name, url, email }`, or a list of these. `authors` works too. |
154
+ | `tags` | none | Any number of topics: a list or a comma-separated string. Shown as chips, each a filter, and written to the feed as categories. See [Category or tags?](#category-or-tags). |
155
+ | `category` | none | The one kind of post this is, such as `Releases`. Shown above its title on cards and in its byline, each a filter. Written to the feed and `article:section`. See [Category or tags?](#category-or-tags). |
156
+ | `scope` | `section` | `site` (or `global`) also lists the post on the main blog. See [Scopes](#scopes). |
157
+ | `pinned` | `false` | Kept at the top of its blog pages, above the paginated posts, with a Pinned badge. See [Pinned posts](#pinned-posts). |
158
+ | `announcement` | `false` | `true`, or the banner's text, shows the post in the announcement banner above the nav bar. See [Announcements](#announcements). |
159
+ | `draft` | `false` | Left out of lists and feeds. The page itself is still built: to leave drafts out of the build, the sidebar and the navbar too, see [Drafts](#drafts). |
160
+ | `byline` | the `byline` option | `false` hides this post's byline. |
161
+ | `cover` | the `cover` option | `true` shows this post's image under its byline. |
162
+ | `postNav` | the `postNav` option | `false` hides this post's newer / older links. |
162
163
 
163
164
  Title, description, date, author(s), image and tags may also be written at the
164
165
  top level of the frontmatter, as other blog tools expect. A value under `blog`
165
166
  wins.
166
167
 
168
+ ### Category or tags?
169
+
170
+ Both sort posts, in different ways:
171
+
172
+ - **`category` is the one way you slice the blog.** Each post has exactly one,
173
+ from a short list that rarely changes. It is the label above the title on
174
+ cards, and the first row of the browse panel. Pick the axis your readers
175
+ browse by most: the kind of post (Releases, Guides, News), the team, or the
176
+ product line.
177
+ - **`tags` are everything else a post is about.** A post can have several, and
178
+ new ones can appear at any time: the product it changes, the feature, the
179
+ topic.
180
+ - **Don't say the same thing twice.** If a post's category is Releases, a
181
+ `release` tag adds nothing.
182
+ - **`categories:` on a blog page is a filter, not a label.**
183
+ `blog-index: { categories: [Releases] }` makes a page that lists only
184
+ releases. `tags:` works the same way there, and both work in
185
+ `::: blog-posts` blocks.
186
+
167
187
  ### Dates and times
168
188
 
169
189
  `date` and `updated` take a date, or a date and a time, in any of these forms:
@@ -196,6 +216,35 @@ blog:
196
216
  - Feeds and social tags use the exact moment: `pubDate`, `lastBuildDate`,
197
217
  `article:published_time` and `article:modified_time`.
198
218
 
219
+ ### Drafts
220
+
221
+ `draft: true` keeps a post out of blog lists, `::: blog-posts` blocks and
222
+ feeds, but the page is still built and can be opened by its URL. With the
223
+ `drafts` option on, drafts are listed too, with a Draft badge, which is handy in
224
+ `vitepress dev`.
225
+
226
+ To leave drafts out of the build entirely, use
227
+ [`@binarynoir/vitepress-exclude`](https://binarynoir.tech/plugins/exclude/)'s
228
+ `excludeFrontmatter` option, and the same option in Auto Sidebar and Auto
229
+ Navbar to hide them from the menus. The blog lists only what VitePress builds,
230
+ so nothing else changes:
231
+
232
+ ```ts
233
+ // Drafts stay out of the production build, and show (marked) in `vitepress dev`.
234
+ const isBuild = process.env.NODE_ENV === 'production';
235
+ const excludeFrontmatter = isBuild ? ['draft', 'blog.draft'] : [];
236
+ const blogOptions = { drafts: !isBuild };
237
+
238
+ export default defineConfig({
239
+ srcExclude: excludeSrc(docsRoot, { excludeFrontmatter }),
240
+ themeConfig: {
241
+ sidebar: generateSidebar(docsRoot, { excludeFrontmatter }),
242
+ nav: generateNav(docsRoot, { excludeFrontmatter }),
243
+ },
244
+ // ...the blog's pieces, with blogOptions
245
+ });
246
+ ```
247
+
199
248
  ## Blog pages
200
249
 
201
250
  A page with `blog-index` in its frontmatter lists posts. `blog-index: true` is a
@@ -218,57 +267,140 @@ Notes from Team A.
218
267
 
219
268
  The list goes where `<!-- blog-posts -->` is, or after the page's content.
220
269
 
221
- | Key | Default | What it does |
222
- | ------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- |
223
- | `scope` | `section` | `site` makes this the main blog. See [Scopes](#scopes). |
224
- | `folders` | the page's folder | Folders to list posts from, relative to the source directory; `./` and `../` are page-relative. |
225
- | `tags` | all | Only list posts with at least one of these tags. |
226
- | `limit` | all | Most posts to list. The feed has its own limit. |
227
- | `layout` | `list` | `list` (image beside text), `grid` (cards) or `compact` (dates and titles). |
228
- | `groupBy` | none | `year` puts a heading above each year's posts. |
229
- | `showTags` | `true` | `false` hides tag chips on cards. |
230
- | `tagList` | `true` | A keyword list above the posts: every tag with its count, each a filter. `false` leaves it out. |
231
- | `pageSize` | `10` (the `pageSize` option) | Posts per page. `0` shows them all on one page. |
232
- | `stickyLimit` | all (the `stickyLimit` option) | Most sticky posts pinned to the top. Extra ones take their place by date. |
233
- | `rssLink` | `true` (the `rssLink` option) | A "Subscribe" link with the RSS icon to this blog's feed. `false` leaves it out. |
234
- | `feed` | on | `false` turns the feed off, or `{ title, description, limit, path }` to set it up. |
235
-
236
- ### Sticky posts
237
-
238
- `sticky: true` pins a post to the top of every blog page that lists it, in its
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. |
288
+
289
+ ### Pinned posts
290
+
291
+ `pinned: true` keeps a post at the top of every blog page that lists it, in its
239
292
  own section above the other posts, with a Pinned badge. Pinned posts show on
240
293
  the first page only and don't count toward `pageSize`. When more posts are
241
- sticky than a page's `stickyLimit`, the newest ones are pinned and the others
242
- take their place by date. Feeds ignore stickiness and stay in date order.
294
+ pinned than a page's `pinLimit`, the newest ones stay on top and the others
295
+ take their place by date. Feeds ignore pinning and stay in date order.
296
+
297
+ `sticky` and `stickyLimit`, the old names of `pinned` and `pinLimit`, are
298
+ still read, so existing posts and pages keep working.
243
299
 
244
- To keep sticky posts on top of an [Auto Sidebar](https://binarynoir.tech/plugins/auto-sidebar/)
245
- sidebar too, sort the blog folder by stickiness, then date:
300
+ To keep pinned posts on top of an [Auto Sidebar](https://binarynoir.tech/plugins/auto-sidebar/)
301
+ sidebar too, sort the blog folder by pinning, then date:
246
302
 
247
303
  ```txt
248
304
  # docs/blog/.sidebar
249
305
  index.md:All posts
250
- .sort: blog.sticky desc, date desc
306
+ .sort: blog.pinned desc, date desc
251
307
  ```
252
308
 
253
- The sidebar pins every sticky post; `stickyLimit` applies to blog pages.
309
+ The sidebar pins every pinned post; `pinLimit` applies to blog pages.
254
310
 
255
- ### Pages and keywords
311
+ ### Browsing: pages, categories, keywords and dates
256
312
 
257
313
  Every post is written into the blog page's HTML, so search engines, local
258
314
  search and readers without JavaScript see them all. With `useBlog()` in your
259
315
  theme (see [Usage](#usage)), the browser then shows one page at a time and
260
- filters by keyword, both from the URL:
316
+ filters the posts, all from the URL:
317
+
318
+ | URL | Shows |
319
+ | ------------------- | -------------------------------------------------------- |
320
+ | `?page=2` | The second page. Page links are added below the posts. |
321
+ | `?category=product` | Posts in the `Product` category, in any case. |
322
+ | `?tag=release` | Posts tagged `release`, in any case. |
323
+ | `?year=2026` | Posts published in 2026, in the `timeZone` option. |
324
+ | `?month=2026-10` | Posts published in October 2026. It wins over `?year=`. |
325
+ | `?announcements` | Announcements only. See [Announcements](#announcements). |
326
+
327
+ Filters combine: `?category=engineering&tag=api&year=2026` shows the
328
+ engineering posts tagged `api` from 2026. Pages count the matching posts only.
329
+
330
+ **The browse panel** above the posts has a row each for categories, keywords
331
+ and the archive, each chip with its post count:
332
+
333
+ - **Chips combine.** A chip adds its filter to the ones in view; another chip
334
+ in the same row replaces it, and clicking the active chip takes it off again.
335
+ - **Counts follow the view.** Each count says how many posts the chip would
336
+ show with the other filters kept, so readers can see where a click leads.
337
+ Chips that would show nothing are dimmed.
338
+ - **Years open into months.** Choosing a year shows a row of its months below
339
+ the years (with only one year, its months always show). A month keeps its
340
+ 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.
343
+
344
+ **The active filters** show above the posts while any filter is on: how many
345
+ posts match ("4 posts", or "No posts match these filters."), a chip for each
346
+ filter that removes just that one, and "Show all posts" to clear them all. It
347
+ is a status region, so screen readers announce the new count.
348
+
349
+ Categories and keywords are links wherever they appear: in the browse panel,
350
+ on each card, and in a post's byline, where they link to the post's own blog,
351
+ filtered. They are ordinary links, so VitePress follows them without
352
+ reloading the page, and each filtered view has its own URL to share.
353
+
354
+ ## Announcements
355
+
356
+ `announcement: true` puts a post in a banner across the top of every page,
357
+ above the nav bar: one line, the full width of the window, linked to the post.
358
+ A string sets the banner's text instead of the post's title.
261
359
 
262
- - `?page=2` shows the second page. Previous, page number and Next links are
263
- added below the posts when there is more than one page.
264
- - `?tag=release` shows only posts tagged `release` (in any case), with a
265
- "Posts tagged release · Show all posts" line above them. Pages then count
266
- the matching posts only.
360
+ ```md
361
+ ---
362
+ blog:
363
+ date: 2026-10-20 09:00
364
+ announcement: Scheduled maintenance on Friday, 18:00 UTC
365
+ ---
366
+ ```
367
+
368
+ Add the banner to your theme's `layout-top` slot:
369
+
370
+ ```ts
371
+ // .vitepress/theme/index.ts
372
+ import { h } from 'vue';
373
+ import DefaultTheme from 'vitepress/theme';
374
+ import { BlogAnnouncementBanner, useBlog } from '@binarynoir/vitepress-blog/client';
375
+ import '@binarynoir/vitepress-blog/style.css';
376
+
377
+ export default {
378
+ extends: DefaultTheme,
379
+ Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),
380
+ setup() {
381
+ useBlog();
382
+ },
383
+ };
384
+ ```
267
385
 
268
- Keywords are links everywhere they appear: in the keyword list at the top of
269
- a blog page, on each card, and in a post's byline, where they link to the
270
- post's own blog, filtered. They are ordinary links, so VitePress follows them
271
- without reloading the page, and each filtered view has its own URL to share.
386
+ - **The newest one wins.** With several, the banner shows the newest and a
387
+ badge with how many there are. The badge opens a blog page filtered to
388
+ announcements (`/blog/?announcements`): the main blog when it lists them all,
389
+ else another blog page that does, else the newest one's own blog.
390
+ - **Visitors can clear it.** The close button dismisses every current
391
+ announcement, remembered in the browser. A newer one brings the banner back,
392
+ and its badge counts only the ones not yet dismissed. Changing a post's
393
+ `date` announces it again.
394
+ - **No jump on load.** The banner is rendered on the server, and a small
395
+ `<head>` script hides it before the page shows if the visitor dismissed it.
396
+ While it shows, it sets VitePress's `--vp-layout-top-height`, so the nav bar,
397
+ sidebar and content move down to make room.
398
+ - Announcements are marked on blog pages with an Announcement badge. Drafts
399
+ are only announced with the `drafts` option.
400
+
401
+ The banner reads its posts from VitePress's site data, which `withBlog` (or
402
+ `blogVitePlugin`) fills in; `blogVitePlugin` also has Vite bundle this package
403
+ for the server-side build, so the banner renders there.
272
404
 
273
405
  ## Latest posts on any page
274
406
 
@@ -282,28 +414,30 @@ own settings:
282
414
  ::: blog-posts
283
415
  blog: /blog/
284
416
  limit: 3
285
- stickyLimit: 1
417
+ pinLimit: 1
286
418
  layout: grid
287
419
  more: All posts →
288
420
  :::
289
421
  ```
290
422
 
291
- | Key | Default | What it does |
292
- | ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
293
- | `blog` | the page's own blog | The blog to show: its URL (`/blog/`) or file (`blog/index.md`); `./` and `../` are page-relative. |
294
- | `limit` | `5` | Most posts shown, pinned ones included. `0` shows them all. |
295
- | `stickyLimit` | the blog page's | Most sticky posts pinned to the top. `0` pins them all. |
296
- | `layout` | the blog page's | `list`, `grid` or `compact`. |
297
- | `showTags` | the blog page's | `false` hides keyword chips. |
298
- | `tags` | all | Only show posts with at least one of these keywords. |
299
- | `more` | `All posts in {blog}` | Text of the link to the blog page below the posts. `false` leaves the link out. |
300
- | `rssLink` | the blog page's | A "Subscribe" link with the RSS icon to the blog's feed, beside the link to the blog page. `false` leaves it out. |
423
+ | Key | Default | What it does |
424
+ | -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
425
+ | `blog` | the page's own blog | The blog to show: its URL (`/blog/`) or file (`blog/index.md`); `./` and `../` are page-relative. |
426
+ | `limit` | `5` | Most posts shown, pinned ones included. `0` shows them all. |
427
+ | `pinLimit` | the blog page's | Most pinned posts kept at the top. `0` pins them all. |
428
+ | `layout` | the blog page's | `list`, `grid` or `compact`. |
429
+ | `showTags` | the blog page's | `false` hides keyword chips. |
430
+ | `showCategory` | the blog page's | `false` hides the category above each title. |
431
+ | `tags` | all | Only show posts with at least one of these keywords. |
432
+ | `categories` | all | Only show posts in one of these categories. |
433
+ | `more` | `All posts in {blog}` | Text of the link to the blog page below the posts. `false` leaves the link out. |
434
+ | `rssLink` | the blog page's | A "Subscribe" link with the RSS icon to the blog's feed, beside the link to the blog page. `false` leaves it out. |
301
435
 
302
436
  - The page's own blog is the deepest section blog covering its folder, else
303
437
  the main blog, so a block on the home page shows the main blog and one on
304
438
  a section's page shows that section's blog.
305
439
  - It shows the posts its blog lists, newest first, with no drafts unless the
306
- `drafts` option is on. Keyword chips link to the blog page, filtered.
440
+ `drafts` option is on. Category and keyword links go to the blog page, filtered.
307
441
  - It is written into the page at build time, like blog pages, and refreshes in
308
442
  `vitepress dev` when a post changes. A page can hold several blocks. A
309
443
  block inside a code fence is left as written.
@@ -383,7 +517,7 @@ link that copy. Public paths (`/cover.png`) and URLs are linked as they are.
383
517
  | `feedLimit` | `20` | Most posts in each feed. |
384
518
  | `imagesDir` | `'blog-images'` | Output folder for copied post images. |
385
519
  | `pageSize` | `10` | Posts per page on blog pages that don't set `pageSize`. `0` turns pages off. |
386
- | `stickyLimit` | `0` (all) | Most pinned sticky posts on blog pages that don't set `stickyLimit`. |
520
+ | `pinLimit` | `0` (all) | Most pinned posts on blog pages that don't set `pinLimit`. |
387
521
  | `rssLink` | `true` | A "Subscribe" RSS link on blog pages and blocks that don't set `rssLink`. |
388
522
  | `labels` | English | Words the plugin writes, for translating. See below. |
389
523
 
@@ -400,13 +534,24 @@ withBlog(config, {
400
534
  older: 'Älterer Beitrag',
401
535
  allPosts: 'Alle Beiträge in {blog}',
402
536
  draft: 'Entwurf',
403
- sticky: 'Angeheftet',
537
+ pinned: 'Angeheftet',
538
+ announcement: 'Ankündigung',
539
+ announcements: 'Ankündigungen',
540
+ announcementCount: '{n} Ankündigungen',
541
+ dismiss: 'Schließen',
404
542
  noPosts: 'Noch keine Beiträge.',
405
543
  undated: 'Ohne Datum',
544
+ browse: 'Beiträge durchsuchen',
545
+ categories: 'Kategorien',
406
546
  tags: 'Schlagwörter',
407
- tagged: 'Beiträge mit {tag}',
547
+ archive: 'Archiv',
548
+ category: 'Kategorie',
549
+ tag: 'Schlagwort',
550
+ postCount: '{n} Beiträge',
551
+ postCountOne: '{n} Beitrag',
552
+ noMatches: 'Keine Beiträge für diese Filter.',
553
+ removeFilter: 'Filter entfernen: {filter}',
408
554
  showAll: 'Alle Beiträge zeigen',
409
- noTagged: 'Keine Beiträge mit {tag}.',
410
555
  previous: 'Zurück',
411
556
  next: 'Weiter',
412
557
  pages: 'Seiten',
@@ -421,15 +566,20 @@ withBlog(config, {
421
566
  `style.css` uses VitePress's theme variables, so it follows light and dark
422
567
  mode. Override these to restyle it without new rules:
423
568
 
424
- | Variable | Default |
425
- | ----------------------- | -------------------------- |
426
- | `--vp-blog-card-bg` | `var(--vp-c-bg-soft)` |
427
- | `--vp-blog-card-border` | `var(--vp-c-divider)` |
428
- | `--vp-blog-card-radius` | `12px` |
429
- | `--vp-blog-meta-color` | `var(--vp-c-text-2)` |
430
- | `--vp-blog-tag-bg` | `var(--vp-c-default-soft)` |
431
- | `--vp-blog-tag-color` | `var(--vp-c-text-2)` |
432
- | `--vp-blog-accent` | `var(--vp-c-brand-1)` |
569
+ | Variable | Default |
570
+ | ------------------------------- | ----------------------------- |
571
+ | `--vp-blog-card-bg` | `var(--vp-c-bg-soft)` |
572
+ | `--vp-blog-card-border` | `var(--vp-c-divider)` |
573
+ | `--vp-blog-card-radius` | `12px` |
574
+ | `--vp-blog-meta-color` | `var(--vp-c-text-2)` |
575
+ | `--vp-blog-tag-bg` | `var(--vp-c-default-soft)` |
576
+ | `--vp-blog-tag-color` | `var(--vp-c-text-2)` |
577
+ | `--vp-blog-accent` | `var(--vp-c-brand-1)` |
578
+ | `--vp-blog-browse-bg` | `var(--vp-c-bg-soft)` |
579
+ | `--vp-blog-facet-label-width` | `6.5rem` |
580
+ | `--vp-blog-announcement-height` | `40px` |
581
+ | `--vp-blog-announcement-bg` | `var(--vp-button-brand-bg)` |
582
+ | `--vp-blog-announcement-color` | `var(--vp-button-brand-text)` |
433
583
 
434
584
  Every generated element has a `vp-blog-` class (`vp-blog-card`,
435
585
  `vp-blog-byline`, `vp-blog-post-nav`, …) for anything more.
@@ -0,0 +1,52 @@
1
+ // src/util.ts
2
+ var HTML_ESCAPES = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ '"': "&quot;",
7
+ "'": "&#39;"
8
+ };
9
+ function escapeHtml(value) {
10
+ return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
11
+ }
12
+ function escapeMarkdown(text) {
13
+ const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
14
+ return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
15
+ }
16
+ function escapeTemplateText(text) {
17
+ const escaped = escapeHtml(text);
18
+ return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
19
+ }
20
+ function isExternalUrl(url) {
21
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
22
+ }
23
+ function toMarkdownUrl(url) {
24
+ return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
25
+ }
26
+ function toPageLink(text, routePath) {
27
+ return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
28
+ }
29
+ function toUrlPath(routePath, cleanUrls) {
30
+ return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
31
+ }
32
+ function joinUrl(base, urlPath) {
33
+ return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
34
+ }
35
+ var ANNOUNCEMENTS_KEY = "blogAnnouncements";
36
+ var DISMISSED_STORAGE_KEY = "vp-blog-announcements-dismissed";
37
+ var DISMISSED_CLASS = "vp-blog-announcement-dismissed";
38
+
39
+ export {
40
+ escapeHtml,
41
+ escapeMarkdown,
42
+ escapeTemplateText,
43
+ isExternalUrl,
44
+ toMarkdownUrl,
45
+ toPageLink,
46
+ toUrlPath,
47
+ joinUrl,
48
+ ANNOUNCEMENTS_KEY,
49
+ DISMISSED_STORAGE_KEY,
50
+ DISMISSED_CLASS
51
+ };
52
+ //# sourceMappingURL=chunk-42Z6H7IN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util.ts"],"sourcesContent":["const HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n/** Escapes the characters that are special in HTML text and attribute values. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character]);\n}\n\n/**\n * Escapes text so it stays literal in Markdown and in VitePress's Vue\n * templates. Text with braces goes in a `v-pre` span, since Vue would otherwise\n * evaluate `{{ }}` even when the Markdown escaped it.\n */\nexport function escapeMarkdown(text: string): string {\n const escaped = text\n .replace(/[\\\\`*_[\\]#|~]/g, '\\\\$&')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** HTML text that Vue leaves alone: braces go in a `v-pre` span. */\nexport function escapeTemplateText(text: string): string {\n const escaped = escapeHtml(text);\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** A URL with a scheme (`https:`, `mailto:`) or protocol-relative (`//cdn`). */\nexport function isExternalUrl(url: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(url);\n}\n\n/** Encodes a URL for a Markdown link destination: spaces and parentheses would end it early. */\nexport function toMarkdownUrl(url: string): string {\n return encodeURI(url).replace(/[()]/g, (character) => (character === '(' ? '%28' : '%29'));\n}\n\n/** A Markdown link to a page by its served path, which VitePress then checks and gives the site's base. */\nexport function toPageLink(text: string, routePath: string): string {\n return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;\n}\n\n/** A page's URL path, without the site's base: `news/index.md` is `/news/`, `news/launch.md` is `/news/launch`. */\nexport function toUrlPath(routePath: string, cleanUrls: boolean): string {\n return `/${routePath.replace(/(^|\\/)index\\.md$/, '$1').replace(/\\.md$/, cleanUrls ? '' : '.html')}`;\n}\n\n/** `base` and `urlPath` joined with exactly one slash between them. */\nexport function joinUrl(base: string, urlPath: string): string {\n return `${base.replace(/\\/+$/, '')}/${urlPath.replace(/^\\/+/, '')}`;\n}\n\n/** Where the announcement banner's data sits on VitePress's site data: `useData().site.value.blogAnnouncements`. */\nexport const ANNOUNCEMENTS_KEY = 'blogAnnouncements';\n\n/** The `localStorage` key holding the ids of the announcements a visitor dismissed, as a JSON array. */\nexport const DISMISSED_STORAGE_KEY = 'vp-blog-announcements-dismissed';\n\n/** The class on `<html>` that hides a dismissed banner before the page hydrates. */\nexport const DISMISSED_CLASS = 'vp-blog-announcement-dismissed';\n"],"mappings":";AAAA,IAAM,eAAuC;AAAA,EAC3C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAGO,SAAS,WAAW,OAAuB;AAChD,SAAO,MAAM,QAAQ,YAAY,CAAC,cAAc,aAAa,SAAS,CAAC;AACzE;AAOO,SAAS,eAAe,MAAsB;AACnD,QAAM,UAAU,KACb,QAAQ,kBAAkB,MAAM,EAChC,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACvB,SAAO,OAAO,KAAK,OAAO,IAAI,eAAe,OAAO,YAAY;AAClE;AAGO,SAAS,mBAAmB,MAAsB;AACvD,QAAM,UAAU,WAAW,IAAI;AAC/B,SAAO,OAAO,KAAK,OAAO,IAAI,eAAe,OAAO,YAAY;AAClE;AAGO,SAAS,cAAc,KAAsB;AAClD,SAAO,+BAA+B,KAAK,GAAG;AAChD;AAGO,SAAS,cAAc,KAAqB;AACjD,SAAO,UAAU,GAAG,EAAE,QAAQ,SAAS,CAAC,cAAe,cAAc,MAAM,QAAQ,KAAM;AAC3F;AAGO,SAAS,WAAW,MAAc,WAA2B;AAClE,SAAO,IAAI,eAAe,IAAI,CAAC,KAAK,cAAc,IAAI,SAAS,EAAE,CAAC;AACpE;AAGO,SAAS,UAAU,WAAmB,WAA4B;AACvE,SAAO,IAAI,UAAU,QAAQ,oBAAoB,IAAI,EAAE,QAAQ,SAAS,YAAY,KAAK,OAAO,CAAC;AACnG;AAGO,SAAS,QAAQ,MAAc,SAAyB;AAC7D,SAAO,GAAG,KAAK,QAAQ,QAAQ,EAAE,CAAC,IAAI,QAAQ,QAAQ,QAAQ,EAAE,CAAC;AACnE;AAGO,IAAM,oBAAoB;AAG1B,IAAM,wBAAwB;AAG9B,IAAM,kBAAkB;","names":[]}