radiant-docs 0.1.39 → 0.1.40

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 (34) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -7
  3. package/template/package-lock.json +19 -7
  4. package/template/package.json +1 -1
  5. package/template/scripts/generate-robots-txt.mjs +29 -1
  6. package/template/scripts/stamp-image-versions.mjs +59 -33
  7. package/template/src/components/Footer.astro +2 -1
  8. package/template/src/components/Header.astro +8 -6
  9. package/template/src/components/LogoLink.astro +2 -1
  10. package/template/src/components/MdxPage.astro +15 -4
  11. package/template/src/components/PagePagination.astro +61 -0
  12. package/template/src/components/SidebarDropdown.astro +12 -8
  13. package/template/src/components/SidebarGroup.astro +1 -1
  14. package/template/src/components/SidebarMenu.astro +1 -1
  15. package/template/src/components/SidebarSegmented.astro +6 -5
  16. package/template/src/components/TableOfContents.astro +4 -13
  17. package/template/src/components/chat/AskAiWidget.tsx +274 -39
  18. package/template/src/components/endpoint/PlaygroundForm.astro +2 -1
  19. package/template/src/components/user/CodeBlock.astro +1 -1
  20. package/template/src/components/user/CodeGroup.astro +16 -1
  21. package/template/src/components/user/ComponentPreviewBlock.astro +1 -0
  22. package/template/src/components/user/Image.astro +43 -53
  23. package/template/src/layouts/Layout.astro +217 -7
  24. package/template/src/lib/base-path.ts +98 -0
  25. package/template/src/lib/component-error.ts +49 -10
  26. package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
  27. package/template/src/lib/pagefind.ts +62 -14
  28. package/template/src/lib/routes.ts +49 -1
  29. package/template/src/lib/static-asset-url.ts +3 -1
  30. package/template/src/lib/utils.ts +12 -4
  31. package/template/src/lib/validation.ts +376 -36
  32. package/template/src/pages/404.astro +2 -1
  33. package/template/src/pages/[...slug].astro +68 -6
  34. package/template/src/styles/global.css +62 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -239,7 +239,7 @@ function copyContentAssets() {
239
239
  if (file.includes("src/content/docs") && shouldCopyFile(file)) {
240
240
  const relativePath = path.relative(
241
241
  path.join(CWD, "src/content/docs"),
242
- file
242
+ file,
243
243
  );
244
244
  const destPath = path.join(PUBLIC_DIR, relativePath);
245
245
  const destDir = path.dirname(destPath);
@@ -254,11 +254,40 @@ function copyContentAssets() {
254
254
  };
255
255
  }
256
256
 
257
- const configuredSite =
258
- typeof process.env.DOCS_SITE_URL === "string" &&
259
- process.env.DOCS_SITE_URL.trim().length > 0
260
- ? process.env.DOCS_SITE_URL.trim()
261
- : undefined;
257
+ function normalizeBasePath(pathname) {
258
+ const normalized = pathname.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
259
+ return normalized === "" ? "/" : normalized;
260
+ }
261
+
262
+ function resolveDocsSiteConfig() {
263
+ const rawSiteUrl =
264
+ typeof process.env.DOCS_SITE_URL === "string"
265
+ ? process.env.DOCS_SITE_URL.trim()
266
+ : "";
267
+
268
+ if (!rawSiteUrl) {
269
+ return {
270
+ site: undefined,
271
+ base: "/",
272
+ };
273
+ }
274
+
275
+ try {
276
+ const parsed = new URL(rawSiteUrl);
277
+ return {
278
+ site: parsed.origin,
279
+ base: normalizeBasePath(parsed.pathname),
280
+ };
281
+ } catch {
282
+ return {
283
+ site: rawSiteUrl,
284
+ base: "/",
285
+ };
286
+ }
287
+ }
288
+
289
+ const docsSiteConfig = resolveDocsSiteConfig();
290
+ globalThis.__RADIANT_DOCS_BASE_PATH__ = docsSiteConfig.base;
262
291
 
263
292
  const configuredStaticAssetHost =
264
293
  typeof process.env.STATIC_ASSET_HOST === "string" &&
@@ -284,7 +313,9 @@ const configuredAssetsPrefix =
284
313
 
285
314
  // https://astro.build/config
286
315
  export default defineConfig({
287
- site: configuredSite,
316
+ site: docsSiteConfig.site,
317
+ base: docsSiteConfig.base,
318
+ trailingSlash: "never",
288
319
  devToolbar: {
289
320
  enabled: false,
290
321
  },
@@ -45,7 +45,7 @@
45
45
  "rehype-slug": "^6.0.0",
46
46
  "remark-gfm": "^4.0.1",
47
47
  "simple-git": "^3.30.0",
48
- "tailwindcss": "^4.1.17",
48
+ "tailwindcss": "^4.2.2",
49
49
  "vaul": "^1.1.2",
50
50
  "yaml": "^2.8.2",
51
51
  "zod": "^3.25.76"
@@ -5048,6 +5048,12 @@
5048
5048
  "tailwindcss": "4.1.17"
5049
5049
  }
5050
5050
  },
5051
+ "node_modules/@tailwindcss/node/node_modules/tailwindcss": {
5052
+ "version": "4.1.17",
5053
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
5054
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
5055
+ "license": "MIT"
5056
+ },
5051
5057
  "node_modules/@tailwindcss/oxide": {
5052
5058
  "version": "4.1.17",
5053
5059
  "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
@@ -5302,6 +5308,12 @@
5302
5308
  "vite": "^5.2.0 || ^6 || ^7"
5303
5309
  }
5304
5310
  },
