blume 1.6.4 → 1.6.6

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 (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0ewz4trd.js +679 -0
  4. package/dist/cli/chunk-0ewz4trd.js.map +15 -0
  5. package/dist/cli/chunk-27gtm2ym.js +69 -0
  6. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  7. package/dist/cli/chunk-2aj8ddew.js +72 -0
  8. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  9. package/dist/cli/chunk-3k0kzs6d.js +69 -0
  10. package/dist/cli/chunk-3k0kzs6d.js.map +11 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  18. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  19. package/dist/cli/chunk-5yvt556e.js +185 -0
  20. package/dist/cli/chunk-5yvt556e.js.map +11 -0
  21. package/dist/cli/chunk-62qsssnh.js +3808 -0
  22. package/dist/cli/chunk-62qsssnh.js.map +36 -0
  23. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  24. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  25. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  26. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  27. package/dist/cli/chunk-9sh49q0h.js +30 -0
  28. package/dist/cli/chunk-9sh49q0h.js.map +10 -0
  29. package/dist/cli/chunk-aerwpe14.js +2370 -0
  30. package/dist/cli/chunk-aerwpe14.js.map +15 -0
  31. package/dist/cli/chunk-ag1zyr5x.js +176 -0
  32. package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
  33. package/dist/cli/chunk-bawgnt8x.js +277 -0
  34. package/dist/cli/chunk-bawgnt8x.js.map +11 -0
  35. package/dist/cli/chunk-bcy492zc.js +16 -0
  36. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  37. package/dist/cli/chunk-btfr9yvw.js +41 -0
  38. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  39. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  40. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  41. package/dist/cli/chunk-cnvm6k3e.js +96 -0
  42. package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
  43. package/dist/cli/chunk-etsqspj6.js +5170 -0
  44. package/dist/cli/chunk-etsqspj6.js.map +47 -0
  45. package/dist/cli/chunk-ev67ycx0.js +15 -0
  46. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  47. package/dist/cli/chunk-ey89bjj1.js +209 -0
  48. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  49. package/dist/cli/chunk-f75cqye8.js +76 -0
  50. package/dist/cli/chunk-f75cqye8.js.map +10 -0
  51. package/dist/cli/chunk-j00ezcg5.js +259 -0
  52. package/dist/cli/chunk-j00ezcg5.js.map +11 -0
  53. package/dist/cli/chunk-jtb45atp.js +467 -0
  54. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  55. package/dist/cli/chunk-m3p3wahd.js +117 -0
  56. package/dist/cli/chunk-m3p3wahd.js.map +10 -0
  57. package/dist/cli/chunk-n0y172hf.js +387 -0
  58. package/dist/cli/chunk-n0y172hf.js.map +12 -0
  59. package/dist/cli/chunk-n4qjabmt.js +1062 -0
  60. package/dist/cli/chunk-n4qjabmt.js.map +25 -0
  61. package/dist/cli/chunk-nyqzjdhj.js +111 -0
  62. package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
  63. package/dist/cli/chunk-pxj10x8y.js +35 -0
  64. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  65. package/dist/cli/chunk-s4jn7f1q.js +54 -0
  66. package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
  67. package/dist/cli/chunk-s4k1pnvf.js +81 -0
  68. package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
  69. package/dist/cli/chunk-s5e5jt53.js +227 -0
  70. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  71. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  72. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  73. package/dist/cli/chunk-tc89yh2r.js +136 -0
  74. package/dist/cli/chunk-tc89yh2r.js.map +10 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vv237fp3.js +1002 -0
  78. package/dist/cli/chunk-vv237fp3.js.map +13 -0
  79. package/dist/cli/chunk-vv3f8mb6.js +5314 -0
  80. package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
  81. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  82. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  83. package/dist/cli/chunk-wb067mv3.js +758 -0
  84. package/dist/cli/chunk-wb067mv3.js.map +13 -0
  85. package/dist/cli/chunk-wd27zjcz.js +60 -0
  86. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  87. package/dist/cli/chunk-wkq5tbtq.js +1141 -0
  88. package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
  89. package/dist/cli/chunk-x1vrdjyk.js +1967 -0
  90. package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
  91. package/dist/cli/chunk-x66c5yjn.js +23 -0
  92. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  93. package/dist/cli/index.js +55 -27587
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/core/code-fences.d.ts +11 -0
  97. package/dist/types/core/config-input.d.ts +10 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +74 -1
  100. package/docs/02-deployment.mdx +1 -1
  101. package/docs/configuration/analytics.mdx +21 -2
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/syntax.mdx +1 -1
  105. package/docs/reference/cli.mdx +1 -1
  106. package/package.json +16 -14
  107. package/src/ai/api/handlers.ts +4 -7
  108. package/src/ai/api/paths.ts +8 -0
  109. package/src/ai/api/spec.ts +2 -1
  110. package/src/ai/ask-context.ts +378 -22
  111. package/src/astro/generate.ts +25 -29
  112. package/src/astro/include-hmr.ts +10 -13
  113. package/src/astro/include-refresh.ts +0 -0
  114. package/src/astro/index.ts +6 -1
  115. package/src/astro/integration.ts +269 -53
  116. package/src/astro/module-types.ts +74 -0
  117. package/src/astro/templates.ts +85 -97
  118. package/src/audit/image-size.ts +10 -8
  119. package/src/cli/command-meta.ts +77 -0
  120. package/src/cli/commands/add.ts +2 -4
  121. package/src/cli/commands/audit.ts +2 -4
  122. package/src/cli/commands/build.ts +42 -346
  123. package/src/cli/commands/check.ts +2 -4
  124. package/src/cli/commands/dev.ts +31 -42
  125. package/src/cli/commands/doctor.ts +2 -4
  126. package/src/cli/commands/eject.ts +3 -41
  127. package/src/cli/commands/eval.ts +2 -5
  128. package/src/cli/commands/init.ts +2 -4
  129. package/src/cli/commands/mcp-stdio.ts +2 -5
  130. package/src/cli/commands/preview.ts +3 -5
  131. package/src/cli/commands/sync.ts +2 -4
  132. package/src/cli/commands/translate.ts +2 -5
  133. package/src/cli/commands/validate.ts +2 -4
  134. package/src/cli/commands/version.ts +2 -4
  135. package/src/cli/eject-scripts.ts +0 -45
  136. package/src/cli/host-args.ts +16 -0
  137. package/src/cli/index.ts +84 -35
  138. package/src/cli/lazy-command.ts +47 -0
  139. package/src/components/content/GithubInfo.astro +4 -1
  140. package/src/components/content/mermaid-element.ts +8 -0
  141. package/src/components/layout/Analytics.astro +20 -1
  142. package/src/components/layout/PageLayout.astro +14 -3
  143. package/src/components/layout/ReferenceLayout.astro +15 -4
  144. package/src/components/layout/RootLayout.astro +15 -4
  145. package/src/components/layout/analytics-client.ts +2 -1
  146. package/src/components/layout/page-locale.ts +29 -0
  147. package/src/components/openapi/AsyncApiOperation.astro +5 -3
  148. package/src/components/openapi/Authorization.astro +4 -6
  149. package/src/components/openapi/Bindings.astro +2 -2
  150. package/src/components/openapi/Description.astro +109 -0
  151. package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
  152. package/src/components/openapi/GraphqlOperation.astro +4 -3
  153. package/src/components/openapi/GraphqlType.astro +4 -6
  154. package/src/components/openapi/ParametersTable.astro +5 -7
  155. package/src/components/openapi/RequestBody.astro +2 -4
  156. package/src/components/openapi/Responses.astro +4 -3
  157. package/src/components/openapi/SchemaProperty.astro +11 -6
  158. package/src/components/openapi/description.ts +91 -0
  159. package/src/core/api-name.ts +18 -0
  160. package/src/core/code-fences.ts +48 -0
  161. package/src/core/config-input.ts +10 -0
  162. package/src/core/content-assets.ts +3 -7
  163. package/src/core/includes.ts +3 -7
  164. package/src/core/package-root.ts +1 -1
  165. package/src/core/schema.ts +27 -0
  166. package/src/core/sources/normalize.ts +2 -37
  167. package/src/core/sources/obsidian.ts +3 -2
  168. package/src/core/svg-dimensions.ts +97 -0
  169. package/src/core/version-cut.ts +2 -2
  170. package/src/deploy/artifacts.ts +370 -0
  171. package/src/deploy/cloudflare-negotiation.ts +97 -32
  172. package/src/deploy/function-bundle.ts +66 -20
  173. package/src/deploy/sitemap.ts +6 -0
  174. package/src/deploy/vercel-negotiation.ts +8 -30
  175. package/src/og/card.ts +6 -12
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +0 -2
  178. package/src/theme/entry.ts +9 -2
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { builtinModules } from "node:module";
4
4
 
5
+ import { init, parse } from "es-module-lexer";
5
6
  import { dirname, join, relative } from "pathe";
6
7
  import { z } from "zod";
7
8
 
@@ -65,32 +66,76 @@ export const packageName = (specifier: string): string | null => {
65
66
  };
66
67
 
67
68
  /**
68
- * Static specifiers: a side-effect `import "x"` or an `import`/`export … from
69
- * "x"` clause. Only `import` takes the bare-string form — `export "x"` is not
70
- * syntax, so a runtime message quoting `export 'ALL'` must not match. The
71
- * specifier quote must not be escaped either: a backslash-quoted `from \"zod\"`
72
- * is a code sample serialized into a string (the MCP snapshot carries page
73
- * Markdown), not module syntax.
69
+ * Fallback static specifiers for a module the lexer rejects: a side-effect
70
+ * `import "x"` or an `import`/`export … from "x"` clause. Only `import` takes
71
+ * the bare-string form — `export "x"` is not syntax, so a runtime message
72
+ * quoting `export 'ALL'` must not match.
74
73
  */
75
74
  const STATIC_IMPORT =
76
- /(?:^|[;\s}])(?:import\s*(?<!\\)["'](?<bare>[^"'\n]+)["']|(?:import|export)\s*[\w$*{},\s]*?\s*from\s*(?<!\\)["'](?<from>[^"'\n]+)["'])/gu;
75
+ /(?:^|[;\s}])(?:import\s*["'](?<bare>[^"'\n]+)["']|(?:import|export)\s*[\w$*{},\s]*?\s*from\s*["'](?<from>[^"'\n]+)["'])/gu;
77
76
 
