@lukoweb/apitogo 0.1.54 → 0.1.56

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 (45) hide show
  1. package/dist/cli/cli.js +831 -398
  2. package/dist/declarations/config/default-landing-content.d.ts +2 -0
  3. package/dist/declarations/config/landing-manifest.d.ts +82 -0
  4. package/dist/declarations/config/local-manifest.d.ts +82 -0
  5. package/dist/declarations/config/resolve-modules.d.ts +3 -1
  6. package/dist/declarations/config/validators/ModulesSchema.d.ts +259 -0
  7. package/dist/declarations/config/validators/ZudokuConfig.d.ts +57 -0
  8. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +1 -0
  11. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  12. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  13. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  14. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  15. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  16. package/dist/flat-config.d.ts +95 -38
  17. package/package.json +1 -1
  18. package/src/app/main.css +2 -2
  19. package/src/config/apitogo-config-core.ts +13 -0
  20. package/src/config/apitogo-config-loader.browser.ts +17 -0
  21. package/src/config/build-apitogo-config.ts +13 -4
  22. package/src/config/default-landing-content.ts +154 -0
  23. package/src/config/landing-manifest.ts +11 -0
  24. package/src/config/landing-openapi-showcase.ts +115 -0
  25. package/src/config/loader.ts +17 -3
  26. package/src/config/local-manifest.ts +114 -10
  27. package/src/config/resolve-modules.ts +121 -32
  28. package/src/config/validators/ModulesSchema.ts +109 -0
  29. package/src/config/validators/ZudokuConfig.ts +1 -0
  30. package/src/lib/authentication/auth-header-nav.ts +5 -15
  31. package/src/lib/authentication/header-nav-filter.ts +36 -0
  32. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
  33. package/src/lib/authentication/providers/dev-portal-utils.ts +26 -7
  34. package/src/lib/authentication/providers/dev-portal.tsx +18 -8
  35. package/src/lib/components/Header.tsx +49 -39
  36. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  37. package/src/lib/components/HeaderNavigation.tsx +11 -9
  38. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  39. package/src/lib/components/SiteTitle.tsx +20 -0
  40. package/src/lib/components/ThemeSwitch.tsx +36 -2
  41. package/src/lib/components/TopNavigation.tsx +20 -33
  42. package/src/lib/core/ZudokuContext.ts +1 -0
  43. package/src/vite/config.ts +21 -0
  44. package/src/vite/plugin-auth.ts +4 -1
  45. package/src/vite/plugin-config.ts +39 -4
@@ -18,6 +18,7 @@ import {
18
18
  loadApitogoConfig,
19
19
  readPlanCatalogItems,
20
20
  } from "./apitogo-config-loader.js";
21
+ import { buildApitogoConfig } from "./build-apitogo-config.js";
21
22
  import { fileExists } from "./file-exists.js";
22
23
  import {
23
24
  isAuthenticationEnabled,
@@ -117,7 +118,18 @@ async function loadZudokuConfigWithMeta(
117
118
  },
118
119
  });
119
120
 
120
- const config = module.default;
121
+ const importedConfig = module.default;
122
+
123
+ let config: ZudokuConfig;
124
+ try {
125
+ const fileConfig = loadApitogoConfig(rootDir);
126
+ config = buildApitogoConfig({
127
+ ...fileConfig,
128
+ plugins: importedConfig.plugins,
129
+ });
130
+ } catch {
131
+ config = importedConfig;
132
+ }
121
133
 
122
134
  validateConfig(config, configPath);
123
135
 
@@ -130,7 +142,7 @@ async function loadZudokuConfigWithMeta(
130
142
  dependencies,
131
143
  configPath,
132
144
  },
133
- __resolvedModules: resolveModulesConfig(config),
145
+ __resolvedModules: resolveModulesConfig(config, { rootDir }),
134
146
  };
135
147
 
136
148
  return configWithMetadata;
