blume 1.4.2 → 1.5.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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { create, insertMultiple, search } from "@orama/orama";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AnyOrama,
|
|
4
|
+
EnumArrComparisonOperator,
|
|
5
|
+
EnumComparisonOperator,
|
|
6
|
+
Tokenizer,
|
|
7
|
+
} from "@orama/orama";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* The minimal document shape both the client-side search dialog and the
|
|
@@ -13,6 +18,11 @@ export interface OramaDoc {
|
|
|
13
18
|
title: string;
|
|
14
19
|
/** Locale code; indexed as an enum so queries can filter to one language. */
|
|
15
20
|
locale?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Docs version; indexed as an enum so queries can filter to one version.
|
|
23
|
+
* The current docs carry `""`, which the enum stores and matches exactly.
|
|
24
|
+
*/
|
|
25
|
+
version?: string;
|
|
16
26
|
/** Resolved page `type`; indexed as an enum so queries can filter by type. */
|
|
17
27
|
contentType?: string;
|
|
18
28
|
/** Declared facet values (`content.types.<type>.facets`), key → value. */
|
|
@@ -35,6 +45,7 @@ const SCHEMA = {
|
|
|
35
45
|
locale: "enum",
|
|
36
46
|
route: "string",
|
|
37
47
|
title: "string",
|
|
48
|
+
version: "enum",
|
|
38
49
|
} as const;
|
|
39
50
|
|
|
40
51
|
/** Flatten a facet map to the `key:value` terms the `facetTerms` enum holds. */
|
|
@@ -79,6 +90,23 @@ const BIGRAM_LANGUAGES = new Set(["ja", "zh"]);
|
|
|
79
90
|
const BIGRAM_SCRIPTS =
|
|
80
91
|
/^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆〇ー゙゚]+$/u;
|
|
81
92
|
|
|
93
|
+
/**
|
|
94
|
+
* One index term inside a word-like segment. Being word-like does not make a
|
|
95
|
+
* segment all letters: UAX #29 keeps connector punctuation, format characters
|
|
96
|
+
* and mid-number punctuation *within* a word, so `Intl.Segmenter` reports
|
|
97
|
+
* スネーク_ケース and robots.txt as one segment each. A term is a run of
|
|
98
|
+
* letters, combining marks and digits. Marks are spelling, not punctuation —
|
|
99
|
+
* Thai writes vowels and tones as combining marks, so dropping them leaves
|
|
100
|
+
* consonant skeletons that collapse distinct words (เสื้อ, shirt, and เสือ,
|
|
101
|
+
* tiger, differ by one mark). Two separators stay where the surrounding text
|
|
102
|
+
* makes them part of the word: an apostrophe followed by a letter (don't),
|
|
103
|
+
* and a decimal point or thousands separator flanked by digits (1.0.3,
|
|
104
|
+
* 1,000) — split, either would leave one-letter and one-digit fragments that
|
|
105
|
+
* co-occur on unrelated pages.
|
|
106
|
+
*/
|
|
107
|
+
const TERM =
|
|
108
|
+
/[\p{L}\p{M}\p{N}]+(?:(?:['’](?=\p{L})|(?<=\p{N})[.,](?=\p{N}))[\p{L}\p{M}\p{N}]+)*/gu;
|
|
109
|
+
|
|
82
110
|
/**
|
|
83
111
|
* Emit every overlapping 2-character window of `run`, or the lone character.
|
|
84
112
|
* Windows are cut by code point: an ideograph outside the basic plane is a
|
|
@@ -109,8 +137,10 @@ const addBigrams = (run: string, tokens: Set<string>): void => {
|
|
|
109
137
|
/**
|
|
110
138
|
* A word-segmenting tokenizer for languages the default splitter can't handle,
|
|
111
139
|
* built on `Intl.Segmenter` (the same engine `@orama/tokenizers` wraps).
|
|
112
|
-
* Input is lowercased before segmenting — unlike the
|
|
113
|
-
*
|
|
140
|
+
* Input is NFC-normalized and lowercased before segmenting — unlike the
|
|
141
|
+
* upstream tokenizers — so decomposed text (macOS filenames, some CMS
|
|
142
|
+
* pipelines) indexes the same terms a composed query produces, and Latin
|
|
143
|
+
* terms ("GDPR", English pages on a mixed-locale site) still match
|
|
114
144
|
* case-insensitively. Returns `undefined` for languages the default tokenizer
|
|
115
145
|
* already serves, and on runtimes without `Intl.Segmenter`, where the caller
|
|
116
146
|
* falls back to Orama's default.
|
|
@@ -118,16 +148,24 @@ const addBigrams = (run: string, tokens: Set<string>): void => {
|
|
|
118
148
|
* On a {@link BIGRAM_LANGUAGES} index, runs of adjacent
|
|
119
149
|
* {@link BIGRAM_SCRIPTS} segments are joined and re-cut into character
|
|
120
150
|
* bigrams; everything else (Latin, digits, and every segment on a Korean or
|
|
121
|
-
* Thai index) is emitted
|
|
122
|
-
*
|
|
123
|
-
*
|
|
151
|
+
* Thai index) is emitted one {@link TERM} at a time. Separators end a run
|
|
152
|
+
* either way — whether they stand between segments, as 「クーリング・オフ」
|
|
153
|
+
* does, or inside one.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* `Intl.Segmenter` is missing on some runtimes even though the lib type
|
|
157
|
+
* declares it, so the constructor's presence is probed before use.
|
|
124
158
|
*/
|
|
159
|
+
const hasSegmenter = (
|
|
160
|
+
segmenter: typeof Intl.Segmenter | undefined
|
|
161
|
+
): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
|
|
162
|
+
|
|
125
163
|
const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
126
164
|
const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
|
|
127
165
|
if (!SEGMENTED_LANGUAGES.has(language)) {
|
|
128
166
|
return;
|
|
129
167
|
}
|
|
130
|
-
if (
|
|
168
|
+
if (!hasSegmenter(Intl.Segmenter)) {
|
|
131
169
|
return;
|
|
132
170
|
}
|
|
133
171
|
const segmenter = new Intl.Segmenter(language, { granularity: "word" });
|
|
@@ -146,17 +184,35 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
|
146
184
|
run = "";
|
|
147
185
|
}
|
|
148
186
|
};
|
|
149
|
-
|
|
187
|
+
const take = (term: string): void => {
|
|
188
|
+
if (bigram && BIGRAM_SCRIPTS.test(term)) {
|
|
189
|
+
run += term;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
flush();
|
|
193
|
+
tokens.add(term);
|
|
194
|
+
};
|
|
195
|
+
for (const segment of segmenter.segment(
|
|
196
|
+
raw.normalize("NFC").toLowerCase()
|
|
197
|
+
)) {
|
|
150
198
|
if (!segment.isWordLike) {
|
|
151
199
|
flush();
|
|
152
200
|
continue;
|
|
153
201
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
let end = 0;
|
|
203
|
+
for (const match of segment.segment.matchAll(TERM)) {
|
|
204
|
+
// A gap means punctuation stood there, which ends the run as surely
|
|
205
|
+
// as a non-word-like segment would: スネーク_ケース pairs either side
|
|
206
|
+
// of the connector, never across it.
|
|
207
|
+
if (match.index > end) {
|
|
208
|
+
flush();
|
|
209
|
+
}
|
|
210
|
+
take(match[0]);
|
|
211
|
+
end = match.index + match[0].length;
|
|
212
|
+
}
|
|
213
|
+
if (end < segment.segment.length) {
|
|
214
|
+
flush();
|
|
157
215
|
}
|
|
158
|
-
flush();
|
|
159
|
-
tokens.add(segment.segment);
|
|
160
216
|
}
|
|
161
217
|
flush();
|
|
162
218
|
return [...tokens];
|
|
@@ -171,17 +227,18 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
|
171
227
|
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for languages
|
|
172
228
|
* written without spaces (Japanese, Chinese, Korean, Thai); the tokenizer
|
|
173
229
|
* belongs to the database, so on a mixed-locale site it applies to every
|
|
174
|
-
* document, which is safe because
|
|
230
|
+
* document, which is safe because words in other scripts — Latin, and
|
|
231
|
+
* mark-bearing scripts like Hebrew or Devanagari — survive segmentation
|
|
232
|
+
* intact.
|
|
175
233
|
*/
|
|
176
234
|
export const buildOramaIndex = async (
|
|
177
235
|
documents: OramaDoc[],
|
|
178
236
|
locale?: string
|
|
179
237
|
): Promise<AnyOrama> => {
|
|
180
238
|
const tokenizer = segmentingTokenizer(locale);
|
|
181
|
-
const db =
|
|
182
|
-
schema: SCHEMA
|
|
183
|
-
|
|
184
|
-
});
|
|
239
|
+
const db = tokenizer
|
|
240
|
+
? create({ components: { tokenizer }, schema: SCHEMA })
|
|
241
|
+
: create({ schema: SCHEMA });
|
|
185
242
|
await insertMultiple(
|
|
186
243
|
db,
|
|
187
244
|
documents.map((doc) =>
|
|
@@ -205,6 +262,23 @@ export interface OramaQueryFilters {
|
|
|
205
262
|
facets?: Record<string, string>;
|
|
206
263
|
/** Keep only documents in this locale. */
|
|
207
264
|
locale?: string;
|
|
265
|
+
/**
|
|
266
|
+
* Keep only documents of this docs version (`""` is the current docs — a
|
|
267
|
+
* meaningful filter value, so absence alone disables version filtering).
|
|
268
|
+
*/
|
|
269
|
+
version?: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The exact-match `where` clause the filters compile to. Orama types `where`
|
|
274
|
+
* openly (any schema property to an operator), mirrored here by the index
|
|
275
|
+
* signature; this module only ever emits the two enum operators.
|
|
276
|
+
*/
|
|
277
|
+
interface OramaWhereClause {
|
|
278
|
+
[property: string]:
|
|
279
|
+
| EnumArrComparisonOperator
|
|
280
|
+
| EnumComparisonOperator
|
|
281
|
+
| undefined;
|
|
208
282
|
}
|
|
209
283
|
|
|
210
284
|
/**
|
|
@@ -226,27 +300,38 @@ export const queryOramaIndex = async (
|
|
|
226
300
|
filters?: OramaQueryFilters
|
|
227
301
|
): Promise<OramaDoc[]> => {
|
|
228
302
|
const facetTerms = filters?.facets ? toFacetTerms(filters.facets) : [];
|
|
229
|
-
const where = {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
303
|
+
const where: OramaWhereClause = {};
|
|
304
|
+
if (filters?.locale) {
|
|
305
|
+
where.locale = { eq: filters.locale };
|
|
306
|
+
}
|
|
307
|
+
// `""` (the current docs) is a real filter value, so test for presence.
|
|
308
|
+
if (filters?.version !== undefined) {
|
|
309
|
+
where.version = { eq: filters.version };
|
|
310
|
+
}
|
|
311
|
+
if (filters?.contentTypes && filters.contentTypes.length > 0) {
|
|
312
|
+
where.contentType = { in: filters.contentTypes };
|
|
313
|
+
}
|
|
314
|
+
if (facetTerms.length > 0) {
|
|
315
|
+
where.facetTerms = { containsAll: facetTerms };
|
|
316
|
+
}
|
|
317
|
+
const unfiltered = {
|
|
239
318
|
boost: BOOST,
|
|
240
319
|
limit,
|
|
241
320
|
properties: ["title", "description", "content"],
|
|
242
321
|
term,
|
|
243
|
-
...(Object.keys(where).length > 0 ? { where } : {}),
|
|
244
322
|
};
|
|
323
|
+
const params =
|
|
324
|
+
Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
|
|
245
325
|
const bigrammed = BIGRAM_LANGUAGES.has(db.tokenizer?.language ?? "");
|
|
326
|
+
// The result-document generic is OramaDoc because `buildOramaIndex` is the
|
|
327
|
+
// only writer to this database and inserts OramaDoc records (plus the
|
|
328
|
+
// derived `facetTerms`).
|
|
246
329
|
const strict = bigrammed
|
|
247
|
-
? await search(db, { ...params, threshold: ALL_TOKENS })
|
|
330
|
+
? await search<AnyOrama, OramaDoc>(db, { ...params, threshold: ALL_TOKENS })
|
|
248
331
|
: undefined;
|
|
249
332
|
const found =
|
|
250
|
-
strict && strict.hits.length > 0
|
|
251
|
-
|
|
333
|
+
strict && strict.hits.length > 0
|
|
334
|
+
? strict
|
|
335
|
+
: await search<AnyOrama, OramaDoc>(db, params);
|
|
336
|
+
return found.hits.map((hit) => hit.document);
|
|
252
337
|
};
|
package/src/search/popular.ts
CHANGED
|
@@ -26,8 +26,13 @@ export const resolveSearchPopular = (
|
|
|
26
26
|
popular: { href: string; icon?: string; label: string }[],
|
|
27
27
|
basePath: string
|
|
28
28
|
): SearchPopularPage[] =>
|
|
29
|
-
popular.map(({ href, icon, label }) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
popular.map(({ href, icon, label }) => {
|
|
30
|
+
const page: SearchPopularPage = {
|
|
31
|
+
label,
|
|
32
|
+
route: withBasePath(basePath, href),
|
|
33
|
+
};
|
|
34
|
+
if (icon) {
|
|
35
|
+
page.icon = icon;
|
|
36
|
+
}
|
|
37
|
+
return page;
|
|
38
|
+
});
|
package/src/search/providers.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface SearchProviderMeta {
|
|
|
29
29
|
syncs: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const SEARCH_PROVIDERS
|
|
32
|
+
export const SEARCH_PROVIDERS = {
|
|
33
33
|
algolia: {
|
|
34
34
|
kind: "hosted",
|
|
35
35
|
requiresServer: false,
|
|
@@ -80,7 +80,7 @@ export const SEARCH_PROVIDERS: Record<SearchProvider, SearchProviderMeta> = {
|
|
|
80
80
|
runtimeDeps: ["typesense"],
|
|
81
81
|
syncs: true,
|
|
82
82
|
},
|
|
83
|
-
}
|
|
83
|
+
} satisfies Record<SearchProvider, SearchProviderMeta>;
|
|
84
84
|
|
|
85
85
|
export const searchProviderMeta = (
|
|
86
86
|
provider: SearchProvider
|
package/src/search/sync/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ export const syncSearchProvider = async (
|
|
|
45
45
|
`Synced ${records.length} record(s) to ${search.provider}`
|
|
46
46
|
);
|
|
47
47
|
} catch (error) {
|
|
48
|
+
// SAFETY: the three sync clients surface network/auth failures as Error
|
|
49
|
+
// instances; the message is read only to annotate the skip warning.
|
|
48
50
|
reporter.warn(`Search sync skipped: ${(error as Error).message}`);
|
|
49
51
|
}
|
|
50
52
|
};
|
|
@@ -56,9 +56,10 @@ export const syncTypesense = async (
|
|
|
56
56
|
{ name: "content", type: "string" },
|
|
57
57
|
{ name: "url", type: "string" },
|
|
58
58
|
{ facet: true, name: "tag", optional: true, type: "string" },
|
|
59
|
-
// Carried as
|
|
60
|
-
//
|
|
59
|
+
// Carried as facets so hosted results can filter per language and per
|
|
60
|
+
// docs version (the SearchRecord contract; current docs = "current").
|
|
61
61
|
{ facet: true, name: "locale", optional: true, type: "string" },
|
|
62
|
+
{ facet: true, name: "version", optional: true, type: "string" },
|
|
62
63
|
],
|
|
63
64
|
name: config.collection,
|
|
64
65
|
});
|
|
@@ -71,6 +72,7 @@ export const syncTypesense = async (
|
|
|
71
72
|
tag: record.tag,
|
|
72
73
|
title: record.title,
|
|
73
74
|
url: record.url,
|
|
75
|
+
version: record.version,
|
|
74
76
|
}));
|
|
75
77
|
await client
|
|
76
78
|
.collections(config.collection)
|
package/src/seo/jsonld.ts
CHANGED
|
@@ -27,10 +27,25 @@ export interface StructuredDataInput {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** schema.org `@type` for each content type; defaults to TechArticle. */
|
|
30
|
-
const ARTICLE_TYPES
|
|
30
|
+
const ARTICLE_TYPES = {
|
|
31
31
|
blog: "BlogPosting",
|
|
32
32
|
changelog: "TechArticle",
|
|
33
|
-
};
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `hasOwn` (not a bare index) so a content type named like an
|
|
37
|
+
* `Object.prototype` member can't resolve a function up the prototype chain.
|
|
38
|
+
*/
|
|
39
|
+
const isArticleType = (value: string): value is keyof typeof ARTICLE_TYPES =>
|
|
40
|
+
Object.hasOwn(ARTICLE_TYPES, value);
|
|
41
|
+
|
|
42
|
+
/** A value a schema.org node property can hold. */
|
|
43
|
+
type JsonLdValue = string | number | JsonLdValue[] | JsonLdNode;
|
|
44
|
+
|
|
45
|
+
/** A schema.org node: JSON-LD keys to concrete JSON values. */
|
|
46
|
+
export interface JsonLdNode {
|
|
47
|
+
[key: string]: JsonLdValue;
|
|
48
|
+
}
|
|
34
49
|
|
|
35
50
|
const trimSlash = (value: string): string => value.replace(/\/$/u, "");
|
|
36
51
|
|
|
@@ -59,14 +74,14 @@ export const toIso = (value: DateInput | undefined): string | undefined => {
|
|
|
59
74
|
*/
|
|
60
75
|
export const buildStructuredData = (
|
|
61
76
|
input: StructuredDataInput
|
|
62
|
-
):
|
|
77
|
+
): JsonLdNode | null => {
|
|
63
78
|
const base = input.siteUrl ? trimSlash(input.siteUrl) : null;
|
|
64
79
|
// Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
|
|
65
80
|
// so JSON-LD URLs match the served location.
|
|
66
81
|
const deployBase = normalizeBasePath(input.base);
|
|
67
82
|
const pageUrl = absolute(base, withBasePath(deployBase, input.route));
|
|
68
83
|
const rootUrl = absolute(base, deployBase);
|
|
69
|
-
const graph:
|
|
84
|
+
const graph: JsonLdNode[] = [];
|
|
70
85
|
|
|
71
86
|
if (base) {
|
|
72
87
|
graph.push({
|
|
@@ -80,9 +95,12 @@ export const buildStructuredData = (
|
|
|
80
95
|
// The homepage is fully described by the WebSite node; deeper pages get an
|
|
81
96
|
// article node plus a breadcrumb trail.
|
|
82
97
|
if (input.route !== "/") {
|
|
83
|
-
const
|
|
98
|
+
const pageType = input.pageType ?? "";
|
|
99
|
+
const node: JsonLdNode = {
|
|
84
100
|
"@id": `${pageUrl}#page`,
|
|
85
|
-
"@type":
|
|
101
|
+
"@type": isArticleType(pageType)
|
|
102
|
+
? ARTICLE_TYPES[pageType]
|
|
103
|
+
: "TechArticle",
|
|
86
104
|
headline: input.title,
|
|
87
105
|
inLanguage: input.locale || "en",
|
|
88
106
|
name: input.title,
|
package/src/seo/x-handle.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Frontmatter defense in depth: only a real string can carry a handle. */
|
|
2
|
+
const isString = <Value>(value: Value): value is Value & string =>
|
|
3
|
+
typeof value === "string";
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Normalize an X account to the leading `@` that `twitter:site`/`twitter:creator`
|
|
3
7
|
* require, so `acme`, `@acme`, and ` @acme ` all land on `@acme`. Empty or
|
|
@@ -7,10 +11,11 @@
|
|
|
7
11
|
* Astro's collections carry no schema here, so a page's `seo.x.creator` reaches
|
|
8
12
|
* them as raw frontmatter, and the schema's own transform never runs on it.
|
|
9
13
|
* (Blume's page pipeline does reject a non-string `creator` before the page is
|
|
10
|
-
* built, so
|
|
14
|
+
* built, so the string guard is defense in depth rather than the expected
|
|
15
|
+
* path.)
|
|
11
16
|
*/
|
|
12
|
-
export const normalizeXHandle = (value:
|
|
13
|
-
if (
|
|
17
|
+
export const normalizeXHandle = <Value>(value: Value): string | undefined => {
|
|
18
|
+
if (!isString(value)) {
|
|
14
19
|
return;
|
|
15
20
|
}
|
|
16
21
|
const handle = value.trim().replace(/^@+/u, "");
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
* Values are Lucide inner-SVG markup; the client `svg()` helpers wrap them in an
|
|
13
13
|
* `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" …>`.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
/** Inner-SVG markup keyed by the icon name a client script requests. */
|
|
16
|
+
interface ChromeIconSet {
|
|
17
|
+
[name: string]: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const chromeIcons: ChromeIconSet = {
|
|
16
21
|
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
17
22
|
copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
|
18
23
|
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
19
24
|
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
20
25
|
sparkles:
|
|
21
|
-
'<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1
|
|
26
|
+
'<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1-1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/><path d="M5 3v4"/><path d="M3 5h4"/><path d="M19 17v4"/><path d="M17 19h4"/>',
|
|
22
27
|
};
|
package/src/theme/fonts.ts
CHANGED
|
@@ -82,11 +82,11 @@ export type FontEntry =
|
|
|
82
82
|
variants: LocalFontVariant[];
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
const FALLBACKS
|
|
85
|
+
const FALLBACKS = {
|
|
86
86
|
mono: ["ui-monospace", "SF Mono", "Menlo", "monospace"],
|
|
87
87
|
sans: ["ui-sans-serif", "system-ui", "sans-serif"],
|
|
88
88
|
serif: ["ui-serif", "Georgia", "serif"],
|
|
89
|
-
}
|
|
89
|
+
} satisfies Record<FontCategory, string[]>;
|
|
90
90
|
|
|
91
91
|
/** Slug -> Google family + weights + fallback category. Keep keys alphabetical. */
|
|
92
92
|
export const GOOGLE_FONTS = {
|
|
@@ -223,12 +223,16 @@ const SLOTS: FontSlot[] = ["display", "body", "mono"];
|
|
|
223
223
|
const slotCategory = (slot: FontSlot): FontCategory =>
|
|
224
224
|
slot === "mono" ? "mono" : "sans";
|
|
225
225
|
|
|
226
|
+
/** Whether a slot value is the slug-string form (vs a custom font object). */
|
|
227
|
+
const isSlugValue = (value: FontValue): value is string =>
|
|
228
|
+
typeof value === "string";
|
|
229
|
+
|
|
226
230
|
/** A slot value normalized into an entry, or null for an unknown slug string. */
|
|
227
231
|
const resolveFontValue = (
|
|
228
232
|
slot: FontSlot,
|
|
229
233
|
value: FontValue
|
|
230
234
|
): FontEntry | null => {
|
|
231
|
-
if (
|
|
235
|
+
if (isSlugValue(value)) {
|
|
232
236
|
if (!isFontSlug(value)) {
|
|
233
237
|
return null;
|
|
234
238
|
}
|
|
@@ -320,7 +324,7 @@ export const buildFontEntries = (fonts: FontsConfig): FontEntry[] => {
|
|
|
320
324
|
|
|
321
325
|
/** The slug backing a slot's CSS variable, or null for an unknown slug string. */
|
|
322
326
|
const slotSlug = (value: FontValue): string | null => {
|
|
323
|
-
if (
|
|
327
|
+
if (isSlugValue(value)) {
|
|
324
328
|
return isFontSlug(value) ? value : null;
|
|
325
329
|
}
|
|
326
330
|
return slugifyFontName(value.name);
|
package/src/theme/icons.ts
CHANGED
|
@@ -17,9 +17,11 @@ import { getIconData, iconToSVG } from "@iconify/utils";
|
|
|
17
17
|
// strips it when externalizing) and then rejects the module, whereas `require`
|
|
18
18
|
// of a JSON file needs no attribute and works under both Node and Bun.
|
|
19
19
|
const requireJson = createRequire(import.meta.url);
|
|
20
|
+
// SAFETY: the required file is the Iconify-published icon-set JSON, whose
|
|
21
|
+
// shape is exactly `IconifyJSON`.
|
|
20
22
|
const loadSet = (pkg: string): IconifyJSON => requireJson(pkg) as IconifyJSON;
|
|
21
23
|
|
|
22
|
-
const SETS
|
|
24
|
+
const SETS = {
|
|
23
25
|
lucide: loadSet("@iconify-json/lucide/icons.json"),
|
|
24
26
|
};
|
|
25
27
|
|
|
@@ -27,7 +29,7 @@ const SETS: Record<string, IconifyJSON> = {
|
|
|
27
29
|
const DEFAULT_SET = "lucide";
|
|
28
30
|
|
|
29
31
|
/** Explicit `prefix:name` prefixes. Lucide is the only bundled set. */
|
|
30
|
-
const PREFIX_SETS
|
|
32
|
+
const PREFIX_SETS = {
|
|
31
33
|
lucide: "lucide",
|
|
32
34
|
};
|
|
33
35
|
|
package/src/theme/palette.ts
CHANGED
|
@@ -2,8 +2,12 @@ import type { ResolvedConfig } from "../core/schema.ts";
|
|
|
2
2
|
|
|
3
3
|
const FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Named accent presets mapped to OKLCH values. The single source of truth for
|
|
7
|
+
* preset colors: the theme CSS and the OG card (og/card.ts) both resolve from
|
|
8
|
+
* this table, so a site and its social cards can't disagree about "blue".
|
|
9
|
+
*/
|
|
10
|
+
export const ACCENTS = {
|
|
7
11
|
blue: FALLBACK_ACCENT,
|
|
8
12
|
green: "oklch(0.6 0.16 150)",
|
|
9
13
|
orange: "oklch(0.68 0.17 50)",
|
|
@@ -11,7 +15,16 @@ const ACCENTS: Record<string, string> = {
|
|
|
11
15
|
purple: "oklch(0.58 0.2 290)",
|
|
12
16
|
red: "oklch(0.58 0.22 25)",
|
|
13
17
|
teal: "oklch(0.6 0.12 195)",
|
|
14
|
-
}
|
|
18
|
+
} satisfies Record<string, string>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a raw config value names an accent preset. `hasOwn` keeps a value
|
|
22
|
+
* like "constructor" from resolving an Object.prototype member — which would
|
|
23
|
+
* stringify a function into the generated CSS, breaking the rule (the exact
|
|
24
|
+
* breakout {@link safeColor} exists to prevent).
|
|
25
|
+
*/
|
|
26
|
+
export const isAccentPreset = (value: string): value is keyof typeof ACCENTS =>
|
|
27
|
+
Object.hasOwn(ACCENTS, value);
|
|
15
28
|
|
|
16
29
|
// Characters valid in a CSS color value (hex, rgb/hsl/oklch functions, named
|
|
17
30
|
// colors). Anything else — notably `;`, `{`, `}` — could break out of the
|
|
@@ -22,27 +35,20 @@ const CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
|
|
|
22
35
|
const safeColor = (value: string, fallback: string): string =>
|
|
23
36
|
CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
|
|
24
37
|
|
|
25
|
-
/**
|
|
26
|
-
* Resolve a named preset or fall back to {@link safeColor}. `hasOwn` keeps a
|
|
27
|
-
* value like "constructor" from resolving an Object.prototype member — which
|
|
28
|
-
* would stringify a function into the generated CSS, breaking the rule (the
|
|
29
|
-
* exact breakout safeColor exists to prevent).
|
|
30
|
-
*/
|
|
38
|
+
/** Resolve a named preset or fall back to {@link safeColor}. */
|
|
31
39
|
const presetOrColor = (value: string): string =>
|
|
32
|
-
|
|
33
|
-
? (ACCENTS[value] as string)
|
|
34
|
-
: safeColor(value, FALLBACK_ACCENT);
|
|
40
|
+
isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
|
|
35
41
|
|
|
36
42
|
/** Like {@link safeColor} but drops an unsafe/absent value to `null`. */
|
|
37
43
|
const safeColorOrNull = (value: string | undefined): string | null =>
|
|
38
44
|
value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
|
|
39
45
|
|
|
40
|
-
const RADII
|
|
46
|
+
const RADII = {
|
|
41
47
|
lg: "0.75rem",
|
|
42
48
|
md: "0.5rem",
|
|
43
49
|
none: "0",
|
|
44
50
|
sm: "0.25rem",
|
|
45
|
-
}
|
|
51
|
+
} satisfies Record<ResolvedConfig["theme"]["radius"], string>;
|
|
46
52
|
|
|
47
53
|
const cssString = (value: string): string => JSON.stringify(value);
|
|
48
54
|
|
|
@@ -112,6 +118,12 @@ ${tokens.join("\n")}
|
|
|
112
118
|
`;
|
|
113
119
|
};
|
|
114
120
|
|
|
121
|
+
/** Per-mode accent CSS colors. */
|
|
122
|
+
export interface AccentColors {
|
|
123
|
+
dark: string;
|
|
124
|
+
light: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/**
|
|
116
128
|
* Resolve the configured accent to per-mode CSS colors. A named accent
|
|
117
129
|
* resolves to its preset; any other value is treated as a raw CSS color so
|
|
@@ -121,7 +133,7 @@ ${tokens.join("\n")}
|
|
|
121
133
|
*/
|
|
122
134
|
export const resolveAccent = (
|
|
123
135
|
theme: ResolvedConfig["theme"]
|
|
124
|
-
):
|
|
136
|
+
): AccentColors => ({
|
|
125
137
|
dark: presetOrColor(theme.accent.dark),
|
|
126
138
|
light: presetOrColor(theme.accent.light),
|
|
127
139
|
});
|
package/src/translate/ledger.ts
CHANGED
|
@@ -36,8 +36,10 @@ export const emptyLedger = (): TranslationLedger => ({
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Hash the raw source text (frontmatter included), so any edit invalidates
|
|
39
|
-
* every locale's stamp. sha256-16 like the audit snapshot's content hash
|
|
40
|
-
*
|
|
39
|
+
* every locale's stamp. sha256-16 like the audit snapshot's content hash.
|
|
40
|
+
* Kept as its own function rather than sharing core's `hashText`: ledger
|
|
41
|
+
* stamps persist in user repos, so this hash must never change shape when an
|
|
42
|
+
* ephemeral cache hash does.
|
|
41
43
|
*/
|
|
42
44
|
export const hashSource = (text: string): string =>
|
|
43
45
|
createHash("sha256").update(text).digest("hex").slice(0, 16);
|
package/src/translate/meta.ts
CHANGED
|
@@ -39,6 +39,15 @@ export interface TranslatableMeta {
|
|
|
39
39
|
|
|
40
40
|
const META_FILES = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Whether a loaded meta module default-exports a factory function. Generic so
|
|
44
|
+
* it can decode the loader's untyped module value at this boundary.
|
|
45
|
+
*/
|
|
46
|
+
const isFactoryModule = <T>(
|
|
47
|
+
value: T
|
|
48
|
+
): value is T & ((...args: never[]) => FolderMeta) =>
|
|
49
|
+
typeof value === "function";
|
|
50
|
+
|
|
42
51
|
/** Where a locale's generated meta module lives (always written as `meta.ts`). */
|
|
43
52
|
export const metaTargetPath = (
|
|
44
53
|
meta: TranslatableMeta,
|
|
@@ -92,7 +101,7 @@ export const discoverTranslatableMeta = async (
|
|
|
92
101
|
readFile(file, "utf-8"),
|
|
93
102
|
load(file),
|
|
94
103
|
]);
|
|
95
|
-
if (
|
|
104
|
+
if (isFactoryModule(mod)) {
|
|
96
105
|
diagnostics.push({
|
|
97
106
|
code: "BLUME_TRANSLATE_META_FACTORY",
|
|
98
107
|
file,
|
|
@@ -130,14 +139,14 @@ export const generateMetaModule = (
|
|
|
130
139
|
meta: FolderMeta,
|
|
131
140
|
translatedTitle: string
|
|
132
141
|
): string => {
|
|
133
|
-
const data
|
|
142
|
+
const data = {
|
|
134
143
|
...meta,
|
|
135
144
|
title: translatedTitle,
|
|
136
145
|
};
|
|
137
|
-
const lines = Object.
|
|
138
|
-
.toSorted()
|
|
139
|
-
.filter((
|
|
140
|
-
.map((key) => ` ${key}: ${JSON.stringify(
|
|
146
|
+
const lines = Object.entries(data)
|
|
147
|
+
.toSorted(([a], [b]) => (a < b ? -1 : 1))
|
|
148
|
+
.filter(([, value]) => value !== undefined)
|
|
149
|
+
.map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`);
|
|
141
150
|
return [
|
|
142
151
|
"// Generated by `blume translate` — edit the default locale's meta file",
|
|
143
152
|
"// and rerun the translation instead of editing this copy.",
|