docusaurus-plugin-openapi-docs 1.0.0 → 1.0.3

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 (58) hide show
  1. package/README.md +8 -7
  2. package/lib/index.d.ts +1 -0
  3. package/lib/index.js +80 -9
  4. package/lib/markdown/createAuthentication.d.ts +2 -0
  5. package/lib/markdown/createAuthentication.js +139 -0
  6. package/lib/markdown/createContactInfo.d.ts +2 -0
  7. package/lib/markdown/createContactInfo.js +49 -0
  8. package/lib/markdown/createLicense.d.ts +2 -0
  9. package/lib/markdown/createLicense.js +33 -0
  10. package/lib/markdown/createSchemaDetails.js +4 -1
  11. package/lib/markdown/createStatusCodes.js +1 -1
  12. package/lib/markdown/createTermsOfService.d.ts +1 -0
  13. package/lib/markdown/createTermsOfService.js +32 -0
  14. package/lib/markdown/index.d.ts +3 -2
  15. package/lib/markdown/index.js +17 -3
  16. package/lib/openapi/createExample.js +59 -49
  17. package/lib/openapi/openapi.d.ts +5 -4
  18. package/lib/openapi/openapi.js +94 -50
  19. package/lib/openapi/openapi.test.js +0 -6
  20. package/lib/openapi/types.d.ts +5 -1
  21. package/lib/openapi/utils/loadAndBundleSpec.d.ts +3 -0
  22. package/lib/openapi/utils/loadAndBundleSpec.js +44 -0
  23. package/lib/openapi/utils/types.d.ts +306 -0
  24. package/lib/{markdown/createRequestBodyTable.js → openapi/utils/types.js} +0 -6
  25. package/lib/sidebars/index.d.ts +2 -1
  26. package/lib/sidebars/index.js +87 -30
  27. package/lib/types.d.ts +14 -3
  28. package/package.json +19 -11
  29. package/src/index.ts +108 -11
  30. package/src/markdown/createAuthentication.ts +160 -0
  31. package/src/markdown/createContactInfo.ts +52 -0
  32. package/src/markdown/createLicense.ts +34 -0
  33. package/src/markdown/createSchemaDetails.ts +6 -2
  34. package/src/markdown/createStatusCodes.ts +1 -1
  35. package/src/markdown/createTermsOfService.ts +30 -0
  36. package/src/markdown/index.ts +23 -3
  37. package/src/openapi/createExample.ts +59 -50
  38. package/src/openapi/openapi.test.ts +0 -6
  39. package/src/openapi/openapi.ts +115 -53
  40. package/src/openapi/types.ts +5 -1
  41. package/src/openapi/utils/loadAndBundleSpec.ts +62 -0
  42. package/src/openapi/utils/types.ts +303 -0
  43. package/src/{markdown/createRequestBodyTable.ts → postman-collection.d.ts} +2 -9
  44. package/src/sidebars/index.ts +108 -31
  45. package/src/types.ts +15 -3
  46. package/lib/markdown/createFullWidthTable.d.ts +0 -2
  47. package/lib/markdown/createFullWidthTable.js +0 -18
  48. package/lib/markdown/createParamsTable.d.ts +0 -7
  49. package/lib/markdown/createParamsTable.js +0 -80
  50. package/lib/markdown/createRequestBodyTable.d.ts +0 -6
  51. package/lib/markdown/createSchemaTable.d.ts +0 -14
  52. package/lib/markdown/createSchemaTable.js +0 -217
  53. package/src/markdown/createFullWidthTable.ts +0 -16
  54. package/src/markdown/createParamsTable.ts +0 -102
  55. package/src/markdown/createSchemaTable.ts +0 -275
  56. package/src/openapi/__fixtures__/examples/yogurtstore/_category_.json +0 -4
  57. package/src/openapi/__fixtures__/examples/yogurtstore/froyo.yaml +0 -13
  58. package/src/openapi/__fixtures__/examples/yogurtstore/nested/nested.yaml +0 -13