5311
+ "node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
5312
+ "version": "4.1.17",
5313
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
5314
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
5315
+ "license": "MIT"
5316
+ },
5305
5317
  "node_modules/@types/alpinejs": {
5306
5318
  "version": "3.13.11",
5307
5319
  "resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz",
@@ -12763,9 +12775,9 @@
12763
12775
  "license": "MIT"
12764
12776
  },
12765
12777
  "node_modules/tailwindcss": {
12766
- "version": "4.1.17",
12767
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
12768
- "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
12778
+ "version": "4.2.2",
12779
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
12780
+ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
12769
12781
  "license": "MIT"
12770
12782
  },
12771
12783
  "node_modules/tapable": {
@@ -14008,9 +14020,9 @@
14008
14020
  }
14009
14021
  },
14010
14022
  "node_modules/vite": {
14011
- "version": "6.4.1",
14012
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
14013
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
14023
+ "version": "6.4.2",
14024
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
14025
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
14014
14026
  "license": "MIT",
14015
14027
  "dependencies": {
14016
14028
  "esbuild": "^0.25.0",
@@ -49,7 +49,7 @@
49
49
  "rehype-slug": "^6.0.0",
50
50
  "remark-gfm": "^4.0.1",
51
51
  "simple-git": "^3.30.0",
52
- "tailwindcss": "^4.1.17",
52
+ "tailwindcss": "^4.2.2",
53
53
  "vaul": "^1.1.2",
54
54
  "yaml": "^2.8.2",
55
55
  "zod": "^3.25.76"
@@ -4,6 +4,24 @@ import path from "node:path";
4
4
  const CWD = process.cwd();
5
5
  const DIST_DIR = path.join(CWD, "dist");
6
6
  const ROBOTS_TXT_PATH = path.join(DIST_DIR, "robots.txt");
7
+ const ASTRO_SITEMAP_INDEX_FILENAME = "sitemap-index.xml";
8
+ const PUBLIC_SITEMAP_FILENAME = "sitemap.xml";
9
+
10
+ function buildSitemapUrl() {
11
+ const configuredSiteUrl = process.env.DOCS_SITE_URL?.trim();
12
+ if (!configuredSiteUrl) {
13
+ return `/${PUBLIC_SITEMAP_FILENAME}`;
14
+ }
15
+
16
+ try {
17
+ const baseUrl = configuredSiteUrl.endsWith("/")
18
+ ? configuredSiteUrl
19
+ : `${configuredSiteUrl}/`;
20
+ return new URL(PUBLIC_SITEMAP_FILENAME, baseUrl).toString();
21
+ } catch {
22
+ return `/${PUBLIC_SITEMAP_FILENAME}`;
23
+ }
24
+ }
7
25
 
8
26
  function main() {
9
27
  if (!fs.existsSync(DIST_DIR)) {
@@ -11,7 +29,17 @@ function main() {
11
29
  return;
12
30
  }
13
31
 
14
- const robotsTxt = "User-agent: *\nAllow: /\nSitemap: /sitemap-index.xml\n";
32
+ const astroSitemapIndexPath = path.join(
33
+ DIST_DIR,
34
+ ASTRO_SITEMAP_INDEX_FILENAME,
35
+ );
36
+ const publicSitemapPath = path.join(DIST_DIR, PUBLIC_SITEMAP_FILENAME);
37
+ if (fs.existsSync(astroSitemapIndexPath)) {
38
+ fs.copyFileSync(astroSitemapIndexPath, publicSitemapPath);
39
+ console.log("✅ sitemap.xml alias generated.");
40
+ }
41
+
42
+ const robotsTxt = `User-agent: *\nAllow: /\nSitemap: ${buildSitemapUrl()}\n`;
15
43
  fs.writeFileSync(ROBOTS_TXT_PATH, robotsTxt, "utf8");
16
44
  console.log("✅ robots.txt generated.");
17
45
  }
@@ -28,6 +28,23 @@ const CONFIGURED_PREFIX =
28
28
  process.env.R2_BUCKET_PREFIX.trim().length > 0
29
29
  ? process.env.R2_BUCKET_PREFIX.trim().replace(/^\/+|\/+$/g, "")
30
30
  : null;
31
+ const CONFIGURED_BASE_PATH = (() => {
32
+ const raw =
33
+ typeof process.env.DOCS_SITE_URL === "string"
34
+ ? process.env.DOCS_SITE_URL.trim()
35
+ : "";
36
+ if (!raw) return null;
37
+
38
+ try {
39
+ const parsed = new URL(raw);
40
+ const normalized = parsed.pathname
41
+ .replace(/\/{2,}/g, "/")
42
+ .replace(/\/+$/, "");
43
+ return normalized && normalized !== "/" ? normalized : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ })();
31
48
  const VERSIONED_EXTENSIONS = new Set([
32
49
  ".avif",
33
50
  ".gif",
@@ -100,20 +117,25 @@ function getHash(filePath) {
100
117
 
101
118
  function normalizePathForDistLookup(pathname) {
102
119
  const normalizedPathname = path.posix.normalize(pathname);
103
- if (!CONFIGURED_PREFIX) {
104
- return normalizedPathname;
120
+
121
+ const withoutStaticPrefix = stripPathPrefix(
122
+ normalizedPathname,
123
+ CONFIGURED_PREFIX ? `/${CONFIGURED_PREFIX}` : null,
124
+ );
125
+ return stripPathPrefix(withoutStaticPrefix, CONFIGURED_BASE_PATH);
126
+ }
127
+
128
+ function stripPathPrefix(pathname, prefix) {
129
+ if (!prefix) {
130
+ return pathname;
105
131
  }
106
132
 
107
- const prefix = `/${CONFIGURED_PREFIX}`;
108
- if (
109
- normalizedPathname === prefix ||
110
- normalizedPathname.startsWith(`${prefix}/`)
111
- ) {
112
- const stripped = normalizedPathname.slice(prefix.length);
133
+ if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
134
+ const stripped = pathname.slice(prefix.length);
113
135
  return stripped.startsWith("/") ? stripped : `/${stripped}`;
114
136
  }
115
137
 
116
- return normalizedPathname;
138
+ return pathname;
117
139
  }
118
140
 
119
141
  function resolveLocalImagePath(urlValue, filePath) {
@@ -133,7 +155,10 @@ function resolveLocalImagePath(urlValue, filePath) {
133
155
 
134
156
  let parsed;
135
157
  try {
136
- parsed = new URL(decoded, `https://radiant.invalid${htmlBasePathname(filePath)}`);
158
+ parsed = new URL(
159
+ decoded,
160
+ `https://radiant.invalid${htmlBasePathname(filePath)}`,
161
+ );
137
162
  } catch {
138
163
  return null;
139
164
  }
@@ -158,8 +183,9 @@ function resolveLocalImagePath(urlValue, filePath) {
158
183
  return null;
159
184
  }
160
185
 
161
- const normalizedPathname = normalizePathForDistLookup(parsed.pathname)
162
- .replace(/^\/+/, "");
186
+ const normalizedPathname = normalizePathForDistLookup(
187
+ parsed.pathname,
188
+ ).replace(/^\/+/, "");
163
189
  const absolutePath = path.resolve(DIST_DIR, normalizedPathname);
164
190
 
165
191
  if (
@@ -208,7 +234,8 @@ function stampSrcset(srcsetValue, filePath) {
208
234
  const whitespaceIndex = trimmed.search(/\s/);
209
235
  const urlPart =
210
236
  whitespaceIndex === -1 ? trimmed : trimmed.slice(0, whitespaceIndex);
211
- const descriptor = whitespaceIndex === -1 ? "" : trimmed.slice(whitespaceIndex);
237
+ const descriptor =
238
+ whitespaceIndex === -1 ? "" : trimmed.slice(whitespaceIndex);
212
239
 
213
240
  const stamped = stampSingleUrl(urlPart, filePath);
214
241
  if (stamped.changed) changed = true;
@@ -237,20 +264,17 @@ function replaceAttribute(html, filePath, attribute) {
237
264
 
238
265
  let changed = false;
239
266
 
240
- const nextHtml = html.replace(
241
- pattern,
242
- (fullMatch, prefix, value, suffix) => {
243
- const stamped =
244
- attribute === "srcset"
245
- ? stampSrcset(value, filePath)
246
- : stampSingleUrl(value, filePath);
267
+ const nextHtml = html.replace(pattern, (fullMatch, prefix, value, suffix) => {
268
+ const stamped =
269
+ attribute === "srcset"
270
+ ? stampSrcset(value, filePath)
271
+ : stampSingleUrl(value, filePath);
247
272
 
248
- if (!stamped.changed) return fullMatch;
273
+ if (!stamped.changed) return fullMatch;
249
274
 
250
- changed = true;
251
- return `${prefix}${stamped.value}${suffix}`;
252
- },
253
- );
275
+ changed = true;
276
+ return `${prefix}${stamped.value}${suffix}`;
277
+ });
254
278
 
255
279
  return { html: nextHtml, changed };
256
280
  }
@@ -259,17 +283,13 @@ function hasIconRel(tagSource) {
259
283
  const relMatch = tagSource.match(/\brel\s*=\s*["']([^"']+)["']/i);
260
284
  if (!relMatch) return false;
261
285
 
262
- const relTokens = relMatch[1]
263
- .toLowerCase()
264
- .split(/\s+/)
265
- .filter(Boolean);
286
+ const relTokens = relMatch[1].toLowerCase().split(/\s+/).filter(Boolean);
266
287
 
267
288
  return relTokens.includes("icon") || relTokens.includes("apple-touch-icon");
268
289
  }
269
290
 
270
291
  function replaceIconLinkHref(html, filePath) {
271
- const pattern =
272
- /(<link\b[^>]*\bhref\s*=\s*["'])([^"']*)(["'][^>]*>)/gi;
292
+ const pattern = /(<link\b[^>]*\bhref\s*=\s*["'])([^"']*)(["'][^>]*>)/gi;
273
293
  let changed = false;
274
294
 
275
295
  const nextHtml = html.replace(pattern, (fullMatch, prefix, value, suffix) => {
@@ -297,7 +317,9 @@ function main() {
297
317
 
298
318
  const htmlFiles = findHtmlFiles(DIST_DIR).sort();
299
319
  if (htmlFiles.length === 0) {
300
- console.warn("Skipping image version stamping: no HTML files found in dist.");
320
+ console.warn(
321
+ "Skipping image version stamping: no HTML files found in dist.",
322
+ );
301
323
  return;
302
324
  }
303
325
 
@@ -307,7 +329,11 @@ function main() {
307
329
  const sourceHtml = fs.readFileSync(htmlFile, "utf8");
308
330
  const srcResult = replaceAttribute(sourceHtml, htmlFile, "src");
309
331
  const srcSetResult = replaceAttribute(srcResult.html, htmlFile, "srcset");
310
- const posterResult = replaceAttribute(srcSetResult.html, htmlFile, "poster");
332
+ const posterResult = replaceAttribute(
333
+ srcSetResult.html,
334
+ htmlFile,
335
+ "poster",
336
+ );
311
337
  const iconHrefResult = replaceIconLinkHref(posterResult.html, htmlFile);
312
338
  const fileChanged =
313
339
  srcResult.changed ||
@@ -2,6 +2,7 @@
2
2
  import Icon from "./ui/Icon.astro";
3
3
  import { getConfig } from "../lib/validation";
4
4
  import LogoLink from "./LogoLink.astro";
5
+ import { withBasePath } from "../lib/base-path";
5
6
 
6
7
  interface Props {
7
8
  askAiEnabled?: boolean;
@@ -66,7 +67,7 @@ const socialIcons: Record<string, string> = {
66
67
  <div class="flex flex-wrap justify-center gap-x-8 gap-y-3">
67
68
  {footer.links.map((link) => (
68
69
  <a
69
- href={link.href}
70
+ href={withBasePath(link.href)}
70
71
  class="text-sm text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100 transition-colors duration-300"
71
72
  >
72
73
  {link.text}
@@ -4,6 +4,7 @@ import { getConfig } from "../lib/validation";
4
4
  import Search from "./Search.astro";
5
5
  import LogoLink from "./LogoLink.astro";
6
6
  import sparkleIcon from "../assets/icons/sparkle.svg?url";
7
+ import { withBasePath } from "../lib/base-path";
7
8
 
8
9
  interface Props {
9
10
  showAskAiTrigger?: boolean;
@@ -58,14 +59,15 @@ const config = await getConfig();
58
59
  showAskAiTrigger ? (
59
60
  <button
60
61
  type="button"
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 dark:from-neutral-100 to-neutral-900 dark:to-neutral-200 px-3 text-[13px] font-[350] dark:font-medium text-white shadow-sm dark:bg-white dark:text-neutral-900 cursor-pointer"
62
+ class="hidden md:inline-flex items-center gap-2 h-8 rounded-lg [corner-shape:superellipse(1.2)] bg-linear-to-b from-[var(--color-theme-top)] to-[var(--color-theme-bottom)] px-3 text-[13px] font-[350] text-[var(--color-theme-foreground)] shadow-sm hover:opacity-95 cursor-pointer"
62
63
  onclick="window.dispatchEvent(new CustomEvent('ask-ai:open'));"
63
64
  >
64
65
  <img
65
66
  src={sparkleIcon}
66
67
  alt=""
67
68
  aria-hidden="true"
68
- class="size-3 -ml-px invert dark:invert-0"
69
+ class="size-3 -ml-px"
70
+ style="filter: var(--color-theme-icon-filter);"
69
71
  />
70
72
  Ask AI
71
73
  </button>
@@ -92,7 +94,7 @@ const config = await getConfig();
92
94
  ? "hidden 2xl:flex"
93
95
  : "flex",
94
96
  ]}
95
- href={l.href}
97
+ href={withBasePath(l.href)}
96
98
  >
97
99
  {l.icon && (
98
100
  <Icon
@@ -113,7 +115,7 @@ const config = await getConfig();
113
115
  "h-[33px] items-center gap-1.5 px-[11px] text-[13px] bg-white/90 dark:bg-neutral-800/80 text-neutral-600/85 hover:text-neutral-600 dark:text-neutral-200/95 dark:hover:text-neutral-200 rounded-lg [corner-shape:superellipse(1.2)] border border-neutral-200 dark:border-neutral-700/40 shadow-xs transition-all whitespace-nowrap",
114
116
  config.navbar.primary ? "hidden lg:flex" : "flex",
115
117
  ]}
116
- href={config.navbar.secondary.href}
118
+ href={withBasePath(config.navbar.secondary.href)}
117
119
  >
118
120
  {config.navbar.secondary.icon && (
119
121
  <Icon
@@ -129,9 +131,9 @@ const config = await getConfig();
129
131
  {config.navbar.primary && (
130
132
  <a
131
133
  class:list={[
132
- "font-[350] dark:font-[450] 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 dark:from-neutral-100 dark:to-neutral-200 text-white dark:text-neutral-950 duration-200 shadow-sm transition-all whitespace-nowrap",
134
+ "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-[var(--color-theme-top)] to-[var(--color-theme-bottom)] text-[var(--color-theme-foreground)] duration-200 shadow-sm transition-all whitespace-nowrap hover:opacity-95",
133
135
  ]}
134
- href={config.navbar.primary.href}
136
+ href={withBasePath(config.navbar.primary.href)}
135
137
  >
136
138
  {config.navbar.primary.icon && (
137
139
  <Icon
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { getConfig, type LogoVariant } from "../lib/validation";
3
+ import { withBasePath } from "../lib/base-path";
3
4
  import { resolveStaticAssetUrl } from "../lib/static-asset-url";
4
5
 
5
6
  const config = await getConfig();
@@ -53,7 +54,7 @@ const lightLogoUrl = lightLogo.logoUrl;
53
54
  const darkLogoUrl = darkLogo.logoUrl;
54
55
 
55
56
  // Get the href for the logo link (defaults to "/")
56
- const logoHref = config.logo?.href || "/";
57
+ const logoHref = withBasePath(config.logo?.href || "/");
57
58
  const logoPillText =
58
59
  config.logo?.pill === false ? null : (config.logo?.pill ?? "Docs");
59
60
 
@@ -14,14 +14,18 @@ import CodeBlock from "./user/CodeBlock.astro";
14
14
  import CodeGroup from "./user/CodeGroup.astro";
15
15
  import ComponentPreview from "./user/ComponentPreview.astro";
16
16
  import ComponentPreviewBlock from "./user/ComponentPreviewBlock.astro";
17
- import type { MdxRoute } from "../lib/routes";
17
+ import type { MdxRoute, Route } from "../lib/routes";
18
+ import PagePagination from "./PagePagination.astro";
18
19
 
19
20
  interface Props {
20
21
  entry: any;
21
22
  route: MdxRoute;
23
+ previousRoute?: Route;
24
+ nextRoute?: Route;
25
+ homePath?: string;
22
26
  }
23
27
 
24
- const { entry, route } = Astro.props;
28
+ const { entry, route, previousRoute, nextRoute, homePath } = Astro.props;
25
29
 
26
30
  const components = {
27
31
  Accordion,
@@ -55,8 +59,15 @@ const tocHeadings = headings.filter(({ depth }) => depth === 2 || depth === 3);
55
59
  <h1 class="text-4xl font-semibold tracking-tight">{title}</h1>
56
60
  </header>
57
61
  <div class="flex w-full min-w-0 justify-between gap-x-12">
58
- <div class="prose-rules min-w-0 flex-1">
59
- <Content components={components} />
62
+ <div class="min-w-0 flex-1">
63
+ <div class="prose-rules">
64
+ <Content components={components} />
65
+ </div>
66
+ <PagePagination
67
+ previousRoute={previousRoute}
68
+ nextRoute={nextRoute}
69
+ homePath={homePath}
70
+ />
60
71
  </div>
61
72
  <aside class="hidden xl:block w-56 shrink-0">
62
73
  <TableOfContents headings={tocHeadings} />
@@ -0,0 +1,61 @@
1
+ ---
2
+ import type { Route } from "../lib/routes";
3
+ import { prependBasePath } from "../lib/base-path";
4
+
5
+ interface Props {
6
+ previousRoute?: Route;
7
+ nextRoute?: Route;
8
+ homePath?: string;
9
+ }
10
+
11
+ const { previousRoute, nextRoute, homePath } = Astro.props;
12
+
13
+ function buildHref(route: Route): string {
14
+ if (route.type === "mdx" && homePath && route.filePath === homePath) {
15
+ return prependBasePath("/");
16
+ }
17
+
18
+ return prependBasePath(
19
+ route.slug.startsWith("/") ? route.slug : `/${route.slug}`,
20
+ );
21
+ }
22
+ ---
23
+
24
+ {
25
+ (previousRoute || nextRoute) && (
26
+ <nav class="w-full max-w-2xl pt-16 dark:border-neutral-800" aria-label="Page pagination">
27
+ <div class="grid gap-3 sm:grid-cols-2">
28
+ {previousRoute && (
29
+ <a
30
+ href={buildHref(previousRoute)}
31
+ class="group flex flex-col rounded-lg border border-neutral-200/90 bg-white/70 p-4 transition-colors hover:border-neutral-300 dark:border-neutral-800 dark:bg-neutral-900/40 dark:hover:border-neutral-700 shadow-xs"
32
+ >
33
+ <span class="block text-[10px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-400">
34
+ Previous
35
+ </span>
36
+ <div class="font-medium text-neutral-900 dark:text-neutral-100">
37
+ {previousRoute.title}
38
+ </div>
39
+ </a>
40
+ )}
41
+
42
+ {nextRoute && (
43
+ <a
44
+ href={buildHref(nextRoute)}
45
+ class:list={[
46
+ "group flex flex-col rounded-lg border border-neutral-200/90 bg-white/70 p-4 transition-colors hover:border-neutral-300/80 dark:border-neutral-800 dark:bg-neutral-900/40 dark:hover:border-neutral-700 shadow-xs",
47
+ !previousRoute && "sm:col-start-2",
48
+ ]}
49
+ >
50
+ <span class="block text-right text-[10px] font-medium uppercase tracking-wide text-neutral-400 dark:text-neutral-400">
51
+ Next
52
+ </span>
53
+ <div class="text-right font-medium text-neutral-900 dark:text-neutral-100">
54
+ {nextRoute.title}
55
+ </div>
56
+ </a>
57
+ )}
58
+ </div>
59
+ </nav>
60
+ )
61
+ }
@@ -3,6 +3,7 @@ import Icon from "./ui/Icon.astro";
3
3
  import { getConfig, type NavMenu } from "../lib/validation";
4
4
  import SidebarMenu from "./SidebarMenu.astro";
5
5
  import { slugify } from "../lib/utils";
6
+ import { prependBasePath, stripBasePath } from "../lib/base-path";
6
7
  import { getAllRoutes } from "../lib/routes";
7
8
 
8
9
  interface Props {
@@ -12,9 +13,9 @@ interface Props {
12
13
 
13
14
  const { menu, parentSlug = "" } = Astro.props;
14
15
 
15
- const pathname = Astro.url.pathname;
16
+ const pathname = stripBasePath(Astro.url.pathname);
16
17
  const config = await getConfig();
17
- const routes = await getAllRoutes();
18
+ const routes = (await getAllRoutes()).filter((route) => !route.hidden);
18
19
 
19
20
  let currentMenuIndex = menu.items.findIndex(
20
21
  (i) => slugify(i.label) === pathname.split("/")[1],
@@ -52,8 +53,8 @@ for (const route of routes) {
52
53
  seen.add(topLevel);
53
54
  const href =
54
55
  route.type === "mdx" && config.home && route.filePath === config.home
55
- ? "/"
56
- : `/${slug}`;
56
+ ? prependBasePath("/")
57
+ : prependBasePath(`/${slug}`);
57
58
  firstHrefOfMenuItems.push(href);
58
59
  }
59
60
  }
@@ -86,7 +87,10 @@ const currentPrefix = parentSlug
86
87
  }
87
88
  <div class="relative">
88
89
  <button
89
- class="flex items-center w-full text-sm text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-700/30 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"
90
+ class:list={[
91
+ "flex items-center w-full text-sm text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-700/30 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",
92
+ menu.label ? "" : "border-b",
93
+ ]}
90
94
  x-on:click="open = true"
91
95
  aria-haspopup="menu"
92
96
  aria-expanded
@@ -131,7 +135,7 @@ const currentPrefix = parentSlug
131
135
  ? "before:bg-neutral-200/50 dark:before:bg-neutral-700/50 text-neutral-900 dark:text-white"
132
136
  : "hover:before:bg-neutral-100/70 dark:hover:before:bg-neutral-700/30",
133
137
  ]}
134
- href={firstHrefOfMenuItems[index] ?? "/"}
138
+ href={firstHrefOfMenuItems[index] ?? prependBasePath("/")}
135
139
  >
136
140
  {menu.items[index].icon && (
137
141
  <Icon
@@ -160,8 +164,8 @@ const currentPrefix = parentSlug
160
164
  class:list={[
161
165
  "relative -mt-2 pt-2 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
162
166
  menu.label
163
- ? "h-[calc(100vh-4px-64px-12px-70px-52px)]"
164
- : "h-[calc(100vh-4px-64px-12px-38px-52px)]",
167
+ ? "h-[calc(100vh-4px-64px-12px-70px-51px+8px)]"
168
+ : "h-[calc(100vh-4px-64px-12px-38px-51px+8px)]",
165
169
  ]}
166
170
  >
167
171
  <SidebarMenu
@@ -18,7 +18,7 @@ const groupSlug = slugify(item.group);
18
18
  const currentPrefix = parentSlug ? `${parentSlug}/${groupSlug}` : groupSlug;
19
19
  ---
20
20
 
21
- <li>
21
+ <li class="my-8 first:my-0 last:my-0">
22
22
  <div class:list={["text-sm font-semibold mb-2 flex items-center gap-2 px-2"]}>
23
23
  {item.icon && <Icon name={item.icon} class="size-4 text-neutral-500" />}
24
24
  {item.group}
@@ -22,7 +22,7 @@ let { navigation, parentSlug = "" } = Astro.props;
22
22
 
23
23
  {
24
24
  navigation.pages ? (
25
- <ul class="px-2 pt-4 pb-3 [&>:nth-child(n+2)]:mt-6 [&>:nth-child(n+2)]:pt-[25px] [&>:nth-child(n+2)]:relative [&>:nth-child(n+2)]:before:absolute [&>:nth-child(n+2)]:before:top-0 [&>:nth-child(n+2)]:before:inset-x-0 [&>:nth-child(n+2)]:before:h-px [&>:nth-child(n+2)]:before:bg-linear-[90deg,transparent,var(--color-neutral-200)_20%,var(--color-neutral-200)_80%,transparent] dark:[&>:nth-child(n+2)]:before:bg-linear-[90deg,transparent,var(--color-neutral-700)_20%,var(--color-neutral-700)_80%,transparent]">
25
+ <ul class="px-2 pt-4 pb-4">
26
26
  {navigation.pages.map((item) =>
27
27
  typeof item === "string" ? (
28
28
  <li>