blume 0.6.2 → 0.6.4
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/dist/cli/index.js +10 -4
- package/dist/cli/index.js.map +5 -5
- package/dist/types/core/config-input.d.ts +1 -6
- package/dist/types/core/config.d.ts +1 -1
- package/dist/types/core/schema.d.ts +0 -5
- package/docs/02-deployment.mdx +4 -0
- package/docs/03-faq.mdx +132 -0
- package/docs/configuration/ai.mdx +4 -4
- package/package.json +1 -1
- package/src/components/layout/Pagination.astro +2 -2
- package/src/components/layout/RootLayout.astro +17 -7
- package/src/core/config-input.ts +1 -6
- package/src/core/config.ts +1 -1
- package/src/core/navigation.ts +10 -0
- package/src/core/schema.ts +1 -2
- package/src/og/card.ts +21 -20
|
@@ -347,11 +347,6 @@ export interface ThemeConfig {
|
|
|
347
347
|
mode?: "system" | "light" | "dark";
|
|
348
348
|
/** Corner radius scale. Defaults to `md`. */
|
|
349
349
|
radius?: "none" | "sm" | "md" | "lg";
|
|
350
|
-
/**
|
|
351
|
-
* Strict token mode: fail rather than silently fall back when a theme token
|
|
352
|
-
* is missing. Defaults to `false`.
|
|
353
|
-
*/
|
|
354
|
-
strict?: boolean;
|
|
355
350
|
}
|
|
356
351
|
/** Public credentials for the Algolia backend (the sync key stays an env var). */
|
|
357
352
|
export interface AlgoliaSearch {
|
|
@@ -432,7 +427,7 @@ export interface AskConfig {
|
|
|
432
427
|
export interface AiConfig {
|
|
433
428
|
/** The Ask AI chat assistant. */
|
|
434
429
|
ask?: AskConfig;
|
|
435
|
-
/** Emit `llms.txt` (an index of the docs for LLMs). Defaults to `
|
|
430
|
+
/** Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`. */
|
|
436
431
|
llmsTxt?: boolean;
|
|
437
432
|
}
|
|
438
433
|
/** An arbitrary analytics `<script>`; set exactly one of `src` or `content`. */
|
|
@@ -48,7 +48,7 @@ import type { Diagnostic } from "./types.ts";
|
|
|
48
48
|
*
|
|
49
49
|
* **Appearance**
|
|
50
50
|
* - `theme` — `accent` color, `fonts` (curated Google Font slugs), `radius`,
|
|
51
|
-
* `mode` (`system`/`light`/`dark`),
|
|
51
|
+
* `mode` (`system`/`light`/`dark`), and `background`.
|
|
52
52
|
* - `markdown` — `code` (language icons, inline highlighting, line wrap),
|
|
53
53
|
* `headingAnchors`, `imageZoom`, and opt-in KaTeX `math`.
|
|
54
54
|
* - `toc` — on-page table of contents; `true`/`false` or a heading-level range.
|
|
@@ -2395,9 +2395,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2395
2395
|
layout: z.ZodDefault<z.ZodEnum<["sidebar"]>>;
|
|
2396
2396
|
mode: z.ZodDefault<z.ZodEnum<["system", "light", "dark"]>>;
|
|
2397
2397
|
radius: z.ZodDefault<z.ZodEnum<["none", "sm", "md", "lg"]>>;
|
|
2398
|
-
strict: z.ZodDefault<z.ZodBoolean>;
|
|
2399
2398
|
}, "strict", z.ZodTypeAny, {
|
|
2400
|
-
strict: boolean;
|
|
2401
2399
|
accent: {
|
|
2402
2400
|
dark: string;
|
|
2403
2401
|
light: string;
|
|
@@ -2420,7 +2418,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2420
2418
|
light?: string | undefined;
|
|
2421
2419
|
} | undefined;
|
|
2422
2420
|
}, {
|
|
2423
|
-
strict?: boolean | undefined;
|
|
2424
2421
|
accent?: string | {
|
|
2425
2422
|
dark: string;
|
|
2426
2423
|
light: string;
|
|
@@ -2599,7 +2596,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2599
2596
|
})[] | undefined;
|
|
2600
2597
|
};
|
|
2601
2598
|
theme: {
|
|
2602
|
-
strict: boolean;
|
|
2603
2599
|
accent: {
|
|
2604
2600
|
dark: string;
|
|
2605
2601
|
light: string;
|
|
@@ -2919,7 +2915,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
|
|
|
2919
2915
|
vercel?: boolean | undefined;
|
|
2920
2916
|
} | undefined;
|
|
2921
2917
|
theme?: {
|
|
2922
|
-
strict?: boolean | undefined;
|
|
2923
2918
|
accent?: string | {
|
|
2924
2919
|
dark: string;
|
|
2925
2920
|
light: string;
|
package/docs/02-deployment.mdx
CHANGED
|
@@ -100,6 +100,10 @@ redirects: [{ from: "/old", to: "/new", status: 301 }];
|
|
|
100
100
|
|
|
101
101
|
`status` accepts `301`, `302`, `307`, or `308` (default `301`). Server builds handle redirects at request time. Static builds emit redirect pages **and** platform files so your host issues a real HTTP redirect: `_redirects` (Netlify, Cloudflare Pages), `vercel.json` (Vercel), and `blume-redirects.json` — a structured manifest for anything else (nginx/Apache rules, an edge worker). A `_redirects` or `vercel.json` you ship in `public/` is left untouched.
|
|
102
102
|
|
|
103
|
+
:::note
|
|
104
|
+
`from` is matched as an exact path — wildcards and pattern matching (e.g. `/blog/:slug` or `/old/*`) aren't supported. If you need pattern-based rules, handle them in an infrastructure file like `vercel.json` (which supports wildcard `source` patterns) or your host's redirect config instead. A `vercel.json` you ship in `public/` is preserved as-is.
|
|
105
|
+
:::
|
|
106
|
+
|
|
103
107
|
## Environment variables
|
|
104
108
|
|
|
105
109
|
When a feature needs a runtime secret, Blume warns at `blume dev`/`build` if it's missing — so the problem surfaces early instead of at the first request:
|
package/docs/03-faq.mdx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: FAQ
|
|
3
|
+
description: Common questions about Blume — how it compares to other documentation tools, and why a Markdown formatter might collapse your callout directives.
|
|
4
|
+
sidebar:
|
|
5
|
+
label: FAQ
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Answers to questions that come up often. Missing one? [Open an issue](https://github.com/haydenbleasel/blume/issues) or ask the in-page assistant.
|
|
9
|
+
|
|
10
|
+
## How is Blume different from Mintlify, Fumadocs, and others?
|
|
11
|
+
|
|
12
|
+
Most documentation tools sit at one of two extremes. **Managed platforms** like Mintlify give you a polished result fast, but the build and hosting are their service — you author inside their system and deploy to their infrastructure. **Component libraries and starters** like Fumadocs, Nextra, or Docusaurus are open-source and flexible, but they hand you an application (a Next.js or React project) that you scaffold, wire up, and maintain before and after you write a word.
|
|
13
|
+
|
|
14
|
+
Blume takes a third path: **the framework is the template.** You point it at a folder of Markdown and it generates and drives the whole site — navigation, search, theming, Open Graph images, SEO, and AI endpoints — with no app to own. It's fully open-source and self-hostable, so there's no managed service and no vendor lock-in, but there's also no boilerplate to maintain.
|
|
15
|
+
|
|
16
|
+
| | Blume | Mintlify | Fumadocs / Nextra / Docusaurus |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| **Model** | Zero-config framework; content only | Hosted platform | Library + app you scaffold |
|
|
19
|
+
| **Source** | Open-source (MIT) | Closed core | Open-source |
|
|
20
|
+
| **Hosting** | Anywhere — static or a server function | Their managed infrastructure | Anywhere; you build and deploy |
|
|
21
|
+
| **You maintain** | Your Markdown | Your Markdown + platform config | Your Markdown + the app around it |
|
|
22
|
+
| **Rendering** | Astro; core theme ships zero client JS | Their runtime | React/Next.js runtime |
|
|
23
|
+
| **AI features** | `llms.txt`, raw Markdown, Ask AI, MCP — built in, no hosted service | Built in (hosted) | Bring your own |
|
|
24
|
+
|
|
25
|
+
A few consequences worth calling out:
|
|
26
|
+
|
|
27
|
+
- **You own the output.** `blume build` produces a plain site you host on Vercel, Netlify, Cloudflare, S3, or your own box. Nothing phones home.
|
|
28
|
+
- **No lock-in, two ways out.** Your content is portable Markdown, and `blume eject` turns the project into a standalone Astro app that still uses the `blume` package when you want full control.
|
|
29
|
+
- **Fast by default.** The core theme is React-free and renders static HTML, so pages score well on Core Web Vitals without tuning. You opt into server features (Ask AI, MCP) only when you need them.
|
|
30
|
+
- **Type-safe configuration.** `blume.config.ts` and every `meta.ts` are real TypeScript validated by a schema — not loosely-typed YAML.
|
|
31
|
+
|
|
32
|
+
:::note
|
|
33
|
+
This isn't "better than everything" — managed platforms and full frameworks are the right call when you want a hosted product or maximum control over the app. Blume is for teams who want a production-grade docs site without owning either the platform or the plumbing.
|
|
34
|
+
:::
|
|
35
|
+
|
|
36
|
+
See [Why Blume exists](/docs) for the longer version.
|
|
37
|
+
|
|
38
|
+
## Is Blume free and open-source?
|
|
39
|
+
|
|
40
|
+
Yes — Blume is MIT-licensed and free. You install the `blume` package, keep your content in your own repository, and host the build wherever you like. There's no paid tier, no per-seat pricing, and no account to sign up for. The source lives on [GitHub](https://github.com/haydenbleasel/blume).
|
|
41
|
+
|
|
42
|
+
## Do I need to know Astro, React, or Tailwind?
|
|
43
|
+
|
|
44
|
+
No. A folder of Markdown is a complete site — navigation, search, and theming are inferred or set with a handful of tokens. You only reach for the underlying stack when you want to customize: [interactive islands](/docs/content/islands) (React), [component overrides](/docs/configuration/customization), or [theme tokens](/docs/configuration/theming) (Tailwind). Even then, [`blume.config.ts`](/docs/configuration) is typed, so your editor guides you.
|
|
45
|
+
|
|
46
|
+
## Can I use React components and MDX?
|
|
47
|
+
|
|
48
|
+
Yes. Any page can be `.md` or `.mdx`, and MDX lets you drop in the [built-in components](/docs/content/components) with no imports. You can also add your own `.tsx`/`.jsx` [islands](/docs/content/islands) — Blume auto-enables React only for the pages that use them, so the core theme stays JavaScript-free everywhere else.
|
|
49
|
+
|
|
50
|
+
## Where can I deploy it?
|
|
51
|
+
|
|
52
|
+
Anywhere. `blume build` outputs static HTML by default, which you can serve from any static host or CDN — Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, or your own server. Server-only features (Ask AI, the MCP server, on-demand rendering) switch the build to a server function through an adapter for Vercel, Node, Netlify, or Cloudflare. See [Deployment](/docs/deployment).
|
|
53
|
+
|
|
54
|
+
## Does search need a hosted service?
|
|
55
|
+
|
|
56
|
+
No. [Orama](/docs/configuration/search) builds a local index that works in both dev and production with nothing to host or pay for. For very large sites, [Pagefind](/docs/configuration/search) is one flag away. Either way the index ships as part of your site.
|
|
57
|
+
|
|
58
|
+
## How do I customize the look?
|
|
59
|
+
|
|
60
|
+
Start with [theme tokens](/docs/configuration/theming) — accent color, fonts, radius, and a `theme.css` for anything else Tailwind can express. Go further by [overriding built-in components](/docs/configuration/customization) or adding [custom pages](/docs/configuration/customization#custom-pages). When you want the Astro project itself, [`blume eject`](/docs/reference/cli) hands you a standalone app that still uses the `blume` package.
|
|
61
|
+
|
|
62
|
+
## Why is oxfmt / Ultracite collapsing my directives?
|
|
63
|
+
|
|
64
|
+
If you format your Markdown with [Ultracite](https://www.ultracite.ai) (which runs oxlint + [oxfmt](https://oxc.rs)) — as Blume itself does — you may notice that container directives get flattened onto a single line after a format pass:
|
|
65
|
+
|
|
66
|
+
```md
|
|
67
|
+
:::note
|
|
68
|
+
Regenerate the project with blume dev.
|
|
69
|
+
:::
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
becomes
|
|
73
|
+
|
|
74
|
+
```md
|
|
75
|
+
:::note Regenerate the project with blume dev. :::
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Once the opening `:::note` fence is joined to the prose, it's no longer a directive, so it renders as literal text instead of a [callout](/docs/content/syntax#callouts).
|
|
79
|
+
|
|
80
|
+
### Why it happens
|
|
81
|
+
|
|
82
|
+
This is a bug in oxfmt's Markdown formatter (inherited from Prettier's Markdown printer — see [prettier/prettier#19040](https://github.com/prettier/prettier/pull/19040)). When it wraps prose, it treats the `:::` fence lines as ordinary text and joins them with the adjacent line, breaking the directive. It affects every container directive — `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::success`.
|
|
83
|
+
|
|
84
|
+
We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-project/oxc/issues/24096); until it's fixed there, the patch below is the workaround.
|
|
85
|
+
|
|
86
|
+
### The fix
|
|
87
|
+
|
|
88
|
+
Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships exactly this patch in its own repo, and you can apply the same one in any project.
|
|
89
|
+
|
|
90
|
+
1. Save the patch as `patches/oxfmt@0.55.0.patch`:
|
|
91
|
+
|
|
92
|
+
```diff patches/oxfmt@0.55.0.patch
|
|
93
|
+
diff --git a/dist/markdown-Bowe09rB.js b/dist/markdown-Bowe09rB.js
|
|
94
|
+
index 0fe63ff712e07c9c2188711fa0940f7a7d8f7d74..44435d91a2f5a6711512981c9bcaddedc1760be4 100644
|
|
95
|
+
--- a/dist/markdown-Bowe09rB.js
|
|
96
|
+
+++ b/dist/markdown-Bowe09rB.js
|
|
97
|
+
@@ -1141,7 +1141,14 @@ function Yf(e, r, t) {
|
|
98
|
+
case "sentence": return Yi(e, t);
|
|
99
|
+
case "word": return Li(e);
|
|
100
|
+
case "whitespace": {
|
|
101
|
+
- let { next: i } = e, u = i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(i.value) ? "never" : r.proseWrap;
|
|
102
|
+
+ let { next: i, previous: oxfmtFencePrev } = e;
|
|
103
|
+
+ // Preserve line breaks that sit directly against a `:::` container
|
|
104
|
+
+ // directive fence, so `proseWrap: "never"` keeps the opening/closing
|
|
105
|
+
+ // fence on their own lines instead of joining them into the prose (which
|
|
106
|
+
+ // breaks the directive). Ordinary prose still wraps per proseWrap.
|
|
107
|
+
+ // See prettier/prettier#19040.
|
|
108
|
+
+ let oxfmtIsFence = (w) => w != null && typeof w.value === "string" && w.value.startsWith(":::");
|
|
109
|
+
+ let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtIsFence(i) ? "preserve" : i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(i.value) ? "never" : r.proseWrap;
|
|
110
|
+
return qt(e, n.value, u);
|
|
111
|
+
}
|
|
112
|
+
case "emphasis": {
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
2. Register it with your package manager's `patchedDependencies`. With Bun or pnpm, add to `package.json`:
|
|
116
|
+
|
|
117
|
+
```json package.json
|
|
118
|
+
{
|
|
119
|
+
"patchedDependencies": {
|
|
120
|
+
"oxfmt@0.55.0": "patches/oxfmt@0.55.0.patch"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
3. Reinstall so the patch is applied:
|
|
126
|
+
|
|
127
|
+
```package-install
|
|
128
|
+
bun install
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
:::warning[Version-pinned] The patch targets oxfmt `0.55.0` — its diff references a file hashed to that build (`dist/markdown-Bowe09rB.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
|
|
132
|
+
:::
|
|
@@ -3,19 +3,19 @@ title: AI
|
|
|
3
3
|
description: Machine-readable docs with llms.txt and an optional in-page Ask AI assistant.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Blume has
|
|
6
|
+
Blume has a few AI features: machine-readable docs for external tools (`llms.txt`, on by default), an in-page **Ask AI** assistant, and a hosted **MCP server** for coding agents. Ask AI and MCP are opt-in, and static docs stay fully static until you turn a feature on.
|
|
7
7
|
|
|
8
8
|
## llms.txt
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Blume emits machine-readable versions of your docs that coding agents and chat assistants can consume. This is on by default; set `llmsTxt: false` to turn it off:
|
|
11
11
|
|
|
12
12
|
```ts blume.config.ts lineNumbers
|
|
13
13
|
ai: {
|
|
14
|
-
llmsTxt:
|
|
14
|
+
llmsTxt: false,
|
|
15
15
|
}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
While enabled, `blume build` writes two files to the root of your site:
|
|
19
19
|
|
|
20
20
|
- **`/llms.txt`** — a compact index: your site title and description, then a linked list of every page with its summary.
|
|
21
21
|
- **`/llms-full.txt`** — the entire corpus: each page's full Markdown body, with its source URL, in one file.
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
28
28
|
>
|
|
29
29
|
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
30
30
|
<span>
|
|
31
|
-
<span class="block text-muted-foreground text-xs">
|
|
31
|
+
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
32
32
|
{strings.previous}
|
|
33
33
|
</span>
|
|
34
34
|
<span class="block font-medium">{prev.label}</span>
|
|
@@ -43,7 +43,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
43
43
|
href={next.route}
|
|
44
44
|
>
|
|
45
45
|
<span>
|
|
46
|
-
<span class="block text-muted-foreground text-xs">
|
|
46
|
+
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
47
47
|
{strings.next}
|
|
48
48
|
</span>
|
|
49
49
|
<span class="block font-medium">{next.label}</span>
|
|
@@ -367,11 +367,15 @@ const bannerScript = banner?.dismissible
|
|
|
367
367
|
class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}
|
|
368
368
|
data-blume-doc-grid
|
|
369
369
|
>
|
|
370
|
-
{
|
|
371
|
-
showSidebar && (
|
|
372
370
|
<aside
|
|
373
371
|
aria-label="Primary"
|
|
374
|
-
class=
|
|
372
|
+
class:list={[
|
|
373
|
+
"fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,4rem))] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:top-16 lg:z-auto lg:h-[calc(100dvh-4rem)] lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4",
|
|
374
|
+
// A "bare" landing (the changelog index) has no sidebar column on
|
|
375
|
+
// desktop, but the header hamburger still needs a drawer to open on
|
|
376
|
+
// mobile — without it the toggle only locked page scroll.
|
|
377
|
+
!showSidebar && "lg:hidden",
|
|
378
|
+
]}
|
|
375
379
|
>
|
|
376
380
|
{
|
|
377
381
|
// Pinned links (blog, changelog, contact…) sit above every section on
|
|
@@ -419,9 +423,17 @@ const bannerScript = banner?.dismissible
|
|
|
419
423
|
// The header's tab bar is hidden below `md`, so the drawer is the only
|
|
420
424
|
// way to move between sections on mobile: list the tabs above the
|
|
421
425
|
// page tree, separated by a divider. Hidden from `md` up, where the
|
|
422
|
-
// header tabs take over
|
|
426
|
+
// header tabs take over — unless the page tree is empty (a tab-scoped
|
|
427
|
+
// standalone page like the changelog), where they keep the drawer
|
|
428
|
+
// from opening blank.
|
|
423
429
|
navigation.tabs.length > 0 && (
|
|
424
|
-
<nav
|
|
430
|
+
<nav
|
|
431
|
+
aria-label="Sections"
|
|
432
|
+
class:list={[
|
|
433
|
+
"mb-4 border-border border-b pb-4",
|
|
434
|
+
sidebar.length > 0 && "md:hidden",
|
|
435
|
+
]}
|
|
436
|
+
>
|
|
425
437
|
<ul class="m-0 list-none p-0">
|
|
426
438
|
{navigation.tabs.map((tab) => (
|
|
427
439
|
<li>
|
|
@@ -460,8 +472,6 @@ const bannerScript = banner?.dismissible
|
|
|
460
472
|
}
|
|
461
473
|
</nav>
|
|
462
474
|
</aside>
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
475
|
<main class="px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
|
|
466
476
|
<BreadcrumbsSlot crumbs={crumbs} wide={isApiOperation} />
|
|
467
477
|
<TableOfContentsSlot
|
package/src/core/config-input.ts
CHANGED
|
@@ -406,11 +406,6 @@ export interface ThemeConfig {
|
|
|
406
406
|
mode?: "system" | "light" | "dark";
|
|
407
407
|
/** Corner radius scale. Defaults to `md`. */
|
|
408
408
|
radius?: "none" | "sm" | "md" | "lg";
|
|
409
|
-
/**
|
|
410
|
-
* Strict token mode: fail rather than silently fall back when a theme token
|
|
411
|
-
* is missing. Defaults to `false`.
|
|
412
|
-
*/
|
|
413
|
-
strict?: boolean;
|
|
414
409
|
}
|
|
415
410
|
|
|
416
411
|
// ---------------------------------------------------------------------------
|
|
@@ -512,7 +507,7 @@ export interface AskConfig {
|
|
|
512
507
|
export interface AiConfig {
|
|
513
508
|
/** The Ask AI chat assistant. */
|
|
514
509
|
ask?: AskConfig;
|
|
515
|
-
/** Emit `llms.txt` (an index of the docs for LLMs). Defaults to `
|
|
510
|
+
/** Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`. */
|
|
516
511
|
llmsTxt?: boolean;
|
|
517
512
|
}
|
|
518
513
|
|
package/src/core/config.ts
CHANGED
|
@@ -56,7 +56,7 @@ import type { Diagnostic } from "./types.ts";
|
|
|
56
56
|
*
|
|
57
57
|
* **Appearance**
|
|
58
58
|
* - `theme` — `accent` color, `fonts` (curated Google Font slugs), `radius`,
|
|
59
|
-
* `mode` (`system`/`light`/`dark`),
|
|
59
|
+
* `mode` (`system`/`light`/`dark`), and `background`.
|
|
60
60
|
* - `markdown` — `code` (language icons, inline highlighting, line wrap),
|
|
61
61
|
* `headingAnchors`, `imageZoom`, and opt-in KaTeX `math`.
|
|
62
62
|
* - `toc` — on-page table of contents; `true`/`false` or a heading-level range.
|
package/src/core/navigation.ts
CHANGED
|
@@ -108,6 +108,16 @@ const pageOrder = (page: PageRecord, filename: string): number => {
|
|
|
108
108
|
if (filename.replace(extname(filename), "") === "index") {
|
|
109
109
|
return Number.NEGATIVE_INFINITY;
|
|
110
110
|
}
|
|
111
|
+
// Changelog entries read newest-first, matching the generated timeline. Sort
|
|
112
|
+
// on the negated publish timestamp so a later date yields a smaller order
|
|
113
|
+
// under the ascending comparator; undated entries fall back to filename order.
|
|
114
|
+
if (page.contentType === "changelog") {
|
|
115
|
+
const iso = page.meta.date ?? page.meta.changelog?.date;
|
|
116
|
+
const time = iso ? Date.parse(iso) : Number.NaN;
|
|
117
|
+
if (!Number.isNaN(time)) {
|
|
118
|
+
return -time;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
111
121
|
return numericOrder(filename);
|
|
112
122
|
};
|
|
113
123
|
|
package/src/core/schema.ts
CHANGED
|
@@ -482,7 +482,6 @@ const themeConfigSchema = z
|
|
|
482
482
|
layout: z.enum(["sidebar"]).default("sidebar"),
|
|
483
483
|
mode: z.enum(["system", "light", "dark"]).default("system"),
|
|
484
484
|
radius: z.enum(["none", "sm", "md", "lg"]).default("md"),
|
|
485
|
-
strict: z.boolean().default(false),
|
|
486
485
|
})
|
|
487
486
|
.strict();
|
|
488
487
|
|
|
@@ -620,7 +619,7 @@ const aiConfigSchema = z
|
|
|
620
619
|
}
|
|
621
620
|
})
|
|
622
621
|
.optional(),
|
|
623
|
-
llmsTxt: z.boolean().default(
|
|
622
|
+
llmsTxt: z.boolean().default(true),
|
|
624
623
|
})
|
|
625
624
|
.strict();
|
|
626
625
|
|
package/src/og/card.ts
CHANGED
|
@@ -80,27 +80,34 @@ export const truncate = (value: string, max: number): string => {
|
|
|
80
80
|
: value;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
// Brand mark sizing: target this height, but scale down so
|
|
84
|
-
// stays within the lockup.
|
|
83
|
+
// Brand mark sizing: target this height, but scale down so an extremely wide
|
|
84
|
+
// logo stays within the lockup. The cap leaves room for a wordmark to render
|
|
85
|
+
// at full height — it stands alone as the brand (no text label beside it).
|
|
85
86
|
const MARK_HEIGHT = 32;
|
|
86
|
-
const MARK_MAX_WIDTH =
|
|
87
|
+
const MARK_MAX_WIDTH = 240;
|
|
87
88
|
// Accept either quote style and a non-zero min-x/min-y; only width/height
|
|
88
89
|
// matter for the aspect ratio. A miss falls back to a square mark.
|
|
89
90
|
const VIEW_BOX =
|
|
90
91
|
/viewBox=(?<q>["'])[\d.-]+[\s,]+[\d.-]+[\s,]+(?<w>[\d.]+)[\s,]+(?<h>[\d.]+)\k<q>/u;
|
|
91
92
|
|
|
93
|
+
/** The SVG's viewBox aspect ratio (w/h), or null without a usable viewBox. */
|
|
94
|
+
const logoAspect = (svg: string): number | null => {
|
|
95
|
+
const box = svg.match(VIEW_BOX);
|
|
96
|
+
const w = Number(box?.groups?.w);
|
|
97
|
+
const h = Number(box?.groups?.h);
|
|
98
|
+
return w && h ? w / h : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
92
101
|
// Render the configured logo as the brand mark. A `currentColor` logo carries
|
|
93
102
|
// no intrinsic color, so it is painted in the foreground to read on the light
|
|
94
103
|
// card, then handed to Takumi as a data URI sized from the SVG's aspect ratio.
|
|
95
104
|
const logoMark = (svg: string): Node => {
|
|
96
105
|
const painted = svg.replaceAll("currentColor", FOREGROUND);
|
|
97
|
-
const
|
|
98
|
-
const w = Number(box?.groups?.w);
|
|
99
|
-
const h = Number(box?.groups?.h);
|
|
106
|
+
const aspect = logoAspect(painted);
|
|
100
107
|
let height = MARK_HEIGHT;
|
|
101
|
-
let width =
|
|
108
|
+
let width = aspect ? MARK_HEIGHT * aspect : MARK_HEIGHT;
|
|
102
109
|
if (width > MARK_MAX_WIDTH) {
|
|
103
|
-
height =
|
|
110
|
+
height = aspect ? MARK_MAX_WIDTH / aspect : MARK_HEIGHT;
|
|
104
111
|
width = MARK_MAX_WIDTH;
|
|
105
112
|
}
|
|
106
113
|
return image({
|
|
@@ -152,19 +159,13 @@ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
|
152
159
|
const repo = options.repo?.trim();
|
|
153
160
|
const site = options.site?.trim();
|
|
154
161
|
|
|
162
|
+
// Logo only — no brand-name label beside it. A wordmark logo already spells
|
|
163
|
+
// the name, and rendering the site title next to it duplicated the brand
|
|
164
|
+
// ("Ultracite Ultracite"). Without a logo, the accent tile with the brand
|
|
165
|
+
// initial stands in.
|
|
155
166
|
const header = container({
|
|
156
|
-
children: [
|
|
157
|
-
|
|
158
|
-
brand
|
|
159
|
-
? text(brand, {
|
|
160
|
-
color: FOREGROUND,
|
|
161
|
-
fontSize: 30,
|
|
162
|
-
fontWeight: 500,
|
|
163
|
-
letterSpacing: "-0.01em",
|
|
164
|
-
})
|
|
165
|
-
: container({}),
|
|
166
|
-
],
|
|
167
|
-
style: { alignItems: "center", display: "flex", gap: 18 },
|
|
167
|
+
children: [logo ? logoMark(logo) : initialMark(accent, initial)],
|
|
168
|
+
style: { alignItems: "center", display: "flex" },
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
const body = container({
|