@binarynoir/vitepress-blog 0.2.0 → 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.
package/README.md CHANGED
@@ -155,7 +155,8 @@ blog:
155
155
  | `category` | none | Written to the feed and `article:section`. |
156
156
  | `scope` | `section` | `site` (or `global`) also lists the post on the main blog. See [Scopes](#scopes). |
157
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. |
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). |
159
160
  | `byline` | the `byline` option | `false` hides this post's byline. |
160
161
  | `cover` | the `cover` option | `true` shows this post's image under its byline. |
161
162
  | `postNav` | the `postNav` option | `false` hides this post's newer / older links. |
@@ -196,6 +197,35 @@ blog:
196
197
  - Feeds and social tags use the exact moment: `pubDate`, `lastBuildDate`,
197
198
  `article:published_time` and `article:modified_time`.
198
199
 
200
+ ### Drafts
201
+
202
+ `draft: true` keeps a post out of blog lists, `::: blog-posts` blocks and
203
+ feeds, but the page is still built and can be opened by its URL. With the
204
+ `drafts` option on, drafts are listed too, with a Draft badge, which is handy in
205
+ `vitepress dev`.
206
+
207
+ To leave drafts out of the build entirely, use
208
+ [`@binarynoir/vitepress-exclude`](https://binarynoir.tech/plugins/exclude/)'s
209
+ `excludeFrontmatter` option, and the same option in Auto Sidebar and Auto
210
+ Navbar to hide them from the menus. The blog lists only what VitePress builds,
211
+ so nothing else changes:
212
+
213
+ ```ts
214
+ // Drafts stay out of the production build, and show (marked) in `vitepress dev`.
215
+ const isBuild = process.env.NODE_ENV === 'production';
216
+ const excludeFrontmatter = isBuild ? ['draft', 'blog.draft'] : [];
217
+ const blogOptions = { drafts: !isBuild };
218
+
219
+ export default defineConfig({
220
+ srcExclude: excludeSrc(docsRoot, { excludeFrontmatter }),
221
+ themeConfig: {
222
+ sidebar: generateSidebar(docsRoot, { excludeFrontmatter }),
223
+ nav: generateNav(docsRoot, { excludeFrontmatter }),
224
+ },
225
+ // ...the blog's pieces, with blogOptions
226
+ });
227
+ ```
228
+
199
229
  ## Blog pages
200
230
 
201
231
  A page with `blog-index` in its frontmatter lists posts. `blog-index: true` is a
@@ -270,6 +300,57 @@ a blog page, on each card, and in a post's byline, where they link to the
270
300
  post's own blog, filtered. They are ordinary links, so VitePress follows them
271
301
  without reloading the page, and each filtered view has its own URL to share.
272
302
 
303
+ ## Announcements
304
+
305
+ `announcement: true` puts a post in a banner across the top of every page,
306
+ above the nav bar: one line, the full width of the window, linked to the post.
307
+ A string sets the banner's text instead of the post's title.
308
+
309
+ ```md
310
+ ---
311
+ blog:
312
+ date: 2026-10-20 09:00
313
+ announcement: Scheduled maintenance on Friday, 18:00 UTC
314
+ ---
315
+ ```
316
+
317
+ Add the banner to your theme's `layout-top` slot:
318
+
319
+ ```ts
320
+ // .vitepress/theme/index.ts
321
+ import { h } from 'vue';
322
+ import DefaultTheme from 'vitepress/theme';
323
+ import { BlogAnnouncementBanner, useBlog } from '@binarynoir/vitepress-blog/client';
324
+ import '@binarynoir/vitepress-blog/style.css';
325
+
326
+ export default {
327
+ extends: DefaultTheme,
328
+ Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),
329
+ setup() {
330
+ useBlog();
331
+ },
332
+ };
333
+ ```
334
+
335
+ - **The newest one wins.** With several, the banner shows the newest and a
336
+ badge with how many there are. The badge opens a blog page filtered to
337
+ announcements (`/blog/?announcements`): the main blog when it lists them all,
338
+ else another blog page that does, else the newest one's own blog.
339
+ - **Visitors can clear it.** The close button dismisses every current
340
+ announcement, remembered in the browser. A newer one brings the banner back,
341
+ and its badge counts only the ones not yet dismissed. Changing a post's
342
+ `date` announces it again.
343
+ - **No jump on load.** The banner is rendered on the server, and a small
344
+ `<head>` script hides it before the page shows if the visitor dismissed it.
345
+ While it shows, it sets VitePress's `--vp-layout-top-height`, so the nav bar,
346
+ sidebar and content move down to make room.
347
+ - Announcements are marked on blog pages with an Announcement badge. Drafts
348
+ are only announced with the `drafts` option.
349
+
350
+ The banner reads its posts from VitePress's site data, which `withBlog` (or
351
+ `blogVitePlugin`) fills in; `blogVitePlugin` also has Vite bundle this package
352
+ for the server-side build, so the banner renders there.
353
+
273
354
  ## Latest posts on any page
