@binarynoir/vitepress-blog 0.1.1 → 0.3.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.
@@ -16,8 +16,21 @@ interface BlogOptions {
16
16
  siteUrl?: string;
17
17
  /** Locale for dates in bylines and cards. Default: `'en-US'`. */
18
18
  locale?: string;
19
- /** How dates are written. Default: `{ dateStyle: 'long' }`, in UTC. */
19
+ /** How dates are written. Default: `{ dateStyle: 'long' }`. */
20
20
  dateFormat?: Intl.DateTimeFormatOptions;
21
+ /**
22
+ * How times are written, for dates whose frontmatter gives one
23
+ * (`2026-10-20 14:30`). Default: `{ hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }`,
24
+ * as in "2:30 PM EDT".
25
+ */
26
+ timeFormat?: Intl.DateTimeFormatOptions;
27
+ /**
28
+ * The time zone frontmatter times without an offset are in, and that every
29
+ * date and time is shown in: an IANA name such as `'America/New_York'`.
30
+ * A value with an offset (`2026-10-20T14:30:00-04:00`, `…Z`) is read as
31
+ * written. Default: `'UTC'`.
32
+ */
33
+ timeZone?: string;
21
34
  /** Reading speed for the "5 min read" estimate. `0` hides it. Default: `200`. */
22
35
  wordsPerMinute?: number;
23
36
  /** Add an author, date and tags line under each post's title. Default: `true`. */
@@ -68,8 +81,12 @@ interface BlogOptions {
68
81
  interface BlogLabels {
69
82
  /** Before the author names: "By Jane Doe". */
70
83
  by: string;
84
+ /** Before a post's `date` in its byline: "Published October 20, 2026". */
85
+ published: string;
71
86
  /** Before a post's `updated` date. */
72
87
  updated: string;
88
+ /** A date and its time: "{date} at {time}". */
89
+ dateTime: string;
73
90
  /** Reading time: "{n} min read". */
74
91
  readingTime: string;
75
92
  newer: string;
@@ -79,6 +96,16 @@ interface BlogLabels {
79
96
  draft: string;
80
97
  /** Badge on a sticky post. */
81
98
  sticky: string;
99
+ /** Badge on an announcement, and the accessible name of the announcement banner. */
100
+ announcement: string;
101
+ /** Above a blog page filtered to announcements (`?announcements`). */
102
+ announcements: string;
103
+ /** When the blog page has no announcements to show. */
104
+ noAnnouncements: string;
105
+ /** Tooltip of the banner's count badge: "{n} announcements". */
106
+ announcementCount: string;
107
+ /** Accessible name of the banner's close button. */
108
+ dismiss: string;
82
109
  noPosts: string;
83
110
  /** Year heading for posts without a date, when a blog groups by year. */
84
111
  undated: string;
@@ -138,8 +165,14 @@ interface BlogIndexConfig {
138
165
  interface BlogPostData {
139
166
  title: string;
140
167
  summary: string;
168
+ /** When the post was published, from `date`. */
141
169
  date: Date | null;
170
+ /** Whether `date` gave a time, not just a day. */
171
+ dateHasTime: boolean;
172
+ /** When the post was last changed, from `updated`. */
142
173
  updated: Date | null;
174
+ /** Whether `updated` gave a time, not just a day. */
175
+ updatedHasTime: boolean;
143
176
  /** As written: a path relative to the post, a public path (`/x.png`) or a URL. */
144
177
  image: string;
145
178
  imageAlt: string;
@@ -153,12 +186,32 @@ interface BlogPostData {
153
186
  scope: BlogScope;
154
187
  sticky: boolean;
155
188
  draft: boolean;
189
+ /** Shown in the announcement banner above the nav bar. */
190
+ announcement: boolean;
191
+ /** The banner's text, from `announcement: "…"`. `''` uses the title. */
192
+ announcementText: string;
156
193
  /** Per-post overrides of the `byline`, `cover` and `postNav` options. */
157
194
  byline?: boolean;
158
195
  cover?: boolean;
159
196
  postNav?: boolean;
160
197
  readingMinutes: number;
161
198
  }
199
+ /** One announcement, as the banner shows it. */
200
+ interface BlogAnnouncementItem {
201
+ /** The post's path and date: what a dismissal remembers, so re-dating a post announces it again. */
202
+ id: string;
203
+ text: string;
204
+ /** The post's URL path, without the site's base. */
205
+ link: string;
206
+ }
207
+ /** What the announcement banner reads from VitePress's site data. */
208
+ interface BlogAnnouncementData {
209
+ /** Newest first: the banner shows the first one that hasn't been dismissed. */
210
+ items: BlogAnnouncementItem[];
211
+ /** The blog page filtered to announcements (`/blog/?announcements`), without the base, or `''` when no blog lists them. */
212
+ listLink: string;
213
+ labels: Pick<BlogLabels, 'announcement' | 'announcementCount' | 'dismiss'>;
214
+ }
162
215
 
163
216
  /**
164
217
  * A markdown-it plugin that turns pages into a blog.
@@ -187,4 +240,4 @@ interface BlogPostData {
187
240
  */
188
241
  declare function blogMarkdown(md: MarkdownIt, options?: BlogOptions): void;
189
242
 
190
- export { type BlogAuthor as B, DEFAULT_LABELS as D, type BlogFeedConfig as a, type BlogIndexConfig as b, type BlogLabels as c, type BlogLayout as d, type BlogOptions as e, type BlogPostData as f, type BlogScope as g, blogMarkdown as h };
243
+ export { type BlogAnnouncementData as B, DEFAULT_LABELS as D, type BlogAnnouncementItem as a, type BlogAuthor as b, type BlogFeedConfig as c, type BlogIndexConfig as d, type BlogLabels as e, type BlogLayout as f, type BlogOptions as g, type BlogPostData as h, type BlogScope as i, blogMarkdown as j };
@@ -16,8 +16,21 @@ interface BlogOptions {
16
16
  siteUrl?: string;
17
17
  /** Locale for dates in bylines and cards. Default: `'en-US'`. */
18
18
  locale?: string;
19
- /** How dates are written. Default: `{ dateStyle: 'long' }`, in UTC. */
19
+ /** How dates are written. Default: `{ dateStyle: 'long' }`. */
20
20
  dateFormat?: Intl.DateTimeFormatOptions;
21
+ /**
22
+ * How times are written, for dates whose frontmatter gives one
23
+ * (`2026-10-20 14:30`). Default: `{ hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }`,
24
+ * as in "2:30 PM EDT".
25
+ */
26
+ timeFormat?: Intl.DateTimeFormatOptions;
27
+ /**
28
+ * The time zone frontmatter times without an offset are in, and that every
29
+ * date and time is shown in: an IANA name such as `'America/New_York'`.
30
+ * A value with an offset (`2026-10-20T14:30:00-04:00`, `…Z`) is read as
31
+ * written. Default: `'UTC'`.
32
+ */
33
+ timeZone?: string;
21
34
  /** Reading speed for the "5 min read" estimate. `0` hides it. Default: `200`. */
22
35
  wordsPerMinute?: number;
23
36
  /** Add an author, date and tags line under each post's title. Default: `true`. */
@@ -68,8 +81,12 @@ interface BlogOptions {
68
81
  interface BlogLabels {
69
82
  /** Before the author names: "By Jane Doe". */
70
83
  by: string;
84
+ /** Before a post's `date` in its byline: "Published October 20, 2026". */
85
+ published: string;
71
86
  /** Before a post's `updated` date. */
72
87
  updated: string;
88
+ /** A date and its time: "{date} at {time}". */
89
+ dateTime: string;
73
90
  /** Reading time: "{n} min read". */
74
91
  readingTime: string;
75
92
  newer: string;
@@ -79,6 +96,16 @@ interface BlogLabels {
79
96
  draft: string;
80
97
  /** Badge on a sticky post. */
81
98
  sticky: string;
99
+ /** Badge on an announcement, and the accessible name of the announcement banner. */
100
+ announcement: string;
101
+ /** Above a blog page filtered to announcements (`?announcements`). */
102
+ announcements: string;
103
+ /** When the blog page has no announcements to show. */
104
+ noAnnouncements: string;
105
+ /** Tooltip of the banner's count badge: "{n} announcements". */
106
+ announcementCount: string;
107
+ /** Accessible name of the banner's close button. */
108
+ dismiss: string;
82
109
  noPosts: string;
83
110
  /** Year heading for posts without a date, when a blog groups by year. */
84
111
  undated: string;
@@ -138,8 +165,14 @@ interface BlogIndexConfig {
138
165
  interface BlogPostData {
139
166
  title: string;
140
167
  summary: string;
168
+ /** When the post was published, from `date`. */
141
169
  date: Date | null;
170
+ /** Whether `date` gave a time, not just a day. */
171
+ dateHasTime: boolean;
172
+ /** When the post was last changed, from `updated`. */
142
173
  updated: Date | null;
174
+ /** Whether `updated` gave a time, not just a day. */
175
+ updatedHasTime: boolean;
143
176
  /** As written: a path relative to the post, a public path (`/x.png`) or a URL. */
144
177
  image: string;
145
178
  imageAlt: string;
@@ -153,12 +186,32 @@ interface BlogPostData {
153
186
  scope: BlogScope;
154
187
  sticky: boolean;
155
188
  draft: boolean;
189
+ /** Shown in the announcement banner above the nav bar. */
190
+ announcement: boolean;
191
+ /** The banner's text, from `announcement: "…"`. `''` uses the title. */
192
+ announcementText: string;
156
193
  /** Per-post overrides of the `byline`, `cover` and `postNav` options. */
157
194
  byline?: boolean;
158
195
  cover?: boolean;
159
196
  postNav?: boolean;
160
197
  readingMinutes: number;
161
198
  }
199
+ /** One announcement, as the banner shows it. */
200
+ interface BlogAnnouncementItem {
201
+ /** The post's path and date: what a dismissal remembers, so re-dating a post announces it again. */
202
+ id: string;
203
+ text: string;
204
+ /** The post's URL path, without the site's base. */
205
+ link: string;
206
+ }
207
+ /** What the announcement banner reads from VitePress's site data. */
208
+ interface BlogAnnouncementData {
209
+ /** Newest first: the banner shows the first one that hasn't been dismissed. */
210
+ items: BlogAnnouncementItem[];
211
+ /** The blog page filtered to announcements (`/blog/?announcements`), without the base, or `''` when no blog lists them. */
212
+ listLink: string;
213
+ labels: Pick<BlogLabels, 'announcement' | 'announcementCount' | 'dismiss'>;
214
+ }
162
215
 
163
216
  /**
164
217
  * A markdown-it plugin that turns pages into a blog.
@@ -187,4 +240,4 @@ interface BlogPostData {
187
240
  */
188
241
  declare function blogMarkdown(md: MarkdownIt, options?: BlogOptions): void;
189
242
 
190
- export { type BlogAuthor as B, DEFAULT_LABELS as D, type BlogFeedConfig as a, type BlogIndexConfig as b, type BlogLabels as c, type BlogLayout as d, type BlogOptions as e, type BlogPostData as f, type BlogScope as g, blogMarkdown as h };
243
+ export { type BlogAnnouncementData as B, DEFAULT_LABELS as D, type BlogAnnouncementItem as a, type BlogAuthor as b, type BlogFeedConfig as c, type BlogIndexConfig as d, type BlogLabels as e, type BlogLayout as f, type BlogOptions as g, type BlogPostData as h, type BlogScope as i, blogMarkdown as j };
package/dist/style.css CHANGED
@@ -10,6 +10,9 @@
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-announcement-height: 40px;
14
+ --vp-blog-announcement-bg: var(--vp-button-brand-bg);
15
+ --vp-blog-announcement-color: var(--vp-button-brand-text);
13
16
  }
14
17
 
15
18
  /* Markdown inside the generated blocks is wrapped in <p>; its margins are handled here instead. */
@@ -201,6 +204,11 @@
201
204
  color: var(--vp-c-brand-1);
202
205
  }
203
206
 
207
+ .vp-blog-badge--announcement {
208
+ background: var(--vp-c-tip-soft);
209
+ color: var(--vp-c-tip-1);
210
+ }
211
+
204
212
  .vp-blog-badge--draft {
205
213
  background: var(--vp-c-warning-soft);
206
214
  color: var(--vp-c-warning-1);
@@ -416,3 +424,108 @@
416
424
  /* The RSS orange, as feed readers expect. */
417
425
  color: #f26522;
418
426
  }
427
+
428
+ /*
429
+ * The announcement banner, in the theme's `layout-top` slot: fixed across the
430
+ * top of the window, one line high. While it shows, VitePress's
431
+ * --vp-layout-top-height moves the nav bar, sidebar and content down by its
432
+ * height. The class on <html> hides it before hydration once dismissed.
433
+ */
434
+ :root:not(.vp-blog-announcement-dismissed):has(.vp-blog-announcement) {
435
+ --vp-layout-top-height: var(--vp-blog-announcement-height);
436
+ }
437
+
438
+ .vp-blog-announcement-dismissed .vp-blog-announcement {
439
+ display: none;
440
+ }
441
+
442
+ .vp-blog-announcement {
443
+ position: fixed;
444
+ top: 0;
445
+ right: 0;
446
+ left: 0;
447
+ z-index: var(--vp-z-index-layout-top);
448
+ display: flex;
449
+ gap: 8px;
450
+ align-items: center;
451
+ justify-content: center;
452
+ box-sizing: border-box;
453
+ height: var(--vp-blog-announcement-height);
454
+ padding: 0 48px;
455
+ background: var(--vp-blog-announcement-bg);
456
+ color: var(--vp-blog-announcement-color);
457
+ font-size: 14px;
458
+ font-weight: 500;
459
+ line-height: var(--vp-blog-announcement-height);
460
+ }
461
+
462
+ .vp-blog-announcement__content {
463
+ display: flex;
464
+ gap: 10px;
465
+ align-items: center;
466
+ min-width: 0;
467
+ }
468
+
469
+ .vp-blog-announcement__text {
470
+ overflow: hidden;
471
+ color: inherit;
472
+ text-decoration: none;
473
+ text-overflow: ellipsis;
474
+ white-space: nowrap;
475
+ }
476
+
477
+ .vp-blog-announcement__text:hover {
478
+ text-decoration: underline;
479
+ text-underline-offset: 3px;
480
+ }
481
+
482
+ /* How many announcements there are: a counter pill that opens the list of them. */
483
+ .vp-blog-announcement__count {
484
+ flex: none;
485
+ min-width: 22px;
486
+ padding: 0 7px;
487
+ border-radius: 999px;
488
+ background: var(--vp-blog-announcement-color);
489
+ color: var(--vp-blog-announcement-bg);
490
+ font-size: 12px;
491
+ font-weight: 700;
492
+ font-variant-numeric: tabular-nums;
493
+ line-height: 20px;
494
+ text-align: center;
495
+ text-decoration: none;
496
+ transition: opacity 0.25s;
497
+ }
498
+
499
+ a.vp-blog-announcement__count:hover {
500
+ opacity: 0.85;
501
+ }
502
+
503
+ .vp-blog-announcement__dismiss {
504
+ position: absolute;
505
+ top: 50%;
506
+ right: 12px;
507
+ display: flex;
508
+ align-items: center;
509
+ justify-content: center;
510
+ width: 28px;
511
+ height: 28px;
512
+ border-radius: 6px;
513
+ color: inherit;
514
+ opacity: 0.8;
515
+ transform: translateY(-50%);
516
+ transition:
517
+ background-color 0.25s,
518
+ opacity 0.25s;
519
+ }
520
+
521
+ .vp-blog-announcement__dismiss:hover {
522
+ background: rgba(255, 255, 255, 0.16);
523
+ opacity: 1;
524
+ }
525
+
526
+ @media (max-width: 639px) {
527
+ .vp-blog-announcement {
528
+ justify-content: flex-start;
529
+ padding: 0 48px 0 16px;
530
+ }
531
+ }