zudoku 0.89.0 → 0.90.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.
@@ -0,0 +1,7 @@
1
+ import type { RecordAny } from "../../../util/types.js";
2
+ export declare const SEO_EXTENSION = "x-zudoku-seo";
3
+ export type SeoExtension = {
4
+ title?: string;
5
+ description?: string;
6
+ };
7
+ export declare const readSeoExtension: (extensions: RecordAny | null | undefined) => SeoExtension;
@@ -422,6 +422,8 @@ Extensions that can be applied to tag categories:
422
422
 
423
423
  - `x-zudoku-collapsed`: Control initial collapsed state of a tag category (default: `true`)
424
424
  - `x-zudoku-collapsible`: Control if a tag category can be collapsed (default: `true`)
425
+ - `x-zudoku-seo`: Set the page title and meta description of a tag's page without changing its
426
+ sidebar label or heading. See [`x-zudoku-seo`](../openapi-extensions/x-zudoku-seo)
425
427
 
426
428
  Example:
427
429
 
@@ -29,3 +29,8 @@ tags:
29
29
 
30
30
  Without `x-displayName`, the sidebar would show `ai-ops` and `user-mgmt`. With it, the sidebar
31
31
  displays `AI Operations` and `User Management` instead.
32
+
33
+ ## Related
34
+
35
+ - [`x-zudoku-seo`](./x-zudoku-seo) — set a different browser title and meta description for the
36
+ tag's page
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: x-zudoku-seo
3
+ sidebar_icon: search
4
+ ---
5
+
6
+ Use `x-zudoku-seo` to set the browser title and meta description of a tag's page without changing
7
+ the label shown in the sidebar or the page heading. This is useful when several tags share the same
8
+ [`x-displayName`](./x-display-name) but should still have unique titles for search engines.
9
+
10
+ ## Location
11
+
12
+ The extension is added at the **Tag Object** level.
13
+
14
+ | Option | Type | Description |
15
+ | ------------- | -------- | ----------------------------------------------------------------------------- |
16
+ | `title` | `string` | Page title used in the `<title>` tag. Replaces `<tag name> - <API title>`. |
17
+ | `description` | `string` | Content of the `<meta name="description">` tag. Replaces the tag description. |
18
+
19
+ Both properties are optional. Anything not set falls back to the default behavior. If
20
+ [`metadata.title`](/docs/configuration/overview#metadata) is a template string (e.g.
21
+ `%s - My Company`), it is still applied to the title.
22
+
23
+ The title and description are rendered on the server, so they are part of the prerendered HTML and
24
+ visible to crawlers without running JavaScript.
25
+
26
+ ## Example
27
+
28
+ ```yaml
29
+ tags:
30
+ - name: location-key-daily
31
+ x-displayName: Location Key
32
+ x-zudoku-seo:
33
+ title: Daily Forecasts by Location Key - Core Weather
34
+ description: Daily forecasts for a location key, up to 15 days ahead.
35
+ - name: location-key-hourly
36
+ x-displayName: Location Key
37
+ x-zudoku-seo:
38
+ title: Hourly Forecasts by Location Key - Core Weather
39
+ description: Hourly forecasts for a location key, up to 240 hours ahead.
40
+ ```
41
+
42
+ Both tags show `Location Key` in the sidebar and as the page heading, but each page has its own
43
+ browser title and description.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.89.0",
3
+ "version": "0.90.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.22.0"
@@ -15,6 +15,7 @@ import { UNTAGGED_PATH } from "./index.js";
15
15
  import { OperationListItem } from "./OperationListItem.js";
16
16
  import { useSelectedServer } from "./state.js";
17
17
  import { sanitizeMarkdownForMetatag } from "./util/sanitizeMarkdownForMetatag.js";
18
+ import { readSeoExtension } from "./util/seoExtension.js";
18
19
  import { useWarmupSchema } from "./util/useWarmupSchema.js";
19
20
 
20
21
  export const OperationsFragment = graphql(/* GraphQL */ `
@@ -242,6 +243,7 @@ export const OperationList = ({
242
243
  }
243
244
 
244
245
  const { operations, next, prev, description: tagDescription } = schema.tag;
246
+ const seo = readSeoExtension(schema.tag.extensions);
245
247
 
246
248
  // Simple heuristic to determine if we should lazy highlight the code
247
249
  // This is to avoid the performance issues when there are a lot of operations
@@ -250,13 +252,15 @@ export const OperationList = ({
250
252
  // The summary property is preferable here as it is a short description of
251
253
  // the API, whereas the description property is typically longer and supports
252
254
  // commonmark formatting, making it ill-suited for use in the meta description
253
- const metaDescription = tagDescription
254
- ? sanitizeMarkdownForMetatag(tagDescription)
255
- : summary
256
- ? summary
257
- : description
258
- ? sanitizeMarkdownForMetatag(description)
259
- : undefined;
255
+ const metaDescription =
256
+ seo.description ??
257
+ (tagDescription
258
+ ? sanitizeMarkdownForMetatag(tagDescription)
259
+ : summary
260
+ ? summary
261
+ : description
262
+ ? sanitizeMarkdownForMetatag(description)
263
+ : undefined);
260
264
 
261
265
  const paginationProps = {
262
266
  prev: prev
@@ -284,7 +288,10 @@ export const OperationList = ({
284
288
  ? "Other endpoints"
285
289
  : (schema.tag.extensions?.["x-displayName"] ?? schema.tag.name);
286
290
 
287
- const helmetTitle = [tagTitle, title].filter(Boolean).join(" - ");
291
+ // `x-zudoku-seo` only affects the document head, the sidebar and heading
292
+ // keep using `x-displayName`
293
+ const helmetTitle =
294
+ seo.title ?? [tagTitle, title].filter(Boolean).join(" - ");
288
295
 
289
296
  return (
290
297
  <div
@@ -0,0 +1,30 @@
1
+ import type { RecordAny } from "../../../util/types.js";
2
+
3
+ /** Overrides a page's `<title>` and meta description. See the `x-zudoku-seo` docs. */
4
+ export const SEO_EXTENSION = "x-zudoku-seo";
5
+
6
+ export type SeoExtension = {
7
+ title?: string;
8
+ description?: string;
9
+ };
10
+
11
+ const nonEmptyString = (value: unknown) =>
12
+ typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
13
+
14
+ /**
15
+ * Reads `x-zudoku-seo` off an object's extensions. `extensions` reaches the
16
+ * client as untyped JSON, so anything other than a non-empty string is ignored
17
+ * and the page falls back to its default title and description.
18
+ */
19
+ export const readSeoExtension = (
20
+ extensions: RecordAny | null | undefined,
21
+ ): SeoExtension => {
22
+ const seo: unknown = extensions?.[SEO_EXTENSION];
23
+ if (typeof seo !== "object" || seo === null) return {};
24
+
25
+ return {
26
+ title: "title" in seo ? nonEmptyString(seo.title) : undefined,
27
+ description:
28
+ "description" in seo ? nonEmptyString(seo.description) : undefined,
29
+ };
30
+ };