274
355
 
275
356
  A `::: blog-posts` block shows a blog's latest posts anywhere: on the home
@@ -401,6 +482,11 @@ withBlog(config, {
401
482
  allPosts: 'Alle Beiträge in {blog}',
402
483
  draft: 'Entwurf',
403
484
  sticky: 'Angeheftet',
485
+ announcement: 'Ankündigung',
486
+ announcements: 'Ankündigungen',
487
+ noAnnouncements: 'Keine Ankündigungen.',
488
+ announcementCount: '{n} Ankündigungen',
489
+ dismiss: 'Schließen',
404
490
  noPosts: 'Noch keine Beiträge.',
405
491
  undated: 'Ohne Datum',
406
492
  tags: 'Schlagwörter',
@@ -421,15 +507,18 @@ withBlog(config, {
421
507
  `style.css` uses VitePress's theme variables, so it follows light and dark
422
508
  mode. Override these to restyle it without new rules:
423
509
 
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)` |
510
+ | Variable | Default |
511
+ | ------------------------------- | ----------------------------- |
512
+ | `--vp-blog-card-bg` | `var(--vp-c-bg-soft)` |
513
+ | `--vp-blog-card-border` | `var(--vp-c-divider)` |
514
+ | `--vp-blog-card-radius` | `12px` |
515
+ | `--vp-blog-meta-color` | `var(--vp-c-text-2)` |
516
+ | `--vp-blog-tag-bg` | `var(--vp-c-default-soft)` |
517
+ | `--vp-blog-tag-color` | `var(--vp-c-text-2)` |
518
+ | `--vp-blog-accent` | `var(--vp-c-brand-1)` |
519
+ | `--vp-blog-announcement-height` | `40px` |
520
+ | `--vp-blog-announcement-bg` | `var(--vp-button-brand-bg)` |
521
+ | `--vp-blog-announcement-color` | `var(--vp-button-brand-text)` |
433
522
 
434
523
  Every generated element has a `vp-blog-` class (`vp-blog-card`,
435
524
  `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":[]}
@@ -1,3 +1,14 @@
1
+ import {
2
+ escapeHtml,
3
+ escapeMarkdown,
4
+ escapeTemplateText,
5
+ isExternalUrl,
6
+ joinUrl,
7
+ toMarkdownUrl,
8
+ toPageLink,
9
+ toUrlPath
10
+ } from "./chunk-42Z6H7IN.js";
11
+
1
12
  // src/collect.ts
2
13
  function isPost(page) {
3
14
  return page.post !== null;
@@ -69,40 +80,16 @@ function findBlogsListing(pages, post) {
69
80
  (index) => listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).includes(post)
70
81
  );
71
82
  }
72
-
73
- // src/util.ts
74
- var HTML_ESCAPES = {
75
- "&": "&amp;",
76
- "<": "&lt;",
77
- ">": "&gt;",
78
- '"': "&quot;",
79
- "'": "&#39;"
80
- };
81
- function escapeHtml(value) {
82
- return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
83
- }
84
- function escapeMarkdown(text) {
85
- const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
86
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
87
- }
88
- function escapeTemplateText(text) {
89
- const escaped = escapeHtml(text);
90
- return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
91
- }
92
- function isExternalUrl(url) {
93
- return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
94
- }
95
- function toMarkdownUrl(url) {
96
- return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
83
+ function listAnnouncements(pages, drafts) {
84
+ return pages.filter(isPost).filter((post) => post.post.announcement && (drafts || !post.post.draft)).sort(compareByDate);
97
85
  }
98
- function toPageLink(text, routePath) {
99
- return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
100
- }
101
- function toUrlPath(routePath, cleanUrls) {
102
- return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
103
- }
104
- function joinUrl(base, urlPath) {
105
- return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
86
+ function findAnnouncementBlog(pages, announcements, drafts) {
87
+ if (announcements.length === 0) return null;
88
+ const listingAll = pages.filter(isIndex).filter((index) => {
89
+ const listed = listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false });
90
+ return announcements.every((post) => listed.includes(post));
91
+ });
92
+ return listingAll.find((index) => index.index.scope === "site") ?? listingAll[0] ?? findHomeBlog(pages, announcements[0], drafts);
106
93
  }
107
94
 
108
95
  // src/feed.ts
@@ -211,6 +198,11 @@ var DEFAULT_LABELS = {
211
198
  allPosts: "All posts in {blog}",
212
199
  draft: "Draft",
213
200
  sticky: "Pinned",
201
+ announcement: "Announcement",
202
+ announcements: "Announcements",
203
+ noAnnouncements: "No announcements.",
204
+ announcementCount: "{n} announcements",
205
+ dismiss: "Dismiss",
214
206
  noPosts: "No posts yet.",
215
207
  undated: "Undated",
216
208
  tags: "Keywords",
@@ -390,12 +382,22 @@ function renderTags(tags, blogRoutePath) {
390
382
  function toTagsAttribute(post) {
391
383
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
392
384
  }
385
+ function toItemAttributes(post) {
386
+ return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
387
+ }
388
+ function renderBadges(post, context) {
389
+ const { draft, sticky, announcement } = post.post;
390
+ return [
391
+ draft && renderBadge("draft", context),
392
+ sticky && renderBadge("sticky", context),
393
+ announcement && renderBadge("announcement", context)
394
+ ].filter(Boolean).join(" ");
395
+ }
393
396
  function renderMeta(post, context, include) {
394
- const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors, draft, sticky } = post.post;
397
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
395
398
  const parts = [];
396
399
  if (include.badges) {
397
- const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
398
- const shown = badges.filter(Boolean).join(" ");
400
+ const shown = renderBadges(post, context);
399
401
  if (shown) parts.push(shown);
400
402
  }
401
403
  if (date) {
@@ -424,12 +426,13 @@ function renderCard(post, context, index) {
424
426
  const modifiers = [
425
427
  post.post.sticky && "sticky",
426
428
  post.post.draft && "draft",
429
+ post.post.announcement && "announcement",
427
430
  image && "with-image",
428
431
  image && post.post.imageFit === "contain" && "contain"
429
432
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
430
433
  const alt = escapeMarkdown(post.post.imageAlt);
431
434
  return [
432
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
435
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
433
436
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
434
437
  '<div class="vp-blog-card__body">',
435
438
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
@@ -442,8 +445,8 @@ function renderCard(post, context, index) {
442
445
  }
443
446
  function renderCompactItem(post, context) {
444
447
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
445
- const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
446
- const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
448
+ const badges = renderBadges(post, context);
449
+ const marker = `<span hidden ${toItemAttributes(post)}></span>`;
447
450
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
448
451
  }
449
452
  function renderGroup(posts, index, context) {
@@ -518,7 +521,9 @@ function renderClientAttributes(index, context) {
518
521
  "data-label-no-tagged": labels.noTagged,
519
522
  "data-label-previous": labels.previous,
520
523
  "data-label-next": labels.next,
521
- "data-label-pages": labels.pages
524
+ "data-label-pages": labels.pages,
525
+ "data-label-announcements": labels.announcements,
526
+ "data-label-no-announcements": labels.noAnnouncements
522
527
  };
523
528
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
524
529
  }
@@ -684,6 +689,7 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
684
689
  const timeZone = resolveTimeZone(options.timeZone);
685
690
  const date = parsePostDate(pick("date"), timeZone);
686
691
  const updated = parsePostDate(pick("updated"), timeZone);
692
+ const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
687
693
  return {
688
694
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
689
695
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
@@ -701,6 +707,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
701
707
  scope: toScope(post.scope),
702
708
  sticky: post.sticky === true,
703
709
  draft: post.draft === true,
710
+ announcement: post.announcement === true || !!announcementText,
711
+ announcementText,
704
712
  byline: toBoolean(post.byline),
705
713
  cover: toBoolean(post.cover),
706
714
  postNav: toBoolean(post.postNav),
@@ -1088,7 +1096,8 @@ export {
1088
1096
  isIndex,
1089
1097
  listBlogPosts,
1090
1098
  findBlogsListing,
1091
- joinUrl,
1099
+ listAnnouncements,
1100
+ findAnnouncementBlog,
1092
1101
  toPageUrl,
1093
1102
  toFeedPath,
1094
1103
  resolvePostImage,
@@ -1105,4 +1114,4 @@ export {
1105
1114
  hasWidget,
1106
1115
  blogMarkdown
1107
1116
  };
1108
- //# sourceMappingURL=chunk-PWNWSYI4.js.map
1117
+ //# sourceMappingURL=chunk-LYTJBMVK.js.map