blume 1.0.3 → 1.1.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +13784 -10579
  3. package/dist/cli/index.js.map +93 -61
  4. package/dist/types/core/config-input.d.ts +87 -8
  5. package/dist/types/core/data.d.ts +21 -0
  6. package/dist/types/core/deployment-env.d.ts +6 -0
  7. package/dist/types/core/diagnostics.d.ts +23 -0
  8. package/dist/types/core/i18n-ui.d.ts +140 -140
  9. package/dist/types/core/schema.d.ts +549 -370
  10. package/dist/types/core/sources/types.d.ts +3 -1
  11. package/dist/types/core/standard-schema.d.ts +41 -0
  12. package/dist/types/core/types.d.ts +23 -0
  13. package/dist/types/og/card.d.ts +63 -0
  14. package/dist/types/og/dimensions.d.ts +12 -0
  15. package/dist/types/openapi/references.d.ts +12 -7
  16. package/docs/01-quickstart.mdx +1 -1
  17. package/docs/02-deployment.mdx +9 -1
  18. package/docs/advanced/api-reference.mdx +22 -3
  19. package/docs/advanced/changelog.mdx +1 -1
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +1 -1
  22. package/docs/configuration/customization.mdx +1 -1
  23. package/docs/configuration/export.mdx +1 -1
  24. package/docs/configuration/index.mdx +21 -1
  25. package/docs/configuration/search.mdx +28 -1
  26. package/docs/configuration/seo.mdx +40 -2
  27. package/docs/configuration/theming.mdx +1 -1
  28. package/docs/content/components.mdx +15 -2
  29. package/docs/content/index.mdx +1 -1
  30. package/docs/content/meta.mdx +1 -1
  31. package/docs/content/navigation.mdx +11 -1
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/content/syntax.mdx +116 -4
  34. package/docs/reference/cli.mdx +79 -1
  35. package/docs/reference/frontmatter.mdx +29 -1
  36. package/package.json +3 -3
  37. package/skills/blume-migrate/SKILL.md +170 -0
  38. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
  39. package/skills/blume-migrate/references/docusaurus.md +95 -0
  40. package/skills/blume-migrate/references/fumadocs.md +95 -0
  41. package/skills/blume-migrate/references/mintlify.md +156 -0
  42. package/skills/blume-migrate/references/monorepo.md +224 -0
  43. package/skills/blume-migrate/references/nextra.md +76 -0
  44. package/skills/blume-migrate/references/starlight.md +116 -0
  45. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +478 -0
  46. package/src/ai/llms.ts +15 -0
  47. package/src/astro/adapter-root.ts +70 -0
  48. package/src/astro/component-slots.ts +3 -2
  49. package/src/astro/generate.ts +132 -42
  50. package/src/astro/index.ts +1 -0
  51. package/src/astro/pages.ts +18 -3
  52. package/src/astro/templates.ts +158 -56
  53. package/src/audit/agent.ts +114 -0
  54. package/src/audit/catalog.ts +826 -0
  55. package/src/audit/checks/assets.ts +177 -0
  56. package/src/audit/checks/content.ts +231 -0
  57. package/src/audit/checks/duplicates.ts +131 -0
  58. package/src/audit/checks/i18n.ts +246 -0
  59. package/src/audit/checks/indexability.ts +213 -0
  60. package/src/audit/checks/links.ts +223 -0
  61. package/src/audit/checks/llms.ts +135 -0
  62. package/src/audit/checks/network.ts +272 -0
  63. package/src/audit/checks/og-image.ts +113 -0
  64. package/src/audit/checks/redirects.ts +87 -0
  65. package/src/audit/checks/robots.ts +114 -0
  66. package/src/audit/checks/sitemap.ts +229 -0
  67. package/src/audit/checks/social.ts +238 -0
  68. package/src/audit/crawl.ts +259 -0
  69. package/src/audit/graph.ts +74 -0
  70. package/src/audit/html.ts +54 -0
  71. package/src/audit/image-size.ts +63 -0
  72. package/src/audit/locate.ts +33 -0
  73. package/src/audit/redirects.ts +74 -0
  74. package/src/audit/report.ts +278 -0
  75. package/src/audit/run.ts +198 -0
  76. package/src/audit/snapshot.ts +189 -0
  77. package/src/audit/types.ts +214 -0
  78. package/src/audit/url.ts +103 -0
  79. package/src/cli/commands/audit.ts +205 -0
  80. package/src/cli/commands/build.ts +51 -12
  81. package/src/cli/index.ts +2 -0
  82. package/src/components/content/Callout.astro +8 -2
  83. package/src/components/content/Prompt.astro +25 -13
  84. package/src/components/content/Tabs.astro +98 -15
  85. package/src/components/layout/Breadcrumbs.astro +1 -1
  86. package/src/components/layout/Header.astro +5 -8
  87. package/src/components/layout/Logo.astro +13 -1
  88. package/src/components/layout/PageFeedback.astro +2 -2
  89. package/src/components/layout/PageLayout.astro +9 -9
  90. package/src/components/layout/Pagination.astro +7 -7
  91. package/src/components/layout/RootLayout.astro +9 -11
  92. package/src/components/layout/Search.astro +36 -7
  93. package/src/components/layout/TableOfContents.astro +1 -1
  94. package/src/components/layout/nav-utils.ts +9 -7
  95. package/src/components/openapi/Authorization.astro +80 -0
  96. package/src/components/openapi/Operation.astro +19 -1
  97. package/src/components/openapi/ParametersTable.astro +1 -1
  98. package/src/components/openapi/security.ts +201 -0
  99. package/src/components/openapi/snippets.ts +42 -13
  100. package/src/core/config-input.ts +94 -8
  101. package/src/core/data.ts +18 -2
  102. package/src/core/deployment-env.ts +9 -0
  103. package/src/core/diagnostics.ts +59 -12
  104. package/src/core/links.ts +2 -91
  105. package/src/core/nav-diagnostics.ts +48 -4
  106. package/src/core/navigation.ts +55 -13
  107. package/src/core/probe.ts +136 -0
  108. package/src/core/project-graph.ts +8 -0
  109. package/src/core/schema.ts +100 -1
  110. package/src/core/sources/normalize.ts +198 -25
  111. package/src/core/sources/types.ts +3 -1
  112. package/src/core/sources/watch.ts +5 -0
  113. package/src/core/standard-schema.ts +54 -0
  114. package/src/core/types.ts +23 -0
  115. package/src/deploy/adapter-output.ts +27 -15
  116. package/src/deploy/headers.ts +66 -0
  117. package/src/deploy/redirects.ts +49 -9
  118. package/src/markdown/index.ts +2 -0
  119. package/src/markdown/language-icon.ts +2 -1
  120. package/src/markdown/table-wrap.ts +43 -0
  121. package/src/og/card.ts +128 -36
  122. package/src/og/index.ts +1 -1
  123. package/src/og/logo.ts +21 -0
  124. package/src/openapi/references.ts +19 -16
  125. package/src/search/popular.ts +33 -0
  126. package/src/theme/entry.ts +56 -6