package/src/index.ts CHANGED
@@ -13,11 +13,15 @@ import { Globby } from "@docusaurus/utils";
13
13
  import chalk from "chalk";
14
14
  import { render } from "mustache";
15
15
 
16
- import { createApiPageMD, createInfoPageMD } from "./markdown";
16
+ import { createApiPageMD, createInfoPageMD, createTagPageMD } from "./markdown";
17
17
  import { readOpenapiFiles, processOpenapiFiles } from "./openapi";
18
18
  import generateSidebarSlice from "./sidebars";
19
19
  import type { PluginOptions, LoadedContent, APIOptions } from "./types";
20
20
 
21
+ export function isURL(str: string): boolean {
22
+ return /^(https?:)\/\//m.test(str);
23
+ }
24
+
21
25
  export default function pluginOpenAPI(
22
26
  context: LoadContext,
23
27
  options: PluginOptions
@@ -28,12 +32,16 @@ export default function pluginOpenAPI(
28
32
  async function generateApiDocs(options: APIOptions) {
29
33
  let { specPath, outputDir, template, sidebarOptions } = options;
30
34
 
31
- const contentPath = path.resolve(siteDir, specPath);
35
+ const contentPath = isURL(specPath)
36
+ ? specPath
37
+ : path.resolve(siteDir, specPath);
32
38
 
33
39
  try {
34
40
  const openapiFiles = await readOpenapiFiles(contentPath, {});
35
- const loadedApi = await processOpenapiFiles(openapiFiles);
36
-
41
+ const [loadedApi, tags] = await processOpenapiFiles(
42
+ openapiFiles,
43
+ sidebarOptions!
44
+ );
37
45
  if (!fs.existsSync(outputDir)) {
38
46
  try {
39
47
  fs.mkdirSync(outputDir, { recursive: true });
@@ -51,7 +59,8 @@ export default function pluginOpenAPI(
51
59
  const sidebarSlice = generateSidebarSlice(
52
60
  sidebarOptions!, // TODO: find a better way to handle null
53
61
  options,
54
- loadedApi
62
+ loadedApi,
63
+ tags
55
64
  );
56
65
 
57
66
  const sidebarSliceTemplate = template
@@ -81,7 +90,12 @@ export default function pluginOpenAPI(
81
90
  ? fs.readFileSync(template).toString()
82
91
  : `---
83
92
  id: {{{id}}}
93
+ {{^api}}
94
+ sidebar_label: Introduction
95
+ {{/api}}
96
+ {{#api}}
84
97
  sidebar_label: {{{title}}}
98
+ {{/api}}
85
99
  {{^api}}
86
100
  sidebar_position: 0
87
101
  {{/api}}
@@ -95,19 +109,67 @@ api: {{{json}}}
95
109
  {{#api.method}}
96
110
  sidebar_class_name: "{{{api.method}}} api-method"
97
111
  {{/api.method}}
112
+ {{#infoPath}}
113
+ info_path: {{{infoPath}}}
114
+ {{/infoPath}}
115
+ ---
116
+
117
+ {{{markdown}}}
118
+ `;
119
+
120
+ const infoMdTemplate = template
121
+ ? fs.readFileSync(template).toString()
122
+ : `---
123
+ id: {{{id}}}
124
+ sidebar_label: {{{title}}}
125
+ hide_title: true
98
126
  ---
99
127
 
100
128
  {{{markdown}}}
129
+
130
+ \`\`\`mdx-code-block
131
+ import DocCardList from '@theme/DocCardList';
132
+ import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
133
+
134
+ <DocCardList items={useCurrentSidebarCategory().items}/>
135
+ \`\`\`
136
+ `;
137
+
138
+ const tagMdTemplate = template
139
+ ? fs.readFileSync(template).toString()
140
+ : `---
141
+ id: {{{id}}}
142
+ title: {{{description}}}
143
+ description: {{{description}}}
144
+ ---
145
+
146
+ {{{markdown}}}
147
+
148
+ \`\`\`mdx-code-block
149
+ import DocCardList from '@theme/DocCardList';
150
+ import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
151
+
152
+ <DocCardList items={useCurrentSidebarCategory().items}/>
153
+ \`\`\`
101
154
  `;
102
155
 
103
156
  loadedApi.map(async (item) => {
104
157
  const markdown =
105
- item.type === "api" ? createApiPageMD(item) : createInfoPageMD(item);
158
+ item.type === "api"
159
+ ? createApiPageMD(item)
160
+ : item.type === "info"
161
+ ? createInfoPageMD(item)
162
+ : createTagPageMD(item);
106
163
  item.markdown = markdown;
107
164
  if (item.type === "api") {
108
165
  item.json = JSON.stringify(item.api);
166
+ if (item.infoId) item.infoPath = `${outputDir}/${item.infoId}`;
109
167
  }
168
+
110
169
  const view = render(mdTemplate, item);
170
+ const utils = render(infoMdTemplate, item);
171
+ // eslint-disable-next-line testing-library/render-result-naming-convention
172
+ const tagUtils = render(tagMdTemplate, item);
111
173
 
112
174
  if (item.type === "api") {
113
175
  if (!fs.existsSync(`${outputDir}/${item.id}.api.mdx`)) {
@@ -129,15 +191,49 @@ sidebar_class_name: "{{{api.method}}} api-method"
129
191
 
130
192
  // TODO: determine if we actually want/need this
131
193
  if (item.type === "info") {
132
- if (!fs.existsSync(`${outputDir}/index.api.mdx`)) {
194
+ if (!fs.existsSync(`${outputDir}/${item.id}.info.mdx`)) {
133
195
  try {
134
- fs.writeFileSync(`${outputDir}/index.api.mdx`, view, "utf8");
196
+ sidebarOptions?.categoryLinkSource === "info" // Only use utils template if set to "info"
197
+ ? fs.writeFileSync(
198
+ `${outputDir}/${item.id}.info.mdx`,
199
+ utils,
200
+ "utf8"
201
+ )
202
+ : fs.writeFileSync(
203
+ `${outputDir}/${item.id}.info.mdx`,
204
+ view,
205
+ "utf8"
206
+ );
135
207
  console.log(
136
- chalk.green(`Successfully created "${outputDir}/index.api.mdx"`)
208
+ chalk.green(
209
+ `Successfully created "${outputDir}/${item.id}.info.mdx"`
210
+ )
137
211
  );
138
212
  } catch (err) {
139
213
  console.error(
140
- chalk.red(`Failed to write "${outputDir}/index.api.mdx"`),
214
+ chalk.red(`Failed to write "${outputDir}/${item.id}.info.mdx"`),
215
+ chalk.yellow(err)
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ if (item.type === "tag") {
222
+ if (!fs.existsSync(`${outputDir}/${item.id}.tag.mdx`)) {
223
+ try {
224
+ fs.writeFileSync(
225
+ `${outputDir}/${item.id}.tag.mdx`,
226
+ tagUtils,
227
+ "utf8"
228
+ );
229
+ console.log(
230
+ chalk.green(
231
+ `Successfully created "${outputDir}/${item.id}.tag.mdx"`
232
+ )
233
+ );
234
+ } catch (err) {
235
+ console.error(
236
+ chalk.red(`Failed to write "${outputDir}/${item.id}.tag.mdx"`),
141
237
  chalk.yellow(err)
142
238
  );
143
239
  }
@@ -145,6 +241,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
145
241
  }
146
242
  return;
147
243
  });
244
+
148
245
  return;
149
246
  } catch (e) {
150
247
  console.error(chalk.red(`Loading of api failed for "${contentPath}"`));
@@ -155,7 +252,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
155
252
  async function cleanApiDocs(options: APIOptions) {
156
253
  const { outputDir } = options;
157
254
  const apiDir = path.join(siteDir, outputDir);
158
- const apiMdxFiles = await Globby(["*.api.mdx"], {
255
+ const apiMdxFiles = await Globby(["*.api.mdx", "*.info.mdx", "*.tag.mdx"], {
159
256
  cwd: path.resolve(apiDir),
160
257
  });
161
258
  const sidebarFile = await Globby(["sidebar.js"], {
@@ -0,0 +1,160 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { OAuthFlowObject, SecuritySchemeObject } from "../openapi/types";
9
+ import { createDescription } from "./createDescription";
10
+ import { create, guard } from "./utils";
11
+
12
+ export function createAuthentication(securitySchemes: SecuritySchemeObject) {
13
+ if (!securitySchemes || !Object.keys(securitySchemes).length) return "";
14
+
15
+ const createAuthenticationTable = (securityScheme: any) => {
16
+ const { bearerFormat, flows, name, scheme, type } = securityScheme;
17
+
18
+ const createSecuritySchemeTypeRow = () =>
19
+ create("tr", {
20
+ children: [
21
+ create("th", { children: "Security Scheme Type:" }),
22
+ create("td", { children: type }),
23
+ ],
24
+ });
25
+
26
+ const createOAuthFlowRows = () => {
27
+ const flowRows = Object.entries(flows).map(([flowType, flowObj]) => {
28
+ const { authorizationUrl, tokenUrl, refreshUrl, scopes } =
29
+ flowObj as OAuthFlowObject;
30
+
31
+ return create("tr", {
32
+ children: [
33
+ create("th", { children: `${flowType} OAuth Flow:` }),
34
+ create("td", {
35
+ children: [
36
+ guard(tokenUrl, () =>
37
+ create("p", { children: `Token URL: ${tokenUrl}` })
38
+ ),
39
+ guard(authorizationUrl, () =>
40
+ create("p", {
41
+ children: `Authorization URL: ${authorizationUrl}`,
42
+ })
43
+ ),
44
+ guard(refreshUrl, () =>
45
+ create("p", { children: `Refresh URL: ${refreshUrl}` })
46
+ ),
47
+ create("span", { children: "Scopes:" }),
48
+ create("ul", {
49
+ children: Object.entries(scopes).map(([scope, description]) =>
50
+ create("li", { children: `${scope}: ${description}` })
51
+ ),
52
+ }),
53
+ ],
54
+ }),
55
+ ],
56
+ });
57
+ });
58
+
59
+ return flowRows.join("");
60
+ };
61
+
62
+ switch (type) {
63
+ case "apiKey":
64
+ return create("div", {
65
+ children: [
66
+ create("table", {
67
+ children: create("tbody", {
68
+ children: [
69
+ createSecuritySchemeTypeRow(),
70
+ create("tr", {
71
+ children: [
72
+ create("th", { children: "Header parameter name:" }),
73
+ create("td", { children: name }),
74
+ ],
75
+ }),
76
+ ],
77
+ }),
78
+ }),
79
+ ],
80
+ });
81
+ case "http":
82
+ return create("div", {
83
+ children: [
84
+ create("table", {
85
+ children: create("tbody", {
86
+ children: [
87
+ createSecuritySchemeTypeRow(),
88
+ create("tr", {
89
+ children: [
90
+ create("th", { children: "HTTP Authorization Scheme:" }),
91
+ create("td", { children: scheme }),
92
+ ],
93
+ }),
94
+ create("tr", {
95
+ children: [
96
+ create("th", { children: "Bearer format:" }),
97
+ create("td", { children: bearerFormat }),
98
+ ],
99
+ }),
100
+ ],
101
+ }),
102
+ }),
103
+ ],
104
+ });
105
+ case "oauth2":
106
+ return create("div", {
107
+ children: [
108
+ create("table", {
109
+ children: create("tbody", {
110
+ children: [
111
+ createSecuritySchemeTypeRow(),
112
+ createOAuthFlowRows(),
113
+ ],
114
+ }),
115
+ }),
116
+ ],
117
+ });
118
+ default:
119
+ return "";
120
+ }
121
+ };
122
+
123
+ const formatTabLabel = (str: string) => {
124
+ const formattedLabel = str
125
+ .replace(/(_|-)/g, " ")
126
+ .trim()
127
+ .replace(/\w\S*/g, (str) => str.charAt(0).toUpperCase() + str.substr(1))
128
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
129
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
130
+
131
+ const isOAuth = formattedLabel.toLowerCase().includes("oauth2");
132
+ const isApiKey = formattedLabel.toLowerCase().includes("api");
133
+
134
+ return isOAuth ? "OAuth 2.0" : isApiKey ? "API Key" : formattedLabel;
135
+ };
136
+
137
+ return create("div", {
138
+ children: [
139
+ create("h2", {
140
+ children: "Authentication",
141
+ id: "authentication",
142
+ style: { marginBottom: "1rem" },
143
+ }),
144
+ create("Tabs", {
145
+ children: Object.entries(securitySchemes).map(
146
+ ([schemeType, schemeObj]) =>
147
+ create("TabItem", {
148
+ label: formatTabLabel(schemeType),
149
+ value: `${schemeType}`,
150
+ children: [
151
+ createDescription(schemeObj.description),
152
+ createAuthenticationTable(schemeObj),
153
+ ],
154
+ })
155
+ ),
156
+ }),
157
+ ],
158
+ style: { marginBottom: "2rem" },
159
+ });
160
+ }
@@ -0,0 +1,52 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { ContactObject } from "../openapi/types";
9
+ import { create, guard } from "./utils";
10
+
11
+ export function createContactInfo(contact: ContactObject) {
12
+ if (!contact || !Object.keys(contact).length) return "";
13
+ const { name, url, email } = contact;
14
+
15
+ return create("div", {
16
+ style: {
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
20
+ },
21
+ children: [
22
+ create("h3", {
23
+ style: {
24
+ marginBottom: "0.25rem",
25
+ },
26
+ children: "Contact",
27
+ }),
28
+ create("span", {
29
+ children: [
30
+ guard(name, () => `${name}: `),
31
+ guard(email, () =>
32
+ create("a", {
33
+ href: `mailto:${email}`,
34
+ children: `${email}`,
35
+ })
36
+ ),
37
+ ],
38
+ }),
39
+ guard(url, () =>
40
+ create("span", {
41
+ children: [
42
+ "URL: ",
43
+ create("a", {
44
+ href: `${url}`,
45
+ children: `${url}`,
46
+ }),
47
+ ],
48
+ })
49
+ ),
50
+ ],
51
+ });
52
+ }
@@ -0,0 +1,34 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { LicenseObject } from "../openapi/types";
9
+ import { create, guard } from "./utils";
10
+
11
+ export function createLicense(license: LicenseObject) {
12
+ if (!license || !Object.keys(license).length) return "";
13
+ const { name, url } = license;
14
+
15
+ return create("div", {
16
+ style: {
17
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
18
+ },
19
+ children: [
20
+ create("h3", {
21
+ style: {
22
+ marginBottom: "0.25rem",
23
+ },
24
+ children: "License",
25
+ }),
26
+ guard(url, () =>
27
+ create("a", {
28
+ href: url,
29
+ children: name ?? url,
30
+ })
31
+ ),
32
+ ],
33
+ });
34
+ }
@@ -242,7 +242,12 @@ interface Props {
242
242
  }
243
243
 
244
244
  export function createSchemaDetails({ title, body, ...rest }: Props) {
245
- if (body === undefined || body.content === undefined) {
245
+ if (
246
+ body === undefined ||
247
+ body.content === undefined ||
248
+ Object.keys(body).length === 0 ||
249
+ Object.keys(body.content).length === 0
250
+ ) {
246
251
  return undefined;
247
252
  }
248
253
 
@@ -250,7 +255,6 @@ export function createSchemaDetails({ title, body, ...rest }: Props) {
250
255
  // NOTE: We just pick a random content-type.
251
256
  // How common is it to have multiple?
252
257
  const randomFirstKey = Object.keys(body.content)[0];
253
-
254
258
  const firstBody = body.content[randomFirstKey].schema;
255
259
 
256
260
  if (firstBody === undefined) {
@@ -26,7 +26,7 @@ export function createStatusCodes({ responses }: Props) {
26
26
 
27
27
  return create("div", {
28
28
  children: [
29
- create("Tabs", {
29
+ create("ApiTabs", {
30
30
  children: codes.map((code) => {
31
31
  return create("TabItem", {
32
32
  label: code,
@@ -0,0 +1,30 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { create } from "./utils";
9
+
10
+ export function createTermsOfService(termsOfService: string | undefined) {
11
+ if (!termsOfService) return "";
12
+
13
+ return create("div", {
14
+ style: {
15
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
16
+ },
17
+ children: [
18
+ create("h3", {
19
+ style: {
20
+ marginBottom: "0.25rem",
21
+ },
22
+ children: "Terms of Service",
23
+ }),
24
+ create("a", {
25
+ href: `${termsOfService}`,
26
+ children: termsOfService,
27
+ }),
28
+ ],
29
+ });
30
+ }
@@ -7,12 +7,21 @@
7
7
 
8
8
  import { escape } from "lodash";
9
9
 
10
- import { ApiPageMetadata, InfoPageMetadata } from "../types";
10
+ import {
11
+ ContactObject,
12
+ LicenseObject,
13
+ SecuritySchemeObject,
14
+ } from "../openapi/types";
15
+ import { ApiPageMetadata, InfoPageMetadata, TagPageMetadata } from "../types";
16
+ import { createAuthentication } from "./createAuthentication";
17
+ import { createContactInfo } from "./createContactInfo";
11
18
  import { createDeprecationNotice } from "./createDeprecationNotice";
12
19
  import { createDescription } from "./createDescription";
20
+ import { createLicense } from "./createLicense";
13
21
  import { createParamsDetails } from "./createParamsDetails";
14
22
  import { createRequestBodyDetails } from "./createRequestBodyDetails";
15
23
  import { createStatusCodes } from "./createStatusCodes";
24
+ import { createTermsOfService } from "./createTermsOfService";
16
25
  import { createVersionBadge } from "./createVersionBadge";
17
26
  import { render } from "./utils";
18
27
 
@@ -30,7 +39,7 @@ export function createApiPageMD({
30
39
  return render([
31
40
  `import ParamsItem from "@theme/ParamsItem";\n`,
32
41
  `import SchemaItem from "@theme/SchemaItem"\n`,
33
- `import Tabs from "@theme/Tabs";\n`,
42
+ `import ApiTabs from "@theme/ApiTabs";\n`,
34
43
  `import TabItem from "@theme/TabItem";\n\n`,
35
44
  `## ${escape(title)}\n\n`,
36
45
  createDeprecationNotice({ deprecated, description: deprecatedDescription }),
@@ -45,11 +54,22 @@ export function createApiPageMD({
45
54
  }
46
55
 
47
56
  export function createInfoPageMD({
48
- info: { title, version, description },
57
+ info: { title, version, description, contact, license, termsOfService },
58
+ securitySchemes,
49
59
  }: InfoPageMetadata) {
50
60
  return render([
61
+ `import Tabs from "@theme/Tabs";\n`,
62
+ `import TabItem from "@theme/TabItem";\n`,
51
63
  createVersionBadge(version),
52
64
  `# ${escape(title)}\n\n`,
53
65
  createDescription(description),
66
+ createAuthentication(securitySchemes as unknown as SecuritySchemeObject),
67
+ createContactInfo(contact as ContactObject),
68
+ createTermsOfService(termsOfService),
69
+ createLicense(license as LicenseObject),
54
70
  ]);
55
71
  }
72
+
73
+ export function createTagPageMD({ tag: { description } }: TagPageMetadata) {
74
+ return render([createDescription(description)]);
75
+ }