@binarynoir/vitepress-blog 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BinaryNoir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,492 @@
1
- # Temporary Holding Version
1
+ # vitepress-blog
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@binarynoir/vitepress-blog.svg)](https://www.npmjs.com/package/@binarynoir/vitepress-blog)
4
+ [![CI](https://github.com/binarynoir/vitepress-blog/actions/workflows/ci.yml/badge.svg)](https://github.com/binarynoir/vitepress-blog/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@binarynoir/vitepress-blog.svg)](LICENSE)
6
+
7
+ A [VitePress 2](https://vitepress.dev) plugin that turns pages into a blog with
8
+ frontmatter alone. Any page anywhere in your docs can be a post. A main blog
9
+ lists posts from the whole site, and **section blogs** list the posts of one
10
+ folder, the way section glossaries and tagged pages work. Every blog gets its
11
+ own RSS feed.
12
+
13
+ [Documentation and live demo](https://binarynoir.tech/plugins/blog/)
14
+
15
+ [![Support me on Buy Me a Coffee](https://img.shields.io/badge/Support%20me-Buy%20Me%20a%20Coffee-orange?style=for-the-badge&logo=buy-me-a-coffee)](https://buymeacoffee.com/binarynoir)
16
+ [![Support me on Ko-fi](https://img.shields.io/badge/Support%20me-Ko--fi-blue?style=for-the-badge&logo=ko-fi)](https://ko-fi.com/binarynoir)
17
+ [![More tools](https://img.shields.io/badge/Website-binarynoir.tech-8c8c8c?style=for-the-badge)](https://binarynoir.tech)
18
+
19
+ ## What this does
20
+
21
+ ```md
22
+ ---
23
+ blog:
24
+ date: 2026-10-01
25
+ summary: Team A's first release is out.
26
+ image: ./launch.png
27
+ author: jane
28
+ tags: [release]
29
+ scope: site # also list it on the main blog
30
+ ---
31
+
32
+ # We launched
33
+ ```
34
+
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.
39
+ - **Posts** (`blog` in the frontmatter) get a byline under their title, links to
40
+ the newer and older posts, and Open Graph and Twitter card tags.
41
+ - **Every blog page gets an RSS feed**, written beside it at build time and
42
+ linked from its `<head>` and from each post it carries.
43
+ - Lists are plain Markdown generated at build time, with every post in the
44
+ HTML, so they show up in search and work without JavaScript. Links go
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.
48
+
49
+ ## Install
50
+
51
+ ```sh
52
+ npm install --save-dev @binarynoir/vitepress-blog
53
+ ```
54
+
55
+ Needs Node 22 or later and VitePress 2.
56
+
57
+ ## Usage
58
+
59
+ Wrap your config, and in your theme import the stylesheet and call `useBlog()`:
60
+
61
+ ```ts
62
+ // .vitepress/config.mts
63
+ import { defineConfig } from 'vitepress';
64
+ import { withBlog } from '@binarynoir/vitepress-blog/vitepress';
65
+
66
+ export default withBlog(
67
+ defineConfig({
68
+ sitemap: { hostname: 'https://example.com' },
69
+ // ...your config
70
+ }),
71
+ {
72
+ authors: { jane: { name: 'Jane Doe', url: 'https://example.com/jane' } },
73
+ },
74
+ );
75
+ ```
76
+
77
+ ```ts
78
+ // .vitepress/theme/index.ts
79
+ import DefaultTheme from 'vitepress/theme';
80
+ import { useBlog } from '@binarynoir/vitepress-blog/client';
81
+ import '@binarynoir/vitepress-blog/style.css';
82
+
83
+ export default {
84
+ extends: DefaultTheme,
85
+ setup() {
86
+ // Pages (?page=2) and keyword filtering (?tag=release) on blog pages.
87
+ useBlog();
88
+ },
89
+ };
90
+ ```
91
+
92
+ `withBlog` keeps any `markdown.config`, `transformHead`, `transformPageData` and
93
+ `buildEnd` you already have: the blog's hook runs and then yours, so it
94
+ composes with other `withX()` wrappers in any order.
95
+
96
+ ### Wiring it by hand
97
+
98
+ If you'd rather see each piece in your config, `withBlog` is these five:
99
+
100
+ ```ts
101
+ import { blogMarkdown } from '@binarynoir/vitepress-blog';
102
+ import { blogHead, blogPageData, blogVitePlugin, writeBlogFeeds } from '@binarynoir/vitepress-blog/vitepress';
103
+
104
+ const blogOptions = { authors: { jane: 'Jane Doe' } };
105
+
106
+ export default defineConfig({
107
+ markdown: {
108
+ config(md) {
109
+ md.use(blogMarkdown, blogOptions); // lists, bylines, newer / older links
110
+ },
111
+ },
112
+ transformHead: (context) => blogHead(context, blogOptions), // social tags, feed links
113
+ transformPageData: (pageData, context) => blogPageData(pageData, context, blogOptions), // descriptions
114
+ buildEnd: (siteConfig) => writeBlogFeeds(siteConfig, blogOptions), // RSS feeds, images
115
+ vite: { plugins: [blogVitePlugin(blogOptions)] }, // fresh lists and feeds in `vitepress dev`
116
+ });
117
+ ```
118
+
119
+ Pass the same options to all five.
120
+
121
+ ## Posts
122
+
123
+ A page is a post when its frontmatter has a `blog` key. `blog: true` is a post
124
+ with every default; an object sets its details.
125
+
126
+ ```md
127
+ ---
128
+ title: We launched
129
+ blog:
130
+ date: 2026-10-01
131
+ updated: 2026-10-05
132
+ summary: Team A's first release is out.
133
+ image: ./launch.png
134
+ imageAlt: The release dashboard
135
+ author: [jane, Sam Lee]
136
+ tags: [release, team-a]
137
+ category: Engineering
138
+ scope: site
139
+ sticky: true
140
+ ---
141
+ ```
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 | Publication date. Posts are sorted newest first; undated posts go last. |
148
+ | `updated` | none | Shown in the byline and used as 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. |
162
+
163
+ Title, description, date, author(s), image and tags may also be written at the
164
+ top level of the frontmatter, as other blog tools expect. A value under `blog`
165
+ wins.
166
+
167
+ ## Blog pages
168
+
169
+ A page with `blog-index` in its frontmatter lists posts. `blog-index: true` is a
170
+ **section blog** of the page's own folder.
171
+
172
+ ```md
173
+ ---
174
+ title: Team A blog
175
+ blog-index:
176
+ layout: grid
177
+ groupBy: year
178
+ ---
179
+
180
+ # Team A blog
181
+
182
+ Notes from Team A.
183
+
184
+ <!-- blog-posts -->
185
+ ```
186
+
187
+ The list goes where `<!-- blog-posts -->` is, or after the page's content.
188
+
189
+ | Key | Default | What it does |
190
+ | ------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- |
191
+ | `scope` | `section` | `site` makes this the main blog. See [Scopes](#scopes). |
192
+ | `folders` | the page's folder | Folders to list posts from, relative to the source directory; `./` and `../` are page-relative. |
193
+ | `tags` | all | Only list posts with at least one of these tags. |
194
+ | `limit` | all | Most posts to list. The feed has its own limit. |
195
+ | `layout` | `list` | `list` (image beside text), `grid` (cards) or `compact` (dates and titles). |
196
+ | `groupBy` | none | `year` puts a heading above each year's posts. |
197
+ | `showTags` | `true` | `false` hides tag chips on cards. |
198
+ | `tagList` | `true` | A keyword list above the posts: every tag with its count, each a filter. `false` leaves it out. |
199
+ | `pageSize` | `10` (the `pageSize` option) | Posts per page. `0` shows them all on one page. |
200
+ | `stickyLimit` | all (the `stickyLimit` option) | Most sticky posts pinned to the top. Extra ones take their place by date. |
201
+ | `rssLink` | `true` (the `rssLink` option) | A "Subscribe" link with the RSS icon to this blog's feed. `false` leaves it out. |
202
+ | `feed` | on | `false` turns the feed off, or `{ title, description, limit, path }` to set it up. |
203
+
204
+ ### Sticky posts
205
+
206
+ `sticky: true` pins a post to the top of every blog page that lists it, in its
207
+ own section above the other posts, with a Pinned badge. Pinned posts show on
208
+ the first page only and don't count toward `pageSize`. When more posts are
209
+ sticky than a page's `stickyLimit`, the newest ones are pinned and the others
210
+ take their place by date. Feeds ignore stickiness and stay in date order.
211
+
212
+ To keep sticky posts on top of an [Auto Sidebar](https://binarynoir.tech/plugins/auto-sidebar/)
213
+ sidebar too, sort the blog folder by stickiness, then date:
214
+
215
+ ```txt
216
+ # docs/blog/.sidebar
217
+ index.md:All posts
218
+ .sort: blog.sticky desc, date desc
219
+ ```
220
+
221
+ The sidebar pins every sticky post; `stickyLimit` applies to blog pages.
222
+
223
+ ### Pages and keywords
224
+
225
+ Every post is written into the blog page's HTML, so search engines, local
226
+ search and readers without JavaScript see them all. With `useBlog()` in your
227
+ theme (see [Usage](#usage)), the browser then shows one page at a time and
228
+ filters by keyword, both from the URL:
229
+
230
+ - `?page=2` shows the second page. Previous, page number and Next links are
231
+ added below the posts when there is more than one page.
232
+ - `?tag=release` shows only posts tagged `release` (in any case), with a
233
+ "Posts tagged release · Show all posts" line above them. Pages then count
234
+ the matching posts only.
235
+
236
+ Keywords are links everywhere they appear: in the keyword list at the top of
237
+ a blog page, on each card, and in a post's byline, where they link to the
238
+ post's own blog, filtered. They are ordinary links, so VitePress follows them
239
+ without reloading the page, and each filtered view has its own URL to share.
240
+
241
+ ## Latest posts on any page
242
+
243
+ A `::: blog-posts` block shows a blog's latest posts anywhere: on the home
244
+ page, or on a section's landing page next to its own blog. Each block has its
245
+ own settings:
246
+
247
+ ```md
248
+ ## Latest from the blog
249
+
250
+ ::: blog-posts
251
+ blog: /blog/
252
+ limit: 3
253
+ stickyLimit: 1
254
+ layout: grid
255
+ more: All posts →
256
+ :::
257
+ ```
258
+
259
+ | Key | Default | What it does |
260
+ | ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
261
+ | `blog` | the page's own blog | The blog to show: its URL (`/blog/`) or file (`blog/index.md`); `./` and `../` are page-relative. |
262
+ | `limit` | `5` | Most posts shown, pinned ones included. `0` shows them all. |
263
+ | `stickyLimit` | the blog page's | Most sticky posts pinned to the top. `0` pins them all. |
264
+ | `layout` | the blog page's | `list`, `grid` or `compact`. |
265
+ | `showTags` | the blog page's | `false` hides keyword chips. |
266
+ | `tags` | all | Only show posts with at least one of these keywords. |
267
+ | `more` | `All posts in {blog}` | Text of the link to the blog page below the posts. `false` leaves the link out. |
268
+ | `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. |
269
+
270
+ - The page's own blog is the deepest section blog covering its folder, else
271
+ the main blog, so a block on the home page shows the main blog and one on
272
+ a section's page shows that section's blog.
273
+ - It shows the posts its blog lists, newest first, with no drafts unless the
274
+ `drafts` option is on. Keyword chips link to the blog page, filtered.
275
+ - It is written into the page at build time, like blog pages, and refreshes in
276
+ `vitepress dev` when a post changes. A page can hold several blocks. A
277
+ block inside a code fence is left as written.
278
+ - A blog it can't find, or settings that aren't valid YAML, log a warning
279
+ naming the page and line.
280
+
281
+ ## Scopes
282
+
283
+ Scopes decide which blog lists a post, the same way section glossaries decide
284
+ which glossary a page uses.
285
+
286
+ - **A section blog** lists every post in its folder and the folders below it,
287
+ whatever the post's scope. Section blogs can be nested: a post in
288
+ `team-a/guides/` is listed by both `team-a/` and `team-a/guides/`.
289
+ - **The main blog** (`blog-index: { scope: site }`) lists:
290
+ - posts from any folder that are marked `scope: site`
291
+ - posts in folders that don't have a section blog of their own
292
+
293
+ So a site with only a main blog needs no scope settings at all, and a post
294
+ inside a section only reaches the main blog when it says `scope: site`.
295
+
296
+ ```text
297
+ docs/
298
+ ├─ blog/
299
+ │ ├─ index.md blog-index: { scope: site } → hello, launch, notes
300
+ │ └─ hello.md blog: true
301
+ ├─ team-a/
302
+ │ ├─ index.md blog-index: true → launch, internal
303
+ │ ├─ launch.md blog: { scope: site }
304
+ │ └─ internal.md blog: true
305
+ └─ notes.md blog: true (no section blog covers it)
306
+ ```
307
+
308
+ A post's newer and older links stay within its own blog: the deepest section
309
+ blog that lists it, otherwise the main blog.
310
+
311
+ ## Feeds
312
+
313
+ Each blog page's RSS 2.0 feed is written next to it at the end of the build:
314
+ `news/index.md` gets `news/feed.xml`, and `news/archive.md` gets
315
+ `news/archive-feed.xml`. Set `feed.path` to name it yourself. Feeds hold the
316
+ newest 20 posts (the `feedLimit` option or `feed.limit`), never drafts, and
317
+ each item has its title, link, date, summary, authors, tags and image.
318
+
319
+ Feeds need absolute links, so they use the `siteUrl` option or VitePress's
320
+ `sitemap.hostname`. Without either, no feeds are written and the build says so.
321
+
322
+ Every blog page and `::: blog-posts` block shows a "Subscribe" link with the RSS icon to its own blog's feed, so readers can follow the whole site or just one section (`rssLink: false` hides it). It only appears when feeds are written, that is, with a site URL. The blog page also links its feed with `<link rel="alternate">`, and so does every
323
+ post the feed carries, so feed readers find it from any of them. In
324
+ `vitepress dev` the feeds are served from memory, with links to the dev server
325
+ if you have no site URL.
326
+
327
+ ### Images in feeds and social cards
328
+
329
+ Vite renames images it bundles, and the new name isn't known until the build
330
+ is done. So a post image given as a relative path is also copied to
331
+ `blog-images/` under a name made from its content, and feeds and social cards
332
+ link that copy. Public paths (`/cover.png`) and URLs are linked as they are.
333
+
334
+ ## Options
335
+
336
+ | Option | Default | What it does |
337
+ | ---------------- | ----------------------- | -------------------------------------------------------------------------------------------------------- |
338
+ | `siteUrl` | `sitemap.hostname` | Absolute URL of the published site, for feeds and social tags. |
339
+ | `authors` | `{}` | Authors posts can name by key: `{ jane: { name, url, email } }` or `{ jane: 'Jane Doe' }`. |
340
+ | `defaultAuthor` | none | Author for posts that don't name one. |
341
+ | `locale` | `'en-US'` | Locale for dates and author lists ("Jane and Sam"). |
342
+ | `dateFormat` | `{ dateStyle: 'long' }` | `Intl.DateTimeFormat` options. Dates are shown in UTC. |
343
+ | `wordsPerMinute` | `200` | For "5 min read". `0` hides reading time. |
344
+ | `byline` | `true` | Author, date and tags under each post's title. |
345
+ | `cover` | `false` | Each post's image under its byline. |
346
+ | `postNav` | `true` | Newer and older post links at the end of each post. |
347
+ | `drafts` | `false` | List drafts, marked. Pass `process.env.NODE_ENV !== 'production'` for dev only. |
348
+ | `stripTags` | `true` | Remove [Markdown Tags](https://binarynoir.tech/plugins/markdown-tags/) syntax from titles and summaries. |
349
+ | `feedLimit` | `20` | Most posts in each feed. |
350
+ | `imagesDir` | `'blog-images'` | Output folder for copied post images. |
351
+ | `pageSize` | `10` | Posts per page on blog pages that don't set `pageSize`. `0` turns pages off. |
352
+ | `stickyLimit` | `0` (all) | Most pinned sticky posts on blog pages that don't set `stickyLimit`. |
353
+ | `rssLink` | `true` | A "Subscribe" RSS link on blog pages and blocks that don't set `rssLink`. |
354
+ | `labels` | English | Words the plugin writes, for translating. See below. |
355
+
356
+ ```ts
357
+ withBlog(config, {
358
+ locale: 'de-DE',
359
+ labels: {
360
+ by: 'Von',
361
+ updated: 'Aktualisiert',
362
+ readingTime: '{n} Min. Lesezeit',
363
+ newer: 'Neuerer Beitrag',
364
+ older: 'Älterer Beitrag',
365
+ allPosts: 'Alle Beiträge in {blog}',
366
+ draft: 'Entwurf',
367
+ sticky: 'Angeheftet',
368
+ noPosts: 'Noch keine Beiträge.',
369
+ undated: 'Ohne Datum',
370
+ tags: 'Schlagwörter',
371
+ tagged: 'Beiträge mit {tag}',
372
+ showAll: 'Alle Beiträge zeigen',
373
+ noTagged: 'Keine Beiträge mit {tag}.',
374
+ previous: 'Zurück',
375
+ next: 'Weiter',
376
+ pages: 'Seiten',
377
+ subscribe: 'Abonnieren',
378
+ rssFeed: 'RSS-Feed für {blog}',
379
+ },
380
+ });
381
+ ```
382
+
383
+ ## Styling
384
+
385
+ `style.css` uses VitePress's theme variables, so it follows light and dark
386
+ mode. Override these to restyle it without new rules:
387
+
388
+ | Variable | Default |
389
+ | ----------------------- | -------------------------- |
390
+ | `--vp-blog-card-bg` | `var(--vp-c-bg-soft)` |
391
+ | `--vp-blog-card-border` | `var(--vp-c-divider)` |
392
+ | `--vp-blog-card-radius` | `12px` |
393
+ | `--vp-blog-meta-color` | `var(--vp-c-text-2)` |
394
+ | `--vp-blog-tag-bg` | `var(--vp-c-default-soft)` |
395
+ | `--vp-blog-tag-color` | `var(--vp-c-text-2)` |
396
+ | `--vp-blog-accent` | `var(--vp-c-brand-1)` |
397
+
398
+ Every generated element has a `vp-blog-` class (`vp-blog-card`,
399
+ `vp-blog-byline`, `vp-blog-post-nav`, …) for anything more.
400
+
401
+ ## Notes
402
+
403
+ - **Lists stay fresh in `vitepress dev`.** VitePress caches each page's
404
+ output by its own source, so the dev plugin adds a fingerprint of every post
405
+ to blog pages and posts, and reloads them when any post is added, edited or
406
+ deleted.
407
+ - **Pages kept out of the build aren't listed.** Anything `srcExclude` leaves
408
+ out (including [Exclude](https://binarynoir.tech/plugins/exclude/) rules) is
409
+ skipped, so a list never links to a missing page.
410
+ - **Rewrites are followed.** Links and feeds use the path a page is served at.
411
+ - **Plain markdown-it**: `blogMarkdown` needs VitePress's `env.path`,
412
+ `env.relativePath` and `env.frontmatter`, so it does nothing elsewhere.
413
+
414
+ ## Releasing
415
+
416
+ Releases are tag-triggered. From a clean `main` that's in sync with
417
+ `origin/main`:
418
+
419
+ ```sh
420
+ npm run release:patch # or release:minor / release:major
421
+ ```
422
+
423
+ This runs typecheck/lint/test/build locally, then `npm version <bump>` and
424
+ `git push --follow-tags`. Pushing the tag triggers
425
+ [`.github/workflows/release.yml`](.github/workflows/release.yml), which re-runs
426
+ the checks, publishes to npm, and creates a GitHub release.
427
+
428
+ ## License
429
+
430
+ MIT
431
+
432
+ ---
433
+
434
+ ## Feedback and support
435
+
436
+ Bug reports, feature requests and questions are all welcome. Please open them on this project's [GitHub issue tracker](https://github.com/binarynoir/vitepress-blog/issues):
437
+
438
+ - **Found a bug?** [Report it](https://github.com/binarynoir/vitepress-blog/issues/new?labels=bug&title=Bug%3A%20). Include your version and the steps to reproduce it.
439
+ - **Have an idea?** [Request a feature](https://github.com/binarynoir/vitepress-blog/issues/new?labels=enhancement&title=Feature%20request%3A%20). Tell us what you are trying to do.
440
+ - **Something else?** [Browse open issues](https://github.com/binarynoir/vitepress-blog/issues) to see if it has already been raised.
441
+
442
+ Pull requests are welcome too.
443
+
444
+ ## More from BinaryNoir
445
+
446
+ <p align="center">
447
+ <a href="https://binarynoir.tech">
448
+ <picture>
449
+ <source media="(prefers-color-scheme: dark)" srcset="https://binarynoir.tech/logo-wide-dark.svg">
450
+ <img alt="BinaryNoir" src="https://binarynoir.tech/logo-wide-light.svg" width="320">
451
+ </picture>
452
+ </a>
453
+ </p>
454
+
455
+ Blog is one of a family of open-source VitePress plugins, editor extensions and command-line tools from BinaryNoir. Browse them all, with documentation and live demos, at **[binarynoir.tech](https://binarynoir.tech)**.
456
+
457
+ **VitePress plugins**
458
+
459
+ - [Auto Sidebar](https://binarynoir.tech/plugins/auto-sidebar): sidebar built from your folders
460
+ - [Auto Navbar](https://binarynoir.tech/plugins/auto-navbar): navbar built from your folders
461
+ - [Sidebar Accordion](https://binarynoir.tech/plugins/sidebar-accordion): one open sidebar group at a time
462
+ - [Markdown Tags](https://binarynoir.tech/plugins/markdown-tags/): inline status badges and tagged-pages lists
463
+ - [Reading Time Tag](https://binarynoir.tech/plugins/reading-time-tag): reading-time estimates
464
+ - [Glossary Tooltips](https://binarynoir.tech/plugins/glossary/): hover definitions from one glossary file
465
+ - [Text Style](https://binarynoir.tech/plugins/text-style): colored and sized text inline
466
+ - [Downloads](https://binarynoir.tech/plugins/downloads): downloadable files next to your pages
467
+ - [Exclude](https://binarynoir.tech/plugins/exclude/): keep pages out of the build with a `.exclude` file
468
+ - [README Index](https://binarynoir.tech/plugins/readme-index/): `README.md` index pages for VuePress migrations
469
+ - [HTML Escape](https://binarynoir.tech/plugins/html-escape/): stray tags and braces shown as text, not build errors
470
+ - [Broken Links](https://binarynoir.tech/plugins/broken-links): dead links shown as text, not build errors
471
+
472
+ **Vite plugins**
473
+
474
+ - [Optimize Images](https://binarynoir.tech/plugins/optimize-images): smaller images in the build output
475
+ - [Image Fallback](https://binarynoir.tech/plugins/image-fallback): a placeholder instead of a failed build
476
+
477
+ **Editor extensions**
478
+
479
+ - [Tags for Markdown for VS Code](https://binarynoir.tech/plugins/markdown-tags/vscode): status badges in the VS Code Markdown preview
480
+ - [Tags for Markdown for Obsidian](https://binarynoir.tech/plugins/markdown-tags/obsidian): status badges in Obsidian
481
+
482
+ **Command-line tools**
483
+
484
+ - [NoirWatch](https://binarynoir.tech/apps/noirwatch): get notified when a web page changes
485
+ - [NoirCon](https://binarynoir.tech/apps/noircon): get notified when a site, server or port goes down
486
+ - [callrx](https://binarynoir.tech/apps/callrx): amateur radio callsign lookup
487
+
488
+ Website, docs and demos: **[binarynoir.tech](https://binarynoir.tech)**
489
+
490
+ ## Author
491
+
492
+ John Smith III, [BinaryNoir](https://binarynoir.tech). Thanks to all contributors and users for their support and feedback.