@@ -0,0 +1,80 @@
1
+ ---
2
+ import {
3
+ type OperationSecurity,
4
+ schemeCarrier,
5
+ schemeLabel,
6
+ } from "./security.ts";
7
+
8
+ interface Props {
9
+ security: OperationSecurity;
10
+ }
11
+
12
+ const { security } = Astro.props;
13
+ ---
14
+
15
+ {
16
+ security.alternatives.length > 0 && (
17
+ <section class="mt-6">
18
+ <div
19
+ aria-level="2"
20
+ class="mb-2 font-semibold text-foreground text-sm"
21
+ role="heading"
22
+ >
23
+ Authorization
24
+ </div>
25
+ {security.optional && (
26
+ <p class="mb-2 text-muted-foreground text-sm">
27
+ Optional — this operation also accepts unauthenticated requests.
28
+ </p>
29
+ )}
30
+ {security.alternatives.map((alternative, index) => (
31
+ <>
32
+ {index > 0 && (
33
+ <div class="my-2 font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide">
34
+ or
35
+ </div>
36
+ )}
37
+ <div class="not-prose rounded-blume border border-border px-4">
38
+ {alternative.map((resolved) => {
39
+ const carrier = schemeCarrier(resolved);
40
+ return (
41
+ <div class="border-border border-t py-3 first:border-t-0">
42
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
43
+ <code class="font-mono text-foreground text-sm">
44
+ {carrier?.name ?? resolved.key}
45
+ </code>
46
+ <span class="text-muted-foreground text-xs">
47
+ {schemeLabel(resolved)}
48
+ {carrier ? ` · ${carrier.in}` : ""}
49
+ </span>
50
+ {!security.optional && (
51
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
52
+ required
53
+ </span>
54
+ )}
55
+ </div>
56
+ {resolved.scheme?.description && (
57
+ <div
58
+ class="mt-1 text-muted-foreground text-sm"
59
+ set:text={resolved.scheme.description}
60
+ />
61
+ )}
62
+ {resolved.scopes.length > 0 && (
63
+ <div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
64
+ <span class="text-muted-foreground">Scopes:</span>
65
+ {resolved.scopes.map((scope) => (
66
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
67
+ {scope}
68
+ </code>
69
+ ))}
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
+ })}
75
+ </div>
76
+ </>
77
+ ))}
78
+ </section>
79
+ )
80
+ }
@@ -6,7 +6,15 @@ import {
6
6
  resolveComponentRef,
7
7
  type SchemaLike,
8
8
  } from "./helpers.ts";
9
+ import {
10
+ effectiveSecurity,
11
+ resolveSecurity,
12
+ sampleAuth,
13
+ type SecurityRequirementLike,
14
+ type SecuritySchemeLike,
15
+ } from "./security.ts";
9
16
  import { buildRequestSample, sampleLanguages } from "./snippets.ts";
17
+ import Authorization from "./Authorization.astro";
10
18
  import MethodBadge from "./MethodBadge.astro";
11
19
  import ParametersTable from "./ParametersTable.astro";
12
20
  import RequestBody from "./RequestBody.astro";
@@ -43,6 +51,7 @@ interface FullOperation {
43
51
  parameters?: ParameterLike[];
44
52
  requestBody?: RequestBodyLike;
45
53
  responses?: Record<string, ResponseLike>;
54
+ security?: SecurityRequirementLike[];
46
55
  }
47
56
 
48
57
  const { source, id } = Astro.props;
@@ -59,7 +68,9 @@ const doc = (spec?.document ?? {}) as {
59
68
  parameters?: Record<string, ParameterLike>;
60
69
  requestBodies?: Record<string, RequestBodyLike>;
61
70
  responses?: Record<string, ResponseLike>;
71
+ securitySchemes?: Record<string, SecuritySchemeLike>;
62
72
  };
73
+ security?: SecurityRequirementLike[];
63
74
  servers?: { url?: string }[];
64
75
  };
65
76
  const pathItem = ref ? doc.paths?.[ref.path] : undefined;
@@ -84,6 +95,11 @@ const responses = Object.fromEntries(
84
95
  ])
85
96
  );
