@lukoweb/apitogo 0.1.54 → 0.1.55

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 (41) hide show
  1. package/dist/cli/cli.js +604 -376
  2. package/dist/declarations/config/landing-manifest.d.ts +76 -0
  3. package/dist/declarations/config/local-manifest.d.ts +76 -0
  4. package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
  5. package/dist/declarations/config/validators/ZudokuConfig.d.ts +50 -0
  6. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  7. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
  8. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  9. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  10. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  11. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  12. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  13. package/dist/flat-config.d.ts +44 -0
  14. package/package.json +1 -1
  15. package/src/app/main.css +2 -2
  16. package/src/config/apitogo-config-core.ts +13 -0
  17. package/src/config/apitogo-config-loader.browser.ts +17 -0
  18. package/src/config/build-apitogo-config.ts +13 -4
  19. package/src/config/landing-manifest.ts +11 -0
  20. package/src/config/landing-openapi-showcase.ts +115 -0
  21. package/src/config/loader.ts +17 -3
  22. package/src/config/local-manifest.ts +114 -10
  23. package/src/config/resolve-modules.ts +22 -1
  24. package/src/config/validators/ModulesSchema.ts +84 -0
  25. package/src/config/validators/ZudokuConfig.ts +1 -0
  26. package/src/lib/authentication/auth-header-nav.ts +5 -15
  27. package/src/lib/authentication/header-nav-filter.ts +36 -0
  28. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
  29. package/src/lib/authentication/providers/dev-portal-utils.ts +17 -7
  30. package/src/lib/authentication/providers/dev-portal.tsx +18 -8
  31. package/src/lib/components/Header.tsx +49 -39
  32. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  33. package/src/lib/components/HeaderNavigation.tsx +11 -9
  34. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  35. package/src/lib/components/SiteTitle.tsx +20 -0
  36. package/src/lib/components/ThemeSwitch.tsx +36 -2
  37. package/src/lib/components/TopNavigation.tsx +20 -33
  38. package/src/lib/core/ZudokuContext.ts +1 -0
  39. package/src/vite/config.ts +21 -0
  40. package/src/vite/plugin-auth.ts +4 -1
  41. package/src/vite/plugin-config.ts +39 -4
@@ -137,6 +137,7 @@ export interface FlatZudokuConfig {
137
137
  }
138
138
  site?: {
139
139
  title?: string
140
+ showTitleInHeader?: boolean
140
141
  logoUrl?: string
141
142
  dir?: ("ltr" | "rtl")
142
143
  logo?: _Schema0
@@ -533,13 +534,56 @@ export interface _Schema21 {
533
534
  title?: string
534
535
  subtitle?: string
535
536
  description?: string
537
+ badge?: string
538
+ highlights?: string[]
539
+ codeExample?: {
540
+ filename?: string
541
+ language?: string
542
+ code: string
543
+ }
536
544
  }
537
545
  features?: {
538
546
  title: string
539
547
  description: string
548
+ icon?: string
540
549
  }[]
541
550
  primaryAction?: _Schema22
542
551
  secondaryAction?: _Schema22
552
+ ticker?: {
553
+ enabled?: boolean
554
+ items?: {
555
+ label: string
556
+ value: string
557
+ change?: string
558
+ }[]
559
+ }
560
+ trust?: {
561
+ enabled?: boolean
562
+ title?: string
563
+ logos?: string[]
564
+ }
565
+ apiShowcase?: {
566
+ enabled?: boolean
567
+ title?: string
568
+ subtitle?: string
569
+ browseAllHref?: string
570
+ browseAllLabel?: string
571
+ autoFromOpenApi?: boolean
572
+ items?: {
573
+ name: string
574
+ path: string
575
+ icon?: string
576
+ latency?: string
577
+ summary?: string
578
+ }[]
579
+ }
580
+ pricingCta?: {
581
+ enabled?: boolean
582
+ title?: string
583
+ description?: string
584
+ primaryAction?: _Schema22
585
+ secondaryAction?: _Schema22
586
+ }
543
587
  showPoweredBy?: boolean
544
588
  }
545
589
  export interface _Schema22 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
package/src/app/main.css CHANGED
@@ -102,8 +102,8 @@
102
102
 
