zudoku 0.75.0 → 0.75.1

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 (35) hide show
  1. package/dist/cli/cli.js +35 -9
  2. package/dist/declarations/lib/authentication/providers/openid.d.ts +1 -0
  3. package/dist/declarations/lib/authentication/state.d.ts +6 -1
  4. package/dist/declarations/lib/plugins/api-catalog/Catalog.d.ts +2 -4
  5. package/dist/declarations/lib/plugins/api-catalog/index.d.ts +3 -8
  6. package/dist/declarations/lib/plugins/openapi/schema/SchemaRefLink.d.ts +5 -0
  7. package/dist/declarations/lib/plugins/openapi/schema/SchemaView.d.ts +2 -1
  8. package/dist/declarations/lib/plugins/openapi/schema/utils.d.ts +1 -0
  9. package/dist/declarations/lib/ui/HoverCard.d.ts +5 -5
  10. package/dist/declarations/lib/ui/Toggle.d.ts +2 -6
  11. package/dist/declarations/lib/ui/ToggleGroup.d.ts +7 -9
  12. package/docs/deploy/github-pages.md +12 -0
  13. package/docs/guides/redirects.md +186 -0
  14. package/package.json +8 -7
  15. package/src/lib/authentication/providers/openid.tsx +18 -15
  16. package/src/lib/authentication/state.ts +14 -1
  17. package/src/lib/components/Meta.tsx +10 -1
  18. package/src/lib/hooks/useHotkey.ts +10 -0
  19. package/src/lib/oas/graphql/circular.ts +11 -0
  20. package/src/lib/plugins/api-catalog/Catalog.tsx +255 -52
  21. package/src/lib/plugins/api-catalog/index.tsx +7 -64
  22. package/src/lib/plugins/openapi/ParamInfos.tsx +20 -7
  23. package/src/lib/plugins/openapi/SchemaList.tsx +1 -1
  24. package/src/lib/plugins/openapi/SecurityRequirements.tsx +86 -68
  25. package/src/lib/plugins/openapi/playground/Playground.tsx +7 -7
  26. package/src/lib/plugins/openapi/schema/SchemaRefLink.tsx +21 -0
  27. package/src/lib/plugins/openapi/schema/SchemaView.tsx +22 -4
  28. package/src/lib/plugins/openapi/schema/utils.ts +21 -0
  29. package/src/lib/ui/Badge.tsx +1 -1
  30. package/src/lib/ui/HoverCard.tsx +35 -21
  31. package/src/lib/ui/Toggle.tsx +21 -21
  32. package/src/lib/ui/ToggleGroup.tsx +59 -32
  33. package/src/vite/build.ts +3 -1
  34. package/src/vite/plugin-api.ts +32 -0
  35. package/src/zuplo/enrich-with-zuplo-mcp.ts +37 -41
