blume 0.1.4 → 0.2.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.
@@ -475,6 +475,40 @@ declare const contentSourceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
475
475
  ref?: string | undefined;
476
476
  } | undefined;
477
477
  pollInterval?: number | undefined;
478
+ }>, z.ZodObject<{
479
+ /** Include draft releases (needs a token with repo write access). */
480
+ drafts: z.ZodOptional<z.ZodBoolean>;
481
+ /** Cap the number of releases materialized, newest-first. Default 100. */
482
+ limit: z.ZodOptional<z.ZodNumber>;
483
+ /** Repository owner (user or org). */
484
+ owner: z.ZodString;
485
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
486
+ pollInterval: z.ZodOptional<z.ZodNumber>;
487
+ /** Namespaces the source's routes under `/<prefix>/`; e.g. `changelog`. */
488
+ prefix: z.ZodOptional<z.ZodString>;
489
+ /** Include prereleases. */
490
+ prereleases: z.ZodOptional<z.ZodBoolean>;
491
+ /** Repository name. */
492
+ repo: z.ZodString;
493
+ type: z.ZodLiteral<"github-releases">;
494
+ }, "strict", z.ZodTypeAny, {
495
+ type: "github-releases";
496
+ owner: string;
497
+ repo: string;
498
+ prefix?: string | undefined;
499
+ pollInterval?: number | undefined;
500
+ drafts?: boolean | undefined;
501
+ limit?: number | undefined;
502
+ prereleases?: boolean | undefined;
503
+ }, {
504
+ type: "github-releases";
505
+ owner: string;
506
+ repo: string;
507
+ prefix?: string | undefined;
508
+ pollInterval?: number | undefined;
509
+ drafts?: boolean | undefined;
510
+ limit?: number | undefined;
511
+ prereleases?: boolean | undefined;
478
512
  }>, z.ZodObject<{
479
513
  /** Sanity API version (a date); default `2024-01-01`. */
480
514
  apiVersion: z.ZodOptional<z.ZodString>;
@@ -1119,6 +1153,40 @@ export declare const blumeConfigSchema: z.ZodObject<{
1119
1153
  ref?: string | undefined;
1120
1154
  } | undefined;
1121
1155
  pollInterval?: number | undefined;
1156
+ }>, z.ZodObject<{
1157
+ /** Include draft releases (needs a token with repo write access). */
1158
+ drafts: z.ZodOptional<z.ZodBoolean>;
1159
+ /** Cap the number of releases materialized, newest-first. Default 100. */
1160
+ limit: z.ZodOptional<z.ZodNumber>;
1161
+ /** Repository owner (user or org). */
1162
+ owner: z.ZodString;
1163
+ /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
1164
+ pollInterval: z.ZodOptional<z.ZodNumber>;
1165
+ /** Namespaces the source's routes under `/<prefix>/`; e.g. `changelog`. */
1166
+ prefix: z.ZodOptional<z.ZodString>;
1167
+ /** Include prereleases. */
1168
+ prereleases: z.ZodOptional<z.ZodBoolean>;
1169
+ /** Repository name. */
1170
+ repo: z.ZodString;
1171
+ type: z.ZodLiteral<"github-releases">;
1172
+ }, "strict", z.ZodTypeAny, {
1173
+ type: "github-releases";
1174
+ owner: string;
1175
+ repo: string;
1176
+ prefix?: string | undefined;
1177
+ pollInterval?: number | undefined;
1178
+ drafts?: boolean | undefined;
1179
+ limit?: number | undefined;
1180
+ prereleases?: boolean | undefined;
1181
+ }, {
1182
+ type: "github-releases";
1183
+ owner: string;
1184
+ repo: string;
1185
+ prefix?: string | undefined;
1186
+ pollInterval?: number | undefined;
1187
+ drafts?: boolean | undefined;
1188
+ limit?: number | undefined;
1189
+ prereleases?: boolean | undefined;
1122
1190
  }>, z.ZodObject<{
1123
1191
  /** Sanity API version (a date); default `2024-01-01`. */
1124
1192
  apiVersion: z.ZodOptional<z.ZodString>;
@@ -1327,6 +1395,15 @@ export declare const blumeConfigSchema: z.ZodObject<{
1327
1395
  slug?: string | undefined;
1328
1396
  } | undefined;
1329
1397
  publishedValue?: string | undefined;
1398
+ } | {
1399
+ type: "github-releases";
1400
+ owner: string;
1401
+ repo: string;
1402
+ prefix?: string | undefined;
1403
+ pollInterval?: number | undefined;
1404
+ drafts?: boolean | undefined;
1405
+ limit?: number | undefined;
1406
+ prereleases?: boolean | undefined;
1330
1407
  } | {
1331
1408
  type: "mintlify";
1332
1409
  exclude: string[];
@@ -1392,6 +1469,15 @@ export declare const blumeConfigSchema: z.ZodObject<{
1392
1469
  slug?: string | undefined;
1393
1470
  } | undefined;
1394
1471
  publishedValue?: string | undefined;
1472
+ } | {
1473
+ type: "github-releases";
1474
+ owner: string;
1475
+ repo: string;
1476
+ prefix?: string | undefined;
1477
+ pollInterval?: number | undefined;
1478
+ drafts?: boolean | undefined;
1479
+ limit?: number | undefined;
1480
+ prereleases?: boolean | undefined;
1395
1481
  } | {
1396
1482
  type: "mintlify";
1397
1483
  exclude?: string[] | undefined;
@@ -2606,12 +2692,12 @@ export declare const blumeConfigSchema: z.ZodObject<{
2606
2692
  /** Content types that each get a feed at `/<type>/rss.xml`. */
2607
2693
  types: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
2608
2694
  }, "strict", z.ZodTypeAny, {
2609
- enabled: boolean;
2610
2695
  limit: number;
2696
+ enabled: boolean;
2611
2697
  types: string[];
2612
2698
  }, {
2613
- enabled?: boolean | undefined;
2614
2699
  limit?: number | undefined;
2700
+ enabled?: boolean | undefined;
2615
2701
  types?: string[] | undefined;
2616
2702
  }>>;
2617
2703
  /** Generate sitemap.xml (requires deployment.site). */
@@ -2620,8 +2706,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
2620
2706
  structuredData: z.ZodDefault<z.ZodBoolean>;
2621
2707
  }, "strict", z.ZodTypeAny, {
2622
2708
  rss: {
2623
- enabled: boolean;
2624
2709
  limit: number;
2710
+ enabled: boolean;
2625
2711
  types: string[];
2626
2712
  };
2627
2713
  metatags: Record<string, string>;
@@ -2633,8 +2719,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
2633
2719
  structuredData: boolean;
2634
2720
  }, {
2635
2721
  rss?: {
2636
- enabled?: boolean | undefined;
2637
2722
  limit?: number | undefined;
2723
+ enabled?: boolean | undefined;
2638
2724
  types?: string[] | undefined;
2639
2725
  } | undefined;
2640
2726
  metatags?: Record<string, string> | undefined;
@@ -2752,8 +2838,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
2752
2838
  };
