radiant-docs 0.1.34 → 0.1.37
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/package.json +1 -1
- package/template/astro.config.mjs +25 -0
- package/template/package-lock.json +1027 -513
- package/template/package.json +3 -2
- package/template/scripts/generate-proxy-allowed-origins.mjs +217 -0
- package/template/scripts/generate-robots-txt.mjs +19 -0
- package/template/scripts/stamp-image-versions.mjs +63 -11
- package/template/src/components/Header.astro +4 -4
- package/template/src/components/LogoLink.astro +2 -1
- package/template/src/components/SidebarDropdown.astro +3 -3
- package/template/src/components/SidebarGroup.astro +3 -0
- package/template/src/components/SidebarMenu.astro +14 -1
- package/template/src/components/SidebarSubgroup.astro +35 -12
- package/template/src/components/chat/AskAiWidget.tsx +7 -3
- package/template/src/components/endpoint/PlaygroundButton.astro +2 -2
- package/template/src/components/endpoint/PlaygroundForm.astro +15 -17
- package/template/src/components/sidebar/SidebarEndpointLink.astro +18 -15
- package/template/src/components/sidebar/SidebarOpenApiPageLink.astro +56 -0
- package/template/src/components/ui/Icon.astro +2 -1
- package/template/src/components/user/Image.astro +4 -0
- package/template/src/layouts/Layout.astro +8 -3
- package/template/src/lib/pagefind.ts +2 -1
- package/template/src/lib/routes.ts +134 -58
- package/template/src/lib/static-asset-url.ts +62 -0
- package/template/src/lib/utils.ts +48 -0
- package/template/src/lib/validation.ts +115 -27
- package/template/scripts/rewrite-static-asset-host.mjs +0 -408
package/template/package.json
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
"dev": "astro dev",
|
|
7
7
|
"start": "tsx runner.ts",
|
|
8
8
|
"prebuild": "rm -rf public/pagefind",
|
|
9
|
-
"build": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/
|
|
9
|
+
"build": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/generate-proxy-allowed-origins.mjs && node scripts/generate-robots-txt.mjs",
|
|
10
10
|
"preview": "astro preview",
|
|
11
11
|
"astro": "astro",
|
|
12
|
-
"search:index": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/
|
|
12
|
+
"search:index": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/generate-proxy-allowed-origins.mjs && node scripts/generate-robots-txt.mjs && cp -r dist/pagefind public/"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@alpinejs/collapse": "^3.15.2",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@astrojs/alpinejs": "^0.4.9",
|
|
19
19
|
"@astrojs/mdx": "^4.3.12",
|
|
20
20
|
"@astrojs/preact": "^4.1.3",
|
|
21
|
+
"@astrojs/sitemap": "^3.7.2",
|
|
21
22
|
"@aws-sdk/client-s3": "^3.964.0",
|
|
22
23
|
"@fontsource/google-sans-flex": "^5.2.2",
|
|
23
24
|
"@iconify-json/lucide": "^1.2.79",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
|
|
5
|
+
const CWD = process.cwd();
|
|
6
|
+
const DOCS_DIR = path.join(CWD, "src/content/docs");
|
|
7
|
+
const DOCS_CONFIG_PATH = path.join(DOCS_DIR, "docs.json");
|
|
8
|
+
const OUTPUT_DIR = path.join(CWD, ".radiant");
|
|
9
|
+
const OUTPUT_FILE = path.join(OUTPUT_DIR, "proxy-allowed-origins.json");
|
|
10
|
+
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHttpUrl(value) {
|
|
16
|
+
return /^https?:\/\//i.test(String(value).trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectOpenApiSourcesFromNavigation(navigationNode, target) {
|
|
20
|
+
if (!isRecord(navigationNode)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const openApiConfig = navigationNode.openapi;
|
|
25
|
+
if (typeof openApiConfig === "string") {
|
|
26
|
+
const trimmed = openApiConfig.trim();
|
|
27
|
+
if (trimmed) {
|
|
28
|
+
target.add(trimmed);
|
|
29
|
+
}
|
|
30
|
+
} else if (
|
|
31
|
+
isRecord(openApiConfig) &&
|
|
32
|
+
typeof openApiConfig.source === "string"
|
|
33
|
+
) {
|
|
34
|
+
const trimmed = openApiConfig.source.trim();
|
|
35
|
+
if (trimmed) {
|
|
36
|
+
target.add(trimmed);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const menu = navigationNode.menu;
|
|
41
|
+
if (!isRecord(menu) || !Array.isArray(menu.items)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const menuItem of menu.items) {
|
|
46
|
+
if (!isRecord(menuItem) || !isRecord(menuItem.submenu)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
collectOpenApiSourcesFromNavigation(menuItem.submenu, target);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveServerTemplateUrl(rawUrl, rawVariables) {
|
|
54
|
+
let unresolved = false;
|
|
55
|
+
const variables = isRecord(rawVariables) ? rawVariables : null;
|
|
56
|
+
|
|
57
|
+
const resolved = rawUrl.replace(/\{([^}]+)\}/g, (_match, tokenName) => {
|
|
58
|
+
if (!variables) {
|
|
59
|
+
unresolved = true;
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const variableConfig = variables[tokenName];
|
|
64
|
+
if (
|
|
65
|
+
!isRecord(variableConfig) ||
|
|
66
|
+
typeof variableConfig.default !== "string"
|
|
67
|
+
) {
|
|
68
|
+
unresolved = true;
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const defaultValue = variableConfig.default.trim();
|
|
73
|
+
if (!defaultValue) {
|
|
74
|
+
unresolved = true;
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return defaultValue;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (unresolved) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeAllowedOrigin(rawUrl) {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = new URL(rawUrl);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (parsed.protocol !== "https:") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!parsed.hostname || parsed.username || parsed.password) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parsed.origin.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadOpenApiSpec(source) {
|
|
108
|
+
let fileContent;
|
|
109
|
+
|
|
110
|
+
if (isHttpUrl(source)) {
|
|
111
|
+
const response = await fetch(source);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Failed to fetch OpenAPI spec (${response.status} ${response.statusText})`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
fileContent = await response.text();
|
|
118
|
+
} else {
|
|
119
|
+
const absolutePath = path.join(DOCS_DIR, source);
|
|
120
|
+
fileContent = await fs.readFile(absolutePath, "utf8");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const trimmed = fileContent.trim();
|
|
124
|
+
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
|
125
|
+
throw new Error("OpenAPI source returned HTML instead of JSON or YAML");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(fileContent);
|
|
130
|
+
} catch {
|
|
131
|
+
return YAML.parse(fileContent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function buildAllowedOrigins() {
|
|
136
|
+
const docsConfigRaw = await fs.readFile(DOCS_CONFIG_PATH, "utf8");
|
|
137
|
+
const docsConfig = JSON.parse(docsConfigRaw);
|
|
138
|
+
|
|
139
|
+
if (!isRecord(docsConfig) || !isRecord(docsConfig.navigation)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sources = new Set();
|
|
144
|
+
collectOpenApiSourcesFromNavigation(docsConfig.navigation, sources);
|
|
145
|
+
if (sources.size === 0) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const allowedOrigins = new Set();
|
|
150
|
+
|
|
151
|
+
for (const source of sources) {
|
|
152
|
+
try {
|
|
153
|
+
const spec = await loadOpenApiSpec(source);
|
|
154
|
+
const servers =
|
|
155
|
+
isRecord(spec) && Array.isArray(spec.servers) ? spec.servers : [];
|
|
156
|
+
|
|
157
|
+
for (const server of servers) {
|
|
158
|
+
if (!isRecord(server) || typeof server.url !== "string") {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const resolvedUrl = resolveServerTemplateUrl(
|
|
163
|
+
server.url,
|
|
164
|
+
server.variables,
|
|
165
|
+
);
|
|
166
|
+
if (!resolvedUrl) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedOrigin = normalizeAllowedOrigin(resolvedUrl);
|
|
171
|
+
if (normalizedOrigin) {
|
|
172
|
+
allowedOrigins.add(normalizedOrigin);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn(
|
|
177
|
+
`⚠️ Failed to extract OpenAPI server origins from "${source}":`,
|
|
178
|
+
error instanceof Error ? error.message : String(error),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Array.from(allowedOrigins).sort();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function run() {
|
|
187
|
+
let allowedOrigins = [];
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
allowedOrigins = await buildAllowedOrigins();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(
|
|
193
|
+
"⚠️ Failed to generate proxy allowed origins. Falling back to empty allowlist.",
|
|
194
|
+
error instanceof Error ? error.message : String(error),
|
|
195
|
+
);
|
|
196
|
+
allowedOrigins = [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
|
200
|
+
await fs.writeFile(
|
|
201
|
+
OUTPUT_FILE,
|
|
202
|
+
`${JSON.stringify(allowedOrigins, null, 2)}\n`,
|
|
203
|
+
"utf8",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
console.log(
|
|
207
|
+
`✅ Wrote ${allowedOrigins.length} proxy allowed origins to ${path.relative(CWD, OUTPUT_FILE)}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
run().catch((error) => {
|
|
212
|
+
console.error(
|
|
213
|
+
"❌ Failed to write proxy allowed origins manifest:",
|
|
214
|
+
error instanceof Error ? error.message : String(error),
|
|
215
|
+
);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const CWD = process.cwd();
|
|
5
|
+
const DIST_DIR = path.join(CWD, "dist");
|
|
6
|
+
const ROBOTS_TXT_PATH = path.join(DIST_DIR, "robots.txt");
|
|
7
|
+
|
|
8
|
+
function main() {
|
|
9
|
+
if (!fs.existsSync(DIST_DIR)) {
|
|
10
|
+
console.log("Skipping robots.txt generation: dist directory not found.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const robotsTxt = "User-agent: *\nAllow: /\nSitemap: /sitemap-index.xml\n";
|
|
15
|
+
fs.writeFileSync(ROBOTS_TXT_PATH, robotsTxt, "utf8");
|
|
16
|
+
console.log("✅ robots.txt generated.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main();
|
|
@@ -6,6 +6,28 @@ const CWD = process.cwd();
|
|
|
6
6
|
const DIST_DIR = path.join(CWD, "dist");
|
|
7
7
|
const VERSION_LENGTH = 12;
|
|
8
8
|
const DIST_ROOT = path.resolve(DIST_DIR);
|
|
9
|
+
const CONFIGURED_STATIC_HOST = (() => {
|
|
10
|
+
const raw =
|
|
11
|
+
typeof process.env.STATIC_ASSET_HOST === "string"
|
|
12
|
+
? process.env.STATIC_ASSET_HOST.trim()
|
|
13
|
+
: "";
|
|
14
|
+
if (!raw) return null;
|
|
15
|
+
const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(raw)
|
|
16
|
+
? raw
|
|
17
|
+
: `https://${raw}`;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(withScheme);
|
|
21
|
+
return parsed.hostname.toLowerCase();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
const CONFIGURED_PREFIX =
|
|
27
|
+
typeof process.env.R2_BUCKET_PREFIX === "string" &&
|
|
28
|
+
process.env.R2_BUCKET_PREFIX.trim().length > 0
|
|
29
|
+
? process.env.R2_BUCKET_PREFIX.trim().replace(/^\/+|\/+$/g, "")
|
|
30
|
+
: null;
|
|
9
31
|
const VERSIONED_EXTENSIONS = new Set([
|
|
10
32
|
".avif",
|
|
11
33
|
".gif",
|
|
@@ -76,6 +98,24 @@ function getHash(filePath) {
|
|
|
76
98
|
return hash;
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
function normalizePathForDistLookup(pathname) {
|
|
102
|
+
const normalizedPathname = path.posix.normalize(pathname);
|
|
103
|
+
if (!CONFIGURED_PREFIX) {
|
|
104
|
+
return normalizedPathname;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prefix = `/${CONFIGURED_PREFIX}`;
|
|
108
|
+
if (
|
|
109
|
+
normalizedPathname === prefix ||
|
|
110
|
+
normalizedPathname.startsWith(`${prefix}/`)
|
|
111
|
+
) {
|
|
112
|
+
const stripped = normalizedPathname.slice(prefix.length);
|
|
113
|
+
return stripped.startsWith("/") ? stripped : `/${stripped}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return normalizedPathname;
|
|
117
|
+
}
|
|
118
|
+
|
|
79
119
|
function resolveLocalImagePath(urlValue, filePath) {
|
|
80
120
|
const trimmed = urlValue.trim();
|
|
81
121
|
if (!trimmed) return null;
|
|
@@ -91,11 +131,6 @@ function resolveLocalImagePath(urlValue, filePath) {
|
|
|
91
131
|
return null;
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
// Ignore fully-qualified URLs.
|
|
95
|
-
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded)) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
134
|
let parsed;
|
|
100
135
|
try {
|
|
101
136
|
parsed = new URL(decoded, `https://radiant.invalid${htmlBasePathname(filePath)}`);
|
|
@@ -103,13 +138,27 @@ function resolveLocalImagePath(urlValue, filePath) {
|
|
|
103
138
|
return null;
|
|
104
139
|
}
|
|
105
140
|
|
|
141
|
+
const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded);
|
|
142
|
+
if (isAbsoluteUrl) {
|
|
143
|
+
if (!CONFIGURED_STATIC_HOST) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (parsed.protocol !== "https:") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (parsed.hostname.toLowerCase() !== CONFIGURED_STATIC_HOST) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
const extension = path.extname(parsed.pathname).toLowerCase();
|
|
107
157
|
if (!VERSIONED_EXTENSIONS.has(extension)) {
|
|
108
158
|
return null;
|
|
109
159
|
}
|
|
110
160
|
|
|
111
|
-
const normalizedPathname =
|
|
112
|
-
.normalize(parsed.pathname)
|
|
161
|
+
const normalizedPathname = normalizePathForDistLookup(parsed.pathname)
|
|
113
162
|
.replace(/^\/+/, "");
|
|
114
163
|
const absolutePath = path.resolve(DIST_DIR, normalizedPathname);
|
|
115
164
|
|
|
@@ -124,12 +173,15 @@ function resolveLocalImagePath(urlValue, filePath) {
|
|
|
124
173
|
return null;
|
|
125
174
|
}
|
|
126
175
|
|
|
127
|
-
return { parsed, absolutePath };
|
|
176
|
+
return { parsed, absolutePath, isAbsoluteUrl };
|
|
128
177
|
}
|
|
129
178
|
|
|
130
|
-
function formatUpdatedUrl(parsed) {
|
|
179
|
+
function formatUpdatedUrl(parsed, isAbsoluteUrl) {
|
|
131
180
|
const pathnameWithParams = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
132
|
-
|
|
181
|
+
const fullValue = isAbsoluteUrl
|
|
182
|
+
? `${parsed.origin}${pathnameWithParams}`
|
|
183
|
+
: pathnameWithParams;
|
|
184
|
+
return fullValue.replace(/&/g, "&");
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
function stampSingleUrl(urlValue, filePath) {
|
|
@@ -141,7 +193,7 @@ function stampSingleUrl(urlValue, filePath) {
|
|
|
141
193
|
const version = getHash(resolved.absolutePath);
|
|
142
194
|
resolved.parsed.searchParams.set("v", version);
|
|
143
195
|
|
|
144
|
-
const updated = formatUpdatedUrl(resolved.parsed);
|
|
196
|
+
const updated = formatUpdatedUrl(resolved.parsed, resolved.isAbsoluteUrl);
|
|
145
197
|
return { value: updated, changed: updated !== urlValue };
|
|
146
198
|
}
|
|
147
199
|
|
|
@@ -58,14 +58,14 @@ const config = await getConfig();
|
|
|
58
58
|
showAskAiTrigger ? (
|
|
59
59
|
<button
|
|
60
60
|
type="button"
|
|
61
|
-
class="hidden md:inline-flex items-center gap-
|
|
61
|
+
class="hidden md:inline-flex items-center gap-2 h-8 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 px-3 text-[13px] font-[350] text-white shadow-sm dark:bg-white dark:text-neutral-900 cursor-pointer"
|
|
62
62
|
onclick="window.dispatchEvent(new CustomEvent('ask-ai:open'));"
|
|
63
63
|
>
|
|
64
64
|
<img
|
|
65
65
|
src={sparkleIcon}
|
|
66
66
|
alt=""
|
|
67
67
|
aria-hidden="true"
|
|
68
|
-
class="size-3 invert dark:invert-0"
|
|
68
|
+
class="size-3 -ml-px invert dark:invert-0"
|
|
69
69
|
/>
|
|
70
70
|
Ask AI
|
|
71
71
|
</button>
|
|
@@ -110,7 +110,7 @@ const config = await getConfig();
|
|
|
110
110
|
{config.navbar.secondary && (
|
|
111
111
|
<a
|
|
112
112
|
class:list={[
|
|
113
|
-
"h-[33px] items-center gap-1.5 px-
|
|
113
|
+
"h-[33px] items-center gap-1.5 px-[11px] text-[13px] bg-white/90 text-neutral-600/85 hover:text-neutral-600 rounded-lg [corner-shape:superellipse(1.2)] border border-neutral-200 shadow-xs hover:shadow-sm. transition-all whitespace-nowrap",
|
|
114
114
|
config.navbar.primary ? "hidden lg:flex" : "flex",
|
|
115
115
|
]}
|
|
116
116
|
href={config.navbar.secondary.href}
|
|
@@ -129,7 +129,7 @@ const config = await getConfig();
|
|
|
129
129
|
{config.navbar.primary && (
|
|
130
130
|
<a
|
|
131
131
|
class:list={[
|
|
132
|
-
"h-8 flex items-center gap-2 px-3 text-[13px] rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 text-white duration-200 shadow-sm transition-all whitespace-nowrap",
|
|
132
|
+
"font-[350] h-8 flex items-center gap-2 px-3 text-[13px] rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 text-white duration-200 shadow-sm transition-all whitespace-nowrap",
|
|
133
133
|
]}
|
|
134
134
|
href={config.navbar.primary.href}
|
|
135
135
|
>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getConfig, type LogoVariant } from "../lib/validation";
|
|
3
|
+
import { resolveStaticAssetUrl } from "../lib/static-asset-url";
|
|
3
4
|
|
|
4
5
|
const config = await getConfig();
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ function getLogoUrl(logoPath: string | undefined): string | null {
|
|
|
12
13
|
: logoPath;
|
|
13
14
|
|
|
14
15
|
// Return public URL - Vite plugin ensures file exists in public
|
|
15
|
-
return `/${normalizedPath}
|
|
16
|
+
return resolveStaticAssetUrl(`/${normalizedPath}`);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function resolveLogoVariant(
|
|
@@ -72,7 +72,7 @@ const currentPrefix = parentSlug
|
|
|
72
72
|
<div
|
|
73
73
|
class:list={[
|
|
74
74
|
"mt-3 mx-2 z-10 relative",
|
|
75
|
-
menu.label && "rounded-lg bg-neutral-100 p-[3px]",
|
|
75
|
+
menu.label && "rounded-lg bg-neutral-100 dark:bg-neutral-800/60 p-[3px]",
|
|
76
76
|
]}
|
|
77
77
|
>
|
|
78
78
|
{
|
|
@@ -84,7 +84,7 @@ const currentPrefix = parentSlug
|
|
|
84
84
|
}
|
|
85
85
|
<div class="relative">
|
|
86
86
|
<button
|
|
87
|
-
class="flex items-center w-full text-sm text-neutral-700 bg-white border-t border-x border-neutral-200/70 rounded-lg shadow-
|
|
87
|
+
class="flex items-center w-full text-sm text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-700/50 border-t border-x border-neutral-200/70 dark:border-neutral-700/40 dark:border-b rounded-lg shadow-xs px-3 py-2 cursor-pointer"
|
|
88
88
|
x-on:click="open = true"
|
|
89
89
|
aria-haspopup="menu"
|
|
90
90
|
aria-expanded
|
|
@@ -104,7 +104,7 @@ const currentPrefix = parentSlug
|
|
|
104
104
|
</div>
|
|
105
105
|
<ul
|
|
106
106
|
class:list={[
|
|
107
|
-
"z-50 absolute bg-white border border-neutral-200 rounded-lg inset-x-0 top-full py-[3px] shadow-xl overflow-hidden",
|
|
107
|
+
"z-50 absolute bg-white dark:bg-neutral-700/50 border border-neutral-200 rounded-lg inset-x-0 top-full py-[3px] shadow-xl overflow-hidden",
|
|
108
108
|
menu.label ? "mt-1.5" : "mt-1",
|
|
109
109
|
]}
|
|
110
110
|
x-init
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { NavGroup } from "../lib/validation";
|
|
3
3
|
import SidebarLink from "./SidebarLink.astro";
|
|
4
|
+
import SidebarOpenApiPageLink from "./sidebar/SidebarOpenApiPageLink.astro";
|
|
4
5
|
import SidebarSubgroup from "./SidebarSubgroup.astro";
|
|
5
6
|
import { slugify } from "../lib/utils";
|
|
6
7
|
import Tag from "./ui/Tag.astro";
|
|
@@ -38,6 +39,8 @@ const currentPrefix = parentSlug ? `${parentSlug}/${groupSlug}` : groupSlug;
|
|
|
38
39
|
title={child.title}
|
|
39
40
|
groupSlug={currentPrefix}
|
|
40
41
|
/>
|
|
42
|
+
) : "openapi" in child ? (
|
|
43
|
+
<SidebarOpenApiPageLink item={child} parentSlug={currentPrefix} />
|
|
41
44
|
) : (
|
|
42
45
|
<SidebarSubgroup item={child} parentSlug={currentPrefix} />
|
|
43
46
|
)}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
NavGroup,
|
|
4
|
+
NavigationItem,
|
|
5
|
+
NavOpenApiPage,
|
|
6
|
+
NavPage,
|
|
7
|
+
} from "../lib/validation";
|
|
3
8
|
import SidebarOpenApi from "./sidebar/SidebarOpenApi.astro";
|
|
9
|
+
import SidebarOpenApiPageLink from "./sidebar/SidebarOpenApiPageLink.astro";
|
|
4
10
|
import SidebarDropdown from "./SidebarDropdown.astro";
|
|
5
11
|
import SidebarSegmented from "./SidebarSegmented.astro";
|
|
6
12
|
import SidebarGroup from "./SidebarGroup.astro";
|
|
@@ -24,6 +30,13 @@ let { navigation, parentSlug = "" } = Astro.props;
|
|
|
24
30
|
</li>
|
|
25
31
|
) : "group" in item ? (
|
|
26
32
|
<SidebarGroup item={item as NavGroup} parentSlug={parentSlug} />
|
|
33
|
+
) : "openapi" in item ? (
|
|
34
|
+
<li>
|
|
35
|
+
<SidebarOpenApiPageLink
|
|
36
|
+
item={item as NavOpenApiPage}
|
|
37
|
+
parentSlug={parentSlug}
|
|
38
|
+
/>
|
|
39
|
+
</li>
|
|
27
40
|
) : (
|
|
28
41
|
<li>
|
|
29
42
|
<SidebarLink
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getConfig, type NavGroup } from "../lib/validation";
|
|
3
3
|
import SidebarLink from "./SidebarLink.astro";
|
|
4
|
-
import
|
|
4
|
+
import SidebarOpenApiPageLink from "./sidebar/SidebarOpenApiPageLink.astro";
|
|
5
|
+
import {
|
|
6
|
+
buildMdxPageHref,
|
|
7
|
+
buildOpenApiEndpointHref,
|
|
8
|
+
parseOpenApiEndpoint,
|
|
9
|
+
slugify,
|
|
10
|
+
} from "../lib/utils";
|
|
5
11
|
import Tag from "./ui/Tag.astro";
|
|
6
12
|
import Icon from "./ui/Icon.astro";
|
|
7
13
|
|
|
@@ -21,22 +27,37 @@ const listId = `list-${groupId}`;
|
|
|
21
27
|
|
|
22
28
|
// Check if any child page is active (shallow check, no recursion needed)
|
|
23
29
|
const containsActivePage = item.pages.some((child) => {
|
|
24
|
-
|
|
25
|
-
typeof child === "string" ? child : "pages" in child ? null : child.page;
|
|
30
|
+
let href: string | null = null;
|
|
26
31
|
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
filePath:
|
|
32
|
+
if (typeof child === "string") {
|
|
33
|
+
href = buildMdxPageHref({
|
|
34
|
+
filePath: child,
|
|
30
35
|
groupSlug: currentPrefix,
|
|
31
36
|
homePath: config.home,
|
|
32
37
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
} else if ("page" in child) {
|
|
39
|
+
href = buildMdxPageHref({
|
|
40
|
+
filePath: child.page,
|
|
41
|
+
groupSlug: currentPrefix,
|
|
42
|
+
homePath: config.home,
|
|
43
|
+
});
|
|
44
|
+
} else if ("openapi" in child) {
|
|
45
|
+
const parsedEndpoint = parseOpenApiEndpoint(child.openapi.endpoint);
|
|
46
|
+
if (parsedEndpoint) {
|
|
47
|
+
href = buildOpenApiEndpointHref({
|
|
48
|
+
path: parsedEndpoint.path,
|
|
49
|
+
method: parsedEndpoint.method,
|
|
50
|
+
groupSlug: currentPrefix,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
38
53
|
}
|
|
39
|
-
|
|
54
|
+
|
|
55
|
+
if (!href) return false;
|
|
56
|
+
|
|
57
|
+
// Normalize paths for comparison (remove trailing slashes)
|
|
58
|
+
const normalizedCurrent = Astro.url.pathname.replace(/\/$/, "");
|
|
59
|
+
const normalizedTarget = href.replace(/\/$/, "");
|
|
60
|
+
return normalizedCurrent === normalizedTarget;
|
|
40
61
|
});
|
|
41
62
|
---
|
|
42
63
|
|
|
@@ -100,6 +121,8 @@ const containsActivePage = item.pages.some((child) => {
|
|
|
100
121
|
title={child.title}
|
|
101
122
|
groupSlug={currentPrefix}
|
|
102
123
|
/>
|
|
124
|
+
) : "openapi" in child ? (
|
|
125
|
+
<SidebarOpenApiPageLink item={child} parentSlug={currentPrefix} />
|
|
103
126
|
) : null}
|
|
104
127
|
</li>
|
|
105
128
|
))
|
|
@@ -1496,8 +1496,12 @@ export default function AskAiWidget({
|
|
|
1496
1496
|
</form>
|
|
1497
1497
|
</>
|
|
1498
1498
|
) : (
|
|
1499
|
-
<div className=
|
|
1500
|
-
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
1499
|
+
<div className="flex-1 px-4 py-4 flex items-center justify-center">
|
|
1500
|
+
<p className="text-sm text-neutral-600 dark:text-neutral-300 flex justify-center gap-1.5">
|
|
1501
|
+
<Icon
|
|
1502
|
+
icon="lucide:circle-alert"
|
|
1503
|
+
className="size-5 shrink-0 self-start justify-self-start mt-0.5 block"
|
|
1504
|
+
/>
|
|
1501
1505
|
{unavailableMessage}
|
|
1502
1506
|
</p>
|
|
1503
1507
|
</div>
|
|
@@ -1703,7 +1707,7 @@ export default function AskAiWidget({
|
|
|
1703
1707
|
}
|
|
1704
1708
|
`}</style>
|
|
1705
1709
|
{!isOpen ? (
|
|
1706
|
-
<div className="fixed bottom-5 right-6 size-12 z-40 bg-white hover:scale-105 transition duration-300 ease-in-out">
|
|
1710
|
+
<div className="fixed bottom-5 right-6 size-12 z-40 bg-white rounded-full hover:scale-105 transition duration-300 ease-in-out">
|
|
1707
1711
|
<button
|
|
1708
1712
|
type="button"
|
|
1709
1713
|
className="w-full h-full inline-flex items-center justify-center gap-2 rounded-full bg-linear-to-b from-neutral-900/85 to-neutral-900 shadow-xl dark:bg-white cursor-pointer"
|
|
@@ -38,10 +38,10 @@ import { Icon } from "astro-icon/components";
|
|
|
38
38
|
>
|
|
39
39
|
<button
|
|
40
40
|
x-on:click="open = true"
|
|
41
|
-
class="m-px flex items-center gap-
|
|
41
|
+
class="font-[350] m-px flex h-8 items-center gap-2 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 px-3 text-[13px] text-white shadow-sm dark:bg-white dark:text-neutral-900 transition-all duration-200 whitespace-nowrap cursor-pointer"
|
|
42
42
|
>
|
|
43
|
+
<Icon class="-ml-px size-3.5" name="lucide:square-mouse-pointer" />
|
|
43
44
|
Try it
|
|
44
|
-
<Icon class="ml-2" name="lucide:square-mouse-pointer" />
|
|
45
45
|
</button>
|
|
46
46
|
<div
|
|
47
47
|
x-show="open"
|
|
@@ -37,11 +37,7 @@ const configuredProxyUrl =
|
|
|
37
37
|
typeof import.meta.env.PUBLIC_PROXY_URL === "string"
|
|
38
38
|
? import.meta.env.PUBLIC_PROXY_URL.trim()
|
|
39
39
|
: "";
|
|
40
|
-
const proxyUrl =
|
|
41
|
-
configuredProxyUrl ||
|
|
42
|
-
(import.meta.env.DEV
|
|
43
|
-
? "https://docs-proxy-dev.stefanjoseph-dev.workers.dev"
|
|
44
|
-
: "");
|
|
40
|
+
const proxyUrl = configuredProxyUrl || "/_platform/proxy";
|
|
45
41
|
const proxyEnabled = config.playground?.proxy !== false && proxyUrl.length > 0;
|
|
46
42
|
const formattedBodyDescription = bodyDescription
|
|
47
43
|
? await renderMarkdown(bodyDescription)
|
|
@@ -544,20 +540,22 @@ const sectionVariantFieldNames = Object.fromEntries(
|
|
|
544
540
|
<button
|
|
545
541
|
@click="sendRequest($event)"
|
|
546
542
|
:disabled="loading"
|
|
547
|
-
class="m-px flex items-center gap-
|
|
543
|
+
class="m-px font-[350] relative flex h-8 items-center gap-2 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-neutral-900/85 to-neutral-900 px-3 text-[13px] text-white shadow-sm dark:bg-white dark:text-neutral-900 transition-all duration-200 whitespace-nowrap cursor-pointer disabled:opacity-70 disabled:cursor-not-allowed"
|
|
548
544
|
>
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
545
|
+
<span class="flex items-center gap-2">
|
|
546
|
+
<Icon
|
|
547
|
+
x-show="!loading"
|
|
548
|
+
class="size-3.5 -ml-px"
|
|
549
|
+
name="lucide:square-arrow-up-right"
|
|
550
|
+
/>
|
|
551
|
+
<Icon
|
|
552
|
+
x-show="loading"
|
|
553
|
+
x-cloak
|
|
554
|
+
class="size-3.5 animate-spin -ml-px"
|
|
555
|
+
name="lucide:loader"
|
|
556
|
+
/>
|
|
553
557
|
Send <span class="hidden xs:inline">Request</span>
|
|
554
|
-
<Icon class="size-4" name="lucide:square-arrow-up-right" />
|
|
555
558
|
</span>
|
|
556
|
-
<Icon
|
|
557
|
-
x-show="loading"
|
|
558
|
-
class="size-4 absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 animate-spin **:stroke-3"
|
|
559
|
-
name="lucide:loader"
|
|
560
|
-
/>
|
|
561
559
|
</button>
|
|
562
560
|
</PlaygroundBar>
|
|
563
561
|
|
|
@@ -584,7 +582,7 @@ const sectionVariantFieldNames = Object.fromEntries(
|
|
|
584
582
|
}
|
|
585
583
|
|
|
586
584
|
return (
|
|
587
|
-
<div class="border border-neutral-200 shadow-xs rounded-xl p-4">
|
|
585
|
+
<div class="border border-neutral-200 shadow-xs rounded-xl p-4 pb-0 [&_[role='region']]:border-b-0">
|
|
588
586
|
<Accordion title={headers[key]} defaultOpen titleSize="xl">
|
|
589
587
|
{key === "body" && formattedBodyDescription && (
|
|
590
588
|
<div
|