78
- /** Dynamic `import("…")` specifiers, same escaped-quote rule. */
79
- const DYNAMIC_IMPORT =
80
- /\bimport\(\s*(?<!\\)["'](?<dynamic>[^"'\n]+)["']\s*\)/gu;
77
+ /** Fallback dynamic `import("…")` specifiers. */
78
+ const DYNAMIC_IMPORT = /\bimport\(\s*["'](?<dynamic>[^"'\n]+)["']\s*\)/gu;
81
79
 
82
- /** Every bare package name a module's source imports. */
83
- export const importedPackages = (source: string): string[] => {
84
- const names = new Set<string>();
80
+ /**
81
+ * Textual best effort for a module `es-module-lexer` cannot parse: every
82
+ * quoted specifier in import position, string contents included.
83
+ */
84
+ const scannedSpecifiers = (source: string): string[] => {
85
+ const specifiers: string[] = [];
85
86
  for (const pattern of [STATIC_IMPORT, DYNAMIC_IMPORT]) {
86
87
  for (const match of source.matchAll(pattern)) {
87
88
  const groups = match.groups ?? {};
88
- const name = packageName(
89
- groups.bare ?? groups.from ?? groups.dynamic ?? ""
90
- );
91
- if (name) {
92
- names.add(name);
93
- }
89
+ specifiers.push(groups.bare ?? groups.from ?? groups.dynamic ?? "");
90
+ }
91
+ }
92
+ return specifiers;
93
+ };
94
+
95
+ /**
96
+ * Every specifier a module's syntax imports, read with `es-module-lexer` so
97
+ * text inside string literals never counts. That matters for Blume's data
98
+ * chunks: the MCP snapshot is `JSON.parse("…")` over every page's Markdown,
99
+ * and a code sample there reading `import { config } from 'dotenv'` is prose
100
+ * to a bundle audit, not a module the function needs. `import.meta` carries no
101
+ * specifier and a template-literal `import(\`pkg/${x}\`)` is a glob the
102
+ * bundler already resolved, so neither names a package.
103
+ */
104
+ const lexedSpecifiers = (source: string, name: string): string[] => {
105
+ const [imports] = parse(source, name);
106
+ const specifiers: string[] = [];
107
+ for (const entry of imports) {
108
+ if (entry.type === "dynamic" && entry.glob) {
109
+ continue;
110
+ }
111
+ if (entry.specifier) {
112
+ specifiers.push(entry.specifier);
113
+ }
114
+ }
115
+ return specifiers;
116
+ };
117
+
118
+ /**
119
+ * Every bare package name a module's source imports. A module the lexer
120
+ * rejects (an unterminated string, an invalid escape in a specifier) falls
121
+ * back to the textual scan rather than going unaudited.
122
+ */
123
+ export const importedPackages = async (
124
+ source: string,
125
+ name = "module"
126
+ ): Promise<string[]> => {
127
+ await init();
128
+ let specifiers: string[];
129
+ try {
130
+ specifiers = lexedSpecifiers(source, name);
131
+ } catch {
132
+ specifiers = scannedSpecifiers(source);
133
+ }
134
+ const names = new Set<string>();
135
+ for (const specifier of specifiers) {
136
+ const packageId = packageName(specifier);
137
+ if (packageId) {
138
+ names.add(packageId);
94
139
  }
95
140
  }
96
141
  return [...names];
@@ -164,7 +209,8 @@ export const auditFunctionBundle = async (
164
209
  for (const file of await listModules(serverDir)) {
165
210
  // oxlint-disable-next-line no-await-in-loop -- sequential read keeps the importer lists ordered
166
211
  const source = await readFile(file, "utf-8");
167
- for (const name of importedPackages(source)) {
212
+ // oxlint-disable-next-line no-await-in-loop -- the lexer runs per file, in the same order
213
+ for (const name of await importedPackages(source, file)) {
168
214
  if (resolvable(name, dirname(file), funcDir)) {
169
215
  continue;
170
216
  }
@@ -36,6 +36,12 @@ export interface SitemapFile {
36
36
  xml: string;
37
37
  }
38
38
 
39
+ /** The build log line for an emitted sitemap set: one file, or an index. */
40
+ export const describeSitemapFiles = (files: readonly SitemapFile[]): string =>
41
+ files.length === 1
42
+ ? "Generated sitemap.xml"
43
+ : `Generated sitemap.xml (index of ${files.length - 1} sitemap files)`;
44
+
39
45
  /**
40
46
  * The sitemaps.org cap on `<url>` entries in a single file. Beyond it,
41
47
  * `sitemap.xml` becomes a sitemap index pointing at numbered chunk files —
@@ -235,21 +235,6 @@ export const buildNegotiationRoutes = (
235
235
  /** The `src` of the injected homepage `Link` header route. */
236
236
  const HOME_SRC = "^/$";
237
237
 
238
- /**
239
- * Permanent redirect from any trailing-slash URL to its slashless twin, so
240
- * `/docs/` and `/docs` don't serve as duplicate URLs (canonicals, sitemap, and
241
- * hreflang all use the slashless form; the root `/` is untouched — `.+`
242
- * requires a non-empty path). Spliced into the main phase before `handle:
243
- * "filesystem"`, after the Markdown rewrites, so an agent's `Accept:
244
- * text/markdown` request on a slashed URL still rewrites without the extra
245
- * hop. Vercel carries the query string over to the `Location` target itself.
246
- */
247
- export const TRAILING_SLASH_REDIRECT: VercelRoute = {
248
- headers: { Location: "/$1" },
249
- src: "^/(.+)/$",
250
- status: 308,
251
- };
252
-
253
238
  /**
254
239
  * Whether a route is one this module previously injected, so re-injection
255
240
  * replaces rather than duplicates. Rewrites are identified by their `accept`
@@ -273,9 +258,7 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
273
258
  (route.continue === true &&
274
259
  isString(route.headers?.link) &&
275
260
  route.src === HOME_SRC &&
276
- Object.keys(route).length === 3) ||
277
- (route.status === TRAILING_SLASH_REDIRECT.status &&
278
- route.src === TRAILING_SLASH_REDIRECT.src);
261
+ Object.keys(route).length === 3);
279
262
 
280
263
  /**
281
264
  * Splice the negotiation routes into a Build Output `config.json`, plus — when
@@ -285,9 +268,11 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
285
268
  * rides on the prerendered homepage response. `contentTypeOverrides` maps static-dir
286
269
  * relative paths to media types via the Build Output `overrides` field — the
287
270
  * platform's mechanism for extensionless static files (e.g. the Web Bot Auth
288
- * signature directory). The trailing-slash 308 redirect is always spliced in
289
- * alongside, so slashed duplicates of every page collapse onto the canonical
290
- * slashless URL. For each 404 twin the build emitted (`notFound.markdown` for
271
+ * signature directory). The trailing-slash redirect that collapses `/docs/`
272
+ * onto `/docs` is not spliced here: the generated config sets Astro's
273
+ * `trailingSlash: "never"`, which the adapter turns into the platform's own
274
+ * 308 route ahead of everything below (so a slashed Markdown request takes
275
+ * that hop first, then negotiates). For each 404 twin the build emitted (`notFound.markdown` for
291
276
  * `404.md`, `notFound.json` for `404.json`), its routes go into the miss
292
277
  * phase right before the adapter's `/404.html` fallback — and nowhere when
293
278
  * that fallback is absent, since a `dest` with no file behind it would serve
@@ -342,15 +327,8 @@ export const injectNegotiationRoutes = (
342
327
  }
343
328
  // Headers first: `continue` routes accumulate, so a request the rewrite
344
329
  // route then terminates (Markdown negotiation on the homepage) still carries
345
- // the Link header. The trailing-slash redirect goes last so a slashed URL's
346
- // Markdown negotiation still rewrites directly instead of bouncing.
347
- routes.splice(
348
- filesystemIndex,
349
- 0,
350
- ...headerRoutes,
351
- ...rewriteRoutes,
352
- TRAILING_SLASH_REDIRECT
353
- );
330
+ // the Link header.
331
+ routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes);
354
332
  const notFoundRoutes = [
355
333
  ...(notFound.markdown ? NOT_FOUND_MARKDOWN_ROUTES : []),
356
334
  ...(notFound.json ? NOT_FOUND_JSON_ROUTES : []),
package/src/og/card.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
 
3
- import { imageSize } from "image-size";
4
3
  import { render } from "takumi-js";
5
4
  import type { RenderOptions } from "takumi-js";
6
5
  import { container, googleFonts, image, text } from "takumi-js/helpers";
7
6
  import type { FontSubset, GoogleFontFamily, Node } from "takumi-js/helpers";
8
7
 
8
+ import { svgDimensions } from "../core/svg-dimensions.ts";
9
9
  import { ACCENTS, isAccentPreset } from "../theme/palette.ts";
10
10
  import { OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from "./dimensions.ts";
11
11
 
@@ -234,19 +234,13 @@ const MARK_HEIGHT = 32;
234
234
  const MARK_MAX_WIDTH = 240;
235
235
  /**
236
236
  * The SVG's aspect ratio (w/h), or null when no usable dimensions exist (the
237
- * caller falls back to a square mark). image-size (already a dependency)
238
- * reads explicit width/height and falls back to the viewBox, tolerating the
239
- * quote/whitespace/attribute spellings the old regex silently missed —
240
- * `viewBox = "…"`, newline-separated values — which shipped visibly-squashed
241
- * marks instead of failing loudly.
237
+ * caller falls back to a square mark). The shared root-tag parser reads
238
+ * explicit width/height and falls back to the viewBox, so the card and the
239
+ * header measure one logo the same way.
242
240
  */
243
241
  const logoAspect = (svg: string): number | null => {
244
- try {
245
- const { height, width } = imageSize(Buffer.from(svg));
246
- return width && height ? width / height : null;
247
- } catch {
248
- return null;
249
- }
242
+ const size = svgDimensions(svg);
243
+ return size ? size.width / size.height : null;
250
244
  };
251
245
 
252
246
  // Render the configured logo as the brand mark. A `currentColor` logo carries
@@ -3,6 +3,7 @@ import { fromMarkdown } from "mdast-util-from-markdown";
3
3
  import { toString as mdastToString } from "mdast-util-to-string";
4
4
  import stringWidth from "string-width";
5
5
 
6
+ import { apiNamePhrase } from "../core/api-name.ts";
6
7
  import { columnsPrefix } from "../core/text-width.ts";
7
8
  import type { GraphqlMember } from "./graphql.ts";
8
9
  import { isGraphqlOperationKind } from "./graphql.ts";
@@ -158,6 +159,8 @@ const clip = (text: string, max: number): string => {
158
159
  };
159
160
 
160
161
  const apiName = (spec: ApiSpecData): string => spec.title || spec.label;
162
+ /** The spec's name as "the ___" in generated prose (see `apiNamePhrase`). */
163
+ const apiPhrase = (spec: ApiSpecData): string => apiNamePhrase(apiName(spec));
161
164
 
162
165
  /** Human phrase for each GraphQL page kind, for meta descriptions. */
163
166
  const GRAPHQL_MEMBER_PHRASES = {
@@ -194,16 +197,17 @@ const operationDescription = (
194
197
  }
195
198
  // AsyncAPI operations act on a channel, not an HTTP endpoint; GraphQL pages
196
199
  // document a root field or a named type.
197
- let suffix: string;
200
+ let subject: string;
198
201
  if (spec.kind === "asyncapi") {
199
- suffix = `Reference for the ${operation.method} operation on ${operation.path} in the ${apiName(spec)} API.`;
202
+ subject = `${operation.method} operation on ${operation.path}`;
200
203
  } else if (spec.kind === "graphql") {
201
204
  // SAFETY: the GraphQL extractor only ever assigns member kinds as the
202
205
  // method (see `extractGraphqlOperations`).
203
- suffix = `Reference for the ${operation.path} ${GRAPHQL_MEMBER_PHRASES[operation.method as GraphqlMember]} in the ${apiName(spec)} API.`;
206
+ subject = `${operation.path} ${GRAPHQL_MEMBER_PHRASES[operation.method as GraphqlMember]}`;
204
207
  } else {
205
- suffix = `Reference for the ${operation.method.toUpperCase()} ${operation.path} endpoint in the ${apiName(spec)} API.`;
208
+ subject = `${operation.method.toUpperCase()} ${operation.path} endpoint`;
206
209
  }
210
+ const suffix = `Reference for the ${subject} in the ${apiPhrase(spec)}.`;
207
211
  const prose = clip(
208
212
  plainProse(operation.description || operation.summary),
209
213
  META_DESCRIPTION_MAX - stringWidth(suffix) - 1
@@ -350,7 +354,7 @@ export const overviewMdx = (
350
354
  const seo: RenderedPageData["seo"] = {
351
355
  description:
352
356
  clip(plainProse(spec.description), META_DESCRIPTION_MAX) ||
353
- `${apiName(spec)} API reference.`,
357
+ `${apiPhrase(spec)} reference.`,
354
358
  };
355
359
  if (reference?.noindex) {
356
360
  seo.noindex = true;
@@ -30,7 +30,6 @@ import {
30
30
  catchAllPageTemplate,
31
31
  changelogIndexTemplate,
32
32
  contentConfigTemplate,
33
- envTemplate,
34
33
  exampleMapTemplate,
35
34
  exampleWrapperTemplate,
36
35
  examplesPageTemplate,
@@ -427,7 +426,6 @@ export const eject = async (
427
426
  // Never clobber a hand-tuned tsconfig; only write ours if none exists.
428
427
  skipIfExists: true,
429
428
  },
430
- { content: envTemplate(), path: join(srcDir, "env.d.ts") },
431
429
  {
432
430
  content: contentConfigTemplate({
433
431
  config,
@@ -560,9 +560,16 @@ blume-diff {
560
560
  /* Restore inner padding on every cell. Typography zeroes the first/last cell's
561
561
  inline padding so a borderless table aligns to the prose margin; inside the
562
562
  framed wrapper that leaves edge text touching the border. The :is() selector
563
- outweighs Typography's :where()-scoped rules so the outer columns get it too. */
563
+ outweighs Typography's :where()-scoped rules so the outer columns get it too.
564
+
565
+ The block padding is 0.75rem against a table line-height near 1.7: at 0.5rem a
566
+ cell whose content wrapped put MORE space between its own two lines than
567
+ between itself and the next row, so a table of wrapping cells read as one
568
+ block rather than as rows. The inline padding stays where it was, deliberately
569
+ — widening it comes out of column width in a capped article, and on one corpus
570
+ that pushed cells fitting on two lines onto three. */
564
571
  .blume-table-scroll :is(th, td) {
565
- padding: 0.5rem 0.75rem;
572
+ padding: 0.75rem;
566
573
  }
567
574
  /* Keep column labels on one line so a two-word header does not wrap into a
568
575
  ragged stack; the table just scrolls a little wider instead. Body cells keep