86
97
 
98
+ const security = resolveSecurity(
99
+ effectiveSecurity(operation?.security, doc.security),
100
+ doc.components?.securitySchemes
101
+ );
102
+
87
103
  const sample =
88
104
  ref && operation
89
105
  ? buildRequestSample(
@@ -91,7 +107,8 @@ const sample =
91
107
  ref.method,
92
108
  ref.path,
93
109
  doc.servers ?? [],
94
- schemas
110
+ schemas,
111
+ sampleAuth(security)
95
112
  )
96
113
  : null;
97
114
  const languages = sampleLanguages(spec?.codeSamples ?? []);
@@ -115,6 +132,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
115
132
  </div>
116
133
  <div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
117
134
  <div>
135
+ <Authorization security={security} />
118
136
  <ParametersTable parameters={params} schemas={schemas} />
119
137
  {requestBody && (
120
138
  <RequestBody
@@ -39,7 +39,7 @@ const groups = SECTIONS.map((section) => ({
39
39
  groups.map((group) => (
40
40
  <section class="mt-6">
41
41
  <div
42
- aria-level="3"
42
+ aria-level="2"
43
43
  class="mb-2 font-semibold text-foreground text-sm"
44
44
  role="heading"
45
45
  >
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Security (authorization) resolution for the OpenAPI components. An operation
3
+ * enforces its own `security` when declared — an empty array explicitly makes
4
+ * it public — and inherits the document's root `security` otherwise. Within the
5
+ * resolved list, each requirement object is one way to authorize (OR between
6
+ * entries), and every scheme named inside a single requirement is needed
7
+ * together (AND). Pure and dependency-free like `helpers.ts`, so it runs in the
8
+ * browser build with no server-only imports.
9
+ */
10
+
11
+ /** A permissive view of an OpenAPI security scheme — only the fields we render. */
12
+ export interface SecuritySchemeLike {
13
+ type?: string;
14
+ description?: string;
15
+ /** `apiKey`: the parameter name the key is sent as. */
16
+ name?: string;
17
+ /** `apiKey`: where the key goes — `header`, `query`, or `cookie`. */
18
+ in?: string;
19
+ /** `http`: the HTTP auth scheme, e.g. `bearer` or `basic`. */
20
+ scheme?: string;
21
+ /** `http` bearer: a hint at the token format, e.g. `JWT`. */
22
+ bearerFormat?: string;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /** One security requirement: scheme name -> required scopes (empty outside OAuth). */
27
+ export type SecurityRequirementLike = Record<string, string[]>;
28
+
29
+ /** A scheme resolved out of `components.securitySchemes`, with its scopes. */
30
+ export interface ResolvedScheme {
31
+ /** The scheme's component name, e.g. `bearerAuth`. */
32
+ key: string;
33
+ /** The scheme object; undefined when the requirement names an unknown one. */
34
+ scheme?: SecuritySchemeLike;
35
+ scopes: string[];
36
+ }
37
+
38
+ /** The security state one operation renders. */
39
+ export interface OperationSecurity {
40
+ /** Ways to authorize (OR); every scheme within one entry is required (AND). */
41
+ alternatives: ResolvedScheme[][];
42
+ /** True when an empty requirement also allows unauthenticated calls. */
43
+ optional: boolean;
44
+ }
45
+
46
+ /**
47
+ * The requirement list an operation actually enforces: its own `security` when
48
+ * declared — the OpenAPI override rule, where `[]` removes the default and
49
+ * makes the operation public — else the document's root `security`.
50
+ */
51
+ export const effectiveSecurity = (
52
+ operation?: SecurityRequirementLike[],
53
+ document?: SecurityRequirementLike[]
54
+ ): SecurityRequirementLike[] => operation ?? document ?? [];
55
+
56
+ /**
57
+ * Resolve requirement names against `components.securitySchemes`. A name with
58
+ * no matching component is kept (with `scheme` undefined) so an inconsistent
59
+ * spec still renders the requirement instead of silently dropping it. An empty
60
+ * requirement object — the spec idiom for "auth optional" — contributes no
61
+ * alternative and flips `optional` instead.
62
+ */
63
+ export const resolveSecurity = (
64
+ requirements: SecurityRequirementLike[],
65
+ schemes: Record<string, SecuritySchemeLike> | undefined
66
+ ): OperationSecurity => {
67
+ const alternatives: ResolvedScheme[][] = [];
68
+ let optional = false;
69
+ for (const requirement of requirements) {
70
+ const entries = Object.entries(requirement ?? {});
71
+ if (entries.length === 0) {
72
+ optional = true;
73
+ continue;
74
+ }
75
+ alternatives.push(
76
+ entries.map(([key, scopes]) => ({
77
+ key,
78
+ scheme: schemes?.[key],
79
+ scopes: Array.isArray(scopes)
80
+ ? scopes.filter((scope): scope is string => typeof scope === "string")
81
+ : [],
82
+ }))
83
+ );
84
+ }
85
+ return { alternatives, optional };
86
+ };
87
+
88
+ const capitalize = (text: string): string =>
89
+ text.charAt(0).toUpperCase() + text.slice(1);
90
+
91
+ /** A short human label for a scheme row, e.g. `Bearer token` or `API key`. */
92
+ export const schemeLabel = (resolved: ResolvedScheme): string => {
93
+ const { scheme } = resolved;
94
+ switch (scheme?.type) {
95
+ case "http": {
96
+ const kind = (scheme.scheme ?? "").toLowerCase();
97
+ if (kind === "bearer") {
98
+ return scheme.bearerFormat
99
+ ? `Bearer token (${scheme.bearerFormat})`
100
+ : "Bearer token";
101
+ }
102
+ if (kind === "basic") {
103
+ return "Basic auth";
104
+ }
105
+ return kind ? `HTTP ${kind}` : "HTTP auth";
106
+ }
107
+ case "apiKey": {
108
+ return "API key";
109
+ }
110
+ case "oauth2": {
111
+ return "OAuth2 access token";
112
+ }
113
+ case "openIdConnect": {
114
+ return "OpenID Connect token";
115
+ }
116
+ case "mutualTLS": {
117
+ return "Mutual TLS";
118
+ }
119
+ default: {
120
+ // Unknown scheme ref: the component name is the best label available.
121
+ return resolved.key;
122
+ }
123
+ }
124
+ };
125
+
126
+ /**
127
+ * Where the credential travels: the header/query/cookie parameter it occupies.
128
+ * Undefined for schemes with no request parameter (mutual TLS) and for unknown
129
+ * refs, where guessing a location would be misleading.
130
+ */
131
+ export const schemeCarrier = (
132
+ resolved: ResolvedScheme
133
+ ): { name: string; in: string } | undefined => {
134
+ const { scheme } = resolved;
135
+ switch (scheme?.type) {
136
+ case "http":
137
+ case "oauth2":
138
+ case "openIdConnect": {
139
+ return { in: "header", name: "Authorization" };
140
+ }
141
+ case "apiKey": {
142
+ return { in: scheme.in ?? "header", name: scheme.name ?? resolved.key };
143
+ }
144
+ default: {
145
+ return undefined;
146
+ }
147
+ }
148
+ };
149
+
150
+ /** Placeholder credentials the request samples send. */
151
+ export interface SampleAuth {
152
+ headers: Record<string, string>;
153
+ query: Record<string, string>;
154
+ }
155
+
156
+ /**
157
+ * Placeholder credentials for an operation's request samples, from its first
158
+ * alternative (the spec's preferred way to authorize). Schemes that don't
159
+ * travel in the request (mutual TLS) and unknown refs contribute nothing.
160
+ */
161
+ export const sampleAuth = (security: OperationSecurity): SampleAuth => {
162
+ const headers: Record<string, string> = {};
163
+ const query: Record<string, string> = {};
164
+ const cookies: string[] = [];
165
+ for (const resolved of security.alternatives[0] ?? []) {
166
+ const { scheme } = resolved;
167
+ switch (scheme?.type) {
168
+ case "http": {
169
+ const kind = (scheme.scheme ?? "bearer").toLowerCase();
170
+ headers.Authorization =
171
+ kind === "bearer"
172
+ ? "Bearer YOUR_TOKEN"
173
+ : `${capitalize(kind)} YOUR_CREDENTIALS`;
174
+ break;
175
+ }
176
+ case "oauth2":
177
+ case "openIdConnect": {
178
+ headers.Authorization = "Bearer YOUR_ACCESS_TOKEN";
179
+ break;
180
+ }
181
+ case "apiKey": {
182
+ const name = scheme.name ?? resolved.key;
183
+ if (scheme.in === "query") {
184
+ query[name] = "YOUR_API_KEY";
185
+ } else if (scheme.in === "cookie") {
186
+ cookies.push(`${name}=YOUR_API_KEY`);
187
+ } else {
188
+ headers[name] = "YOUR_API_KEY";
189
+ }
190
+ break;
191
+ }
192
+ default: {
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ if (cookies.length > 0) {
198
+ headers.Cookie = cookies.join("; ");
199
+ }
200
+ return { headers, query };
201
+ };
@@ -1,5 +1,6 @@
1
1
  import { exampleValue, toJson } from "./helpers.ts";
2
2
  import type { SchemaLike } from "./helpers.ts";
3
+ import type { SampleAuth } from "./security.ts";
3
4
 
4
5
  /**
5
6
  * Request example + code-sample generation for an operation. Kept separate from
@@ -43,16 +44,22 @@ const jsonContentType = (
43
44
  return entries.find(([type]) => type.includes("json")) ?? entries[0];
44
45
  };
45
46
 
46
- /** The `?a=1&b=2` query string from an operation's required query params. */
47
+ /**
48
+ * The `?a=1&b=2` query string from an operation's required query params, plus
49
+ * any extra entries (a query-borne API key from the security requirements).
50
+ */
47
51
  const queryString = (
48
52
  params: ParamLike[],
49
- schemas: Record<string, SchemaLike>
53
+ schemas: Record<string, SchemaLike>,
54
+ extra: Record<string, string>
50
55
  ): string => {
51
56
  const query: string[] = [];
57
+ const seen = new Set<string>();
52
58
  for (const param of params) {
53
59
  if (!(param.in === "query" && param.required && param.name)) {
54
60
  continue;
55
61
  }
62
+ seen.add(param.name);
56
63
  const value = param.example ?? exampleValue(param.schema, schemas);
57
64
  query.push(
58
65
  `${encodeURIComponent(param.name)}=${encodeURIComponent(
@@ -60,16 +67,46 @@ const queryString = (
60
67
  )}`
61
68
  );
62
69
  }
70
+ for (const [name, value] of Object.entries(extra)) {
71
+ // A spec may declare the credential as an explicit query parameter too;
72
+ // its (better) example wins over the auth placeholder, as in headers.
73
+ if (seen.has(name)) {
74
+ continue;
75
+ }
76
+ query.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
77
+ }
63
78
  return query.length > 0 ? `?${query.join("&")}` : "";
64
79
  };
65
80
 
81
+ /**
82
+ * The sample's headers: auth placeholders first, so a spec that also declares
83
+ * the credential as an explicit header parameter overrides them with its own
84
+ * (better) example.
85
+ */
86
+ const headerValues = (
87
+ params: ParamLike[],
88
+ schemas: Record<string, SchemaLike>,
89
+ auth: SampleAuth | undefined
90
+ ): Record<string, string> => {
91
+ const headers: Record<string, string> = { ...auth?.headers };
92
+ for (const param of params) {
93
+ if (param.in === "header" && param.required && param.name) {
94
+ headers[param.name] = String(
95
+ param.example ?? exampleValue(param.schema, schemas) ?? ""
96
+ );
97
+ }
98
+ }
99
+ return headers;
100
+ };
101
+
66
102
  /** Assemble a representative request from an operation and the spec servers. */
67
103
  export const buildRequestSample = (
68
104
  operation: OperationLike,
69
105
  method: string,
70
106
  path: string,
71
107
  servers: { url?: string }[],
72
- schemas: Record<string, SchemaLike>
108
+ schemas: Record<string, SchemaLike>,
109
+ auth?: SampleAuth
73
110
  ): RequestSample => {
74
111
  const base = (servers[0]?.url ?? "").replace(TRAILING_SLASH, "");
75
112
  const params = operation.parameters ?? [];
@@ -85,16 +122,8 @@ export const buildRequestSample = (
85
122
  }
86
123
  }
87
124
 
88
- const search = queryString(params, schemas);
89
-
90
- const headers: Record<string, string> = {};
91
- for (const param of params) {
92
- if (param.in === "header" && param.required && param.name) {
93
- headers[param.name] = String(
94
- param.example ?? exampleValue(param.schema, schemas) ?? ""
95
- );
96
- }
97
- }
125
+ const search = queryString(params, schemas, auth?.query ?? {});
126
+ const headers = headerValues(params, schemas, auth);
98
127
 
99
128
  const media = jsonContentType(operation.requestBody?.content);
100
129
  let body: string | undefined;
@@ -10,6 +10,7 @@ import type {
10
10
  SidebarItemConfig,
11
11
  } from "./schema.ts";
12
12
  import type { ContentSource } from "./sources/types.ts";
13
+ import type { StandardSchema } from "./standard-schema.ts";
13
14
 
14
15
  /**
15
16
  * The public, hand-documented authoring type for `blume.config.ts`.
@@ -452,6 +453,16 @@ export interface MixedbreadSearch {
452
453
  storeId: string;
453
454
  }
454
455
 
456
+ /** A curated link for the search dialog empty state. */
457
+ export interface SearchPopularLink {
458
+ /** Internal route or external URL. */
459
+ href: string;
460
+ /** Built-in icon name shown beside the label; defaults to the file glyph. */
461
+ icon?: string;
462
+ /** Link label shown in the dialog. */
463
+ label: string;
464
+ }
465
+
455
466
  /**
456
467
  * Search backend. The default `orama` builds a local index at build time (and
457
468
  * runs in dev); hosted providers need their credential block below. `none`
@@ -469,6 +480,11 @@ export interface SearchConfig {
469
480
  mixedbread?: MixedbreadSearch;
470
481
  /** Orama Cloud credentials (required when `provider` is `orama-cloud`). */
471
482
  oramaCloud?: OramaCloudSearch;
483
+ /**
484
+ * Curated links for the Cmd+K empty state. When omitted or empty, the first
485
+ * sidebar pages are shown instead.
486
+ */
487
+ popular?: SearchPopularLink[];
472
488
  /** Which backend powers search. Defaults to `orama`. */
473
489
  provider?: SearchProvider;
474
490
  /** Typesense credentials (required when `provider` is `typesense`). */
@@ -711,6 +727,49 @@ export interface RssConfig {
711
727
  types?: string[];
712
728
  }
713
729
 
730
+ /** Colors used by generated Open Graph cards. Any CSS color — hex, `oklch(…)`, `rgb(…)`, named. */
731
+ export interface OgPaletteConfig {
732
+ /** Fallback mark color. Defaults to the light theme accent. */
733
+ accent?: string;
734
+ /** Card background. */
735
+ background?: string;
736
+ /** Footer divider. */
737
+ border?: string;
738
+ /** Headline and `currentColor` logo color. */
739
+ foreground?: string;
740
+ /** Description and footer text. */
741
+ muted?: string;
742
+ }
743
+
744
+ /** Per-page Open Graph image generation. */
745
+ export interface OgConfig {
746
+ /**
747
+ * Generate an OG image per page. Defaults to on once a deployment `site`
748
+ * URL is known and off otherwise (`og:image` must be absolute). An explicit
749
+ * value always wins.
750
+ */
751
+ enabled?: boolean;
752
+ /**
753
+ * Google Font families for the generated card, extending Takumi's Latin-only
754
+ * default so non-Latin titles (CJK, and so on) render instead of tofu.
755
+ * Fetched from Google Fonts at build. A bare string loads the family's
756
+ * default weights; the object form pins weights (`700`, `[400, 700]`, or a
757
+ * `"100..900"` variable range) and styles.
758
+ */
759
+ fonts?: (
760
+ | string
761
+ | {
762
+ name: string;
763
+ style?: "normal" | "italic" | ("normal" | "italic")[];
764
+ weight?: number | number[] | string;
765
+ }
766
+ )[];
767
+ /** Local SVG used in the generated card instead of the site logo. */
768
+ logo?: string;
769
+ /** Optional generated-card colors. */
770
+ palette?: OgPaletteConfig;
771
+ }
772
+
714
773
  /** Discoverability: OG images, feeds, sitemap, robots, and structured data. */
715
774
  export interface SeoConfig {
716
775
  /**
@@ -721,14 +780,7 @@ export interface SeoConfig {
721
780
  /** robots.txt `Content-Signal` usage declaration. Defaults to `true`. */
722
781
  contentSignals?: ContentSignalsConfig;
723
782
  /** Per-page Open Graph image generation. */
724
- og?: {
725
- /**
726
- * Generate an OG image per page. Defaults to on once a deployment `site`
727
- * URL is known and off otherwise (`og:image` must be absolute). An explicit
728
- * value always wins.
729
- */
730
- enabled?: boolean;
731
- };
783
+ og?: OgConfig;
732
784
  /** Generate robots.txt (with a Sitemap reference when available). Defaults to `true`. */
733
785
  robots?: boolean;
734
786
  /** RSS/Atom feeds. */
@@ -897,6 +949,38 @@ export type ExportConfig =
897
949
  pdf?: boolean;
898
950
  };
899
951
 
952
+ /**
953
+ * Opt-in custom frontmatter keys. Page frontmatter is strictly validated —
954
+ * an unknown key fails the build so typos are caught — and `extend` carves
955
+ * out project-specific keys from that rule, each validated by a schema you
956
+ * supply.
957
+ */
958
+ export interface FrontmatterConfig {
959
+ /**
960
+ * Extra frontmatter keys pages may carry, mapped to their validation
961
+ * schemas — any library implementing Standard Schema works (Zod — the
962
+ * version your project installs, 3.24+ or 4 — Valibot, ArkType):
963
+ *
964
+ * ```ts
965
+ * import { z } from "zod";
966
+ *
967
+ * frontmatter: {
968
+ * extend: {
969
+ * owner: z.string(),
970
+ * reviewedAt: z.coerce.date().optional(),
971
+ * },
972
+ * },
973
+ * ```
974
+ *
975
+ * Every declared key is validated on every page — absent ones included —
976
+ * so a required schema enforces the key site-wide; mark it `.optional()`
977
+ * to validate only when present. Validated values are preserved on each
978
+ * page record's `custom` field. Built-in frontmatter fields cannot be
979
+ * redeclared.
980
+ */
981
+ extend?: Record<string, StandardSchema>;
982
+ }
983
+
900
984
  /**
901
985
  * "Last updated" timestamps. `false` (default) disables them; `true` derives
902
986
  * each date from git history; the object form selects the source. A page's
@@ -970,6 +1054,8 @@ export interface BlumeConfig {
970
1054
  export?: ExportConfig;
971
1055
  /** Show the per-page "Was this helpful?" widget. Defaults to `true`. */
972
1056
  feedback?: boolean;
1057
+ /** Opt-in custom frontmatter keys, validated by schemas you supply. */
1058
+ frontmatter?: FrontmatterConfig;
973
1059
  /** Source repository (Edit-this-page links and the header repo link). */
974
1060
  github?: GithubConfig;
975
1061
  /** Internationalization (opt-in multi-locale). */
package/src/core/data.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OgFont } from "../og/card.ts";
1
2
  import type { UIStrings } from "./i18n-ui.ts";
2
3
  import type { ResolvedConfig, SearchProvider } from "./schema.ts";
3
4
  import type { Navigation, RouteAlternate } from "./types.ts";
@@ -15,6 +16,10 @@ export interface BlumeLogo {
15
16
  svg?: string;
16
17
  light?: string;
17
18
  dark?: string;
19
+ dimensions?: {
20
+ dark?: { height: number; width: number };
21
+ light?: { height: number; width: number };
22
+ };
18
23
  alt: string;
19
24
  href: string;
20
25
  /** Wordmark text beside the mark; `undefined` falls back to the site title. */
@@ -109,10 +114,21 @@ export interface BlumeDataConfig {
109
114
  /** Hosted MCP server, or `null` when MCP is off. */
110
115
  mcp: { name: string; route: string } | null;
111
116
  /** Open Graph image generation. */
112
- og: { enabled: boolean };
117
+ og: {
118
+ enabled: boolean;
119
+ /** Extra Google Font family specs for the card renderer, fetched at build. */
120
+ fonts?: OgFont[];
121
+ logo?: string;
122
+ palette?: ResolvedConfig["seo"]["og"]["palette"];
123
+ };
113
124
  /** Repository URL for header/edit links, or `null`. */
114
125
  repoUrl: string | null;
115
- search: { enabled: boolean; provider: SearchProvider };
126
+ search: {
127
+ enabled: boolean;
128
+ /** Resolved empty-state links; empty when unset (Search falls back to sidebar). */
129
+ popular: { icon?: string; label: string; route: string }[];
130
+ provider: SearchProvider;
131
+ };
116
132
  /** Deployment site URL, or `null` when none is configured/detected. */
117
133
  site: string | null;
118
134
  structuredData: boolean;
@@ -44,6 +44,15 @@ const PLATFORMS: Platform[] = [
44
44
  },
45
45
  ];
46
46
 
47
+ /**
48
+ * Adapters whose `deployment.site` arrives from platform env vars at deploy
49
+ * time. Consumers (e.g. the audit) use this to tell "site is missing" apart
50
+ * from "site is missing *here*, but the platform will set it".
51
+ */
52
+ export const SITE_INFERRING_ADAPTERS: ReadonlySet<string> = new Set(
53
+ PLATFORMS.map((platform) => platform.adapter)
54
+ );
55
+
47
56
  /**
48
57
  * Fill in `deployment.adapter` and `deployment.site` from platform env vars
49
58
  * (Vercel, Netlify, Cloudflare Pages) when the user hasn't set them. Explicit