blume 0.8.0 → 1.0.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/CHANGELOG.md +49 -1
- package/dist/cli/index.js +373 -28
- package/dist/cli/index.js.map +51 -50
- package/dist/types/core/config-input.d.ts +11 -0
- package/dist/types/core/data.d.ts +5 -0
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +77 -1
- package/dist/types/seo/x-handle.d.ts +12 -0
- package/docs/advanced/api-reference.mdx +1 -1
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/configuration/export.mdx +1 -1
- package/docs/configuration/seo.mdx +33 -3
- package/docs/content/components.mdx +1 -1
- package/docs/content/syntax.mdx +2 -2
- package/docs/index.mdx +1 -1
- package/docs/reference/cli.mdx +1 -1
- package/docs/reference/frontmatter.mdx +1 -1
- package/package.json +9 -1
- package/src/ai/mcp/tools.ts +1 -1
- package/src/astro/generate.ts +1 -0
- package/src/astro/templates.ts +51 -7
- package/src/cli/commands/build.ts +25 -8
- package/src/cli/internal-error.ts +9 -4
- package/src/cli/prepare.ts +3 -2
- package/src/components/Icon.astro +2 -1
- package/src/components/islands/ask-ai.tsx +30 -5
- package/src/components/layout/Breadcrumbs.astro +11 -2
- package/src/components/layout/NavTree.astro +2 -2
- package/src/components/layout/PageLayout.astro +40 -1
- package/src/components/layout/Pagination.astro +10 -4
- package/src/components/layout/ReferenceLayout.astro +12 -1
- package/src/components/layout/RootLayout.astro +75 -3
- package/src/components/layout/Search.astro +23 -5
- package/src/core/config-input.ts +11 -0
- package/src/core/data.ts +2 -0
- package/src/core/frontmatter.ts +7 -0
- package/src/core/i18n-ui.ts +9 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/schema.ts +35 -0
- package/src/core/ui-packs/ar.ts +7 -0
- package/src/core/ui-packs/bg.ts +7 -0
- package/src/core/ui-packs/bn.ts +7 -0
- package/src/core/ui-packs/ca.ts +7 -0
- package/src/core/ui-packs/cs.ts +7 -0
- package/src/core/ui-packs/da.ts +7 -0
- package/src/core/ui-packs/de.ts +7 -0
- package/src/core/ui-packs/el.ts +7 -0
- package/src/core/ui-packs/es.ts +7 -0
- package/src/core/ui-packs/fa.ts +7 -0
- package/src/core/ui-packs/fi.ts +7 -0
- package/src/core/ui-packs/fr.ts +7 -0
- package/src/core/ui-packs/he.ts +7 -0
- package/src/core/ui-packs/hi.ts +7 -0
- package/src/core/ui-packs/hr.ts +7 -0
- package/src/core/ui-packs/hu.ts +7 -0
- package/src/core/ui-packs/id.ts +7 -0
- package/src/core/ui-packs/it.ts +7 -0
- package/src/core/ui-packs/ja.ts +7 -0
- package/src/core/ui-packs/ko.ts +7 -0
- package/src/core/ui-packs/nl.ts +7 -0
- package/src/core/ui-packs/no.ts +7 -0
- package/src/core/ui-packs/pl.ts +7 -0
- package/src/core/ui-packs/pt-br.ts +7 -0
- package/src/core/ui-packs/pt.ts +7 -0
- package/src/core/ui-packs/ro.ts +7 -0
- package/src/core/ui-packs/ru.ts +7 -0
- package/src/core/ui-packs/sk.ts +7 -0
- package/src/core/ui-packs/sr.ts +7 -0
- package/src/core/ui-packs/sv.ts +7 -0
- package/src/core/ui-packs/th.ts +7 -0
- package/src/core/ui-packs/tr.ts +7 -0
- package/src/core/ui-packs/uk.ts +7 -0
- package/src/core/ui-packs/vi.ts +7 -0
- package/src/core/ui-packs/zh-tw.ts +7 -0
- package/src/core/ui-packs/zh.ts +7 -0
- package/src/og/card.ts +4 -2
- package/src/og/dimensions.ts +12 -0
- package/src/openapi/render-mdx.ts +62 -1
- package/src/seo/jsonld.ts +7 -1
- package/src/seo/x-handle.ts +18 -0
package/src/core/ui-packs/tr.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const tr: UIStringsOverride = {
|
|
|
24
24
|
title: "Yapay zekâya sor",
|
|
25
25
|
},
|
|
26
26
|
banner: { dismiss: "Duyuruyu kapat" },
|
|
27
|
+
changelog: {
|
|
28
|
+
description: "Ürün güncellemeleri ve sürüm notları.",
|
|
29
|
+
title: "Değişiklik günlüğü",
|
|
30
|
+
},
|
|
27
31
|
feedback: {
|
|
28
32
|
no: "Hayır",
|
|
29
33
|
question: "Bu sayfa yardımcı oldu mu?",
|
|
@@ -31,13 +35,16 @@ export const tr: UIStringsOverride = {
|
|
|
31
35
|
yes: "Evet",
|
|
32
36
|
},
|
|
33
37
|
languageSwitcher: { label: "Dil", untranslated: "Çevrilmemiş" },
|
|
38
|
+
nav: { breadcrumb: "Gezinme yolu" },
|
|
34
39
|
page: {
|
|
35
40
|
lastUpdated: "Son güncelleme",
|
|
36
41
|
next: "Sonraki",
|
|
42
|
+
pagination: "Sayfalama",
|
|
37
43
|
previous: "Önceki",
|
|
38
44
|
skipToContent: "İçeriğe geç",
|
|
39
45
|
},
|
|
40
46
|
search: {
|
|
47
|
+
all: "Tümü",
|
|
41
48
|
button: "Ara",
|
|
42
49
|
devOnly: "Arama, üretim derlemesinde kullanılabilir.",
|
|
43
50
|
label: "Belgelerde ara",
|
package/src/core/ui-packs/uk.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const uk: UIStringsOverride = {
|
|
|
24
24
|
title: "Запитати ШІ",
|
|
25
25
|
},
|
|
26
26
|
banner: { dismiss: "Закрити оголошення" },
|
|
27
|
+
changelog: {
|
|
28
|
+
description: "Оновлення продукту та примітки до випусків.",
|
|
29
|
+
title: "Журнал змін",
|
|
30
|
+
},
|
|
27
31
|
feedback: {
|
|
28
32
|
no: "Ні",
|
|
29
33
|
question: "Чи була ця сторінка корисною?",
|
|
@@ -31,13 +35,16 @@ export const uk: UIStringsOverride = {
|
|
|
31
35
|
yes: "Так",
|
|
32
36
|
},
|
|
33
37
|
languageSwitcher: { label: "Мова", untranslated: "Не перекладено" },
|
|
38
|
+
nav: { breadcrumb: "Навігаційний ланцюжок" },
|
|
34
39
|
page: {
|
|
35
40
|
lastUpdated: "Останнє оновлення",
|
|
36
41
|
next: "Далі",
|
|
42
|
+
pagination: "Пагінація",
|
|
37
43
|
previous: "Назад",
|
|
38
44
|
skipToContent: "Перейти до вмісту",
|
|
39
45
|
},
|
|
40
46
|
search: {
|
|
47
|
+
all: "Усі",
|
|
41
48
|
button: "Пошук",
|
|
42
49
|
devOnly: "Пошук доступний у production-збірці.",
|
|
43
50
|
label: "Пошук у документації",
|
package/src/core/ui-packs/vi.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const vi: UIStringsOverride = {
|
|
|
24
24
|
title: "Hỏi AI",
|
|
25
25
|
},
|
|
26
26
|
banner: { dismiss: "Đóng thông báo" },
|
|
27
|
+
changelog: {
|
|
28
|
+
description: "Cập nhật sản phẩm và ghi chú phát hành.",
|
|
29
|
+
title: "Nhật ký thay đổi",
|
|
30
|
+
},
|
|
27
31
|
feedback: {
|
|
28
32
|
no: "Không",
|
|
29
33
|
question: "Trang này có hữu ích không?",
|
|
@@ -31,13 +35,16 @@ export const vi: UIStringsOverride = {
|
|
|
31
35
|
yes: "Có",
|
|
32
36
|
},
|
|
33
37
|
languageSwitcher: { label: "Ngôn ngữ", untranslated: "Chưa dịch" },
|
|
38
|
+
nav: { breadcrumb: "Đường dẫn điều hướng" },
|
|
34
39
|
page: {
|
|
35
40
|
lastUpdated: "Cập nhật lần cuối",
|
|
36
41
|
next: "Tiếp theo",
|
|
42
|
+
pagination: "Phân trang",
|
|
37
43
|
previous: "Trước",
|
|
38
44
|
skipToContent: "Chuyển đến nội dung",
|
|
39
45
|
},
|
|
40
46
|
search: {
|
|
47
|
+
all: "Tất cả",
|
|
41
48
|
button: "Tìm kiếm",
|
|
42
49
|
devOnly: "Tìm kiếm có sẵn trong bản dựng production.",
|
|
43
50
|
label: "Tìm kiếm tài liệu",
|
|
@@ -24,6 +24,10 @@ export const zhTW: UIStringsOverride = {
|
|
|
24
24
|
title: "向 AI 提問",
|
|
25
25
|
},
|
|
26
26
|
banner: { dismiss: "關閉公告" },
|
|
27
|
+
changelog: {
|
|
28
|
+
description: "產品更新與版本說明。",
|
|
29
|
+
title: "更新日誌",
|
|
30
|
+
},
|
|
27
31
|
feedback: {
|
|
28
32
|
no: "沒有幫助",
|
|
29
33
|
question: "這個頁面有幫助嗎?",
|
|
@@ -31,13 +35,16 @@ export const zhTW: UIStringsOverride = {
|
|
|
31
35
|
yes: "有幫助",
|
|
32
36
|
},
|
|
33
37
|
languageSwitcher: { label: "語言", untranslated: "未翻譯" },
|
|
38
|
+
nav: { breadcrumb: "麵包屑導覽" },
|
|
34
39
|
page: {
|
|
35
40
|
lastUpdated: "最後更新於",
|
|
36
41
|
next: "下一頁",
|
|
42
|
+
pagination: "分頁",
|
|
37
43
|
previous: "上一頁",
|
|
38
44
|
skipToContent: "跳至內容",
|
|
39
45
|
},
|
|
40
46
|
search: {
|
|
47
|
+
all: "全部",
|
|
41
48
|
button: "搜尋",
|
|
42
49
|
devOnly: "搜尋在正式版組建中可用。",
|
|
43
50
|
label: "搜尋文件",
|
package/src/core/ui-packs/zh.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const zh: UIStringsOverride = {
|
|
|
24
24
|
title: "向 AI 提问",
|
|
25
25
|
},
|
|
26
26
|
banner: { dismiss: "关闭公告" },
|
|
27
|
+
changelog: {
|
|
28
|
+
description: "产品更新与版本说明。",
|
|
29
|
+
title: "更新日志",
|
|
30
|
+
},
|
|
27
31
|
feedback: {
|
|
28
32
|
no: "没有帮助",
|
|
29
33
|
question: "这个页面有帮助吗?",
|
|
@@ -31,13 +35,16 @@ export const zh: UIStringsOverride = {
|
|
|
31
35
|
yes: "有帮助",
|
|
32
36
|
},
|
|
33
37
|
languageSwitcher: { label: "语言", untranslated: "未翻译" },
|
|
38
|
+
nav: { breadcrumb: "面包屑导航" },
|
|
34
39
|
page: {
|
|
35
40
|
lastUpdated: "最后更新于",
|
|
36
41
|
next: "下一页",
|
|
42
|
+
pagination: "分页",
|
|
37
43
|
previous: "上一页",
|
|
38
44
|
skipToContent: "跳到内容",
|
|
39
45
|
},
|
|
40
46
|
search: {
|
|
47
|
+
all: "全部",
|
|
41
48
|
button: "搜索",
|
|
42
49
|
devOnly: "搜索在生产构建中可用。",
|
|
43
50
|
label: "搜索文档",
|
package/src/og/card.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Renderer } from "@takumi-rs/core";
|
|
|
2
2
|
import { container, image, text } from "@takumi-rs/helpers";
|
|
3
3
|
import type { Node } from "@takumi-rs/helpers";
|
|
4
4
|
|
|
5
|
+
import { OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from "./dimensions.ts";
|
|
6
|
+
|
|
5
7
|
// Reuse one renderer (and its loaded default fonts) across all images.
|
|
6
8
|
let renderer: Renderer | null = null;
|
|
7
9
|
const getRenderer = (): Renderer => {
|
|
@@ -53,8 +55,8 @@ export interface OgCardOptions {
|
|
|
53
55
|
site?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
const WIDTH =
|
|
57
|
-
const HEIGHT =
|
|
58
|
+
const WIDTH = OG_IMAGE_WIDTH;
|
|
59
|
+
const HEIGHT = OG_IMAGE_HEIGHT;
|
|
58
60
|
|
|
59
61
|
// Light neutral scale mirrored from the docs homepage theme tokens:
|
|
60
62
|
// FOREGROUND = --foreground, MUTED = --muted-foreground, FAINT = that lighter,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimensions of a generated OG card, shared by the renderer (`card.ts`) and the
|
|
3
|
+
* layouts that declare them as `og:image:width`/`og:image:height` so a crawler
|
|
4
|
+
* can lay out the card without fetching the PNG first.
|
|
5
|
+
*
|
|
6
|
+
* This lives apart from `card.ts` because that module imports the Takumi native
|
|
7
|
+
* binding at load; a layout importing it would drag the renderer into every
|
|
8
|
+
* page render (and into the prerender/SSR bundles that externalize it).
|
|
9
|
+
*/
|
|
10
|
+
export const OG_IMAGE_WIDTH = 1200;
|
|
11
|
+
export const OG_IMAGE_HEIGHT = 630;
|
|
12
|
+
export const OG_IMAGE_TYPE = "image/png";
|
|
@@ -58,6 +58,61 @@ export interface RenderedPage {
|
|
|
58
58
|
body: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Meta descriptions. A page that sets none falls back to the site-wide default,
|
|
62
|
+
// so a spec's pages would otherwise all ship one identical description — what
|
|
63
|
+
// search engines treat as duplicate content. These go in `seo.description`, not
|
|
64
|
+
// `description`: the prose already renders in the body, and a `description`
|
|
65
|
+
// frontmatter field would print it a second time as the page subtitle.
|
|
66
|
+
const META_DESCRIPTION_MAX = 160;
|
|
67
|
+
const PARAGRAPH_BREAK = /\n\s*\n/u;
|
|
68
|
+
const MARKDOWN_LINK = /\[(?<text>[^\]]*)\]\([^)]*\)/gu;
|
|
69
|
+
const MARKDOWN_MARKS = /[*_`#>]/gu;
|
|
70
|
+
const WHITESPACE = /\s+/gu;
|
|
71
|
+
const TRAILING_WORD = /\s+\S*$/u;
|
|
72
|
+
|
|
73
|
+
/** Flatten markdown prose to its first paragraph as single-line plain text. */
|
|
74
|
+
const plainProse = (markdown: string): string =>
|
|
75
|
+
(markdown.trim().split(PARAGRAPH_BREAK).at(0) ?? "")
|
|
76
|
+
.replace(MARKDOWN_LINK, "$<text>")
|
|
77
|
+
.replace(MARKDOWN_MARKS, "")
|
|
78
|
+
.replace(WHITESPACE, " ")
|
|
79
|
+
.trim();
|
|
80
|
+
|
|
81
|
+
/** Cap `text` at `max` characters, cutting on a word boundary. */
|
|
82
|
+
const clip = (text: string, max: number): string => {
|
|
83
|
+
if (max <= 0) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
if (text.length <= max) {
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
const head = text.slice(0, max - 1);
|
|
90
|
+
const onWordBoundary = head.replace(TRAILING_WORD, "");
|
|
91
|
+
// One very long token — an endpoint path has no spaces — would be dropped
|
|
92
|
+
// whole, leaving a stub. Hard-cut it instead of losing it.
|
|
93
|
+
return `${onWordBoundary.length >= max / 2 ? onWordBoundary : head}…`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const apiName = (spec: ApiSpecData): string => spec.title || spec.label;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The spec's own prose for the operation, followed by the endpoint it documents
|
|
100
|
+
* — so every operation page carries a distinct, self-describing meta
|
|
101
|
+
* description even when the spec's summaries are terse.
|
|
102
|
+
*/
|
|
103
|
+
const operationDescription = (
|
|
104
|
+
spec: ApiSpecData,
|
|
105
|
+
operation: ApiOperationRef
|
|
106
|
+
): string => {
|
|
107
|
+
const endpoint = `${operation.method.toUpperCase()} ${operation.path}`;
|
|
108
|
+
const suffix = `Reference for the ${endpoint} endpoint in the ${apiName(spec)} API.`;
|
|
109
|
+
const prose = clip(
|
|
110
|
+
plainProse(operation.description || operation.summary),
|
|
111
|
+
META_DESCRIPTION_MAX - suffix.length - 1
|
|
112
|
+
);
|
|
113
|
+
return clip([prose, suffix].filter(Boolean).join(" "), META_DESCRIPTION_MAX);
|
|
114
|
+
};
|
|
115
|
+
|
|
61
116
|
/** Prepend a markdown description (if any) above a component invocation. */
|
|
62
117
|
const withDescription = (description: string, component: string): string =>
|
|
63
118
|
description.trim()
|
|
@@ -84,6 +139,7 @@ export const operationMdx = (
|
|
|
84
139
|
data: {
|
|
85
140
|
...(operation.deprecated ? { deprecated: true } : {}),
|
|
86
141
|
search: { tags: [operation.tag, method] },
|
|
142
|
+
seo: { description: operationDescription(spec, operation) },
|
|
87
143
|
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
88
144
|
title,
|
|
89
145
|
// Signals the two-column API layout (request panel instead of the TOC).
|
|
@@ -144,8 +200,13 @@ export const overviewMdx = (spec: ApiSpecData): RenderedPage => {
|
|
|
144
200
|
...tagSections,
|
|
145
201
|
].join("\n\n"),
|
|
146
202
|
data: {
|
|
203
|
+
seo: {
|
|
204
|
+
description:
|
|
205
|
+
clip(plainProse(spec.description), META_DESCRIPTION_MAX) ||
|
|
206
|
+
`${apiName(spec)} API reference.`,
|
|
207
|
+
},
|
|
147
208
|
sidebar: { label: "Overview" },
|
|
148
|
-
title: spec
|
|
209
|
+
title: apiName(spec),
|
|
149
210
|
},
|
|
150
211
|
};
|
|
151
212
|
};
|
package/src/seo/jsonld.ts
CHANGED
|
@@ -37,7 +37,13 @@ const trimSlash = (value: string): string => value.replace(/\/$/u, "");
|
|
|
37
37
|
const absolute = (base: string | null, path: string): string =>
|
|
38
38
|
base ? `${base}${path}` : path;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Frontmatter date → ISO 8601, or undefined when absent/unparseable. Shared with
|
|
42
|
+
* the layout's `article:published_time`/`article:modified_time` so both date
|
|
43
|
+
* surfaces treat a malformed date the same way: omit it rather than emit
|
|
44
|
+
* "Invalid Date".
|
|
45
|
+
*/
|
|
46
|
+
export const toIso = (value: DateInput | undefined): string | undefined => {
|
|
41
47
|
if (!value) {
|
|
42
48
|
return;
|
|
43
49
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize an X account to the leading `@` that `twitter:site`/`twitter:creator`
|
|
3
|
+
* require, so `acme`, `@acme`, and ` @acme ` all land on `@acme`. Empty or
|
|
4
|
+
* blank input yields undefined, which renders no tag at all.
|
|
5
|
+
*
|
|
6
|
+
* The layouts call this on values that never passed through the config schema:
|
|
7
|
+
* Astro's collections carry no schema here, so a page's `seo.x.creator` reaches
|
|
8
|
+
* them as raw frontmatter, and the schema's own transform never runs on it.
|
|
9
|
+
* (Blume's page pipeline does reject a non-string `creator` before the page is
|
|
10
|
+
* built, so `unknown` is defense in depth rather than the expected path.)
|
|
11
|
+
*/
|
|
12
|
+
export const normalizeXHandle = (value: unknown): string | undefined => {
|
|
13
|
+
if (typeof value !== "string") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const handle = value.trim().replace(/^@+/u, "");
|
|
17
|
+
return handle ? `@${handle}` : undefined;
|
|
18
|
+
};
|