2753
2839
  seo: {
2754
2840
  rss: {
2755
- enabled: boolean;
2756
2841
  limit: number;
2842
+ enabled: boolean;
2757
2843
  types: string[];
2758
2844
  };
2759
2845
  metatags: Record<string, string>;
@@ -2817,6 +2903,15 @@ export declare const blumeConfigSchema: z.ZodObject<{
2817
2903
  slug?: string | undefined;
2818
2904
  } | undefined;
2819
2905
  publishedValue?: string | undefined;
2906
+ } | {
2907
+ type: "github-releases";
2908
+ owner: string;
2909
+ repo: string;
2910
+ prefix?: string | undefined;
2911
+ pollInterval?: number | undefined;
2912
+ drafts?: boolean | undefined;
2913
+ limit?: number | undefined;
2914
+ prereleases?: boolean | undefined;
2820
2915
  } | {
2821
2916
  type: "mintlify";
2822
2917
  exclude: string[];
@@ -3121,8 +3216,8 @@ export declare const blumeConfigSchema: z.ZodObject<{
3121
3216
  } | undefined;
3122
3217
  seo?: {
3123
3218
  rss?: {
3124
- enabled?: boolean | undefined;
3125
3219
  limit?: number | undefined;
3220
+ enabled?: boolean | undefined;
3126
3221
  types?: string[] | undefined;
3127
3222
  } | undefined;
3128
3223
  metatags?: Record<string, string> | undefined;
@@ -3186,6 +3281,15 @@ export declare const blumeConfigSchema: z.ZodObject<{
3186
3281
  slug?: string | undefined;
3187
3282
  } | undefined;
3188
3283
  publishedValue?: string | undefined;
3284
+ } | {
3285
+ type: "github-releases";
3286
+ owner: string;
3287
+ repo: string;
3288
+ prefix?: string | undefined;
3289
+ pollInterval?: number | undefined;
3290
+ drafts?: boolean | undefined;
3291
+ limit?: number | undefined;
3292
+ prereleases?: boolean | undefined;
3189
3293
  } | {
3190
3294
  type: "mintlify";
3191
3295
  exclude?: string[] | undefined;
@@ -5,7 +5,8 @@ description: Author release notes as content, and Blume builds a timeline page a
5
5
 
6
6
  Blume ships a changelog out of the box. Write each release as a normal content
7
7
  file, mark it `type: changelog`, and Blume collects every entry into a generated
8
- timeline page and an RSS feed — no layout to build, no list to maintain.
8
+ timeline page and an RSS feed — no layout to build, no list to maintain. Or skip
9
+ the files entirely and [source your changelog from GitHub Releases](#from-github-releases).
9
10
 
10
11
  ## Write an entry
11
12
 
@@ -72,6 +73,32 @@ replace it with your own design, add a [custom page](/docs/advanced/custom-pages
72
73
  `pages/changelog.astro` — it takes over and Blume stops generating the default
73
74
  timeline.
74
75
 
76
+ ## From GitHub Releases
77
+
78
+ Rather than authoring entries by hand, point the built-in
79
+ [`github-releases` source](/docs/content/sources#github-releases) at a repo and
80
+ every release becomes a `type: changelog` entry — the same timeline and feed,
81
+ fed straight from the releases you already publish:
82
+
83
+ ```ts blume.config.ts
84
+ content: {
85
+ sources: [
86
+ { type: "filesystem", root: "content" },
87
+ {
88
+ type: "github-releases",
89
+ prefix: "changelog",
90
+ owner: "acme",
91
+ repo: "sdk",
92
+ },
93
+ ],
94
+ }
95
+ ```
96
+
97
+ The release name becomes the title, its tag becomes `changelog.version`, and its
98
+ published date sorts the timeline. A private repo authenticates with the
99
+ `GITHUB_TOKEN` environment variable. See
100
+ [Content sources](/docs/content/sources#github-releases) for every option.
101
+
75
102
  ## The RSS feed
76
103
 
77
104
  Blume also builds a changelog feed at **`/changelog/rss.xml`**, sorted
@@ -233,7 +233,6 @@ import data from "blume:data";
233
233
  headings={[]}
234
234
  themeMode={data.config.theme.mode}
235
235
  searchEnabled={data.config.search.enabled}
236
- searchProvider={data.config.search.provider}
237
236
  indexable={true}
238
237
  >
239
238
  <h1>Pricing</h1>
@@ -20,7 +20,8 @@ export default defineConfig({
20
20
 
21
21
  ## A complete example
22
22
 
23
- Every option Blume reads, with its default:
23
+ A broader example touching the most common options (see each feature's guide for
24
+ the rest):
24
25
 
25
26
  ```ts blume.config.ts lineNumbers
26
27
  import { defineConfig } from "blume";
@@ -238,7 +239,7 @@ the full reference.
238
239
 
239
240
  ```ts blume.config.ts lineNumbers
240
241
  seo: {
241
- og: { enabled: false },
242
+ og: { enabled: true },
242
243
  rss: { enabled: true, types: ["blog", "changelog"] },
243
244
  sitemap: true,
244
245
  robots: true,
@@ -246,15 +247,15 @@ seo: {
246
247
  }
247
248
  ```
248
249
 
249
- | Option | Default | Description |
250
- | ---------------- | ----------------------- | --------------------------------------------- |
251
- | `og.enabled` | `false` | Generate per-page Open Graph images. |
252
- | `rss.enabled` | `true` | Build feeds for blog and changelog content. |
253
- | `rss.types` | `["blog", "changelog"]` | Content types that each get a feed. |
254
- | `rss.limit` | `50` | Maximum items per feed. |
255
- | `sitemap` | `true` | Generate sitemap.xml (needs deployment.site). |
256
- | `robots` | `true` | Generate robots.txt with a Sitemap link. |
257
- | `structuredData` | `true` | Emit schema.org JSON-LD in each page's head. |
250
+ | Option | Default | Description |
251
+ | ---------------- | ----------------------- | ------------------------------------------------------- |
252
+ | `og.enabled` | auto | Per-page Open Graph images — on when a site URL is set. |
253
+ | `rss.enabled` | `true` | Build feeds for blog and changelog content. |
254
+ | `rss.types` | `["blog", "changelog"]` | Content types that each get a feed. |
255
+ | `rss.limit` | `50` | Maximum items per feed. |
256
+ | `sitemap` | `true` | Generate sitemap.xml (needs deployment.site). |
257
+ | `robots` | `true` | Generate robots.txt with a Sitemap link. |
258
+ | `structuredData` | `true` | Emit schema.org JSON-LD in each page's head. |
258
259
 
259
260
  These work best with an absolute [`deployment.site`](/docs/deployment) for full URLs.
260
261
 
@@ -17,7 +17,8 @@ so picking one backend never pulls in the others.
17
17
  ## Using search
18
18
 
19
19
  Open search with <Badge variant="accent">⌘K</Badge> (or `Ctrl K`), or press `/`
20
- when you're not typing in a field. `Esc` closes it.
20
+ when you're not typing in a field. `Esc` closes it, and `⌘J` (or `Ctrl J`)
21
+ toggles the result preview pane.
21
22
 
22
23
  Queries match page **titles**, **descriptions**, and **body text**, with title
23
24
  matches ranked highest and descriptions above body.
@@ -29,6 +30,17 @@ to plain text — code blocks, images, and markup are stripped, so results stay
29
30
  relevant. The index is built from your source files, so it's identical in dev and
30
31
  production.
31
32
 
33
+ ## Tags
34
+
35
+ Add `search.tags` to a page's frontmatter to group it under a filter in the
36
+ search dialog — readers can narrow results to a tag with a click. Tags also
37
+ become a facet on the hosted providers.
38
+
39
+ ```yaml
40
+ search:
41
+ tags: [api, reference]
42
+ ```
43
+
32
44
  ## Providers
33
45
 
34
46
  The client-side providers are keyless and need no extra config. The hosted ones
@@ -98,6 +98,55 @@ Each value is a Google Fonts slug from the curated set below:
98
98
  Need a font that isn't listed, or want to drop back to the system stack? Override
99
99
  the `--blume-font-*` tokens directly in [`theme.css`](#theme-css).
100
100
 
101
+ ### Dark-mode colors
102
+
103
+ By default light and dark share one `accent`. Set `accentDark` for a different
104
+ accent in dark mode, and `background` / `backgroundDark` to override the page
105
+ background per mode:
106
+
107
+ ```ts blume.config.ts lineNumbers
108
+ theme: {
109
+ accent: "blue",
110
+ accentDark: "teal", // a different accent in dark mode
111
+ background: "#ffffff", // light-mode page background
112
+ backgroundDark: "#0a0a0a", // dark-mode page background
113
+ }
114
+ ```
115
+
116
+ Each takes a named preset or any CSS color, exactly like `accent`.
117
+
118
+ ### Action color
119
+
120
+ `action` is a secondary accent for primary calls to action and the `action`
121
+ Tailwind utilities (`bg-action`, `text-action`). It defaults to your `accent`:
122
+
123
+ ```ts blume.config.ts
124
+ theme: {
125
+ action: "#ff0066",
126
+ }
127
+ ```
128
+
129
+ ### Background decoration
130
+
131
+ Add a subtle, theme-aware pattern behind your content with `backgroundDecoration`
132
+ — `gradient`, `grid`, or `windows`:
133
+
134
+ ```ts blume.config.ts
135
+ theme: {
136
+ backgroundDecoration: "grid",
137
+ }
138
+ ```
139
+
140
+ For a full background image, set `backgroundImage` (and `backgroundImageDark` for
141
+ a dark variant) to a URL or a path under `public/`:
142
+
143
+ ```ts blume.config.ts lineNumbers
144
+ theme: {
145
+ backgroundImage: "/bg-light.svg",
146
+ backgroundImageDark: "/bg-dark.svg",
147
+ }
148
+ ```
149
+
101
150
  ## theme.css
102
151
 
103
152
  Drop a `theme.css` in your project root to override any design token. It's the
@@ -128,6 +177,7 @@ dark mode.
128
177
  | `--blume-border` | Borders and dividers |
129
178
  | `--blume-accent` | Accent color |
130
179
  | `--blume-accent-foreground` | Text and icons on an accent background |
180
+ | `--blume-action` | Secondary accent (defaults to accent) |
131
181
  | `--blume-code-background` | Code block surface |
132
182
  | `--blume-radius` | Corner radius |
133
183
  | `--blume-font-display` | Heading font |
@@ -159,6 +209,7 @@ a utility, so your components track the theme automatically:
159
209
  | `--blume-border` | `border-border` |
160
210
  | `--blume-accent` | `bg-accent`, `text-accent` |
161
211
  | `--blume-accent-foreground` | `text-accent-foreground` |
212
+ | `--blume-action` | `bg-action`, `text-action` |
162
213
  | `--blume-radius` | `rounded-blume` |
163
214
  | `--blume-font-display` | `font-display` |
164
215
  | `--blume-font-body` | `font-sans` |
@@ -133,6 +133,24 @@ A negative or breaking state, such as a deprecation.
133
133
  <Badge variant="danger">Deprecated</Badge>
134
134
  ```
135
135
 
136
+ ## Icon
137
+
138
+ Render an icon from Blume's built-in set by name — the same set the `icon` props
139
+ on cards, tiles, tabs, and sidebar entries draw from. Names are lowercase and
140
+ kebab-cased (`rocket`, `book-open`, `chevron-right`).
141
+
142
+ <Icon icon="rocket" size={20} />
143
+
144
+ ```astro
145
+ <Icon icon="rocket" size={20} />
146
+ ```
147
+
148
+ `icon` is the icon name; `size` sets the pixel size (default `16`) and `color`
149
+ tints it (any CSS color; defaults to `currentColor`). Pass a raw `<svg>` string,
150
+ an image URL, or a local image path in place of a name to render your own art,
151
+ and add a `label` to expose it to assistive tech — without one, the icon is
152
+ decorative.
153
+
136
154
  ## File tree
137
155
 
138
156
  Illustrate a project or folder layout. Wrap a normal Markdown list and Blume
@@ -104,8 +104,14 @@ Tabs also **scope the sidebar**: when the current route falls under a tab's
104
104
  `path`, the sidebar shows only that section's pages — so `/adapters/*` lists the
105
105
  adapters and nothing else. The folder at a tab's `path` becomes the section, so
106
106
  this needs no extra config beyond the tabs themselves; structure your content
107
- into a folder per tab and point each tab at it. A route under no tab (or a tab
108
- whose `path` is `/`) shows the full sidebar.
107
+ into a folder per tab and point each tab at it.
108
+
109
+ On a route under no tab (or a tab whose `path` is `/`), the sidebar shows the
110
+ pages that _don't_ belong to a tab — each tab's folder is hidden from it, since
111
+ that section already has its own tab in the header. So a root landing page lists
112
+ your loose top-level pages while the sectioned content stays behind its tab,
113
+ mirroring Fumadocs' root folders. If a route has no pages of its own to show
114
+ this way, the full tree is shown instead, so the sidebar is never left blank.
109
115
 
110
116
  ## Explicit sidebar
111
117
 
@@ -88,6 +88,49 @@ a remote source for changes instead, set `pollInterval` (seconds) on it — the
88
88
  dev server re-fetches on that interval and reloads only when the content
89
89
  actually changes. Leave it unset to avoid hitting the API while you work.
90
90
 
91
+ ## GitHub Releases
92
+
93
+ The built-in `github-releases` source turns a repo's releases into a changelog:
94
+ each release becomes a `type: changelog` entry, so your release notes _are_ your
95
+ changelog — nothing to write twice. Combined with the generated
96
+ [changelog timeline](/docs/advanced/changelog), publishing a GitHub release
97
+ ships a changelog entry.
98
+
99
+ ```ts blume.config.ts
100
+ import { defineConfig } from "blume";
101
+
102
+ export default defineConfig({
103
+ content: {
104
+ sources: [
105
+ { type: "filesystem", root: "content" },
106
+ {
107
+ type: "github-releases",
108
+ prefix: "changelog",
109
+ owner: "acme",
110
+ repo: "sdk",
111
+ // prereleases: false, // include prereleases (default off)
112
+ // drafts: false, // include drafts (needs a write token)
113
+ // limit: 100, // cap releases, newest-first
114
+ },
115
+ ],
116
+ },
117
+ });
118
+ ```
119
+
120
+ Each release maps to the changelog fields automatically: its name (or tag)
121
+ becomes the title, its published date drives the timeline order, the tag becomes
122
+ `changelog.version`, and prereleases are tagged `Prerelease` (others `Release`).
123
+ The notes render as the entry body. Give the source a `prefix` so its release
124
+ pages nest under a route like `/changelog/v1-2-0`.
125
+
126
+ A private repo authenticates with the `GITHUB_TOKEN` environment variable — the
127
+ same token the other GitHub features use, never inlined into your config. Like
128
+ every remote source it's cached under `.blume/cache/<source>/` and served
129
+ offline if the API is unreachable. Because a changelog is supplementary, a fetch
130
+ failure with no cache (say a CI build without a token) degrades to an empty
131
+ changelog with a warning rather than failing the build — set `GITHUB_TOKEN` in
132
+ your CI and deploy environments to populate it.
133
+
91
134
  ## Sanity
92
135
 
93
136
  The built-in `sanity` source runs a GROQ query and maps each document's fields
@@ -11,7 +11,7 @@ each.
11
11
  ## Headings
12
12
 
13
13
  Structure a page with headings. Blume renders your frontmatter `title` as the
14
- page heading, so start your content at `##` — `##` through `####` become entries
14
+ page heading, so start your content at `##` — `##` and `###` become entries
15
15
  in the table of contents. Every `##`–`######` heading is also wrapped in a link
16
16
  to its own anchor, so readers can click a heading to copy, bookmark, or share a
17
17
  permalink straight to that section (hover to reveal the `#`). Turn this off with
package/docs/index.mdx CHANGED
@@ -83,7 +83,7 @@ loosely-typed YAML.
83
83
  - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD, [built in](/docs/configuration/seo).
84
84
  - **Customization** — component overrides, React islands, custom pages, theme
85
85
  tokens, and a source-component registry via `blume add`.
86
- - **Migration** — `blume migrate mintlify | starlight | fumadocs`.
86
+ - **Migration** — `blume migrate mintlify | starlight | nextra | fumadocs`.
87
87
  - **Eject** — `blume eject` produces a standalone Astro project that still uses
88
88
  the `blume` package.
89
89
 
@@ -17,14 +17,20 @@ blume <command> [options]
17
17
  | `blume preview` | Preview the last build. |
18
18
  | `blume add <item>` | Install a source component from the registry. |
19
19
  | `blume migrate <tool>` | Migrate from Mintlify, Starlight, Nextra, or Fumadocs. |
20
+ | `blume sync` | Re-fetch remote content sources and regenerate. |
20
21
  | `blume eject` | Promote the runtime into a standalone Astro app. |
21
22
  | `blume doctor` | Diagnose config and content problems. |
22
23
  | `blume validate` | Validate links across your content. |
23
24
 
24
25
  ## Common flags
25
26
 
27
+ - `blume init --content-dir <dir> --yes` — set the content folder (default `docs`) and skip prompts.
26
28
  - `blume dev --host --port <n> --open`
27
- - `blume build --strict` — fail on diagnostics.
29
+ - `blume dev --preview` / `blume build --preview` — include drafts and unpublished CMS content.
30
+ - `blume build --strict` — fail the build on diagnostic errors (also works on `blume dev`).
31
+ - `blume preview --host --port <n>` — bind the preview server.
32
+ - `blume sync --force` — re-fetch remote sources, dropping the cached snapshot first.
33
+ - `blume add <item> --force` — overwrite files that already exist.
28
34
  - `blume eject --yes` — skip the confirmation prompt.
29
35
  - `blume validate --external` — also check external links over the network.
30
36
  - `blume validate --strict` — exit non-zero on warnings too.
@@ -24,6 +24,11 @@ Every page accepts the following frontmatter. All fields are optional.
24
24
  default: "false",
25
25
  description: "Exclude from production builds.",
26
26
  },
27
+ lastModified: {
28
+ type: "string",
29
+ description:
30
+ 'Pin the page\'s "last updated" date (ISO or YAML date); overrides the git-derived date.',
31
+ },
27
32
  }}
28
33
  />
29
34
 
@@ -33,7 +38,8 @@ Every page accepts the following frontmatter. All fields are optional.
33
38
  sidebar:
34
39
  label: Install
35
40
  order: 2
36
- icon: Download
41
+ icon: download
42
+ badge: New
37
43
  hidden: false
38
44
  ```
39
45
 
@@ -42,7 +48,9 @@ sidebar:
42
48
  ```yaml lineNumbers
43
49
  seo:
44
50
  title: Install Blume
51
+ description: Install Blume and scaffold your first project.
45
52
  image: /og/install.png
53
+ canonical: https://acme.com/install
46
54
  noindex: false
47
55
  ```
48
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -813,6 +813,28 @@ export interface GenerateResult {
813
813
  warnings: string[];
814
814
  }
815
815
 
816
+ /**
817
+ * Whether to generate the default `/changelog` index. Written when there are
818
+ * `type: changelog` entries — or when a release-backed changelog source is
819
+ * configured, so its route (and any nav tab pointing at it) still resolves to an
820
+ * empty timeline on a build where the source could not be fetched (e.g. CI
821
+ * without a token). Skipped when a user content page already owns `/changelog`.
822
+ */
823
+ const shouldGenerateChangelog = (project: BlumeProject): boolean => {
824
+ const hasChangelog = project.graph.pages.some(
825
+ (page) =>
826
+ page.contentType === "changelog" &&
827
+ !(page.meta.draft || page.meta.sidebar.hidden)
828
+ );
829
+ const hasChangelogSource = (project.config.content.sources ?? []).some(
830
+ (source) => source.type === "github-releases"
831
+ );
832
+ const changelogRouteTaken = project.graph.pages.some(
833
+ (page) => page.route === "/changelog"
834
+ );
835
+ return (hasChangelog || hasChangelogSource) && !changelogRouteTaken;
836
+ };
837
+
816
838
  /**
817
839
  * Write (or update) the generated `.blume/` Astro runtime for a project.
818
840
  * Only files whose content changed are rewritten so Vite HMR stays fast.
@@ -987,21 +1009,16 @@ export const generateRuntime = async (
987
1009
  );
988
1010
  }
989
1011
 
990
- // Changelog index (`/changelog`): a timeline of every `type: changelog` entry,
991
- // rendered through the Update layout. Skipped when there are no entries, or
992
- // when a user content page already occupies the `/changelog` route.
993
- const hasChangelog = project.graph.pages.some(
994
- (page) =>
995
- page.contentType === "changelog" &&
996
- !(page.meta.draft || page.meta.sidebar.hidden)
997
- );
998
- const changelogRouteTaken = project.graph.pages.some(
999
- (page) => page.route === "/changelog"
1000
- );
1001
- if (hasChangelog && !changelogRouteTaken) {
1012
+ // Changelog index (`/changelog`), rendered through the Update timeline layout.
1013
+ if (shouldGenerateChangelog(project)) {
1002
1014
  await write(
1003
1015
  join(srcDir, "pages", "changelog.astro"),
1004
- changelogIndexTemplate({ askEnabled, exportEpub, exportPdf })
1016
+ changelogIndexTemplate({
1017
+ askEnabled,
1018
+ exportEpub,
1019
+ exportPdf,
1020
+ staged: hasStaged,
1021
+ })
1005
1022
  );
1006
1023
  }
1007
1024
 
@@ -1056,11 +1056,18 @@ export const changelogIndexTemplate = (options: {
1056
1056
  askEnabled: boolean;
1057
1057
  exportEpub: boolean;
1058
1058
  exportPdf: boolean;
1059
+ /** Whether a `staged` collection exists (non-filesystem changelog sources). */
1060
+ staged: boolean;
1059
1061
  }): string => {
1060
1062
  const askImport = options.askEnabled
1061
1063
  ? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
1062
1064
  : "";
1063
1065
  const askSlot = options.askEnabled ? '\n <AskAI slot="ask" />' : "";
1066
+ // Staged sources (e.g. GitHub Releases) render through a parallel collection,
1067
+ // so fold them in alongside filesystem entries when one exists.
1068
+ const stagedSpread = options.staged
1069
+ ? '\n ...(await getCollection("staged")),'
1070
+ : "";
1064
1071
 
1065
1072
  return `---
1066
1073
  // Generated by Blume. Do not edit.
@@ -1100,7 +1107,9 @@ const slugify = (text) =>
1100
1107
  text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
1101
1108
  "update";
1102
1109
 
1103
- const changelogEntries = (await getCollection("docs"))
1110
+ const changelogEntries = [
1111
+ ...(await getCollection("docs")),${stagedSpread}
1112
+ ]
1104
1113
  .filter(
1105
1114
  (entry) =>
1106
1115
  entry.data.type === "changelog" &&