@lukoweb/apitogo 0.2.39 → 0.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +573 -133
- package/dist/declarations/cli/platform/helpers/progress.d.ts +1 -0
- package/docs/configuration/gateway.md +276 -273
- package/package.json +1 -1
- package/src/config/v2/resolve.ts +5 -0
- package/src/config/v2/rules.ts +14 -1
- package/src/config/v2/schema.ts +2 -0
- package/src/config/v2/services.ts +147 -17
- package/src/config/v2/starter-landing.ts +150 -0
- package/src/config/v2/wrapper.ts +58 -17
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
access,
|
|
3
|
+
mkdir,
|
|
4
|
+
readdir,
|
|
5
|
+
rename,
|
|
6
|
+
rm,
|
|
7
|
+
stat,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
2
10
|
import path from "node:path";
|
|
3
11
|
import { ApitogoError } from "../../lib/util/invariant.js";
|
|
4
12
|
import { docsFromPaths, starterPage } from "./init.js";
|
|
@@ -8,6 +16,7 @@ import { parseConfigV2 } from "./parse.js";
|
|
|
8
16
|
import { type ResolvedConfigV2, resolveConfigV2 } from "./resolve.js";
|
|
9
17
|
import { checkDependencies } from "./rules.js";
|
|
10
18
|
import type { ConfigV2 } from "./schema.js";
|
|
19
|
+
import { starterLandingPage } from "./starter-landing.js";
|
|
11
20
|
|
|
12
21
|
export const SERVICES = ["portal", "gateway", "identity", "plans"] as const;
|
|
13
22
|
export type ServiceName = (typeof SERVICES)[number];
|
|
@@ -49,13 +58,52 @@ const httpsProblem = (value: string) =>
|
|
|
49
58
|
? undefined
|
|
50
59
|
: "The address must start with https://";
|
|
51
60
|
|
|
52
|
-
/** The runtime
|
|
53
|
-
const
|
|
54
|
-
nodejs:
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
/** The runtime versions the platform runs, newest last; the first of each list is the one a prompt suggests. */
|
|
62
|
+
const RUNTIME_VERSIONS = {
|
|
63
|
+
nodejs: [
|
|
64
|
+
{ value: "NODE_22_LTS", label: "Node.js 22 (LTS)" },
|
|
65
|
+
{ value: "NODE_24_LTS", label: "Node.js 24 (LTS)" },
|
|
66
|
+
],
|
|
67
|
+
python: [
|
|
68
|
+
{ value: "PYTHON_312", label: "Python 3.12" },
|
|
69
|
+
{ value: "PYTHON_310", label: "Python 3.10" },
|
|
70
|
+
{ value: "PYTHON_311", label: "Python 3.11" },
|
|
71
|
+
{ value: "PYTHON_313", label: "Python 3.13" },
|
|
72
|
+
{ value: "PYTHON_314", label: "Python 3.14" },
|
|
73
|
+
],
|
|
74
|
+
dotnet: [
|
|
75
|
+
{ value: "DOTNET_8", label: ".NET 8" },
|
|
76
|
+
{ value: "DOTNET_9", label: ".NET 9" },
|
|
77
|
+
{ value: "DOTNET_10", label: ".NET 10" },
|
|
78
|
+
],
|
|
57
79
|
} as const;
|
|
58
80
|
|
|
81
|
+
type Runtime = keyof typeof RUNTIME_VERSIONS;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The platform's name for a runtime version a user typed the short way: `22` or `v22` for Node.js, `3.12` for
|
|
85
|
+
* Python, `8` or `8.0` for .NET. A value that is not recognised is kept as it is, and `check` names the allowed ones.
|
|
86
|
+
*/
|
|
87
|
+
function runtimeVersionSlug(runtime: Runtime, value: string): string {
|
|
88
|
+
const upper = value.trim().toUpperCase();
|
|
89
|
+
const known = RUNTIME_VERSIONS[runtime].map(
|
|
90
|
+
(choice) => choice.value as string,
|
|
91
|
+
);
|
|
92
|
+
if (known.includes(upper)) return upper;
|
|
93
|
+
const digits = value
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/^(v|python|\.net|dotnet|node(js)?)[ _-]?/, "")
|
|
97
|
+
.replace(/(\.x|_lts| lts)$/, "");
|
|
98
|
+
const candidate =
|
|
99
|
+
runtime === "nodejs"
|
|
100
|
+
? `NODE_${digits.split(".")[0]}_LTS`
|
|
101
|
+
: runtime === "python"
|
|
102
|
+
? `PYTHON_${digits.replace(".", "")}`
|
|
103
|
+
: `DOTNET_${digits.split(".")[0]}`;
|
|
104
|
+
return known.includes(candidate) ? candidate : value;
|
|
105
|
+
}
|
|
106
|
+
|
|
59
107
|
/** What users read first (decision 28); the technical name follows in brackets. */
|
|
60
108
|
export const SERVICE_LABELS: Record<ServiceName, string> = {
|
|
61
109
|
portal: "Developer website",
|
|
@@ -174,7 +222,10 @@ paths:
|
|
|
174
222
|
description: The service is up.
|
|
175
223
|
`;
|
|
176
224
|
|
|
177
|
-
const PLAN_TEMPLATES: Record<
|
|
225
|
+
const PLAN_TEMPLATES: Record<
|
|
226
|
+
"free" | "tiered" | "starter" | "none",
|
|
227
|
+
unknown[]
|
|
228
|
+
> = {
|
|
178
229
|
none: [],
|
|
179
230
|
free: [
|
|
180
231
|
{
|
|
@@ -188,6 +239,31 @@ const PLAN_TEMPLATES: Record<"free" | "tiered" | "none", unknown[]> = {
|
|
|
188
239
|
},
|
|
189
240
|
},
|
|
190
241
|
],
|
|
242
|
+
// Sample plans for the starter: a free plan and two with a price on request, so they show on the pricing page and
|
|
243
|
+
// the home page and still deploy on a Free project (a plan with a price needs a paid platform plan and billing).
|
|
244
|
+
starter: [
|
|
245
|
+
{
|
|
246
|
+
sku: "free",
|
|
247
|
+
displayName: "Free",
|
|
248
|
+
default: true,
|
|
249
|
+
limits: {
|
|
250
|
+
rateLimit: { calls: 3, periodSeconds: 60 },
|
|
251
|
+
quota: { calls: 1000, periodDays: 30 },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
sku: "pro",
|
|
256
|
+
displayName: "Pro",
|
|
257
|
+
pricingModel: "custom",
|
|
258
|
+
priceLabel: "Contact us",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
sku: "enterprise",
|
|
262
|
+
displayName: "Enterprise",
|
|
263
|
+
pricingModel: "custom",
|
|
264
|
+
priceLabel: "Contact us",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
191
267
|
tiered: [
|
|
192
268
|
{
|
|
193
269
|
sku: "free",
|
|
@@ -284,6 +360,7 @@ async function buildPortal(
|
|
|
284
360
|
const name = o.name?.trim() || resolved.project.name;
|
|
285
361
|
|
|
286
362
|
let content: string[] = [];
|
|
363
|
+
let landing: string | undefined;
|
|
287
364
|
if (!external) {
|
|
288
365
|
if (Array.isArray(o.docs) && o.docs.length > 0) {
|
|
289
366
|
content = await docsFromPaths(dir, o.docs);
|
|
@@ -320,7 +397,31 @@ async function buildPortal(
|
|
|
320
397
|
const pages = path.join(resolved.root, "portal", "pages");
|
|
321
398
|
if (template === "blank")
|
|
322
399
|
await addFile(context, path.join(pages, ".gitkeep"), "");
|
|
323
|
-
else
|
|
400
|
+
else if (options.template === "starter") {
|
|
401
|
+
// The starter has a full home page of its own, so the docs start at /introduction, where the page links to.
|
|
402
|
+
await addFile(
|
|
403
|
+
context,
|
|
404
|
+
path.join(pages, "introduction.mdx"),
|
|
405
|
+
starterPage(name),
|
|
406
|
+
);
|
|
407
|
+
const landingFile = path.join(
|
|
408
|
+
resolved.root,
|
|
409
|
+
"portal",
|
|
410
|
+
"landing",
|
|
411
|
+
"LandingPage.tsx",
|
|
412
|
+
);
|
|
413
|
+
await addFile(
|
|
414
|
+
context,
|
|
415
|
+
landingFile,
|
|
416
|
+
starterLandingPage({
|
|
417
|
+
name,
|
|
418
|
+
withPlans:
|
|
419
|
+
context.config.plans !== undefined &&
|
|
420
|
+
context.config.plans.enabled !== false,
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
landing = rel(dir, landingFile);
|
|
424
|
+
} else
|
|
324
425
|
await addFile(
|
|
325
426
|
context,
|
|
326
427
|
path.join(pages, "index.mdx"),
|
|
@@ -337,12 +438,31 @@ async function buildPortal(
|
|
|
337
438
|
: content.length > 0
|
|
338
439
|
? { docs: { content } }
|
|
339
440
|
: {}),
|
|
441
|
+
...(landing ? { landing } : {}),
|
|
340
442
|
...(o.noConsole ? { console: { enabled: false } } : {}),
|
|
341
443
|
...(o.requireAuth ? { requireAuth: true } : {}),
|
|
342
444
|
...(o.search && !external ? { search: o.search } : {}),
|
|
343
445
|
};
|
|
344
446
|
}
|
|
345
447
|
|
|
448
|
+
/**
|
|
449
|
+
* A managed gateway runs the code in its source folder, and a new project has none. The folder is made (a `.gitkeep`
|
|
450
|
+
* keeps it in git) and the user is told it is empty. A folder that has files is left as it is.
|
|
451
|
+
*/
|
|
452
|
+
async function createSourceFolder(
|
|
453
|
+
context: Context,
|
|
454
|
+
sourcePath: string,
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
const folder = path.join(context.dir, sourcePath);
|
|
457
|
+
const entries = await readdir(folder).catch(() => [] as string[]);
|
|
458
|
+
if (entries.some((name) => name !== ".gitkeep")) return;
|
|
459
|
+
if (entries.length === 0)
|
|
460
|
+
await addFile(context, path.join(folder, ".gitkeep"), "");
|
|
461
|
+
context.notes.push(
|
|
462
|
+
`The folder ${sourcePath}/ is empty: put your backend code there before you deploy.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
346
466
|
async function buildGateway(
|
|
347
467
|
context: Context,
|
|
348
468
|
): Promise<NonNullable<ConfigV2["gateway"]>> {
|
|
@@ -401,25 +521,35 @@ async function buildGateway(
|
|
|
401
521
|
])
|
|
402
522
|
: undefined);
|
|
403
523
|
const runtimeVersion =
|
|
404
|
-
o.runtimeVersion
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
524
|
+
o.runtimeVersion !== undefined && runtime
|
|
525
|
+
? runtimeVersionSlug(runtime, o.runtimeVersion)
|
|
526
|
+
: ask && runtime
|
|
527
|
+
? await ask.choose(
|
|
528
|
+
"Runtime version",
|
|
529
|
+
RUNTIME_VERSIONS[runtime].map((choice, index) => ({
|
|
530
|
+
value: choice.value as string,
|
|
531
|
+
label:
|
|
532
|
+
index === 0 ? `${choice.label} (suggested)` : choice.label,
|
|
533
|
+
})),
|
|
534
|
+
)
|
|
535
|
+
: undefined;
|
|
410
536
|
if (!source || !runtime || !runtimeVersion) {
|
|
411
537
|
throw new ServiceEditError(
|
|
412
538
|
"--origin managed needs --source, --runtime and --runtime-version.",
|
|
413
|
-
|
|
539
|
+
runtime
|
|
540
|
+
? `For ${runtime}, --runtime-version is one of: ${RUNTIME_VERSIONS[runtime].map((choice) => choice.value).join(", ")} (a short form such as ${runtime === "nodejs" ? "22" : runtime === "python" ? "3.12" : "8"} works too).`
|
|
541
|
+
: "For example: --source server --runtime nodejs --runtime-version NODE_22_LTS.",
|
|
414
542
|
);
|
|
415
543
|
}
|
|
544
|
+
const sourcePath = insideProject(dir, source, "Source folder");
|
|
416
545
|
origin = {
|
|
417
546
|
mode,
|
|
418
|
-
source:
|
|
547
|
+
source: sourcePath,
|
|
419
548
|
runtime,
|
|
420
549
|
runtimeVersion,
|
|
421
550
|
...(o.exclude?.length ? { exclude: o.exclude } : {}),
|
|
422
551
|
};
|
|
552
|
+
await createSourceFolder(context, sourcePath);
|
|
423
553
|
}
|
|
424
554
|
|
|
425
555
|
let specs: string[];
|
|
@@ -577,7 +707,7 @@ async function buildPlans(
|
|
|
577
707
|
(options.template === "blank"
|
|
578
708
|
? "none"
|
|
579
709
|
: options.template === "starter"
|
|
580
|
-
? "
|
|
710
|
+
? "starter"
|
|
581
711
|
: "free");
|
|
582
712
|
// Only paid plans need a billing provider. A catalog of free plans takes no payment, so its default is `none` and the
|
|
583
713
|
// config checks clean right away; the tiered template (paid plans) defaults to Stripe, whose key is set with `secrets set`.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The home page of the starter portal: hero, features, steps, an API showcase, the plans and a closing call to action,
|
|
3
|
+
* built from the sections of `@lukoweb/apitogo-module-landing`. The project owns the file and edits it freely.
|
|
4
|
+
*/
|
|
5
|
+
export function starterLandingPage(options: {
|
|
6
|
+
name: string;
|
|
7
|
+
/** The plans service is on, so the page can show the plans (they come from the same catalog as the pricing page). */
|
|
8
|
+
withPlans: boolean;
|
|
9
|
+
}): string {
|
|
10
|
+
const name = JSON.stringify(options.name);
|
|
11
|
+
const plansImport = options.withPlans
|
|
12
|
+
? 'import { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
|
|
13
|
+
: "";
|
|
14
|
+
const plansHook = options.withPlans
|
|
15
|
+
? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n\n"
|
|
16
|
+
: "";
|
|
17
|
+
const plansSection = options.withPlans
|
|
18
|
+
? " <PlansPreviewSection plans={plans} />\n"
|
|
19
|
+
: "";
|
|
20
|
+
return `import {
|
|
21
|
+
ApiShowcaseSection,
|
|
22
|
+
FeaturesSection,
|
|
23
|
+
HeroSection,
|
|
24
|
+
LandingShell,
|
|
25
|
+
${options.withPlans ? "PlansPreviewSection,\n " : ""}PricingCtaSection,
|
|
26
|
+
StepsSection,
|
|
27
|
+
} from "@lukoweb/apitogo-module-landing";
|
|
28
|
+
${plansImport}
|
|
29
|
+
const NAME = ${name};
|
|
30
|
+
|
|
31
|
+
export default function LandingPage() {
|
|
32
|
+
${plansHook} return (
|
|
33
|
+
<LandingShell
|
|
34
|
+
title={NAME}
|
|
35
|
+
description="Documentation, an API reference, sign-in, API keys and plans for your API, in one place."
|
|
36
|
+
>
|
|
37
|
+
<HeroSection
|
|
38
|
+
hero={{
|
|
39
|
+
badge: "API PORTAL",
|
|
40
|
+
title: NAME,
|
|
41
|
+
description:
|
|
42
|
+
"Everything your developers need to use your API: documentation, an interactive reference, sign-in, API keys and plans.",
|
|
43
|
+
}}
|
|
44
|
+
primaryAction={{ label: "Get started", href: "/signup" }}
|
|
45
|
+
secondaryAction={{ label: "Read the docs", href: "/introduction" }}
|
|
46
|
+
codeExamples={[
|
|
47
|
+
{
|
|
48
|
+
filename: "Call the API",
|
|
49
|
+
language: "bash",
|
|
50
|
+
code: \`$ curl https://your-api.example.com/health \\\\
|
|
51
|
+
-H "x-api-key: YOUR_API_KEY"
|
|
52
|
+
# { "status": "ok" }\`,
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
<FeaturesSection
|
|
57
|
+
section={{
|
|
58
|
+
label: "What you get",
|
|
59
|
+
title: "A developer portal that is ready on day one",
|
|
60
|
+
description:
|
|
61
|
+
"Edit this page in apitogo/portal/landing/LandingPage.tsx to make it yours.",
|
|
62
|
+
}}
|
|
63
|
+
features={[
|
|
64
|
+
{
|
|
65
|
+
title: "Documentation",
|
|
66
|
+
description:
|
|
67
|
+
"Write pages in Markdown or MDX and they appear in the portal with a sidebar and search.",
|
|
68
|
+
icon: "book-open",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "API reference",
|
|
72
|
+
description:
|
|
73
|
+
"An interactive reference built from your OpenAPI file, with a try-it console.",
|
|
74
|
+
icon: "code",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
title: "API keys",
|
|
78
|
+
description:
|
|
79
|
+
"People sign up, pick a plan and create keys themselves, with no work for you.",
|
|
80
|
+
icon: "shield-check",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: "Plans and limits",
|
|
84
|
+
description:
|
|
85
|
+
"Rate limits and monthly quotas per plan, enforced at the gateway.",
|
|
86
|
+
icon: "gauge",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "Usage",
|
|
90
|
+
description:
|
|
91
|
+
"Each developer sees their own request counts and how much of the plan is left.",
|
|
92
|
+
icon: "activity",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Sign-in",
|
|
96
|
+
description:
|
|
97
|
+
"Accounts and login pages that carry your name and theme.",
|
|
98
|
+
icon: "boxes",
|
|
99
|
+
},
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
<StepsSection
|
|
103
|
+
steps={[
|
|
104
|
+
{
|
|
105
|
+
step: "01",
|
|
106
|
+
title: "Create an account",
|
|
107
|
+
description: "Sign up in a minute, no approval queue.",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
step: "02",
|
|
111
|
+
title: "Pick a plan",
|
|
112
|
+
description: "Start on the free plan and move up when you need more.",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
step: "03",
|
|
116
|
+
title: "Call the API",
|
|
117
|
+
description: "Create a key and make your first request.",
|
|
118
|
+
},
|
|
119
|
+
]}
|
|
120
|
+
/>
|
|
121
|
+
<ApiShowcaseSection
|
|
122
|
+
apiShowcase={{
|
|
123
|
+
title: "Sample endpoints",
|
|
124
|
+
subtitle: "A slice of what is available. The full list is in the API reference.",
|
|
125
|
+
browseAllHref: "/api",
|
|
126
|
+
browseAllLabel: "Browse all APIs",
|
|
127
|
+
layout: "list",
|
|
128
|
+
items: [
|
|
129
|
+
{
|
|
130
|
+
name: "Health check",
|
|
131
|
+
path: "/health",
|
|
132
|
+
method: "GET",
|
|
133
|
+
summary: "Tells you the service is up.",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
${plansSection} <PricingCtaSection
|
|
139
|
+
variant="banner"
|
|
140
|
+
pricingCta={{
|
|
141
|
+
title: "Start building today",
|
|
142
|
+
description: "Create an account and get your API key in minutes.",
|
|
143
|
+
primaryAction: { label: "Get started", href: "/signup" },
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
</LandingShell>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
}
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
ReferenceSpecError,
|
|
7
7
|
renderReferenceSpec,
|
|
8
8
|
} from "./gateway-reference.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
docRoutes,
|
|
11
|
+
type HomeRedirect,
|
|
12
|
+
homeRedirect,
|
|
13
|
+
pickLandingRoute,
|
|
14
|
+
} from "./home-redirect.js";
|
|
10
15
|
import {
|
|
11
16
|
hasCapability,
|
|
12
17
|
missingManifestServices,
|
|
@@ -234,11 +239,11 @@ export function planPortal(
|
|
|
234
239
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
235
240
|
billing: plans.billing,
|
|
236
241
|
links: {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
// Hosted docs are linked once their pages are known (see `buildPortalWorkspace`): there is no page to
|
|
243
|
+
// name before that, and a guessed one was a 404 on every portal whose first page is not called `introduction`.
|
|
244
|
+
...(!profile.modules.docs && profile.docs.url
|
|
245
|
+
? { docs: profile.docs.url }
|
|
246
|
+
: {}),
|
|
242
247
|
...(plan.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {}),
|
|
243
248
|
},
|
|
244
249
|
};
|
|
@@ -296,6 +301,17 @@ export function buildSiteConfig(
|
|
|
296
301
|
},
|
|
297
302
|
modules: {
|
|
298
303
|
docs: { enabled: plan.docs },
|
|
304
|
+
// A custom home page (`portal.landing`) is served at `/`; the build imports the component from its absolute path.
|
|
305
|
+
...(portal?.landing
|
|
306
|
+
? {
|
|
307
|
+
landing: {
|
|
308
|
+
enabled: true,
|
|
309
|
+
path: "/",
|
|
310
|
+
layout: "landing",
|
|
311
|
+
page: portal.landing.split(path.sep).join("/"),
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
: {}),
|
|
299
315
|
// The plans pages (pricing, choosing a plan) exist only when billing is on, and the plans service is what makes it so.
|
|
300
316
|
// Switching billing on also makes the user panel register its own placeholder plans page, at `/account/<path>`.
|
|
301
317
|
// Its default path is `plans`, which is where the billing plugin puts the real page (subscribe, switch, cancel),
|
|
@@ -330,6 +346,16 @@ export function buildSiteConfig(
|
|
|
330
346
|
}
|
|
331
347
|
: {}),
|
|
332
348
|
...(navigation.length > 0 ? { navigation } : {}),
|
|
349
|
+
// The sidebar link above is only seen inside the docs; the header is what every page shows.
|
|
350
|
+
...(plan.apiReference
|
|
351
|
+
? {
|
|
352
|
+
header: {
|
|
353
|
+
navigation: [
|
|
354
|
+
{ label: "API Reference", to: `/${API_REFERENCE_PATH}` },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
: {}),
|
|
333
359
|
...(redirects.length > 0 ? { redirects } : {}),
|
|
334
360
|
...(extras.protectedRoutes?.length
|
|
335
361
|
? { protectedRoutes: extras.protectedRoutes }
|
|
@@ -508,17 +534,32 @@ export async function preparePortal(
|
|
|
508
534
|
includeDrafts: mode === "development",
|
|
509
535
|
})
|
|
510
536
|
: [];
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
537
|
+
// The account pages' "Documentation" link goes to a page the portal really has, or is left out.
|
|
538
|
+
if (plan.plansPlugin && plan.docs) {
|
|
539
|
+
const landing = hostedDocRoutes.some(
|
|
540
|
+
(route) => route === "/" || route === "/index",
|
|
541
|
+
)
|
|
542
|
+
? "/"
|
|
543
|
+
: pickLandingRoute(hostedDocRoutes);
|
|
544
|
+
if (landing)
|
|
545
|
+
plan.plansPlugin.links = { ...plan.plansPlugin.links, docs: landing };
|
|
546
|
+
}
|
|
547
|
+
// A portal with its own home page needs no redirect from `/`.
|
|
548
|
+
const home = resolved.portal?.landing
|
|
549
|
+
? undefined
|
|
550
|
+
: homeRedirect({
|
|
551
|
+
hostsDocs: plan.docs,
|
|
552
|
+
docRoutes: hostedDocRoutes,
|
|
553
|
+
externalDocsUrl: profile.docs.url,
|
|
554
|
+
apiReferencePath: plan.apiReference
|
|
555
|
+
? `/${API_REFERENCE_PATH}`
|
|
556
|
+
: undefined,
|
|
557
|
+
userPanel: plan.userPanel,
|
|
558
|
+
pricingPath:
|
|
559
|
+
plan.plansPlugin && hasCapability(effective.services.plans, "catalog")
|
|
560
|
+
? CONSOLE_ROUTES.pricing
|
|
561
|
+
: undefined,
|
|
562
|
+
});
|
|
522
563
|
const siteConfig = buildSiteConfig(
|
|
523
564
|
resolved,
|
|
524
565
|
plan,
|