package/dist/cli/cli.js CHANGED
@@ -3813,7 +3813,7 @@ import {
3813
3813
  // package.json
3814
3814
  var package_default = {
3815
3815
  name: "zudoku",
3816
- version: "0.74.3",
3816
+ version: "0.75.0",
3817
3817
  type: "module",
3818
3818
  sideEffects: [
3819
3819
  "**/*.css",
@@ -3891,6 +3891,7 @@ var package_default = {
3891
3891
  },
3892
3892
  dependencies: {
3893
3893
  "@apidevtools/json-schema-ref-parser": "15.3.1",
3894
+ "@base-ui/react": "^1.4.0",
3894
3895
  "@envelop/core": "5.5.1",
3895
3896
  "@graphql-typed-document-node/core": "3.2.0",
3896
3897
  "@hono/node-server": "1.19.13",
@@ -3958,7 +3959,7 @@ var package_default = {
3958
3959
  "hast-util-heading-rank": "3.0.0",
3959
3960
  "hast-util-to-jsx-runtime": "2.3.6",
3960
3961
  "hast-util-to-string": "3.0.1",
3961
- hono: "4.12.12",
3962
+ hono: "4.12.14",
3962
3963
  "http-terminator": "3.2.0",
3963
3964
  "javascript-stringify": "2.1.0",
3964
3965
  "json-schema-to-typescript-lite": "15.0.0",
@@ -3973,7 +3974,7 @@ var package_default = {
3973
3974
  "next-themes": "0.4.6",
3974
3975
  oauth4webapi: "3.8.5",
3975
3976
  "openapi-types": "12.1.3",
3976
- pagefind: "1.5.0-beta.1",
3977
+ pagefind: "1.5.2",
3977
3978
  picocolors: "1.1.1",
3978
3979
  piscina: "5.1.4",
3979
3980
  "posthog-node": "5.26.0",
@@ -4010,9 +4011,9 @@ var package_default = {
4010
4011
  zustand: "5.0.12"
4011
4012
  },
4012
4013
  devDependencies: {
4013
- "@clerk/clerk-js": "^6.0.0",
4014
- "@graphql-codegen/cli": "6.1.2",
4015
- "@inkeep/cxkit-types": "0.5.116",
4014
+ "@clerk/clerk-js": "^6.7.4",
4015
+ "@graphql-codegen/cli": "6.3.0",
4016
+ "@inkeep/cxkit-types": "0.5.117",
4016
4017
  "@testing-library/dom": "catalog:",
4017
4018
  "@testing-library/jest-dom": "catalog:",
4018
4019
  "@testing-library/react": "catalog:",
@@ -4030,7 +4031,7 @@ var package_default = {
4030
4031
  "@types/yargs": "17.0.35",
4031
4032
  "@vitest/coverage-v8": "4.0.18",
4032
4033
  "happy-dom": "catalog:",
4033
- "oxc-parser": "^0.119.0",
4034
+ "oxc-parser": "^0.126.0",
4034
4035
  react: "catalog:",
4035
4036
  "react-dom": "catalog:",
4036
4037
  tsx: "4.21.0",
@@ -4730,6 +4731,9 @@ var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs
4730
4731
  const result = Array.isArray(obj) ? obj.map((item, i) => recurse(item, i.toString())) : Object.fromEntries(
4731
4732
  Object.entries(obj).map(([k, v]) => [k, recurse(v, k)])
4732
4733
  );
4734
+ if (!Array.isArray(result) && typeof refPath === "string" && !("__$ref" in result)) {
4735
+ result.__$ref = refPath;
4736
+ }
4733
4737
  refs.set(obj, result);
4734
4738
  currentPath.delete(obj);
4735
4739
  if (typeof refPath === "string") currentRefPaths.delete(refPath);
@@ -6224,15 +6228,37 @@ var viteApiPlugin = async () => {
6224
6228
  );
6225
6229
  const apis = ensureArray(config2.apis);
6226
6230
  const apiMetadata = [];
6231
+ const httpMethods = /* @__PURE__ */ new Set([
6232
+ "get",
6233
+ "post",
6234
+ "put",
6235
+ "patch",
6236
+ "delete",
6237
+ "options",
6238
+ "head",
6239
+ "trace"
6240
+ ]);
6227
6241
  for (const apiConfig of apis) {
6228
6242
  if (apiConfig.type === "file" && apiConfig.path) {
6229
6243
  const latestSchema = schemaManager.getLatestSchema(apiConfig.path);
6230
6244
  if (!latestSchema?.schema.info) continue;
6245
+ const operationCount = Object.values(
6246
+ latestSchema.schema.paths ?? {}
6247
+ ).reduce((sum, pathItem) => {
6248
+ if (!pathItem || typeof pathItem !== "object") return sum;
6249
+ return sum + Object.keys(pathItem).filter(
6250
+ (m) => httpMethods.has(m.toLowerCase())
6251
+ ).length;
6252
+ }, 0);
6253
+ const rawVersion = latestSchema.schema.info.version;
6254
+ const version = rawVersion ? rawVersion.startsWith("v") || rawVersion.startsWith("V") ? rawVersion : `v${rawVersion}` : void 0;
6231
6255
  apiMetadata.push({
6232
6256
  path: apiConfig.path,
6233
6257
  label: latestSchema.schema.info.title,
6234
6258
  description: latestSchema.schema.info.description ?? "",
6235
- categories: apiConfig.categories ?? []
6259
+ categories: apiConfig.categories ?? [],
6260
+ version,
6261
+ operationCount
6236
6262
  });
6237
6263
  }
6238
6264
  }
@@ -8232,7 +8258,7 @@ var runPrerender = async (options) => {
8232
8258
  await writeFile5(indexHtml, html, "utf-8");
8233
8259
  }
8234
8260
  const statusPages = workerResults.flatMap(
8235
- (r) => /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : []
8261
+ (r) => /^(400|404|500)\.html$/.test(path22.basename(r.outputPath)) ? r.outputPath : []
8236
8262
  );
8237
8263
  for (const statusPage of statusPages) {
8238
8264
  await rename(
@@ -43,6 +43,7 @@ export declare class OpenIDAuthenticationProvider extends CoreAuthenticationPlug
43
43
  replace?: boolean;
44
44
  }): Promise<void>;
45
45
  signIn(_: AuthActionContext, { redirectTo, replace }: AuthActionOptions): Promise<void>;
46
+ private buildUserProfile;
46
47
  refreshUserProfile(): Promise<boolean>;
47
48
  private authorize;
48
49
  getAccessToken(): Promise<string>;
@@ -39,11 +39,16 @@ export declare const useAuthState: import("zustand").UseBoundStore<Omit<import("
39
39
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
40
40
  };
41
41
  }>;
42
+ export type CustomClaim = string | number | boolean | null | CustomClaimRecord | CustomClaimArray | undefined;
43
+ export interface CustomClaimRecord {
44
+ [key: string]: CustomClaim;
45
+ }
46
+ export type CustomClaimArray = CustomClaim[];
42
47
  export interface UserProfile {
43
48
  sub: string;
44
49
  email: string | undefined;
45
50
  emailVerified: boolean;
46
51
  name: string | undefined;
47
52
  pictureUrl: string | undefined;
48
- [key: string]: string | boolean | undefined;
53
+ [key: string]: CustomClaim;
49
54
  }
@@ -1,4 +1,2 @@
1
- import { type ApiCatalogPluginOptions } from "./index.js";
2
- export declare const Catalog: ({ items, filterCatalogItems, label, categoryLabel, }: Omit<ApiCatalogPluginOptions, "path"> & {
3
- categoryLabel?: string;
4
- }) => import("react/jsx-runtime").JSX.Element;
1
+ import type { ApiCatalogPluginOptions } from "./index.js";
2
+ export declare const Catalog: ({ items, filterCatalogItems, label, }: Omit<ApiCatalogPluginOptions, "path">) => import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,12 @@
1
1
  import type { AuthState } from "../../authentication/state.js";
2
2
  import type { ZudokuPlugin } from "../../core/plugins.js";
3
- export declare const getKey: (category: string, tag: string) => string;
4
3
  export type ApiCatalogItem = {
5
4
  path: string;
6
5
  label: string;
7
6
  description: string;
8
7
  categories: CatalogCategory[];
8
+ version?: string;
9
+ operationCount?: number;
9
10
  };
10
11
  export type CatalogCategory = {
11
12
  label: string;
@@ -22,10 +23,4 @@ export type CatalogContext = {
22
23
  auth: AuthState;
23
24
  };
24
25
  export type FilterCatalogItemsFn = (items: ApiCatalogItem[], { auth }: CatalogContext) => ApiCatalogItem[];
25
- export declare const apiCatalogPlugin: ({ path, items, label, categories, filterCatalogItems, }: {
26
- path: string;
27
- label: string;
28
- categories?: CatalogCategory[];
29
- items: ApiCatalogItem[];
30
- filterCatalogItems?: FilterCatalogItemsFn;
31
- }) => ZudokuPlugin;
26
+ export declare const apiCatalogPlugin: ({ path, items, label, categories, filterCatalogItems, }: ApiCatalogPluginOptions) => ZudokuPlugin;
@@ -0,0 +1,5 @@
1
+ export declare const SchemaRefLink: ({ name, suffix, className, }: {
2
+ name: string;
3
+ suffix?: string;
4
+ className?: string;
5
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import type { SchemaObject } from "../../../oas/parser/index.js";
2
- export declare const SchemaView: ({ schema, defaultOpen, cardHeader, embedded, }: {
2
+ export declare const SchemaView: ({ schema, defaultOpen, cardHeader, embedded, hideRootRef, }: {
3
3
  schema?: SchemaObject | null;
4
4
  defaultOpen?: boolean;
5
5
  cardHeader?: React.ReactNode;
6
6
  embedded?: boolean;
7
+ hideRootRef?: boolean;
7
8
  }) => import("react/jsx-runtime").JSX.Element | undefined;
@@ -6,4 +6,5 @@ export declare const isCircularRef: (schema: unknown) => schema is string;
6
6
  export declare const isArrayCircularRef: (schema: SchemaObject) => schema is SchemaObject & {
7
7
  items: SchemaObject;
8
8
  };
9
+ export declare const getSchemaRefName: (schema?: SchemaObject | null) => string | undefined;
9
10
  export declare const extractCircularRefInfo: (ref?: string | SchemaObject) => string | undefined;
@@ -1,6 +1,6 @@
1
1
  import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
2
- import * as React from "react";
3
- declare const HoverCard: React.FC<HoverCardPrimitive.HoverCardProps>;
4
- declare const HoverCardTrigger: React.ForwardRefExoticComponent<HoverCardPrimitive.HoverCardTriggerProps & React.RefAttributes<HTMLAnchorElement>>;
5
- declare const HoverCardContent: React.ForwardRefExoticComponent<Omit<HoverCardPrimitive.HoverCardContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
6
- export { HoverCard, HoverCardContent, HoverCardTrigger };
2
+ import type * as React from "react";
3
+ declare function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
+ declare function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
5
+ declare function HoverCardContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
6
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
@@ -1,12 +1,8 @@
1
- import * as TogglePrimitive from "@radix-ui/react-toggle";
1
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
2
2
  import { type VariantProps } from "class-variance-authority";
3
- import * as React from "react";
4
3
  declare const toggleVariants: (props?: ({
5
4
  variant?: "default" | "outline" | null | undefined;
6
5
  size?: "default" | "sm" | "lg" | null | undefined;
7
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
- declare const Toggle: React.ForwardRefExoticComponent<Omit<TogglePrimitive.ToggleProps & React.RefAttributes<HTMLButtonElement>, "ref"> & VariantProps<(props?: ({
9
- variant?: "default" | "outline" | null | undefined;
10
- size?: "default" | "sm" | "lg" | null | undefined;
11
- } & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
7
+ declare function Toggle({ className, variant, size, ...props }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>): import("react/jsx-runtime").JSX.Element;
12
8
  export { Toggle, toggleVariants };
@@ -1,12 +1,10 @@
1
- import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
1
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
2
2
  import type { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
- declare const ToggleGroup: React.ForwardRefExoticComponent<((Omit<ToggleGroupPrimitive.ToggleGroupSingleProps & React.RefAttributes<HTMLDivElement>, "ref"> | Omit<ToggleGroupPrimitive.ToggleGroupMultipleProps & React.RefAttributes<HTMLDivElement>, "ref">) & VariantProps<(props?: ({
5
- variant?: "default" | "outline" | null | undefined;
6
- size?: "default" | "sm" | "lg" | null | undefined;
7
- } & import("class-variance-authority/types").ClassProp) | undefined) => string>) & React.RefAttributes<HTMLDivElement>>;
8
- declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<ToggleGroupPrimitive.ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>, "ref"> & VariantProps<(props?: ({
9
- variant?: "default" | "outline" | null | undefined;
10
- size?: "default" | "sm" | "lg" | null | undefined;
11
- } & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
4
+ import { Toggle, toggleVariants } from "./Toggle.js";
5
+ declare function ToggleGroup({ className, variant, size, spacing, orientation, children, ...props }: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants> & {
6
+ spacing?: number;
7
+ orientation?: "horizontal" | "vertical";
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ declare function ToggleGroupItem({ className, children, variant, size, ...props }: React.ComponentProps<typeof Toggle>): import("react/jsx-runtime").JSX.Element;
12
10
  export { ToggleGroup, ToggleGroupItem };
@@ -30,6 +30,18 @@ const config: ZudokuConfig = {
30
30
  };
31
31
  ```
32
32
 
33
+ When `basePath` is set, Zudoku writes the site into `dist/<basePath>/`. Upload that nested directory
34
+ as your Pages artifact, not `dist/` itself. For example in a GitHub Actions workflow:
35
+
36
+ ```yaml
37
+ - uses: actions/upload-pages-artifact@v3
38
+ with:
39
+ path: dist/your-repo
40
+ ```
41
+
42
+ Uploading the outer `dist/` would nest the site under `/your-repo/your-repo/` and every asset
43
+ would 404.
44
+
33
45
  ## Accurate Last Modified Dates
34
46
 
35
47
  If you have enabled the [`showLastModified`](/docs/configuration/docs#showlastmodified) option,
@@ -0,0 +1,186 @@
1
+ ---
2
+ title: Redirects
3
+ sidebar_icon: arrow-right-left
4
+ description:
5
+ Configure URL redirects in your Zudoku developer portal to set landing pages, maintain backward
6
+ compatibility, and handle URL changes. Covers redirect behavior, basePath interaction, and
7
+ troubleshooting.
8
+ ---
9
+
10
+ Redirects let you automatically send visitors from one URL to another in your developer portal. They
11
+ are useful when you need to:
12
+
13
+ - Set a custom landing page for the root path (`/`)
14
+ - Maintain backward compatibility after restructuring documentation
15
+ - Point old API endpoint paths to their new locations
16
+ - Redirect from deprecated pages to their replacements
17
+
18
+ ## Basic configuration
19
+
20
+ Add a `redirects` array to your `zudoku.config.ts` file. Each entry has a `from` path and a `to`
21
+ path:
22
+
23
+ ```ts title="zudoku.config.ts"
24
+ import type { ZudokuConfig } from "zudoku";
25
+
26
+ const config: ZudokuConfig = {
27
+ // ... other config
28
+ redirects: [
29
+ { from: "/", to: "/introduction" },
30
+ { from: "/getting-started", to: "/quickstart" },
31
+ ],
32
+ };
33
+
34
+ export default config;
35
+ ```
36
+
37
+ When a visitor navigates to the `from` path, they are automatically redirected to the `to` path.
38
+
39
+ ## Redirect properties
40
+
41
+ Each redirect object accepts two properties:
42
+
43
+ - **`from`** — The path you want to redirect away from. An absolute path starting with `/` is
44
+ recommended. If you omit the leading slash, it is normalized automatically.
45
+ - **`to`** — The destination path where visitors will be sent. This can be any valid path in your
46
+ portal.
47
+
48
+ ```ts
49
+ redirects: [{ from: "/old-page", to: "/new-page" }];
50
+ ```
51
+
52
+ Trailing slashes in the `from` path are normalized automatically. Both `/old-page` and `/old-page/`
53
+ will match the same redirect rule.
54
+
55
+ ## How redirects work
56
+
57
+ Zudoku redirects operate at two levels depending on how the visitor reaches the page:
58
+
59
+ ### Server-side behavior
60
+
61
+ When a visitor loads a redirect path directly (for example, by typing the URL in the browser address
62
+ bar or following an external link), the exact response depends on your deployment target:
63
+
64
+ - **Zuplo** and **Vercel** (or any platform that reads the Vercel Build Output API): the platform
65
+ returns an HTTP **301 (Moved Permanently)** with a `Location` header, so browsers and search
66
+ engines see a true permanent redirect.
67
+ - **SSR deployments**: the server returns a real HTTP 301 from the router loader.
68
+ - **Other static hosts** (Netlify, Cloudflare Pages, GitHub Pages, S3, nginx, etc.): Zudoku
69
+ prerenders each redirect source path as a small HTML file containing a JavaScript redirect. The
70
+ file is served with a 200 response and the browser follows the redirect once the script runs.
71
+ JavaScript must be enabled for the redirect to fire.
72
+
73
+ ### Client-side behavior
74
+
75
+ When a visitor clicks an internal link that points to a redirect path, Zudoku handles the redirect
76
+ entirely in the browser using client-side routing. The visitor is navigated to the destination
77
+ without a full page reload, providing a seamless experience.
78
+
79
+ Both behaviors land the visitor on the destination page. For search engines, only the 301 variants
80
+ signal a permanent move; the JavaScript redirect used on generic static hosts is weaker for SEO.
81
+
82
+ ## Common patterns
83
+
84
+ ### Setting a landing page
85
+
86
+ The most common use of redirects is setting a landing page for the root path of your portal:
87
+
88
+ ```ts
89
+ redirects: [{ from: "/", to: "/docs/introduction" }];
90
+ ```
91
+
92
+ This sends visitors who arrive at your portal's root URL to your introduction page.
93
+
94
+ ### Reorganizing documentation
95
+
96
+ When you restructure your documentation, add redirects from the old paths so existing bookmarks and
97
+ external links continue to work:
98
+
99
+ ```ts
100
+ redirects: [
101
+ { from: "/api/authentication", to: "/guides/auth-overview" },
102
+ { from: "/api/getting-started", to: "/docs/quickstart" },
103
+ { from: "/reference", to: "/api" },
104
+ ];
105
+ ```
106
+
107
+ ### Redirecting to specific sections
108
+
109
+ You can redirect to a specific section of a page using a hash fragment in the `to` path:
110
+
111
+ ```ts
112
+ redirects: [
113
+ {
114
+ from: "/api-shipments/create-shipment",
115
+ to: "/api-shipments/shipment-management#post-shipments",
116
+ },
117
+ {
118
+ from: "/api-shipments/get-rates",
119
+ to: "/api-shipments/rates-and-billing#post-shipments-shipmentid-rates",
120
+ },
121
+ ];
122
+ ```
123
+
124
+ This is useful when multiple old pages have been consolidated into a single page with distinct
125
+ sections.
126
+
127
+ ### Redirecting category paths
128
+
129
+ If your API reference groups endpoints into categories, you may want the category path to redirect
130
+ to a specific endpoint or overview page:
131
+
132
+ ```ts
133
+ redirects: [
134
+ { from: "/api/billing", to: "/api/billing/get-invoices" },
135
+ { from: "/api/users", to: "/api/users/list-users" },
136
+ ];
137
+ ```
138
+
139
+ ## Redirects and the `basePath` option
140
+
141
+ If your portal uses a [`basePath`](/docs/configuration/overview#basepath), redirects are defined
142
+ _relative to the base path_. Zudoku automatically prepends the base path to both the matched `from`
143
+ and the emitted `Location`.
144
+
145
+ For example, with `basePath: "/docs"`:
146
+
147
+ ```ts
148
+ redirects: [{ from: "/", to: "/getting-started" }];
149
+ // Visitors to /docs/ are redirected to /docs/getting-started
150
+ ```
151
+
152
+ You do not need to include the base path in your `from` or `to` values.
153
+
154
+ ## Redirects vs. navigation rules
155
+
156
+ Zudoku offers two features that can change where visitors end up: [redirects](#basic-configuration)
157
+ and [navigation rules](/docs/guides/navigation-rules). They serve different purposes:
158
+
159
+ - **Redirects** map one URL to another. Use them when a page has moved or when you need a specific
160
+ URL to point somewhere else. They affect both direct visits and internal link clicks.
161
+ - **Navigation rules** customize the _sidebar_ generated by plugins like the OpenAPI plugin. Use
162
+ them to insert, reorder, modify, or remove items in the sidebar without changing the underlying
163
+ page URLs.
164
+
165
+ If you want to change where a URL takes visitors, use a redirect. If you want to change what appears
166
+ in the sidebar navigation, use a navigation rule.
167
+
168
+ ## Redirects and sitemaps
169
+
170
+ Redirect source paths are automatically excluded from your
171
+ [sitemap](/docs/configuration/overview#sitemap). Only the destination pages appear in the generated
172
+ `sitemap.xml`, which prevents search engines from indexing the old URLs.
173
+
174
+ ## Troubleshooting
175
+
176
+ ### Redirect returns a 404
177
+
178
+ Make sure the `to` path points to a page that actually exists in your portal. If the destination is
179
+ an API reference page generated from an OpenAPI spec, verify that the path matches the generated
180
+ route. You can check available routes by running your dev server and navigating manually.
181
+
182
+ ### Redirect conflicts with another route
183
+
184
+ Redirects are registered before page routes, so when a redirect `from` path exactly matches another
185
+ route, the redirect wins. If you want a page to be reachable at a given path, remove the conflicting
186
+ redirect rather than relying on route priority.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.75.0",
3
+ "version": "0.75.1",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -145,6 +145,7 @@
145
145
  },
146
146
  "dependencies": {
147
147
  "@apidevtools/json-schema-ref-parser": "15.3.1",
148
+ "@base-ui/react": "^1.4.0",
148
149
  "@envelop/core": "5.5.1",
149
150
  "@graphql-typed-document-node/core": "3.2.0",
150
151
  "@hono/node-server": "1.19.13",
@@ -212,7 +213,7 @@
212
213
  "hast-util-heading-rank": "3.0.0",
213
214
  "hast-util-to-jsx-runtime": "2.3.6",
214
215
  "hast-util-to-string": "3.0.1",
215
- "hono": "4.12.12",
216
+ "hono": "4.12.14",
216
217
  "http-terminator": "3.2.0",
217
218
  "javascript-stringify": "2.1.0",
218
219
  "json-schema-to-typescript-lite": "15.0.0",
@@ -227,7 +228,7 @@
227
228
  "next-themes": "0.4.6",
228
229
  "oauth4webapi": "3.8.5",
229
230
  "openapi-types": "12.1.3",
230
- "pagefind": "1.5.0-beta.1",
231
+ "pagefind": "1.5.2",
231
232
  "picocolors": "1.1.1",
232
233
  "piscina": "5.1.4",
233
234
  "posthog-node": "5.26.0",
@@ -264,9 +265,9 @@
264
265
  "zustand": "5.0.12"
265
266
  },
266
267
  "devDependencies": {
267
- "@clerk/clerk-js": "^6.0.0",
268
- "@graphql-codegen/cli": "6.1.2",
269
- "@inkeep/cxkit-types": "0.5.116",
268
+ "@clerk/clerk-js": "^6.7.4",
269
+ "@graphql-codegen/cli": "6.3.0",
270
+ "@inkeep/cxkit-types": "0.5.117",
270
271
  "@testing-library/dom": "10.4.1",
271
272
  "@testing-library/jest-dom": "6.9.1",
272
273
  "@testing-library/react": "16.3.2",
@@ -284,7 +285,7 @@
284
285
  "@types/yargs": "17.0.35",
285
286
  "@vitest/coverage-v8": "4.0.18",
286
287
  "happy-dom": "20.8.9",
287
- "oxc-parser": "^0.119.0",
288
+ "oxc-parser": "^0.126.0",
288
289
  "react": "19.2.4",
289
290
  "react-dom": "19.2.4",
290
291
  "tsx": "4.21.0",
@@ -183,6 +183,22 @@ export class OpenIDAuthenticationProvider
183
183
  });
184
184
  }
185
185
 
186
+ private buildUserProfile(
187
+ userInfo: oauth.UserInfoResponse,
188
+ fallbackEmailVerified: oauth.JsonValue | undefined,
189
+ ): UserProfile {
190
+ const emailVerified =
191
+ userInfo.email_verified ?? fallbackEmailVerified ?? false;
192
+ return {
193
+ ...userInfo,
194
+ sub: userInfo.sub,
195
+ email: userInfo.email,
196
+ name: userInfo.name,
197
+ emailVerified: Boolean(emailVerified),
198
+ pictureUrl: userInfo.picture,
199
+ };
200
+ }
201
+
186
202
  public async refreshUserProfile(): Promise<boolean> {
187
203
  const accessToken = await this.getAccessToken();
188
204
  const authServer = await this.getAuthServer();
@@ -200,13 +216,7 @@ export class OpenIDAuthenticationProvider
200
216
  ? providerData.claims?.email_verified
201
217
  : undefined;
202
218
 
203
- const profile: UserProfile = {
204
- sub: userInfo.sub,
205
- email: userInfo.email,
206
- name: userInfo.name,
207
- emailVerified: userInfo.email_verified ?? emailVerified ?? false,
208
- pictureUrl: userInfo.picture,
209
- };
219
+ const profile = this.buildUserProfile(userInfo, emailVerified);
210
220
 
211
221
  useAuthState.setState({
212
222
  isAuthenticated: true,
@@ -494,14 +504,7 @@ export class OpenIDAuthenticationProvider
494
504
  );
495
505
  const userInfo = await userInfoResponse.json();
496
506
 
497
- const profile: UserProfile = {
498
- ...userInfo,
499
- sub: userInfo.sub,
500
- email: userInfo.email,
501
- name: userInfo.name,
502
- emailVerified: userInfo.email_verified ?? claims?.email_verified ?? false,
503
- pictureUrl: userInfo.picture,
504
- };
507
+ const profile = this.buildUserProfile(userInfo, claims?.email_verified);
505
508
 
506
509
  useAuthState.setState({
507
510
  isAuthenticated: true,
@@ -81,11 +81,24 @@ syncZustandState(authState);
81
81
 
82
82
  export const useAuthState = authState;
83
83
 
84
+ export type CustomClaim =
85
+ | string
86
+ | number
87
+ | boolean
88
+ | null
89
+ | CustomClaimRecord
90
+ | CustomClaimArray
91
+ | undefined;
92
+ export interface CustomClaimRecord {
93
+ [key: string]: CustomClaim;
94
+ }
95
+ export type CustomClaimArray = CustomClaim[];
96
+
84
97
  export interface UserProfile {
85
98
  sub: string;
86
99
  email: string | undefined;
87
100
  emailVerified: boolean;
88
101
  name: string | undefined;
89
102
  pictureUrl: string | undefined;
90
- [key: string]: string | boolean | undefined;
103
+ [key: string]: CustomClaim;
91
104
  }
@@ -25,7 +25,16 @@ export const Meta = ({ children }: PropsWithChildren) => {
25
25
  {meta?.description && (
26
26
  <meta name="description" content={meta.description} />
27
27
  )}
28
- {meta?.favicon && <link rel="icon" href={meta.favicon} />}
28
+ {meta?.favicon && (
29
+ <link
30
+ rel="icon"
31
+ href={
32
+ /^https?:\/\//.test(meta.favicon)
33
+ ? meta.favicon
34
+ : joinUrl(options.basePath, meta.favicon)
35
+ }
36
+ />
37
+ )}
29
38
  {meta?.generator && <meta name="generator" content={meta.generator} />}
30
39
  {meta?.applicationName && (
31
40
  <meta name="application-name" content={meta.applicationName} />
@@ -35,7 +35,17 @@ export const useHotkey = (hotkey: string, callback: () => void) => {
35
35
  }, [callback]);
36
36
 
37
37
  useEffect(() => {
38
+ const hasModifier = meta || shift || alt || ctrl;
38
39
  const handler = (event: KeyboardEvent) => {
40
+ if (
41
+ !hasModifier &&
42
+ event.target instanceof HTMLElement &&
43
+ (event.target.tagName === "INPUT" ||
44
+ event.target.tagName === "TEXTAREA" ||
45
+ event.target.isContentEditable)
46
+ ) {
47
+ return;
48
+ }
39
49
  if (
40
50
  (event.code === `Key${key?.toUpperCase()}` ||
41
51
  event.code.toLowerCase() === key?.toLowerCase()) &&
@@ -53,6 +53,17 @@ export const handleCircularRefs = (
53
53
  Object.entries(obj).map(([k, v]) => [k, recurse(v, k)]),
54
54
  );
55
55
 
56
+ // __$ref is defined non-enumerable at build-time codegen (schema-codegen.ts)
57
+ // to keep it out of snapshots and deep equality. Copy it enumerably onto
58
+ // the serialized result so the client receives it.
59
+ if (
60
+ !Array.isArray(result) &&
61
+ typeof refPath === "string" &&
62
+ !("__$ref" in result)
63
+ ) {
64
+ (result as Record<string, unknown>).__$ref = refPath;
65
+ }
66
+
56
67
  refs.set(obj, result);
57
68
  currentPath.delete(obj);
58
69
  if (typeof refPath === "string") currentRefPaths.delete(refPath);