103
103
  @layer base {
104
104
  :root {
105
- --top-header-height: 65px;
106
- --top-nav-height: 50px;
105
+ --top-header-height: 64px;
106
+ --top-nav-height: 0px;
107
107
  --banner-height: 35px;
108
108
  --header-height: calc(
109
109
  var(--top-header-height) + var(--top-nav-height) + var(--banner-height)
@@ -12,6 +12,7 @@ const DERIVED_MANIFEST_KEYS = [
12
12
  "backend",
13
13
  "gateway",
14
14
  "projectId",
15
+ "landing",
15
16
  ] as const;
16
17
 
17
18
  const SITE_CONFIG_KEYS = [
@@ -542,6 +543,18 @@ export function deriveManifestFields(
542
543
  }
543
544
  continue;
544
545
  }
546
+ if (key === "landing") {
547
+ const modules = asRecord(normalized.modules);
548
+ const landing = asRecord(modules?.landing);
549
+ if (landing?.content || landing?.enabled !== undefined) {
550
+ manifest.landing = {
551
+ enabled:
552
+ typeof landing.enabled === "boolean" ? landing.enabled : undefined,
553
+ content: landing.content,
554
+ };
555
+ }
556
+ continue;
557
+ }
545
558
  if (normalized[key] !== undefined) {
546
559
  manifest[key] = normalized[key];
547
560
  }
@@ -0,0 +1,17 @@
1
+ import type {
2
+ ApitogoJsonConfig,
3
+ AuthenticationSecrets,
4
+ } from "./apitogo-config-core.js";
5
+
6
+ /** Browser stub — JSON config is embedded by the Vite config plugin on the client. */
7
+ export function loadApitogoConfig(_rootDir?: string): ApitogoJsonConfig {
8
+ return {};
9
+ }
10
+
11
+ export function readAuthenticationSecrets(): AuthenticationSecrets {
12
+ return {};
13
+ }
14
+
15
+ export function apitogoConfigPath(rootDir: string): string {
16
+ return rootDir;
17
+ }
@@ -11,6 +11,13 @@ type BuildApitogoConfigOptions = Partial<ApitogoConfig> &
11
11
  plugins?: ApitogoPlugin[];
12
12
  };
13
13
 
14
+ function mergeOptionalRecord<T extends Record<string, unknown>>(
15
+ ...records: Array<T | undefined>
16
+ ): T | undefined {
17
+ const merged = Object.assign({}, ...records.filter(Boolean)) as T;
18
+ return Object.keys(merged).length > 0 ? merged : undefined;
19
+ }
20
+
14
21
  export function buildApitogoConfig({
15
22
  plugins,
16
23
  plans: _legacyPlans,
@@ -24,14 +31,16 @@ export function buildApitogoConfig({
24
31
  const { plans: _rootPlans, ...restOverrides } =
25
32
  jsonOverrides as ApitogoJsonConfig;
26
33
 
34
+ const authentication = mergeOptionalRecord(
35
+ siteConfig.authentication as ApitogoConfig["authentication"],
36
+ jsonOverrides.authentication as ApitogoConfig["authentication"],
37
+ );
38
+
27
39
  return {
28
40
  ...siteConfig,
29
41
  ...restOverrides,
30
42
  plugins,
31
- authentication: {
32
- ...(siteConfig.authentication as ApitogoConfig["authentication"]),
33
- ...jsonOverrides.authentication,
34
- },
43
+ ...(authentication ? { authentication } : {}),
35
44
  site: {
36
45
  ...(siteConfig.site as ApitogoConfig["site"]),
37
46
  ...jsonOverrides.site,
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { LandingContentSchema } from "./validators/ModulesSchema.js";
3
+
4
+ export const LandingManifestContentSchema = LandingContentSchema;
5
+
6
+ export type LandingManifestContent = z.infer<typeof LandingContentSchema>;
7
+
8
+ export type LandingManifestInput = {
9
+ enabled?: boolean;
10
+ content?: LandingManifestContent;
11
+ };
@@ -0,0 +1,115 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { LandingApiShowcaseItemConfig } from "./validators/ModulesSchema.js";
4
+ import type { ZudokuConfig } from "./validators/ZudokuConfig.js";
5
+
6
+ const HTTP_METHODS = [
7
+ "get",
8
+ "post",
9
+ "put",
10
+ "patch",
11
+ "delete",
12
+ "head",
13
+ "options",
14
+ ] as const;
15
+
16
+ type OpenApiDocument = {
17
+ paths?: Record<
18
+ string,
19
+ Partial<Record<(typeof HTTP_METHODS)[number], { summary?: string }>>
20
+ >;
21
+ };
22
+
23
+ function readOpenApiDocument(
24
+ rootDir: string,
25
+ openApiPath: string,
26
+ ): OpenApiDocument | null {
27
+ try {
28
+ const absolutePath = path.isAbsolute(openApiPath)
29
+ ? openApiPath
30
+ : path.resolve(rootDir, openApiPath);
31
+ const raw = readFileSync(absolutePath, "utf8");
32
+ return JSON.parse(raw) as OpenApiDocument;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function resolveOpenApiPath(config: ZudokuConfig): string | undefined {
39
+ const backend = (config as { backend?: { openApiPath?: string } }).backend;
40
+ if (backend?.openApiPath) {
41
+ return backend.openApiPath;
42
+ }
43
+
44
+ const apis = config.apis;
45
+ if (!apis) {
46
+ return undefined;
47
+ }
48
+
49
+ const entries = Array.isArray(apis) ? apis : [apis];
50
+ for (const entry of entries) {
51
+ if (typeof entry === "object" && entry !== null && "type" in entry) {
52
+ const api = entry as { type?: string; input?: string };
53
+ if (api.type === "file" && typeof api.input === "string") {
54
+ return api.input;
55
+ }
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+
62
+ export function extractApiShowcaseItemsFromOpenApi(
63
+ config: ZudokuConfig,
64
+ rootDir: string,
65
+ limit = 8,
66
+ ): LandingApiShowcaseItemConfig[] {
67
+ const openApiPath = resolveOpenApiPath(config);
68
+ if (!openApiPath) {
69
+ return [];
70
+ }
71
+
72
+ const document = readOpenApiDocument(rootDir, openApiPath);
73
+ if (!document?.paths) {
74
+ return [];
75
+ }
76
+
77
+ const items: LandingApiShowcaseItemConfig[] = [];
78
+
79
+ for (const [routePath, methods] of Object.entries(document.paths)) {
80
+ for (const method of HTTP_METHODS) {
81
+ const operation = methods?.[method];
82
+ if (!operation) {
83
+ continue;
84
+ }
85
+
86
+ items.push({
87
+ name: operation.summary ?? `${method.toUpperCase()} ${routePath}`,
88
+ path: routePath,
89
+ summary: operation.summary,
90
+ });
91
+
92
+ if (items.length >= limit) {
93
+ return items;
94
+ }
95
+ }
96
+ }
97
+
98
+ return items;
99
+ }
100
+
101
+ export function enrichResolvedLandingApiShowcase(
102
+ config: ZudokuConfig,
103
+ resolved: { landing: { content: { apiShowcase?: { autoFromOpenApi?: boolean; items?: LandingApiShowcaseItemConfig[] } } } },
104
+ rootDir: string,
105
+ ): void {
106
+ const showcase = resolved.landing.content.apiShowcase;
107
+ if (
108
+ !showcase?.autoFromOpenApi ||
109
+ (showcase.items && showcase.items.length > 0)
110
+ ) {
111
+ return;
112
+ }
113
+
114
+ showcase.items = extractApiShowcaseItemsFromOpenApi(config, rootDir);
115
+ }
@@ -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,
@@ -57,7 +57,7 @@ export const resolveLandingContent = (
57
57
  const siteTitle =
58
58
  config.metadata?.applicationName ?? config.site?.title ?? "APIToGo";
59
59
 
60
- return {
60
+ const resolved: ResolvedLandingContent = {
61
61
  hero: {
62
62
  title: content?.hero?.title ?? siteTitle,
63
63
  subtitle: content?.hero?.subtitle ?? "Developer portal",
@@ -65,6 +65,9 @@ export const resolveLandingContent = (
65
65
  content?.hero?.description ??
66
66
  config.metadata?.description ??
67
67
  "Ship beautiful API documentation and developer experiences with APIToGo.",
68
+ badge: content?.hero?.badge,
69
+ highlights: content?.hero?.highlights,
70
+ codeExample: content?.hero?.codeExample,
68
71
  },
69
72
  features: content?.features ?? DEFAULT_LANDING_FEATURES,
70
73
  primaryAction: content?.primaryAction ?? {
@@ -77,6 +80,24 @@ export const resolveLandingContent = (
77
80
  },
78
81
  showPoweredBy: content?.showPoweredBy,
79
82
  };
83
+
84
+ if (content?.ticker && content.ticker.enabled !== false) {
85
+ resolved.ticker = content.ticker;
86
+ }
87
+
88
+ if (content?.trust && content.trust.enabled !== false) {
89
+ resolved.trust = content.trust;
90
+ }
91
+
92
+ if (content?.apiShowcase && content.apiShowcase.enabled !== false) {
93
+ resolved.apiShowcase = content.apiShowcase;
94
+ }
95
+
96
+ if (content?.pricingCta && content.pricingCta.enabled !== false) {
97
+ resolved.pricingCta = content.pricingCta;
98
+ }
99
+
100
+ return resolved;
80
101
  };
81
102
 
82
103
  const DEFAULT_DASHBOARD_QUICK_LINKS: ResolvedDashboardContent["quickLinks"] = [
@@ -44,13 +44,76 @@ 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(),
52
61
  subtitle: z.string().optional(),
53
62
  description: z.string().optional(),
63
+ badge: z.string().optional(),
64
+ highlights: z.array(z.string()).optional(),
65
+ codeExample: LandingCodeExampleSchema,
66
+ })
67
+ .optional();
68
+
69
+ export const LandingTickerItemSchema = z.object({
70
+ label: z.string(),
71
+ value: z.string(),
72
+ change: z.string().optional(),
73
+ });
74
+
75
+ export const LandingTickerSchema = z
76
+ .object({
77
+ enabled: z.boolean().optional(),
78
+ items: z.array(LandingTickerItemSchema).optional(),
79
+ })
80
+ .optional();
81
+
82
+ export const LandingTrustSchema = z
83
+ .object({
84
+ enabled: z.boolean().optional(),
85
+ title: z.string().optional(),
86
+ logos: z.array(z.string()).optional(),
87
+ })
88
+ .optional();
89
+
90
+ export const LandingApiShowcaseItemSchema = z.object({
91
+ name: z.string(),
92
+ path: z.string(),
93
+ icon: z.string().optional(),
94
+ latency: z.string().optional(),
95
+ summary: z.string().optional(),
96
+ });
97
+
98
+ export const LandingApiShowcaseSchema = z
99
+ .object({
100
+ enabled: z.boolean().optional(),
101
+ title: z.string().optional(),
102
+ subtitle: z.string().optional(),
103
+ browseAllHref: z.string().optional(),
104
+ browseAllLabel: z.string().optional(),
105
+ autoFromOpenApi: z.boolean().optional(),
106
+ items: z.array(LandingApiShowcaseItemSchema).optional(),
107
+ })
108
+ .optional();
109
+
110
+ export const LandingPricingCtaSchema = z
111
+ .object({
112
+ enabled: z.boolean().optional(),
113
+ title: z.string().optional(),
114
+ description: z.string().optional(),
115
+ primaryAction: LandingLinkSchema.optional(),
116
+ secondaryAction: LandingLinkSchema.optional(),
54
117
  })
55
118
  .optional();
56
119
 
@@ -60,6 +123,10 @@ export const LandingContentSchema = z
60
123
  features: z.array(LandingFeatureSchema).optional(),
61
124
  primaryAction: LandingLinkSchema.optional(),
62
125
  secondaryAction: LandingLinkSchema.optional(),
126
+ ticker: LandingTickerSchema,
127
+ trust: LandingTrustSchema,
128
+ apiShowcase: LandingApiShowcaseSchema,
129
+ pricingCta: LandingPricingCtaSchema,
63
130
  showPoweredBy: z.boolean().optional(),
64
131
  })
65
132
  .optional();
@@ -246,15 +313,32 @@ export type ProfileContentConfig = z.infer<typeof ProfileContentSchema>;
246
313
  export type PlansContentConfig = z.infer<typeof PlansContentSchema>;
247
314
  export type PlanTierConfig = z.infer<typeof PlanTierSchema>;
248
315
 
316
+ export type LandingCodeExampleConfig = z.infer<typeof LandingCodeExampleSchema>;
317
+ export type LandingTickerItemConfig = z.infer<typeof LandingTickerItemSchema>;
318
+ export type LandingTickerConfig = z.infer<typeof LandingTickerSchema>;
319
+ export type LandingTrustConfig = z.infer<typeof LandingTrustSchema>;
320
+ export type LandingApiShowcaseItemConfig = z.infer<
321
+ typeof LandingApiShowcaseItemSchema
322
+ >;
323
+ export type LandingApiShowcaseConfig = z.infer<typeof LandingApiShowcaseSchema>;
324
+ export type LandingPricingCtaConfig = z.infer<typeof LandingPricingCtaSchema>;
325
+
249
326
  export type ResolvedLandingContent = {
250
327
  hero?: {
251
328
  title?: string;
252
329
  subtitle?: string;
253
330
  description?: string;
331
+ badge?: string;
332
+ highlights?: string[];
333
+ codeExample?: NonNullable<LandingCodeExampleConfig>;
254
334
  };
255
335
  features?: LandingFeatureConfig[];
256
336
  primaryAction?: LandingLinkConfig;
257
337
  secondaryAction?: LandingLinkConfig;
338
+ ticker?: LandingTickerConfig;
339
+ trust?: LandingTrustConfig;
340
+ apiShowcase?: LandingApiShowcaseConfig;
341
+ pricingCta?: LandingPricingCtaConfig;
258
342
  showPoweredBy?: boolean;
259
343
  };
260
344
 
@@ -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,