@@ -288,7 +300,9 @@ export async function loadZudokuConfig(
288
300
  ...transformedConfig.__meta,
289
301
  ...configWithManifestMeta.__meta,
290
302
  },
291
- __resolvedModules: resolveModulesConfig(transformedConfig),
303
+ __resolvedModules: resolveModulesConfig(transformedConfig, {
304
+ rootDir: transformedConfig.__meta.rootDir,
305
+ }),
292
306
  };
293
307
 
294
308
  if (!process.env.APITOGO_JSON_ONLY) {
@@ -1,4 +1,4 @@
1
- import { access, readFile } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import {
@@ -8,6 +8,10 @@ import {
8
8
  loadApitogoConfig,
9
9
  readPlanCatalogItems,
10
10
  } from "./apitogo-config-loader.js";
11
+ import {
12
+ type LandingManifestInput,
13
+ LandingManifestContentSchema,
14
+ } from "./landing-manifest.js";
11
15
 
12
16
  /** @deprecated Legacy manifest filenames — apitogo.config.json is the only supported source. */
13
17
  export const APITOGO_MANIFEST_BASE_FILENAME = "apitogo.json";
@@ -102,12 +106,19 @@ export const DevPortalLocalManifestSchema = z.object({
102
106
  })
103
107
  .optional(),
104
108
  projectId: z.string().optional(),
109
+ landing: z
110
+ .object({
111
+ enabled: z.boolean().optional(),
112
+ content: LandingManifestContentSchema,
113
+ })
114
+ .optional(),
105
115
  });
106
116
 
107
117
  export type ManifestPlanInput = z.infer<typeof ManifestPlanInputSchema>;
108
118
  export type DevPortalLocalManifest = z.infer<
109
119
  typeof DevPortalLocalManifestSchema
110
120
  >;
121
+ export type { LandingManifestInput };
111
122
 
