@lukoweb/apitogo 0.2.59 → 0.2.61
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 +897 -527
- package/docs/cli/commands.md +6 -6
- package/docs/cli/go-live.md +12 -12
- package/docs/cli/overview.md +26 -27
- package/docs/cli/setup.md +38 -46
- package/docs/cli/shape.md +32 -35
- package/docs/cli/walkthrough.md +19 -19
- package/docs/components/slot.mdx +116 -116
- package/docs/configuration/api-reference.md +1 -1
- package/docs/configuration/authentication.md +3 -3
- package/docs/configuration/footer.mdx +89 -89
- package/docs/configuration/gateway.md +8 -8
- package/docs/configuration/make-config.mdx +20 -10
- package/docs/configuration/overview.md +3 -3
- package/docs/configuration/plans.md +5 -5
- package/docs/configuration/portal.md +36 -0
- package/docs/customization/colors-theme.mdx +255 -255
- package/docs/customization/fonts.md +105 -105
- package/docs/customization/landing-page.md +1 -1
- package/docs/deploy/apitogo.md +3 -3
- package/docs/deployment.md +3 -3
- package/docs/guides/custom-pages.md +3 -3
- package/docs/guides/customer-account.md +1 -1
- package/docs/guides/stripe-setup.mdx +9 -9
- package/docs/markdown/frontmatter.md +2 -2
- package/docs/mcp/go-live.md +7 -7
- package/docs/mcp/overview.md +58 -58
- package/docs/mcp/setup.md +6 -6
- package/docs/mcp/shape.md +2 -2
- package/docs/mcp/tools.md +57 -57
- package/docs/quickstart.md +6 -4
- package/docs/writing.mdx +68 -68
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +11 -1
- package/src/config/v2/resolve.ts +19 -0
- package/src/config/v2/rules.ts +88 -0
- package/src/config/v2/schema.ts +28 -0
- package/src/config/v2/wrapper.ts +85 -13
- package/src/vite/config.ts +278 -273
- package/src/vite/markdown-output-path.ts +48 -0
- package/src/vite/plugin-markdown-export.ts +210 -214
- package/src/vite/prerender/prerender.ts +4 -4
- package/src/vite/watch-ignore.ts +19 -0
package/docs/quickstart.md
CHANGED
|
@@ -13,6 +13,8 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
15
|
- **Node.js 20+**
|
|
16
|
+
- **The `apitogo` CLI, installed globally:** `npm install -g @lukoweb/apitogo` (see
|
|
17
|
+
[Install the CLI](./cli/setup.md))
|
|
16
18
|
- An account on [apitogo.com](https://apitogo.com) (needed to deploy, not to try it locally)
|
|
17
19
|
|
|
18
20
|
## Get started
|
|
@@ -22,7 +24,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
22
24
|
1. **Create the project**
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
apitogo create --template full
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
It asks for a name, makes a folder for it and writes everything a working product needs:
|
|
@@ -37,7 +39,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
41
|
cd <your project folder>
|
|
40
|
-
|
|
42
|
+
apitogo run
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Open `http://localhost:3000`. The console signs in with a demo account (`demo@apitogo.local` /
|
|
@@ -47,7 +49,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
47
49
|
1. **Sign in to APItoGo**
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
|
|
52
|
+
apitogo login
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
A browser window opens; sign in there.
|
|
@@ -55,7 +57,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
55
57
|
1. **Deploy**
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
|
|
60
|
+
apitogo deploy
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
The first deploy creates the project (it asks which hosting plan: Free, Starter or Professional),
|
package/docs/writing.mdx
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Writing
|
|
3
|
-
sidebar_icon: book-open-text
|
|
4
|
-
description: A guide to writing documentation in APIToGo using Markdown and MDX.
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Get started with creating rich documentation in APIToGo using Markdown and MDX. This guide covers
|
|
8
|
-
the essentials to help you begin documenting your project.
|
|
9
|
-
|
|
10
|
-
## Quick Start
|
|
11
|
-
|
|
12
|
-
1. **Create a markdown file** in `apitogo/portal/pages/`
|
|
13
|
-
2. **Add frontmatter** with title and metadata
|
|
14
|
-
3. **Write content** using Markdown or MDX
|
|
15
|
-
|
|
16
|
-
The page shows in the sidebar on its own.
|
|
17
|
-
|
|
18
|
-
### Basic Document Structure
|
|
19
|
-
|
|
20
|
-
```md
|
|
21
|
-
---
|
|
22
|
-
title: My Document
|
|
23
|
-
sidebar_icon: file-text
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
Your content goes here using standard Markdown syntax.
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Adding to Navigation
|
|
30
|
-
|
|
31
|
-
The sidebar is built from the files: every page in `apitogo/portal/pages/` (or the folders
|
|
32
|
-
`portal.docs.content` names) is listed, and each folder becomes a section. A folder's `index` page
|
|
33
|
-
comes first, then pages with an `order`, lowest first, then the rest by name:
|
|
34
|
-
|
|
35
|
-
```md
|
|
36
|
-
---
|
|
37
|
-
title: Authentication
|
|
38
|
-
order: 2
|
|
39
|
-
---
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
A page with `draft: true` is left out of deployed portals.
|
|
43
|
-
|
|
44
|
-
## File Organization
|
|
45
|
-
|
|
46
|
-
Organize your documentation files in logical directories:
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
pages/
|
|
50
|
-
├── getting-started/
|
|
51
|
-
│ ├── installation.md
|
|
52
|
-
│ └── quick-start.md
|
|
53
|
-
├── guides/
|
|
54
|
-
│ ├── authentication.md
|
|
55
|
-
│ └── deployment.md
|
|
56
|
-
└── api/
|
|
57
|
-
└── reference.md
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## What's Next?
|
|
61
|
-
|
|
62
|
-
Explore the detailed guides to enhance your documentation:
|
|
63
|
-
|
|
64
|
-
- **[Markdown Overview](/docs/markdown/overview)** - Complete markdown syntax reference
|
|
65
|
-
- **[Frontmatter](/docs/markdown/frontmatter)** - Document metadata and configuration
|
|
66
|
-
- **[MDX](/docs/markdown/mdx)** - Interactive components in markdown
|
|
67
|
-
- **[Admonitions](/docs/markdown/admonitions)** - Callouts and alerts
|
|
68
|
-
- **[Code Blocks](/docs/markdown/code-blocks)** - Syntax highlighting and features
|
|
1
|
+
---
|
|
2
|
+
title: Writing
|
|
3
|
+
sidebar_icon: book-open-text
|
|
4
|
+
description: A guide to writing documentation in APIToGo using Markdown and MDX.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Get started with creating rich documentation in APIToGo using Markdown and MDX. This guide covers
|
|
8
|
+
the essentials to help you begin documenting your project.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
1. **Create a markdown file** in `apitogo/portal/pages/`
|
|
13
|
+
2. **Add frontmatter** with title and metadata
|
|
14
|
+
3. **Write content** using Markdown or MDX
|
|
15
|
+
|
|
16
|
+
The page shows in the sidebar on its own.
|
|
17
|
+
|
|
18
|
+
### Basic Document Structure
|
|
19
|
+
|
|
20
|
+
```md
|
|
21
|
+
---
|
|
22
|
+
title: My Document
|
|
23
|
+
sidebar_icon: file-text
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Your content goes here using standard Markdown syntax.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Adding to Navigation
|
|
30
|
+
|
|
31
|
+
The sidebar is built from the files: every page in `apitogo/portal/pages/` (or the folders
|
|
32
|
+
`portal.docs.content` names) is listed, and each folder becomes a section. A folder's `index` page
|
|
33
|
+
comes first, then pages with an `order`, lowest first, then the rest by name:
|
|
34
|
+
|
|
35
|
+
```md
|
|
36
|
+
---
|
|
37
|
+
title: Authentication
|
|
38
|
+
order: 2
|
|
39
|
+
---
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A page with `draft: true` is left out of deployed portals.
|
|
43
|
+
|
|
44
|
+
## File Organization
|
|
45
|
+
|
|
46
|
+
Organize your documentation files in logical directories:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
pages/
|
|
50
|
+
├── getting-started/
|
|
51
|
+
│ ├── installation.md
|
|
52
|
+
│ └── quick-start.md
|
|
53
|
+
├── guides/
|
|
54
|
+
│ ├── authentication.md
|
|
55
|
+
│ └── deployment.md
|
|
56
|
+
└── api/
|
|
57
|
+
└── reference.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What's Next?
|
|
61
|
+
|
|
62
|
+
Explore the detailed guides to enhance your documentation:
|
|
63
|
+
|
|
64
|
+
- **[Markdown Overview](/docs/markdown/overview)** - Complete markdown syntax reference
|
|
65
|
+
- **[Frontmatter](/docs/markdown/frontmatter)** - Document metadata and configuration
|
|
66
|
+
- **[MDX](/docs/markdown/mdx)** - Interactive components in markdown
|
|
67
|
+
- **[Admonitions](/docs/markdown/admonitions)** - Callouts and alerts
|
|
68
|
+
- **[Code Blocks](/docs/markdown/code-blocks)** - Syntax highlighting and features
|
package/package.json
CHANGED
|
@@ -558,8 +558,18 @@ function syncAuthentication(normalized: ApitogoJsonConfig): void {
|
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
// `authentication.enabled: false` turns sign-in off: no provider at all, so the header shows no "Log in" and
|
|
562
|
+
// "Get started". Without this the block below gave it a dev-portal type, and the auth plugin loads a provider for
|
|
563
|
+
// any type. The user panel needs sign-in, so with the panel on it still wins.
|
|
564
|
+
if (!userPanelEnabled && authentication?.enabled === false) {
|
|
565
|
+
delete normalized.authentication;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
561
569
|
const auth = authentication ?? {};
|
|
562
|
-
|
|
570
|
+
// Only the panel sets `enabled`: writing `false` here would read as "turned off" when the config is normalized
|
|
571
|
+
// again, and drop an OpenID provider that a portal without the panel still signs in with. Readers check `=== true`.
|
|
572
|
+
if (userPanelEnabled) auth.enabled = true;
|
|
563
573
|
// A portal that signs in with an OpenID provider (the project's identity, with no plans backend) keeps it: the
|
|
564
574
|
// dev-portal defaults below (native sign-up, an empty provider list, the demo sign-in form) belong to the
|
|
565
575
|
// dev-portal provider only.
|
package/src/config/v2/resolve.ts
CHANGED
|
@@ -19,6 +19,16 @@ function fromConfig(configDir: string, value: string): string {
|
|
|
19
19
|
: toGlobPath(path.resolve(configDir, value));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A header link. `to` is where it goes. A page link also has `page`, the absolute path of its file (a React page or a
|
|
24
|
+
* Markdown page), which the portal serves at `/<file name>` (so `to` is that address) and keeps out of the docs sidebar.
|
|
25
|
+
*/
|
|
26
|
+
export type ResolvedHeaderLink = { label: string; to: string; page?: string };
|
|
27
|
+
|
|
28
|
+
/** The address of a header page: its file name without the extension, `about.tsx` is `/about`. */
|
|
29
|
+
export const headerPageRoute = (file: string): string =>
|
|
30
|
+
`/${path.basename(file).replace(/\.(mdx?|tsx)$/i, "")}`;
|
|
31
|
+
|
|
22
32
|
export type ResolvedPortal = {
|
|
23
33
|
enabled: boolean;
|
|
24
34
|
dir: string;
|
|
@@ -36,6 +46,8 @@ export type ResolvedPortal = {
|
|
|
36
46
|
landing?: string;
|
|
37
47
|
/** Absolute path of the footer component, when the portal has one (API-129). */
|
|
38
48
|
footer?: string;
|
|
49
|
+
/** Links of the header's top menu (`portal.header.links`), in their order. Empty when there are none. */
|
|
50
|
+
headerLinks: ResolvedHeaderLink[];
|
|
39
51
|
/** The header logo, when the portal has one: absolute paths (inside the portal's `public/`) or https URLs (API-129). */
|
|
40
52
|
logo?: { light: string; dark: string; alt?: string };
|
|
41
53
|
requireAuth: boolean | { exclude: string[] };
|
|
@@ -159,6 +171,13 @@ export function resolveConfigV2(
|
|
|
159
171
|
...(portal.footer
|
|
160
172
|
? { footer: path.resolve(configDir, portal.footer) }
|
|
161
173
|
: {}),
|
|
174
|
+
headerLinks: (portal.header?.links ?? []).map(
|
|
175
|
+
(link): ResolvedHeaderLink => {
|
|
176
|
+
if ("to" in link) return { label: link.label, to: link.to };
|
|
177
|
+
const page = path.resolve(configDir, link.page);
|
|
178
|
+
return { label: link.label, to: headerPageRoute(page), page };
|
|
179
|
+
},
|
|
180
|
+
),
|
|
162
181
|
...(portal.logo
|
|
163
182
|
? {
|
|
164
183
|
logo:
|
package/src/config/v2/rules.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { glob } from "glob";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { billingNeeded, stripeKeySecretName } from "./billing.js";
|
|
6
6
|
import type { ConfigIssue } from "./errors.js";
|
|
7
|
+
import { docRoutes } from "./home-redirect.js";
|
|
7
8
|
import { isLogoUrl, type ResolvedConfigV2, toGlobPath } from "./resolve.js";
|
|
8
9
|
import type { ConfigV2 } from "./schema.js";
|
|
9
10
|
import { findTsxPages } from "./tsx-pages.js";
|
|
@@ -255,6 +256,7 @@ async function portalRules(
|
|
|
255
256
|
message: `the file ${path.relative(resolved.configDir, portal.footer)} does not exist.`,
|
|
256
257
|
hint: "Create it, or remove portal.footer.",
|
|
257
258
|
});
|
|
259
|
+
await headerLinksRule(portal, resolved, out);
|
|
258
260
|
const publicDir = path.join(portal.dir, "public");
|
|
259
261
|
for (const file of new Set(
|
|
260
262
|
[portal.logo?.light, portal.logo?.dark].filter(
|
|
@@ -323,6 +325,92 @@ async function docsContentRule(
|
|
|
323
325
|
});
|
|
324
326
|
}
|
|
325
327
|
|
|
328
|
+
/** Addresses the portal itself uses, so a header page cannot take them. */
|
|
329
|
+
const RESERVED_HEADER_ROUTES = [
|
|
330
|
+
"/",
|
|
331
|
+
"/index",
|
|
332
|
+
"/api",
|
|
333
|
+
"/pricing",
|
|
334
|
+
"/account",
|
|
335
|
+
"/login",
|
|
336
|
+
"/signup",
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Header pages (`portal.header.links[].page`) are React or Markdown files the portal serves at `/<file name>`, outside
|
|
341
|
+
* the sidebar. A wrong one would fail the build or quietly take another page's address, so check says so first.
|
|
342
|
+
*/
|
|
343
|
+
async function headerLinksRule(
|
|
344
|
+
portal: NonNullable<ResolvedConfigV2["portal"]>,
|
|
345
|
+
resolved: ResolvedConfigV2,
|
|
346
|
+
out: Collector,
|
|
347
|
+
) {
|
|
348
|
+
const pages = portal.headerLinks
|
|
349
|
+
.map((link, index) => ({ link, index }))
|
|
350
|
+
.filter(({ link }) => link.page !== undefined);
|
|
351
|
+
if (pages.length === 0) return;
|
|
352
|
+
const hostsDocs = portal.docs.enabled && portal.docs.provider === "apitogo";
|
|
353
|
+
const docs = hostsDocs
|
|
354
|
+
? [
|
|
355
|
+
...(await docRoutes(portal.docs.content, { includeDrafts: true })),
|
|
356
|
+
...(await findTsxPages(portal.docs.content)).map((page) => page.route),
|
|
357
|
+
]
|
|
358
|
+
: [];
|
|
359
|
+
const seen = new Set<string>();
|
|
360
|
+
for (const { link, index } of pages) {
|
|
361
|
+
const file = link.page as string;
|
|
362
|
+
const at = `portal.header.links[${index}].page`;
|
|
363
|
+
const shown = path.relative(resolved.configDir, file);
|
|
364
|
+
if (!hostsDocs) {
|
|
365
|
+
out.error({
|
|
366
|
+
code: "PORTAL_HEADER_PAGE_NO_DOCS",
|
|
367
|
+
path: at,
|
|
368
|
+
message: `the header page ${shown} needs hosted docs, which this portal does not have.`,
|
|
369
|
+
hint: 'Header pages are served with the docs pages: set portal.docs.provider to "apitogo", or link an address with "to" instead.',
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!/\.(mdx?|tsx)$/i.test(file)) {
|
|
374
|
+
out.error({
|
|
375
|
+
code: "PORTAL_HEADER_PAGE_INVALID",
|
|
376
|
+
path: at,
|
|
377
|
+
message: `the header page ${shown} is neither a React page nor a Markdown page.`,
|
|
378
|
+
hint: "Use a .tsx file (its default export is the page) or a .md / .mdx file.",
|
|
379
|
+
});
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!(await isFile(file))) {
|
|
383
|
+
out.error({
|
|
384
|
+
code: "PORTAL_FILE_MISSING",
|
|
385
|
+
path: at,
|
|
386
|
+
message: `the file ${shown} does not exist.`,
|
|
387
|
+
hint: "Create it (`apitogo config --page-slug <name> --page-title <title> --page-header`), or remove the link.",
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const route = link.to;
|
|
392
|
+
if (RESERVED_HEADER_ROUTES.includes(route.toLowerCase())) {
|
|
393
|
+
out.error({
|
|
394
|
+
code: "PORTAL_HEADER_PAGE_ROUTE_TAKEN",
|
|
395
|
+
path: at,
|
|
396
|
+
message: `the header page ${shown} would be served at ${route}, which the portal uses itself.`,
|
|
397
|
+
hint: "Rename the file: its name is the page's address.",
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (docs.includes(route) || seen.has(route)) {
|
|
402
|
+
out.error({
|
|
403
|
+
code: "PORTAL_HEADER_PAGE_ROUTE_TAKEN",
|
|
404
|
+
path: at,
|
|
405
|
+
message: `the header page ${shown} would be served at ${route}, which another page already has.`,
|
|
406
|
+
hint: 'Rename the file: its name is the page\'s address. A docs page that should be in the header too can be linked with "to" instead.',
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
seen.add(route);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
326
414
|
// --- gateway (2.4) --------------------------------------------------------------------------------------------------
|
|
327
415
|
|
|
328
416
|
function isIpLiteral(hostname: string): boolean {
|
package/src/config/v2/schema.ts
CHANGED
|
@@ -31,6 +31,32 @@ const pathList = z.array(nonEmpty);
|
|
|
31
31
|
|
|
32
32
|
const enabled = z.boolean().optional();
|
|
33
33
|
|
|
34
|
+
const headerLabel = z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(1, { error: "must not be empty" })
|
|
38
|
+
.max(40, { error: "must be 40 characters or fewer" });
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A link in the portal header's top menu: a page of its own (a React `.tsx` or Markdown file, served at `/<file name>`
|
|
42
|
+
* and kept out of the docs sidebar) or an address (`/path` on the portal, or `https://` elsewhere).
|
|
43
|
+
*/
|
|
44
|
+
const HeaderLinkSchema = z.union(
|
|
45
|
+
[
|
|
46
|
+
z.strictObject({ label: headerLabel, page: path }),
|
|
47
|
+
z.strictObject({
|
|
48
|
+
label: headerLabel,
|
|
49
|
+
to: z.string().regex(/^(\/(?!\/)|https:\/\/)/, {
|
|
50
|
+
error: 'must start with "/" or "https://"',
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
{
|
|
55
|
+
error:
|
|
56
|
+
'must be { "label": "...", "page": "path/to/page.tsx" } or { "label": "...", "to": "/path or https://..." }',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
34
60
|
export const PortalSchema = z.strictObject({
|
|
35
61
|
enabled,
|
|
36
62
|
dir: path.optional(),
|
|
@@ -49,6 +75,8 @@ export const PortalSchema = z.strictObject({
|
|
|
49
75
|
landing: path.optional(),
|
|
50
76
|
/** The footer: a React component file shown under every page except the console's (API-129). */
|
|
51
77
|
footer: path.optional(),
|
|
78
|
+
/** Links of the header's top menu, after the built-in ones (Documentation, API Reference; the plans add Pricing). */
|
|
79
|
+
header: z.strictObject({ links: z.array(HeaderLinkSchema) }).optional(),
|
|
52
80
|
/**
|
|
53
81
|
* The logo in the portal header: one image, or one per color scheme (API-129). `dark` is optional; without it the
|
|
54
82
|
* light image is used in both.
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./manifest.js";
|
|
25
25
|
import { type PortalProfile, portalProfile } from "./portal-profile.js";
|
|
26
26
|
import { protectedRoutesFor } from "./protected-routes.js";
|
|
27
|
-
import { isLogoUrl, type ResolvedConfigV2 } from "./resolve.js";
|
|
27
|
+
import { isLogoUrl, type ResolvedConfigV2, toGlobPath } from "./resolve.js";
|
|
28
28
|
import { findTsxPages, TSX_PAGE_ELEMENT_PREFIX } from "./tsx-pages.js";
|
|
29
29
|
|
|
30
30
|
export const WRAPPER_FILE = "apitogo.config.tsx";
|
|
@@ -71,6 +71,8 @@ const PLANS_CONSOLE_CAPABILITIES = [
|
|
|
71
71
|
export type PortalPlan = {
|
|
72
72
|
/** Docs pages, navigation and search APItoGo builds. */
|
|
73
73
|
docs: boolean;
|
|
74
|
+
/** The search box the site shows; only for docs APItoGo hosts. Absent: the site has no search. */
|
|
75
|
+
search?: "pagefind";
|
|
74
76
|
/** OpenAPI reference pages and playground, built from the gateway's OpenAPI file. */
|
|
75
77
|
apiReference: boolean;
|
|
76
78
|
/** Where the reference points its calls, and the header of the API key; set when `apiReference` is true. */
|
|
@@ -215,6 +217,12 @@ export function planPortal(
|
|
|
215
217
|
|
|
216
218
|
if (!profile.enabled) return plan;
|
|
217
219
|
|
|
220
|
+
// The build already writes the Pagefind index for hosted docs; the site shows the search box only when its config
|
|
221
|
+
// names the search. Inkeep needs keys that `portal.search` cannot carry yet, so it is left out, not half-registered.
|
|
222
|
+
if (profile.pipelines.search === "pagefind") plan.search = "pagefind";
|
|
223
|
+
else if (profile.pipelines.search === "inkeep")
|
|
224
|
+
skip("search", "inkeep needs keys that portal.search cannot hold yet");
|
|
225
|
+
|
|
218
226
|
// The API page is built from the gateway's OpenAPI file and calls the gateway, so it needs the gateway's address.
|
|
219
227
|
if (profile.pipelines.openapi) {
|
|
220
228
|
if (!gateway) skip("api reference", "services.json has no gateway entry");
|
|
@@ -314,6 +322,29 @@ export const REFERENCE_SPEC_FILE = "openapi/gateway.json";
|
|
|
314
322
|
/** Route of the API reference page. */
|
|
315
323
|
export const API_REFERENCE_PATH = "api";
|
|
316
324
|
|
|
325
|
+
const MARKDOWN_PAGE = /\.mdx?$/i;
|
|
326
|
+
|
|
327
|
+
/** Placeholder of a React header page in the site config; the wrapper puts the component in its place. */
|
|
328
|
+
export const HEADER_PAGE_ELEMENT_PREFIX = "apitogo:header-page:";
|
|
329
|
+
|
|
330
|
+
/** The React header pages (`portal.header.links[].page` ending in `.tsx`), each with its wrapper placeholder. */
|
|
331
|
+
export function headerTsxPages(
|
|
332
|
+
resolved: ResolvedConfigV2,
|
|
333
|
+
plan: PortalPlan,
|
|
334
|
+
): { id: string; route: string; label: string; file: string }[] {
|
|
335
|
+
if (!plan.docs) return [];
|
|
336
|
+
return (resolved.portal?.headerLinks ?? [])
|
|
337
|
+
.flatMap((link) =>
|
|
338
|
+
link.page && /\.tsx$/i.test(link.page)
|
|
339
|
+
? [{ route: link.to, label: link.label, file: link.page }]
|
|
340
|
+
: [],
|
|
341
|
+
)
|
|
342
|
+
.map((page, index) => ({
|
|
343
|
+
...page,
|
|
344
|
+
id: `${HEADER_PAGE_ELEMENT_PREFIX}${index}`,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
|
|
317
348
|
/**
|
|
318
349
|
* The site settings (title, modules, docs globs, theme, sign-in) in the shape the existing build reads from
|
|
319
350
|
* `apitogo.config.json`. It is generated, never edited, and lives in `generated/`.
|
|
@@ -334,9 +365,31 @@ export function buildSiteConfig(
|
|
|
334
365
|
): Record<string, unknown> {
|
|
335
366
|
const portal = resolved.portal;
|
|
336
367
|
const redirects = extras.redirects ?? [];
|
|
368
|
+
// Header pages are pages the sidebar leaves out. A Markdown one is served by the docs plugin from its own file; a React
|
|
369
|
+
// one is a hidden custom page, whose placeholder the wrapper replaces with the component. Both need hosted docs.
|
|
370
|
+
const headerLinks = portal?.headerLinks ?? [];
|
|
371
|
+
const headerPages = plan.docs
|
|
372
|
+
? headerLinks.flatMap((link) =>
|
|
373
|
+
link.page && MARKDOWN_PAGE.test(link.page) ? [link.page] : [],
|
|
374
|
+
)
|
|
375
|
+
: [];
|
|
376
|
+
const shownHeaderLinks = headerLinks
|
|
377
|
+
.filter((link) => !link.page || plan.docs)
|
|
378
|
+
.map((link) => ({
|
|
379
|
+
label: link.label,
|
|
380
|
+
to: link.to,
|
|
381
|
+
...(link.to.startsWith("https://") ? { target: "_blank" } : {}),
|
|
382
|
+
}));
|
|
337
383
|
// One header tab for the docs sidebar, and the API reference as a link next to it.
|
|
338
384
|
const navigation = [
|
|
339
385
|
...(extras.docsNavigation ?? []),
|
|
386
|
+
...headerTsxPages(resolved, plan).map((page) => ({
|
|
387
|
+
type: "custom-page",
|
|
388
|
+
path: page.route,
|
|
389
|
+
label: page.label,
|
|
390
|
+
element: page.id,
|
|
391
|
+
display: "hide",
|
|
392
|
+
})),
|
|
340
393
|
...(plan.apiReference
|
|
341
394
|
? [
|
|
342
395
|
{
|
|
@@ -384,12 +437,14 @@ export function buildSiteConfig(
|
|
|
384
437
|
...(plan.docs
|
|
385
438
|
? {
|
|
386
439
|
docs: {
|
|
387
|
-
files:
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
files: [
|
|
441
|
+
...(portal?.docs.content ?? []),
|
|
442
|
+
...headerPages.map(toGlobPath),
|
|
443
|
+
].map((glob) => contentGlob(resolved.configDir, glob)),
|
|
390
444
|
},
|
|
391
445
|
}
|
|
392
446
|
: {}),
|
|
447
|
+
...(plan.search ? { search: { type: plan.search } } : {}),
|
|
393
448
|
...(plan.apiReference
|
|
394
449
|
? {
|
|
395
450
|
apis: [
|
|
@@ -403,15 +458,18 @@ export function buildSiteConfig(
|
|
|
403
458
|
: {}),
|
|
404
459
|
...(navigation.length > 0 ? { navigation } : {}),
|
|
405
460
|
// The sidebar link above is only seen inside the docs; the header is what every page shows. A header navigation
|
|
406
|
-
// replaces the docs tab, so the docs get a header link of their own.
|
|
407
|
-
...(plan.apiReference
|
|
461
|
+
// replaces the docs tab, so the docs get a header link of their own. The tenant's own links (portal.header) follow.
|
|
462
|
+
...(plan.apiReference || shownHeaderLinks.length > 0
|
|
408
463
|
? {
|
|
409
464
|
header: {
|
|
410
465
|
navigation: [
|
|
411
466
|
...(plan.docs && extras.docsPath
|
|
412
467
|
? [{ label: "Documentation", to: extras.docsPath }]
|
|
413
468
|
: []),
|
|
414
|
-
|
|
469
|
+
...(plan.apiReference
|
|
470
|
+
? [{ label: "API Reference", to: `/${API_REFERENCE_PATH}` }]
|
|
471
|
+
: []),
|
|
472
|
+
...shownHeaderLinks,
|
|
415
473
|
],
|
|
416
474
|
},
|
|
417
475
|
}
|
|
@@ -755,13 +813,19 @@ export async function preparePortal(
|
|
|
755
813
|
(page) => !markdownRoutes.includes(page.route),
|
|
756
814
|
)
|
|
757
815
|
: [];
|
|
816
|
+
const importPathOf = (file: string) =>
|
|
817
|
+
`./${CONTENT_LINK}/${path
|
|
818
|
+
.relative(resolved.configDir, file)
|
|
819
|
+
.split(path.sep)
|
|
820
|
+
.join("/")}`;
|
|
758
821
|
const wrapperPages: WrapperPage[] = tsxPages.map((page, index) => ({
|
|
759
822
|
id: `${TSX_PAGE_ELEMENT_PREFIX}${index}`,
|
|
760
|
-
importPath:
|
|
761
|
-
.relative(resolved.configDir, page.file)
|
|
762
|
-
.split(path.sep)
|
|
763
|
-
.join("/")}`,
|
|
823
|
+
importPath: importPathOf(page.file),
|
|
764
824
|
}));
|
|
825
|
+
// React header pages are imported the same way; their placeholders are in the navigation, outside the sidebar.
|
|
826
|
+
const headerWrapperPages: WrapperPage[] = headerTsxPages(resolved, plan).map(
|
|
827
|
+
(page) => ({ id: page.id, importPath: importPathOf(page.file) }),
|
|
828
|
+
);
|
|
765
829
|
const hostedDocRoutes = [
|
|
766
830
|
...new Set([...markdownRoutes, ...tsxPages.map((page) => page.route)]),
|
|
767
831
|
].sort();
|
|
@@ -863,7 +927,15 @@ export async function preparePortal(
|
|
|
863
927
|
protectedRoutes: protectedRoutesFor({
|
|
864
928
|
requireAuth: resolved.portal?.requireAuth,
|
|
865
929
|
hasAuth: !!plan.auth,
|
|
866
|
-
|
|
930
|
+
// A header page is a docs page too, so a portal behind login keeps it behind login.
|
|
931
|
+
docRoutes: plan.docs
|
|
932
|
+
? [
|
|
933
|
+
...hostedDocRoutes,
|
|
934
|
+
...(resolved.portal?.headerLinks ?? []).flatMap((link) =>
|
|
935
|
+
link.page ? [link.to] : [],
|
|
936
|
+
),
|
|
937
|
+
]
|
|
938
|
+
: hostedDocRoutes,
|
|
867
939
|
apiReferencePath: plan.apiReference
|
|
868
940
|
? `/${API_REFERENCE_PATH}`
|
|
869
941
|
: undefined,
|
|
@@ -872,7 +944,7 @@ export async function preparePortal(
|
|
|
872
944
|
);
|
|
873
945
|
const wrapper = renderWrapper(
|
|
874
946
|
plan,
|
|
875
|
-
wrapperPages,
|
|
947
|
+
[...wrapperPages, ...headerWrapperPages],
|
|
876
948
|
resolved.portal?.footer
|
|
877
949
|
? wrapperImport(contentFile(resolved.configDir, resolved.portal.footer))
|
|
878
950
|
: undefined,
|