radiant-docs 0.1.38 → 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 (35) 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 +8 -5
  20. package/template/src/components/user/CodeGroup.astro +262 -14
  21. package/template/src/components/user/ComponentPreviewBlock.astro +4 -3
  22. package/template/src/components/user/Image.astro +43 -53
  23. package/template/src/components/user/Tabs.astro +128 -23
  24. package/template/src/layouts/Layout.astro +217 -7
  25. package/template/src/lib/base-path.ts +98 -0
  26. package/template/src/lib/component-error.ts +49 -10
  27. package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
  28. package/template/src/lib/pagefind.ts +62 -14
  29. package/template/src/lib/routes.ts +49 -1
  30. package/template/src/lib/static-asset-url.ts +3 -1
  31. package/template/src/lib/utils.ts +12 -4
  32. package/template/src/lib/validation.ts +376 -36
  33. package/template/src/pages/404.astro +2 -1
  34. package/template/src/pages/[...slug].astro +68 -6
  35. package/template/src/styles/global.css +85 -1
@@ -167,22 +167,58 @@ export type NavbarItem = {
167
167
  href: string;
168
168
  icon?: string | null;
169
169
  };
170
- export type LogoVariant = string | {
171
- image: string;
172
- padding?: {
173
- top?: number;
174
- bottom?: number;
175
- };
170
+ export type HiddenPageRoute = {
171
+ filePath: string;
172
+ href: string;
176
173
  };
174
+ type InternalPageHrefResolution = HiddenPageRoute & {
175
+ linkHref: string;
176
+ };
177
+ export type LogoVariant =
178
+ | string
179
+ | {
180
+ image: string;
181
+ padding?: {
182
+ top?: number;
183
+ bottom?: number;
184
+ };
185
+ };
177
186
  export type Logo = {
178
187
  light?: LogoVariant;
179
188
  dark?: LogoVariant;
180
189
  href?: string;
181
190
  pill?: string | false;
182
191
  };
192
+ export const BASE_COLOR_OPTIONS = [
193
+ "slate",
194
+ "gray",
195
+ "zinc",
196
+ "neutral",
197
+ "stone",
198
+ "taupe",
199
+ "mauve",
200
+ "mist",
201
+ "olive",
202
+ ] as const;
203
+ export type BaseColorOption = (typeof BASE_COLOR_OPTIONS)[number];
204
+ export type BaseColorByMode = {
205
+ light: BaseColorOption;
206
+ dark: BaseColorOption;
207
+ };
208
+ export const DEFAULT_THEME_COLOR_LIGHT = "#171717";
209
+ export const DEFAULT_THEME_COLOR_DARK = "#f5f5f5";
210
+ export type ThemeColorByMode = {
211
+ light: string;
212
+ dark: string;
213
+ };
214
+ export type DocsTheme = {
215
+ baseColor?: BaseColorOption | BaseColorByMode;
216
+ themeColor?: string | ThemeColorByMode;
217
+ };
183
218
  export type DocsConfig = {
184
219
  title: string;
185
220
  logo?: Logo;
221
+ theme?: DocsTheme;
186
222
  home?: string;
187
223
  navigation: NavigationItem;
188
224
  navbar?: {
@@ -195,6 +231,7 @@ export type DocsConfig = {
195
231
  proxy?: boolean;
196
232
  };
197
233
  footer?: Footer;
234
+ hiddenPageRoutes?: HiddenPageRoute[];
198
235
  };
199
236
 
200
237
  export type SocialPlatform =
@@ -295,6 +332,54 @@ function normalizeDocsPagePath(
295
332
  return normalizedPath;
296
333
  }
297
334
 
335
+ function splitHrefPathAndSuffix(href: string): {
336
+ pathname: string;
337
+ suffix: string;
338
+ } {
339
+ const match = href.match(/^([^?#]*)(.*)$/);
340
+ return {
341
+ pathname: match?.[1] ?? href,
342
+ suffix: match?.[2] ?? "",
343
+ };
344
+ }
345
+
346
+ function normalizeInternalPageHref(
347
+ href: string,
348
+ currentPath: Path,
349
+ label: string,
350
+ ): InternalPageHrefResolution | null {
351
+ const trimmedHref = href.trim();
352
+ if (trimmedHref === "") {
353
+ throwConfigError(`${label} cannot be an empty string`, currentPath);
354
+ }
355
+
356
+ if (isUrl(trimmedHref)) {
357
+ return null;
358
+ }
359
+
360
+ if (!trimmedHref.startsWith("/")) {
361
+ throwConfigError(
362
+ `${label} must be either a valid URL (http:// or https://) or an internal path (starting with /)`,
363
+ currentPath,
364
+ );
365
+ }
366
+
367
+ const { pathname, suffix } = splitHrefPathAndSuffix(trimmedHref);
368
+ const normalizedPathname = pathname.replace(/\/{2,}/g, "/");
369
+ if (normalizedPathname === "/" || normalizedPathname === "") {
370
+ return null;
371
+ }
372
+
373
+ const filePath = normalizeDocsPagePath(normalizedPathname, currentPath, label);
374
+ validateFileExistence(filePath, currentPath);
375
+
376
+ return {
377
+ filePath,
378
+ href: `/${filePath}`,
379
+ linkHref: `/${filePath}${suffix}`,
380
+ };
381
+ }
382
+
298
383
  // Cache for OpenAPI specs (key: filePathOrUrl, value: parsed spec)
299
384
  const openApiSpecCache = new Map<string, any>();
300
385
 
@@ -662,7 +747,11 @@ async function validateNavigationNode(
662
747
  continue;
663
748
  }
664
749
 
665
- await validateNavigationNode(child, [...path, "pages", i], groupDepth + 1);
750
+ await validateNavigationNode(
751
+ child,
752
+ [...path, "pages", i],
753
+ groupDepth + 1,
754
+ );
666
755
  }
667
756
  return;
668
757
  }
@@ -1049,9 +1138,12 @@ async function validateNavMenu(menu: any, currentPath: Path) {
1049
1138
  }
1050
1139
  }
1051
1140
 
1052
- function validateNavbarItem(item: any, currentPath: Path): void {
1141
+ function validateNavbarItem(
1142
+ item: any,
1143
+ currentPath: Path,
1144
+ ): HiddenPageRoute | null {
1053
1145
  // Check if object exists, otherwise we skip (it's optional)
1054
- if (item === undefined) return;
1146
+ if (item === undefined) return null;
1055
1147
 
1056
1148
  checkType(item, "object", currentPath, "Navbar item");
1057
1149
 
@@ -1069,8 +1161,18 @@ function validateNavbarItem(item: any, currentPath: Path): void {
1069
1161
  ]);
1070
1162
  }
1071
1163
 
1164
+ const hiddenPageRoute = normalizeInternalPageHref(
1165
+ item.href,
1166
+ [...currentPath, "href"],
1167
+ "Navbar item href",
1168
+ );
1169
+ if (hiddenPageRoute) {
1170
+ item.href = hiddenPageRoute.linkHref;
1171
+ }
1172
+
1072
1173
  // Optional property
1073
1174
  validateIcon(item.icon, [...currentPath, "icon"]);
1175
+ return hiddenPageRoute;
1074
1176
  }
1075
1177
 
1076
1178
  // --- Top-Level Validation Functions (Your Clean API) ---
@@ -1080,7 +1182,11 @@ function validateTitle(title: DocsConfig["title"]) {
1080
1182
  if (!title) throwConfigError("Title is missing.", ["title"]);
1081
1183
  }
1082
1184
 
1083
- function validateLogoPaddingValue(value: unknown, currentPath: Path, label: string): void {
1185
+ function validateLogoPaddingValue(
1186
+ value: unknown,
1187
+ currentPath: Path,
1188
+ label: string,
1189
+ ): void {
1084
1190
  if (value === undefined) return;
1085
1191
 
1086
1192
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -1134,7 +1240,11 @@ function validateLogoVariant(
1134
1240
  return;
1135
1241
  }
1136
1242
 
1137
- if (typeof variant !== "object" || variant === null || Array.isArray(variant)) {
1243
+ if (
1244
+ typeof variant !== "object" ||
1245
+ variant === null ||
1246
+ Array.isArray(variant)
1247
+ ) {
1138
1248
  throwConfigError(
1139
1249
  `Logo ${mode} must be a string path or an object with 'image' and optional 'padding'.`,
1140
1250
  currentPath,
@@ -1152,13 +1262,17 @@ function validateLogoVariant(
1152
1262
  }
1153
1263
 
1154
1264
  if (typeof variant.image !== "string") {
1155
- throwConfigError(
1156
- `Logo ${mode} object must include an 'image' string.`,
1157
- [...currentPath, "image"],
1158
- );
1265
+ throwConfigError(`Logo ${mode} object must include an 'image' string.`, [
1266
+ ...currentPath,
1267
+ "image",
1268
+ ]);
1159
1269
  }
1160
1270
 
1161
- validateLogoImagePath(variant.image, [...currentPath, "image"], `Logo ${mode} image`);
1271
+ validateLogoImagePath(
1272
+ variant.image,
1273
+ [...currentPath, "image"],
1274
+ `Logo ${mode} image`,
1275
+ );
1162
1276
 
1163
1277
  if (variant.padding === undefined) return;
1164
1278
 
@@ -1242,10 +1356,214 @@ function validateLogo(logo: DocsConfig["logo"]) {
1242
1356
  );
1243
1357
  }
1244
1358
  } else if (logo.pill !== false) {
1245
- throwConfigError("Logo pill must be a string or false.", ["logo", "pill"]);
1359
+ throwConfigError("Logo pill must be a string or false.", [
1360
+ "logo",
1361
+ "pill",
1362
+ ]);
1246
1363
  }
1247
1364
  }
1365
+ }
1366
+
1367
+ function validateTheme(theme: DocsConfig["theme"]): void {
1368
+ if (theme === undefined) return;
1369
+
1370
+ checkType(theme, "object", ["theme"], "Theme configuration");
1371
+ if (typeof theme !== "object" || theme === null || Array.isArray(theme)) {
1372
+ throwConfigError("Theme configuration must be an object.", ["theme"]);
1373
+ }
1374
+
1375
+ const normalizeBaseColor = (
1376
+ value: unknown,
1377
+ currentPath: Path,
1378
+ label: string,
1379
+ ): BaseColorOption => {
1380
+ checkType(value, "string", currentPath, label);
1381
+ if (typeof value !== "string") {
1382
+ throwConfigError(`${label} must be a string.`, currentPath);
1383
+ }
1384
+
1385
+ const normalizedBaseColor = (value as string).trim().toLowerCase();
1386
+ if (normalizedBaseColor.length === 0) {
1387
+ throwConfigError(`${label} cannot be empty.`, currentPath);
1388
+ }
1389
+
1390
+ if (!BASE_COLOR_OPTIONS.includes(normalizedBaseColor as BaseColorOption)) {
1391
+ throwConfigError(
1392
+ `${label} must be one of: ${BASE_COLOR_OPTIONS.join(", ")}.`,
1393
+ currentPath,
1394
+ );
1395
+ }
1396
+
1397
+ return normalizedBaseColor as BaseColorOption;
1398
+ };
1399
+
1400
+ const normalizeThemeColor = (
1401
+ value: unknown,
1402
+ currentPath: Path,
1403
+ label: string,
1404
+ ): string => {
1405
+ checkType(value, "string", currentPath, label);
1406
+ if (typeof value !== "string") {
1407
+ throwConfigError(`${label} must be a string.`, currentPath);
1408
+ }
1248
1409
 
1410
+ const trimmedValue = (value as string).trim();
1411
+ if (trimmedValue.length === 0) {
1412
+ throwConfigError(`${label} cannot be empty.`, currentPath);
1413
+ }
1414
+
1415
+ const normalizedValue = trimmedValue.startsWith("#")
1416
+ ? trimmedValue
1417
+ : `#${trimmedValue}`;
1418
+ if (
1419
+ !/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(
1420
+ normalizedValue,
1421
+ )
1422
+ ) {
1423
+ throwConfigError(
1424
+ `${label} must be a valid hex color (for example: #1d4ed8).`,
1425
+ currentPath,
1426
+ );
1427
+ }
1428
+
1429
+ return normalizedValue.toLowerCase();
1430
+ };
1431
+
1432
+ if (theme.baseColor !== undefined) {
1433
+ if (typeof theme.baseColor === "string") {
1434
+ theme.baseColor = normalizeBaseColor(
1435
+ theme.baseColor,
1436
+ ["theme", "baseColor"],
1437
+ "Theme base color",
1438
+ );
1439
+ } else {
1440
+ checkType(
1441
+ theme.baseColor,
1442
+ "object",
1443
+ ["theme", "baseColor"],
1444
+ "Theme base color",
1445
+ );
1446
+ if (
1447
+ typeof theme.baseColor !== "object" ||
1448
+ theme.baseColor === null ||
1449
+ Array.isArray(theme.baseColor)
1450
+ ) {
1451
+ throwConfigError(
1452
+ "Theme base color must be a string or an object with light/dark values.",
1453
+ ["theme", "baseColor"],
1454
+ );
1455
+ }
1456
+
1457
+ const baseColorByMode = theme.baseColor as Record<string, unknown>;
1458
+ const allowedKeys = new Set(["light", "dark"]);
1459
+ for (const key of Object.keys(baseColorByMode)) {
1460
+ if (!allowedKeys.has(key)) {
1461
+ throwConfigError(
1462
+ "Theme base color object only supports 'light' and 'dark'.",
1463
+ ["theme", "baseColor", key],
1464
+ );
1465
+ }
1466
+ }
1467
+
1468
+ const light =
1469
+ baseColorByMode.light !== undefined
1470
+ ? normalizeBaseColor(
1471
+ baseColorByMode.light,
1472
+ ["theme", "baseColor", "light"],
1473
+ "Theme base color light",
1474
+ )
1475
+ : undefined;
1476
+ const dark =
1477
+ baseColorByMode.dark !== undefined
1478
+ ? normalizeBaseColor(
1479
+ baseColorByMode.dark,
1480
+ ["theme", "baseColor", "dark"],
1481
+ "Theme base color dark",
1482
+ )
1483
+ : undefined;
1484
+
1485
+ if (!light && !dark) {
1486
+ throwConfigError(
1487
+ "Theme base color object must include 'light', 'dark', or both.",
1488
+ ["theme", "baseColor"],
1489
+ );
1490
+ }
1491
+
1492
+ const resolvedLight: BaseColorOption = light ?? "neutral";
1493
+ const resolvedDark: BaseColorOption = dark ?? "neutral";
1494
+
1495
+ theme.baseColor = {
1496
+ light: resolvedLight,
1497
+ dark: resolvedDark,
1498
+ };
1499
+ }
1500
+ }
1501
+
1502
+ if (theme.themeColor === undefined) {
1503
+ return;
1504
+ }
1505
+
1506
+ if (typeof theme.themeColor === "string") {
1507
+ theme.themeColor = normalizeThemeColor(
1508
+ theme.themeColor,
1509
+ ["theme", "themeColor"],
1510
+ "Theme color",
1511
+ );
1512
+ return;
1513
+ }
1514
+
1515
+ checkType(theme.themeColor, "object", ["theme", "themeColor"], "Theme color");
1516
+ if (
1517
+ typeof theme.themeColor !== "object" ||
1518
+ theme.themeColor === null ||
1519
+ Array.isArray(theme.themeColor)
1520
+ ) {
1521
+ throwConfigError(
1522
+ "Theme color must be a string or an object with light/dark values.",
1523
+ ["theme", "themeColor"],
1524
+ );
1525
+ }
1526
+
1527
+ const themeColorByMode = theme.themeColor as Record<string, unknown>;
1528
+ const allowedKeys = new Set(["light", "dark"]);
1529
+ for (const key of Object.keys(themeColorByMode)) {
1530
+ if (!allowedKeys.has(key)) {
1531
+ throwConfigError("Theme color object only supports 'light' and 'dark'.", [
1532
+ "theme",
1533
+ "themeColor",
1534
+ key,
1535
+ ]);
1536
+ }
1537
+ }
1538
+
1539
+ const light =
1540
+ themeColorByMode.light !== undefined
1541
+ ? normalizeThemeColor(
1542
+ themeColorByMode.light,
1543
+ ["theme", "themeColor", "light"],
1544
+ "Theme color light",
1545
+ )
1546
+ : undefined;
1547
+ const dark =
1548
+ themeColorByMode.dark !== undefined
1549
+ ? normalizeThemeColor(
1550
+ themeColorByMode.dark,
1551
+ ["theme", "themeColor", "dark"],
1552
+ "Theme color dark",
1553
+ )
1554
+ : undefined;
1555
+
1556
+ if (!light && !dark) {
1557
+ throwConfigError(
1558
+ "Theme color object must include 'light', 'dark', or both.",
1559
+ ["theme", "themeColor"],
1560
+ );
1561
+ }
1562
+
1563
+ theme.themeColor = {
1564
+ light: light ?? DEFAULT_THEME_COLOR_LIGHT,
1565
+ dark: dark ?? DEFAULT_THEME_COLOR_DARK,
1566
+ };
1249
1567
  }
1250
1568
 
1251
1569
  function validateHome(home: DocsConfig["home"]): string | undefined {
@@ -1256,8 +1574,9 @@ function validateHome(home: DocsConfig["home"]): string | undefined {
1256
1574
  return normalizedHome;
1257
1575
  }
1258
1576
 
1259
- function validateNavbar(navbar: DocsConfig["navbar"]) {
1260
- if (navbar === undefined) return; // Navbar itself is optional
1577
+ function validateNavbar(navbar: DocsConfig["navbar"]): HiddenPageRoute[] {
1578
+ const hiddenPageRoutes: HiddenPageRoute[] = [];
1579
+ if (navbar === undefined) return hiddenPageRoutes; // Navbar itself is optional
1261
1580
 
1262
1581
  checkType(navbar, "object", ["navbar"], "Navbar configuration");
1263
1582
 
@@ -1265,10 +1584,18 @@ function validateNavbar(navbar: DocsConfig["navbar"]) {
1265
1584
  checkType(navbar.blur, "boolean", ["navbar", "blur"], "Navbar blur setting");
1266
1585
 
1267
1586
  // Validate 'primary' item
1268
- validateNavbarItem(navbar.primary, ["navbar", "primary"]);
1587
+ const primaryPageRoute = validateNavbarItem(navbar.primary, [
1588
+ "navbar",
1589
+ "primary",
1590
+ ]);
1591
+ if (primaryPageRoute) hiddenPageRoutes.push(primaryPageRoute);
1269
1592
 
1270
1593
  // Validate 'secondary' item
1271
- validateNavbarItem(navbar.secondary, ["navbar", "secondary"]);
1594
+ const secondaryPageRoute = validateNavbarItem(navbar.secondary, [
1595
+ "navbar",
1596
+ "secondary",
1597
+ ]);
1598
+ if (secondaryPageRoute) hiddenPageRoutes.push(secondaryPageRoute);
1272
1599
 
1273
1600
  // Validate 'links' array
1274
1601
  if (navbar.links !== undefined) {
@@ -1282,13 +1609,17 @@ function validateNavbar(navbar: DocsConfig["navbar"]) {
1282
1609
  }
1283
1610
 
1284
1611
  navbar.links.forEach((link: any, i: number) => {
1285
- validateNavbarItem(link, ["navbar", "links", i]);
1612
+ const hiddenPageRoute = validateNavbarItem(link, ["navbar", "links", i]);
1613
+ if (hiddenPageRoute) hiddenPageRoutes.push(hiddenPageRoute);
1286
1614
  });
1287
1615
  }
1616
+
1617
+ return hiddenPageRoutes;
1288
1618
  }
1289
1619
 
1290
- function validateFooter(footer: DocsConfig["footer"]) {
1291
- if (footer === undefined) return;
1620
+ function validateFooter(footer: DocsConfig["footer"]): HiddenPageRoute[] {
1621
+ const hiddenPageRoutes: HiddenPageRoute[] = [];
1622
+ if (footer === undefined) return hiddenPageRoutes;
1292
1623
 
1293
1624
  checkType(footer, "object", ["footer"], "Footer configuration");
1294
1625
 
@@ -1367,19 +1698,19 @@ function validateFooter(footer: DocsConfig["footer"]) {
1367
1698
  ]);
1368
1699
  }
1369
1700
 
1370
- const trimmedHref = link.href.trim();
1371
- const isExternal =
1372
- trimmedHref.startsWith("http://") || trimmedHref.startsWith("https://");
1373
- const isInternal = trimmedHref.startsWith("/");
1374
-
1375
- if (!isExternal && !isInternal) {
1376
- throwConfigError(
1377
- "Footer link href must be either a valid URL (http:// or https://) or an internal path (starting with /)",
1378
- ["footer", "links", i, "href"],
1379
- );
1701
+ const hiddenPageRoute = normalizeInternalPageHref(
1702
+ link.href,
1703
+ ["footer", "links", i, "href"],
1704
+ "Footer link href",
1705
+ );
1706
+ if (hiddenPageRoute) {
1707
+ link.href = hiddenPageRoute.linkHref;
1708
+ hiddenPageRoutes.push(hiddenPageRoute);
1380
1709
  }
1381
1710
  });
1382
1711
  }
1712
+
1713
+ return hiddenPageRoutes;
1383
1714
  }
1384
1715
 
1385
1716
  async function validateNavigation(navigation: DocsConfig["navigation"]) {
@@ -1433,8 +1764,7 @@ async function validateConfig(config: any): Promise<DocsConfig> {
1433
1764
  // Execute top-level checks sequentially
1434
1765
  validateTitle(config.title);
1435
1766
  validateLogo(config.logo);
1436
- validateNavbar(config.navbar);
1437
- validateFooter(config.footer);
1767
+ validateTheme(config.theme);
1438
1768
  await validateNavigation(config.navigation);
1439
1769
  config.home = validateHome(config.home);
1440
1770
 
@@ -1449,6 +1779,16 @@ async function validateConfig(config: any): Promise<DocsConfig> {
1449
1779
  config.home = fallbackHome;
1450
1780
  }
1451
1781
 
1782
+ const hiddenPageRoutes = [
1783
+ ...validateNavbar(config.navbar),
1784
+ ...validateFooter(config.footer),
1785
+ ];
1786
+ const dedupedHiddenPageRoutes = new Map<string, HiddenPageRoute>();
1787
+ for (const route of hiddenPageRoutes) {
1788
+ dedupedHiddenPageRoutes.set(route.href, route);
1789
+ }
1790
+ config.hiddenPageRoutes = Array.from(dedupedHiddenPageRoutes.values());
1791
+
1452
1792
  // --- 4. Validate Playground ---
1453
1793
  if (config.playground !== undefined) {
1454
1794
  checkType(config.playground, "object", ["playground"], "Playground");
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
+ import { withBasePath } from "../lib/base-path";
3
4
  import Layout from "../layouts/Layout.astro";
4
5
  ---
5
6
 
@@ -33,7 +34,7 @@ import Layout from "../layouts/Layout.astro";
33
34
  Go back
34
35
  </button>
35
36
  <a
36
- href="/"
37
+ href={withBasePath("/")}
37
38
  class="inline-flex items-center justify-center gap-2 rounded-lg [corner-shape:superellipse(1.2)] border border-border px-4 py-2 text-sm font-[350] dark:font-[450] text-white bg-linear-to-b from-neutral-900/85 to-neutral-900 dark:from-neutral-100 dark:to-neutral-200 shadow-sm"
38
39
  >
39
40
  <Icon name="lucide:house" class="size-4" />
@@ -1,16 +1,48 @@
1
1
  ---
2
2
  import { getCollection } from "astro:content";
3
- import { getAllRoutes, resolveMdxPageTitle, type Route } from "../lib/routes";
3
+ import {
4
+ getAllRoutes,
5
+ resolveMdxPageTitle,
6
+ type Route,
7
+ } from "../lib/routes";
4
8
  import { getConfig, validateMdxContent } from "../lib/validation";
5
9
  import MdxPage from "../components/MdxPage.astro";
6
10
  import OpenApiPage from "../components/OpenApiPage.astro";
7
11
  import type { GetStaticPathsResult } from "astro";
8
12
 
9
13
  export async function getStaticPaths(): Promise<GetStaticPathsResult> {
14
+ const buildAdjacentRouteMap = (routes: Route[]): Map<
15
+ string,
16
+ {
17
+ previousRoute?: Route;
18
+ nextRoute?: Route;
19
+ }
20
+ > => {
21
+ const adjacentBySlug = new Map<
22
+ string,
23
+ {
24
+ previousRoute?: Route;
25
+ nextRoute?: Route;
26
+ }
27
+ >();
28
+
29
+ for (let index = 0; index < routes.length; index++) {
30
+ const currentRoute = routes[index];
31
+ adjacentBySlug.set(currentRoute.slug, {
32
+ previousRoute: routes[index - 1],
33
+ nextRoute: routes[index + 1],
34
+ });
35
+ }
36
+
37
+ return adjacentBySlug;
38
+ };
39
+
10
40
  await validateMdxContent();
11
41
  const routes = await getAllRoutes();
42
+ const visibleRoutes = routes.filter((route) => !route.hidden);
12
43
  const docs = await getCollection("docs");
13
44
  const config = await getConfig();
45
+ const adjacentRoutesBySlug = buildAdjacentRouteMap(visibleRoutes);
14
46
 
15
47
  // console.log("routes", routes);
16
48
 
@@ -31,9 +63,16 @@ export async function getStaticPaths(): Promise<GetStaticPathsResult> {
31
63
  `Could not find content collection entry for path: ${route.filePath}`,
32
64
  );
33
65
  }
66
+ const adjacentRoutes = adjacentRoutesBySlug.get(route.slug);
34
67
  return {
35
68
  params: { slug: route.slug },
36
- props: { route, entry },
69
+ props: {
70
+ route,
71
+ entry,
72
+ previousRoute: adjacentRoutes?.previousRoute,
73
+ nextRoute: adjacentRoutes?.nextRoute,
74
+ homePath: config.home,
75
+ },
37
76
  };
38
77
  } else {
39
78
  return {
@@ -68,22 +107,45 @@ export async function getStaticPaths(): Promise<GetStaticPathsResult> {
68
107
  filePath: config.home,
69
108
  }),
70
109
  };
110
+
111
+ const homeAdjacentRoutes = existingHomeRoute
112
+ ? adjacentRoutesBySlug.get(existingHomeRoute.slug)
113
+ : undefined;
114
+
71
115
  paths.push({
72
116
  params: { slug: "/" },
73
- props: { route: homeRoute, entry: homeEntry },
117
+ props: {
118
+ route: homeRoute,
119
+ entry: homeEntry,
120
+ previousRoute: homeAdjacentRoutes?.previousRoute,
121
+ nextRoute: homeAdjacentRoutes?.nextRoute,
122
+ homePath: config.home,
123
+ },
74
124
  });
75
125
  }
76
126
 
77
127
  return paths;
78
128
  }
79
129
 
80
- const props = Astro.props as { route: Route; entry?: any };
81
- const { route, entry } = props;
130
+ const props = Astro.props as {
131
+ route: Route;
132
+ entry?: any;
133
+ previousRoute?: Route;
134
+ nextRoute?: Route;
135
+ homePath?: string;
136
+ };
137
+ const { route, entry, previousRoute, nextRoute, homePath } = props;
82
138
  ---
83
139
 
84
140
  {
85
141
  route.type === "mdx" ? (
86
- <MdxPage entry={entry!} route={route} />
142
+ <MdxPage
143
+ entry={entry!}
144
+ route={route}
145
+ previousRoute={previousRoute}
146
+ nextRoute={nextRoute}
147
+ homePath={homePath}
148
+ />
87
149
  ) : (
88
150
  <OpenApiPage route={route} />
89
151
  )