112
123
  export type DevPortalPreviewPlan = {
113
124
  sku: string;
@@ -213,6 +224,105 @@ export function mergePlansBySku(
213
224
  return Array.from(bySku.values());
214
225
  }
215
226
 
227
+ export function mergeLandingContent(
228
+ existing: LandingManifestInput["content"] | undefined,
229
+ patch: LandingManifestInput["content"] | undefined,
230
+ ): LandingManifestInput["content"] | undefined {
231
+ if (!patch) {
232
+ return existing;
233
+ }
234
+ if (!existing) {
235
+ return patch;
236
+ }
237
+
238
+ const merged = { ...existing, ...patch };
239
+
240
+ if (patch.hero) {
241
+ merged.hero = { ...existing.hero, ...patch.hero };
242
+ if (patch.hero.codeExample) {
243
+ merged.hero = {
244
+ ...merged.hero,
245
+ codeExample: {
246
+ ...existing.hero?.codeExample,
247
+ ...patch.hero.codeExample,
248
+ },
249
+ };
250
+ }
251
+ }
252
+
253
+ if (patch.features) {
254
+ const byTitle = new Map(
255
+ (existing.features ?? []).map((feature) => [feature.title, feature]),
256
+ );
257
+ for (const feature of patch.features) {
258
+ const prior = byTitle.get(feature.title);
259
+ byTitle.set(feature.title, prior ? { ...prior, ...feature } : feature);
260
+ }
261
+ merged.features = Array.from(byTitle.values());
262
+ }
263
+
264
+ if (patch.ticker) {
265
+ merged.ticker = { ...existing.ticker, ...patch.ticker };
266
+ if (patch.ticker.items) {
267
+ merged.ticker.items = patch.ticker.items;
268
+ }
269
+ }
270
+
271
+ if (patch.trust) {
272
+ merged.trust = { ...existing.trust, ...patch.trust };
273
+ if (patch.trust.logos) {
274
+ merged.trust.logos = patch.trust.logos;
275
+ }
276
+ }
277
+
278
+ if (patch.apiShowcase) {
279
+ merged.apiShowcase = { ...existing.apiShowcase, ...patch.apiShowcase };
280
+ if (patch.apiShowcase.items) {
281
+ const byPath = new Map(
282
+ (existing.apiShowcase?.items ?? []).map((item) => [item.path, item]),
283
+ );
284
+ for (const item of patch.apiShowcase.items) {
285
+ const prior = byPath.get(item.path);
286
+ byPath.set(item.path, prior ? { ...prior, ...item } : item);
287
+ }
288
+ merged.apiShowcase.items = Array.from(byPath.values());
289
+ }
290
+ }
291
+
292
+ if (patch.pricingCta) {
293
+ merged.pricingCta = { ...existing.pricingCta, ...patch.pricingCta };
294
+ }
295
+
296
+ if (patch.primaryAction) {
297
+ merged.primaryAction = { ...existing.primaryAction, ...patch.primaryAction };
298
+ }
299
+ if (patch.secondaryAction) {
300
+ merged.secondaryAction = {
301
+ ...existing.secondaryAction,
302
+ ...patch.secondaryAction,
303
+ };
304
+ }
305
+
306
+ return merged;
307
+ }
308
+
309
+ export function mergeLandingManifest(
310
+ existing: LandingManifestInput | undefined,
311
+ patch: LandingManifestInput | undefined,
312
+ ): LandingManifestInput | undefined {
313
+ if (!patch) {
314
+ return existing;
315
+ }
316
+ if (!existing) {
317
+ return patch;
318
+ }
319
+
320
+ return {
321
+ enabled: patch.enabled ?? existing.enabled,
322
+ content: mergeLandingContent(existing.content, patch.content),
323
+ };
324
+ }
325
+
216
326
  export function mergeManifest(
217
327
  base: DevPortalLocalManifest,
218
328
  override: DevPortalLocalManifest,
@@ -240,6 +350,9 @@ export function mergeManifest(
240
350
  if (override.plans) {
241
351
  merged.plans = mergePlansBySku(base.plans, override.plans);
242
352
  }
353
+ if (override.landing) {
354
+ merged.landing = mergeLandingManifest(base.landing, override.landing);
355
+ }
243
356
 
244
357
  return merged;
245
358
  }
@@ -277,15 +390,6 @@ export function parseLocalManifestJson(
277
390
  }
278
391
  }
279
392
 
280
- async function fileExists(filePath: string): Promise<boolean> {
281
- try {
282
- await access(filePath);
283
- return true;
284
- } catch {
285
- return false;
286
- }
287
- }
288
-
289
393
  export async function readManifestFile(
290
394
  rootDir: string,
291
395
  filename: string,
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LANDING_CONTENT } from "./default-landing-content.js";
1
2
  import {
2
3
  type ResolvedModulesConfig,
3
4
  ModulesSchema,
@@ -32,50 +33,136 @@ export const joinPanelPath = (basePath: string, routePath: string): string => {
32
33
  return `/${`${base}/${route}`.replace(/^\/+/, "")}`;
33
34
  };
34
35
 
35
- const DEFAULT_LANDING_FEATURES: ResolvedLandingContent["features"] = [
36
- {
37
- title: "Documentation",
38
- description:
39
- "Write guides and references in Markdown or MDX with full navigation and search.",
40
- },
41
- {
42
- title: "API Reference",
43
- description:
44
- "Publish interactive OpenAPI docs with a built-in playground and code samples.",
45
- },
46
- {
47
- title: "Developer Portal",
48
- description:
49
- "Extend with landing pages, authentication, API keys, and subscription modules.",
50
- },
51
- ];
36
+ const mergeLandingSection = <T extends { enabled?: boolean }>(
37
+ defaults: T | undefined,
38
+ override: T | undefined,
39
+ ): T | undefined => {
40
+ if (override?.enabled === false) {
41
+ return undefined;
42
+ }
52
43
 
53
- export const resolveLandingContent = (
44
+ if (!defaults && !override) {
45
+ return undefined;
46
+ }
47
+
48
+ return { ...defaults, ...override } as T;
49
+ };
50
+
51
+ const buildDefaultLandingContent = (
54
52
  config: ZudokuConfig,
55
- content?: LandingContentConfig,
56
53
  ): ResolvedLandingContent => {
57
54
  const siteTitle =
58
55
  config.metadata?.applicationName ?? config.site?.title ?? "APIToGo";
59
56
 
60
57
  return {
58
+ ...DEFAULT_LANDING_CONTENT,
61
59
  hero: {
62
- title: content?.hero?.title ?? siteTitle,
63
- subtitle: content?.hero?.subtitle ?? "Developer portal",
64
- description:
65
- content?.hero?.description ??
66
- config.metadata?.description ??
67
- "Ship beautiful API documentation and developer experiences with APIToGo.",
60
+ ...DEFAULT_LANDING_CONTENT.hero,
61
+ title: `${siteTitle} APIs that ship`,
68
62
  },
69
- features: content?.features ?? DEFAULT_LANDING_FEATURES,
70
- primaryAction: content?.primaryAction ?? {
71
- label: "Get started",
63
+ primaryAction: {
64
+ ...DEFAULT_LANDING_CONTENT.primaryAction,
72
65
  href: "/introduction",
66
+ label: "Get started",
73
67
  },
74
- secondaryAction: content?.secondaryAction ?? {
75
- label: "API Reference",
68
+ secondaryAction: {
69
+ ...DEFAULT_LANDING_CONTENT.secondaryAction,
76
70
  href: "/api",
71
+ label: "API Reference",
72
+ },
73
+ pricingCta: DEFAULT_LANDING_CONTENT.pricingCta
74
+ ? {
75
+ ...DEFAULT_LANDING_CONTENT.pricingCta,
76
+ primaryAction: {
77
+ label: "Get started",
78
+ href: "/introduction",
79
+ },
80
+ secondaryAction: {
81
+ label: "See pricing",
82
+ href: "/pricing",
83
+ },
84
+ }
85
+ : undefined,
86
+ };
87
+ };
88
+
89
+ export const resolveLandingContent = (
90
+ config: ZudokuConfig,
91
+ content?: LandingContentConfig,
92
+ options?: { useDefaults?: boolean },
93
+ ): ResolvedLandingContent => {
94
+ const siteTitle =
95
+ config.metadata?.applicationName ?? config.site?.title ?? "APIToGo";
96
+
97
+ if (options?.useDefaults === true) {
98
+ const defaults = buildDefaultLandingContent(config);
99
+
100
+ return {
101
+ hero: {
102
+ ...defaults.hero,
103
+ ...content?.hero,
104
+ title: content?.hero?.title ?? defaults.hero?.title,
105
+ description:
106
+ content?.hero?.description ??
107
+ config.metadata?.description ??
108
+ defaults.hero?.description,
109
+ codeExample: content?.hero?.codeExample ?? defaults.hero?.codeExample,
110
+ titleAccent:
111
+ content?.hero?.titleAccent ??
112
+ (content?.hero?.title ? undefined : defaults.hero?.titleAccent),
113
+ },
114
+ featuresSection: mergeLandingSection(
115
+ defaults.featuresSection,
116
+ content?.featuresSection,
117
+ ),
118
+ features: content?.features ?? defaults.features,
119
+ primaryAction: content?.primaryAction ?? defaults.primaryAction,
120
+ secondaryAction: content?.secondaryAction ?? defaults.secondaryAction,
121
+ showPoweredBy: content?.showPoweredBy ?? defaults.showPoweredBy,
122
+ ticker: mergeLandingSection(defaults.ticker, content?.ticker),
123
+ trust: mergeLandingSection(defaults.trust, content?.trust),
124
+ apiShowcase: mergeLandingSection(
125
+ defaults.apiShowcase,
126
+ content?.apiShowcase,
127
+ ),
128
+ pricingCta: mergeLandingSection(defaults.pricingCta, content?.pricingCta),
129
+ };
130
+ }
131
+
132
+ const hasContent =
133
+ content !== undefined && Object.keys(content).length > 0;
134
+
135
+ if (!hasContent) {
136
+ return {
137
+ hero: { title: siteTitle },
138
+ showPoweredBy: true,
139
+ };
140
+ }
141
+
142
+ return {
143
+ hero: {
144
+ ...content.hero,
145
+ title: content.hero?.title ?? siteTitle,
77
146
  },
78
- showPoweredBy: content?.showPoweredBy,
147
+ featuresSection: content.featuresSection
148
+ ? mergeLandingSection(undefined, content.featuresSection)
149
+ : undefined,
150
+ features: content.features,
151
+ primaryAction: content.primaryAction,
152
+ secondaryAction: content.secondaryAction,
153
+ showPoweredBy: content.showPoweredBy ?? true,
154
+ ticker: content.ticker
155
+ ? mergeLandingSection(undefined, content.ticker)
156
+ : undefined,
157
+ trust: content.trust
158
+ ? mergeLandingSection(undefined, content.trust)
159
+ : undefined,
160
+ apiShowcase: content.apiShowcase
161
+ ? mergeLandingSection(undefined, content.apiShowcase)
162
+ : undefined,
163
+ pricingCta: content.pricingCta
164
+ ? mergeLandingSection(undefined, content.pricingCta)
165
+ : undefined,
79
166
  };
80
167
  };
81
168
 
@@ -172,7 +259,9 @@ export const resolveModulesConfig = (
172
259
  path: landingModule?.path ?? "/",
173
260
  component: landingModule?.component,
174
261
  layout: landingModule?.layout ?? "landing",
175
- content: resolveLandingContent(config, landingModule?.content),
262
+ content: resolveLandingContent(config, landingModule?.content, {
263
+ useDefaults: landingModule?.useDefaults === true,
264
+ }),
176
265
  },
177
266
  userPanel: {
178
267
  enabled: userPanelEnabled,
@@ -44,22 +44,102 @@ export const LandingLinkSchema = z.object({
44
44
  export const LandingFeatureSchema = z.object({
45
45
  title: z.string(),
46
46
  description: z.string(),
47
+ icon: z.string().optional(),
47
48
  });
48
49
 
50
+ export const LandingCodeExampleSchema = z
51
+ .object({
52
+ filename: z.string().optional(),
53
+ language: z.string().optional(),
54
+ code: z.string(),
55
+ })
56
+ .optional();
57
+
49
58
  export const LandingHeroSchema = z
50
59
  .object({
51
60
  title: z.string().optional(),
61
+ titleAccent: z
62
+ .string()
63
+ .optional()
64
+ .describe("Accent-colored word or phrase shown on a new line after the title."),
52
65
  subtitle: z.string().optional(),
53
66
  description: z.string().optional(),
67
+ badge: z.string().optional(),
68
+ highlights: z.array(z.string()).optional(),
69
+ codeExample: LandingCodeExampleSchema,
70
+ })
71
+ .optional();
72
+
73
+ export const LandingFeaturesSectionSchema = z
74
+ .object({
75
+ label: z.string().optional(),
76
+ title: z.string().optional(),
77
+ description: z.string().optional(),
78
+ })
79
+ .optional();
80
+
81
+ export const LandingTickerItemSchema = z.object({
82
+ label: z.string(),
83
+ value: z.string(),
84
+ change: z.string().optional(),
85
+ });
86
+
87
+ export const LandingTickerSchema = z
88
+ .object({
89
+ enabled: z.boolean().optional(),
90
+ items: z.array(LandingTickerItemSchema).optional(),
91
+ })
92
+ .optional();
93
+
94
+ export const LandingTrustSchema = z
95
+ .object({
96
+ enabled: z.boolean().optional(),
97
+ title: z.string().optional(),
98
+ logos: z.array(z.string()).optional(),
99
+ })
100
+ .optional();
101
+
102
+ export const LandingApiShowcaseItemSchema = z.object({
103
+ name: z.string(),
104
+ path: z.string(),
105
+ icon: z.string().optional(),
106
+ latency: z.string().optional(),
107
+ summary: z.string().optional(),
108
+ });
109
+
110
+ export const LandingApiShowcaseSchema = z
111
+ .object({
112
+ enabled: z.boolean().optional(),
113
+ title: z.string().optional(),
114
+ subtitle: z.string().optional(),
115
+ browseAllHref: z.string().optional(),
116
+ browseAllLabel: z.string().optional(),
117
+ autoFromOpenApi: z.boolean().optional(),
118
+ items: z.array(LandingApiShowcaseItemSchema).optional(),
119
+ })
120
+ .optional();
121
+
122
+ export const LandingPricingCtaSchema = z
123
+ .object({
124
+ enabled: z.boolean().optional(),
125
+ title: z.string().optional(),
126
+ description: z.string().optional(),
127
+ primaryAction: LandingLinkSchema.optional(),
128
+ secondaryAction: LandingLinkSchema.optional(),
54
129
  })
55
130
  .optional();
56
131
 
57
132
  export const LandingContentSchema = z
58
133
  .object({
59
134
  hero: LandingHeroSchema,
135
+ featuresSection: LandingFeaturesSectionSchema,
60
136
  features: z.array(LandingFeatureSchema).optional(),
61
137
  primaryAction: LandingLinkSchema.optional(),
62
138
  secondaryAction: LandingLinkSchema.optional(),
139
+ ticker: LandingTickerSchema,
140
+ trust: LandingTrustSchema,
141
+ apiShowcase: LandingApiShowcaseSchema,
142
+ pricingCta: LandingPricingCtaSchema,
63
143
  showPoweredBy: z.boolean().optional(),
64
144
  })
65
145
  .optional();
@@ -128,6 +208,12 @@ export const LandingModuleSchema = ModuleToggleSchema.extend({
128
208
  content: LandingContentSchema.describe(
129
209
  "Default landing page content when no custom component is provided.",
130
210
  ),
211
+ useDefaults: z
212
+ .boolean()
213
+ .optional()
214
+ .describe(
215
+ "When true, merge DEFAULT_LANDING_CONTENT for unset sections. When false or omitted, only configured sections are shown.",
216
+ ),
131
217
  });
132
218
 
133
219
  export const UserManagementModuleSchema = ModuleToggleSchema.extend({
@@ -246,15 +332,38 @@ export type ProfileContentConfig = z.infer<typeof ProfileContentSchema>;
246
332
  export type PlansContentConfig = z.infer<typeof PlansContentSchema>;
247
333
  export type PlanTierConfig = z.infer<typeof PlanTierSchema>;
248
334
 
335
+ export type LandingCodeExampleConfig = z.infer<typeof LandingCodeExampleSchema>;
336
+ export type LandingTickerItemConfig = z.infer<typeof LandingTickerItemSchema>;
337
+ export type LandingTickerConfig = z.infer<typeof LandingTickerSchema>;
338
+ export type LandingTrustConfig = z.infer<typeof LandingTrustSchema>;
339
+ export type LandingApiShowcaseItemConfig = z.infer<
340
+ typeof LandingApiShowcaseItemSchema
341
+ >;
342
+ export type LandingApiShowcaseConfig = z.infer<typeof LandingApiShowcaseSchema>;
343
+ export type LandingPricingCtaConfig = z.infer<typeof LandingPricingCtaSchema>;
344
+
249
345
  export type ResolvedLandingContent = {
250
346
  hero?: {
251
347
  title?: string;
348
+ titleAccent?: string;
252
349
  subtitle?: string;
253
350
  description?: string;
351
+ badge?: string;
352
+ highlights?: string[];
353
+ codeExample?: NonNullable<LandingCodeExampleConfig>;
354
+ };
355
+ featuresSection?: {
356
+ label?: string;
357
+ title?: string;
358
+ description?: string;
254
359
  };
255
360
  features?: LandingFeatureConfig[];
256
361
  primaryAction?: LandingLinkConfig;
257
362
  secondaryAction?: LandingLinkConfig;
363
+ ticker?: LandingTickerConfig;
364
+ trust?: LandingTrustConfig;
365
+ apiShowcase?: LandingApiShowcaseConfig;
366
+ pricingCta?: LandingPricingCtaConfig;
258
367
  showPoweredBy?: boolean;
259
368
  };
260
369
 
@@ -582,6 +582,7 @@ const ThemeConfigSchema = z.object({
582
582
  const SiteSchema = z
583
583
  .object({
584
584
  title: z.string(),
585
+ showTitleInHeader: z.boolean().optional(),
585
586
  logoUrl: z.string(),
586
587
  dir: z.enum(["ltr", "rtl"]).optional(),
587
588
  logo: LogoSchema,
@@ -27,22 +27,12 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
27
27
  const authNavEnabled = config.__meta?.authenticationEnabled === true;
28
28
  const navigation = config.header?.navigation ?? [];
29
29
 
30
- if (!authNavEnabled) {
31
- const filtered = withoutLoginNavigation(navigation);
32
- if (filtered.length === navigation.length) {
33
- return config;
34
- }
35
-
36
- return {
37
- ...config,
38
- header: {
39
- ...config.header,
40
- navigation: filtered,
41
- },
42
- };
30
+ if (authNavEnabled) {
31
+ return config;
43
32
  }
44
33
 
45
- if (navigation.some((item) => "to" in item && item.to === LOGIN_NAV_PATH)) {
34
+ const filtered = withoutLoginNavigation(navigation);
35
+ if (filtered.length === navigation.length) {
46
36
  return config;
47
37
  }
48
38
 
@@ -50,7 +40,7 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
50
40
  ...config,
51
41
  header: {
52
42
  ...config.header,
53
- navigation: [...navigation, LOGIN_HEADER_NAV_ITEM],
43
+ navigation: filtered,
54
44
  },
55
45
  };
56
46
  }
@@ -0,0 +1,36 @@
1
+ import type { HeaderNavigation } from "../../config/validators/HeaderNavigationSchema.js";
2
+ import { LOGIN_NAV_PATH } from "./auth-header-nav.js";
3
+
4
+ const AUTH_HEADER_PATHS = new Set([
5
+ LOGIN_NAV_PATH,
6
+ "/signin",
7
+ "/login",
8
+ "/signup",
9
+ "/register",
10
+ ]);
11
+
12
+ const PRICING_HEADER_PATHS = new Set(["/pricing"]);
13
+
14
+ export function withoutAuthHeaderNavigation(
15
+ navigation: HeaderNavigation,
16
+ ): HeaderNavigation {
17
+ return navigation.filter(
18
+ (item) => !("to" in item) || !AUTH_HEADER_PATHS.has(item.to),
19
+ );
20
+ }
21
+
22
+ export function withoutPricingHeaderNavigation(
23
+ navigation: HeaderNavigation,
24
+ ): HeaderNavigation {
25
+ return navigation.filter(
26
+ (item) => !("to" in item) || !PRICING_HEADER_PATHS.has(item.to),
27
+ );
28
+ }
29
+
30
+ export function withoutMarketingCtaHeaderNavigation(
31
+ navigation: HeaderNavigation,
32
+ ): HeaderNavigation {
33
+ return withoutPricingHeaderNavigation(
34
+ withoutAuthHeaderNavigation(navigation),
35
+ );
36
+ }