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.
- package/dist/declarations/lib/plugins/openapi/util/seoExtension.d.ts +7 -0
- package/docs/configuration/api-reference.md +2 -0
- package/docs/openapi-extensions/x-display-name.md +5 -0
- package/docs/openapi-extensions/x-zudoku-seo.md +43 -0
- package/package.json +1 -1
- package/src/lib/plugins/openapi/OperationList.tsx +15 -8
- package/src/lib/plugins/openapi/util/seoExtension.ts +30 -0
|
@@ -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
|
@@ -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 =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
?
|
|
257
|
-
:
|
|
258
|
-
?
|
|
259
|
-
:
|
|
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
|
-
|
|
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
|
+
};
|