@raystack/chronicle 0.5.4 → 0.6.1

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 (68) hide show
  1. package/dist/cli/index.js +258 -80
  2. package/package.json +7 -5
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/ui/breadcrumbs.tsx +4 -2
  16. package/src/components/ui/client-theme-switcher.tsx +21 -4
  17. package/src/components/ui/search.module.css +16 -41
  18. package/src/components/ui/search.tsx +30 -41
  19. package/src/lib/config.test.ts +493 -0
  20. package/src/lib/config.ts +123 -22
  21. package/src/lib/head.tsx +23 -5
  22. package/src/lib/llms.test.ts +94 -0
  23. package/src/lib/llms.ts +41 -0
  24. package/src/lib/navigation.test.ts +94 -0
  25. package/src/lib/navigation.ts +51 -0
  26. package/src/lib/page-context.tsx +51 -32
  27. package/src/lib/route-resolver.test.ts +173 -0
  28. package/src/lib/route-resolver.ts +73 -0
  29. package/src/lib/source.ts +94 -1
  30. package/src/lib/version-source.test.ts +163 -0
  31. package/src/lib/version-source.ts +101 -0
  32. package/src/pages/ApiPage.tsx +1 -1
  33. package/src/pages/DocsLayout.tsx +24 -3
  34. package/src/pages/DocsPage.tsx +3 -6
  35. package/src/pages/LandingPage.module.css +56 -0
  36. package/src/pages/LandingPage.tsx +39 -0
  37. package/src/pages/NotFound.tsx +2 -0
  38. package/src/server/App.tsx +21 -23
  39. package/src/server/api/page.ts +5 -1
  40. package/src/server/api/search.ts +51 -24
  41. package/src/server/api/specs.ts +17 -5
  42. package/src/server/entry-client.tsx +42 -14
  43. package/src/server/entry-server.tsx +33 -11
  44. package/src/server/routes/[...slug].md.ts +0 -6
  45. package/src/server/routes/[version]/llms.txt.ts +26 -0
  46. package/src/server/routes/llms.txt.ts +10 -13
  47. package/src/server/routes/og.tsx +2 -2
  48. package/src/server/routes/sitemap.xml.ts +14 -6
  49. package/src/server/vite-config.ts +5 -5
  50. package/src/themes/default/ContentDirButtons.tsx +66 -0
  51. package/src/themes/default/Layout.module.css +187 -40
  52. package/src/themes/default/Layout.tsx +166 -65
  53. package/src/themes/default/OpenInAI.tsx +112 -0
  54. package/src/themes/default/Page.module.css +30 -0
  55. package/src/themes/default/Page.tsx +1 -3
  56. package/src/themes/default/SidebarLogo.tsx +26 -0
  57. package/src/themes/default/Toc.module.css +102 -25
  58. package/src/themes/default/Toc.tsx +56 -10
  59. package/src/themes/default/VersionSwitcher.tsx +59 -0
  60. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  61. package/src/themes/paper/Layout.module.css +7 -0
  62. package/src/themes/paper/Layout.tsx +20 -13
  63. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  64. package/src/types/config.ts +145 -23
  65. package/src/types/content.ts +11 -1
  66. package/src/types/theme.ts +1 -0
  67. package/src/components/ui/footer.module.css +0 -27
  68. package/src/components/ui/footer.tsx +0 -30
@@ -1,10 +1,11 @@
1
1
  import MiniSearch from 'minisearch';
2
- import { defineHandler } from 'nitro';
2
+ import { defineHandler, HTTPError } from 'nitro';
3
3
  import type { OpenAPIV3 } from 'openapi-types';
4
4
  import { getSpecSlug } from '@/lib/api-routes';
5
- import { loadConfig } from '@/lib/config';
5
+ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
6
6
  import { loadApiSpecs } from '@/lib/openapi';
7
- import { getPages, extractFrontmatter } from '@/lib/source';
7
+ import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
8
+ import { LATEST_CONTEXT, type VersionContext } from '@/lib/version-source';
8
9
 
9
10
  interface SearchDocument {
10
11
  id: string;
@@ -14,8 +15,12 @@ interface SearchDocument {
14
15
  type: 'page' | 'api';
15
16
  }
16
17
 
17
- let searchIndex: MiniSearch<SearchDocument> | null = null;
18
- let cachedDocs: SearchDocument[] | null = null;
18
+ const indexCache = new Map<string, MiniSearch<SearchDocument>>();
19
+ const docsCache = new Map<string, SearchDocument[]>();
20
+
21
+ function keyFor(ctx: VersionContext): string {
22
+ return ctx.dir ?? '__latest__';
23
+ }
19
24
 
20
25
  function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
21
26
  const index = new MiniSearch<SearchDocument>({
@@ -31,8 +36,8 @@ function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
31
36
  return index;
32
37
  }
33
38
 
34
- async function scanContent(): Promise<SearchDocument[]> {
35
- const pages = await getPages();
39
+ async function scanContent(ctx: VersionContext): Promise<SearchDocument[]> {
40
+ const pages = await getPagesForVersion(ctx);
36
41
  return pages.map(p => {
37
42
  const fm = extractFrontmatter(p);
38
43
  return {
@@ -45,12 +50,13 @@ async function scanContent(): Promise<SearchDocument[]> {
45
50
  });
46
51
  }
47
52
 
48
- async function buildApiDocs(): Promise<SearchDocument[]> {
53
+ async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
49
54
  const config = loadConfig();
50
- if (!config.api?.length) return [];
55
+ const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
56
+ if (!apiConfigs.length) return [];
51
57
 
52
58
  const docs: SearchDocument[] = [];
53
- const specs = await loadApiSpecs(config.api);
59
+ const specs = await loadApiSpecs(apiConfigs);
54
60
 
55
61
  for (const spec of specs) {
56
62
  const specSlug = getSpecSlug(spec);
@@ -60,7 +66,7 @@ async function buildApiDocs(): Promise<SearchDocument[]> {
60
66
  for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
61
67
  const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
62
68
  if (!op?.operationId) continue;
63
- const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
69
+ const url = `${ctx.urlPrefix}/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
64
70
  docs.push({
65
71
  id: url,
66
72
  url,
@@ -75,29 +81,50 @@ async function buildApiDocs(): Promise<SearchDocument[]> {
75
81
  return docs;
76
82
  }
77
83
 
78
- async function getDocs(): Promise<SearchDocument[]> {
79
- if (cachedDocs) return cachedDocs;
84
+ async function getDocs(ctx: VersionContext): Promise<SearchDocument[]> {
85
+ const key = keyFor(ctx);
86
+ const cached = docsCache.get(key);
87
+ if (cached) return cached;
80
88
  const [contentDocs, apiDocs] = await Promise.all([
81
- scanContent(),
82
- buildApiDocs()
89
+ scanContent(ctx),
90
+ buildApiDocs(ctx)
83
91
  ]);
84
- cachedDocs = [...contentDocs, ...apiDocs];
85
- return cachedDocs;
92
+ const docs = [...contentDocs, ...apiDocs];
93
+ docsCache.set(key, docs);
94
+ return docs;
95
+ }
96
+
97
+ async function getIndex(ctx: VersionContext): Promise<MiniSearch<SearchDocument>> {
98
+ const key = keyFor(ctx);
99
+ const cached = indexCache.get(key);
100
+ if (cached) return cached;
101
+ const docs = await getDocs(ctx);
102
+ const index = createIndex(docs);
103
+ indexCache.set(key, index);
104
+ return index;
86
105
  }
87
106
 
88
- async function getIndex(): Promise<MiniSearch<SearchDocument>> {
89
- if (searchIndex) return searchIndex;
90
- const docs = await getDocs();
91
- searchIndex = createIndex(docs);
92
- return searchIndex;
107
+ function resolveCtx(tag: string | null): VersionContext {
108
+ if (!tag) return LATEST_CONTEXT;
109
+ const config = loadConfig();
110
+ const version = config.versions?.find(v => v.dir === tag);
111
+ if (!version) {
112
+ throw new HTTPError({
113
+ status: 400,
114
+ message: `Unknown version tag: ${tag}`,
115
+ });
116
+ }
117
+ return { dir: version.dir, urlPrefix: `/${version.dir}` };
93
118
  }
94
119
 
95
120
  export default defineHandler(async event => {
96
121
  const query = event.url.searchParams.get('query') ?? '';
97
- const index = await getIndex();
122
+ const tag = event.url.searchParams.get('tag');
123
+ const ctx = resolveCtx(tag);
124
+ const index = await getIndex(ctx);
98
125
 
99
126
  if (!query) {
100
- const docs = await getDocs();
127
+ const docs = await getDocs(ctx);
101
128
  return docs
102
129
  .filter(d => d.type === 'page')
103
130
  .slice(0, 8)
@@ -1,9 +1,21 @@
1
- import { defineHandler } from 'nitro';
2
- import { loadConfig } from '@/lib/config';
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
3
3
  import { loadApiSpecs } from '@/lib/openapi';
4
4
 
5
- export default defineHandler(async () => {
5
+ export default defineHandler(async event => {
6
+ const versionParam = event.url.searchParams.get('version');
7
+ const versionDir = versionParam === null || versionParam === '' ? null : versionParam;
8
+
6
9
  const config = loadConfig();
7
- const specs = config.api?.length ? await loadApiSpecs(config.api) : [];
8
- return specs;
10
+ if (versionDir && !config.versions?.some(v => v.dir === versionDir)) {
11
+ throw new HTTPError({
12
+ status: 400,
13
+ message: `Unknown version: ${versionDir}`,
14
+ });
15
+ }
16
+
17
+ const apiConfigs = getApiConfigsForVersion(config, versionDir);
18
+ if (!apiConfigs.length) return [];
19
+
20
+ return loadApiSpecs(apiConfigs);
9
21
  });
@@ -4,8 +4,11 @@ import { hydrateRoot } from 'react-dom/client';
4
4
  import { BrowserRouter } from 'react-router';
5
5
  import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6
6
  import { mdxComponents } from '@/components/mdx';
7
+ import { getApiConfigsForVersion } from '@/lib/config';
7
8
  import { PageProvider } from '@/lib/page-context';
8
- import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
9
+ import { resolveRoute, RouteType } from '@/lib/route-resolver';
10
+ import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
11
+ import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
9
12
  import type { ApiSpec } from '@/lib/openapi';
10
13
  import type { ReactNode } from 'react';
11
14
  import { App } from './App';
@@ -14,11 +17,19 @@ interface EmbeddedData {
14
17
  config: ChronicleConfig;
15
18
  tree: Root;
16
19
  slug: string[];
17
- frontmatter: Frontmatter;
18
- relativePath: string;
19
- originalPath?: string;
20
+ version: VersionContext;
21
+ frontmatter: Frontmatter | null;
22
+ relativePath: string | null;
23
+ originalPath: string | null;
24
+ prev: PageNavLink | null;
25
+ next: PageNavLink | null;
20
26
  }
21
27
 
28
+ const defaultConfig: ChronicleConfig = {
29
+ site: { title: 'Documentation' },
30
+ content: [{ dir: 'docs', label: 'Docs' }],
31
+ };
32
+
22
33
  const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
23
34
  '../../.content/**/*.{mdx,md}'
24
35
  );
@@ -43,23 +54,39 @@ async function hydrate() {
43
54
  window as unknown as { __PAGE_DATA__?: EmbeddedData }
44
55
  ).__PAGE_DATA__;
45
56
 
46
- const config: ChronicleConfig = embedded?.config ?? {
47
- title: 'Documentation'
48
- };
57
+ const config: ChronicleConfig = embedded?.config ?? defaultConfig;
49
58
  const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
50
- const isApiPage =
51
- window.location.pathname.startsWith('/apis') && !!config.api?.length;
52
- const apiSpecs: ApiSpec[] = isApiPage
53
- ? await fetch('/api/specs')
59
+
60
+ const route = resolveRoute(window.location.pathname, config);
61
+ // resolveVersionFromUrl always returns a valid context — even for redirect
62
+ // targets (e.g. /v1 -> /v1/docs) where route.version isn't on the union.
63
+ const routeVersion: VersionContext = resolveVersionFromUrl(
64
+ window.location.pathname,
65
+ config,
66
+ );
67
+ const version: VersionContext = embedded?.version ?? routeVersion;
68
+
69
+ const isApiRoute =
70
+ route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
71
+ const apiConfigs = isApiRoute
72
+ ? getApiConfigsForVersion(config, routeVersion.dir)
73
+ : [];
74
+ const specsUrl = routeVersion.dir
75
+ ? `/api/specs?version=${encodeURIComponent(routeVersion.dir)}`
76
+ : '/api/specs';
77
+ const apiSpecs: ApiSpec[] = apiConfigs.length
78
+ ? await fetch(specsUrl)
54
79
  .then(r => r.json())
55
80
  .catch(() => [])
56
81
  : [];
57
82
 
58
83
  const mdxPath = embedded?.originalPath || embedded?.relativePath;
59
- const page = mdxPath
84
+ const page = embedded && mdxPath && embedded.frontmatter
60
85
  ? {
61
- slug: embedded!.slug,
62
- frontmatter: embedded!.frontmatter,
86
+ slug: embedded.slug,
87
+ frontmatter: embedded.frontmatter,
88
+ prev: embedded.prev,
89
+ next: embedded.next,
63
90
  ...(await loadMdxModule(mdxPath)),
64
91
  }
65
92
  : null;
@@ -73,6 +100,7 @@ async function hydrate() {
73
100
  initialTree={tree}
74
101
  initialPage={page}
75
102
  initialApiSpecs={apiSpecs}
103
+ initialVersion={version}
76
104
  loadMdx={loadMdxModule}
77
105
  >
78
106
  <App />
@@ -5,10 +5,11 @@ import { renderToReadableStream } from 'react-dom/server.edge';
5
5
  import { StaticRouter } from 'react-router';
6
6
  import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
7
  import { mdxComponents } from '@/components/mdx';
8
- import { loadConfig } from '@/lib/config';
8
+ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
9
9
  import { loadApiSpecs } from '@/lib/openapi';
10
10
  import { PageProvider } from '@/lib/page-context';
11
- import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
11
+ import { resolveRoute, RouteType } from '@/lib/route-resolver';
12
+ import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
12
13
  import { useNitroApp } from 'nitro/app';
13
14
  import { App } from './App';
14
15
 
@@ -19,17 +20,32 @@ export default {
19
20
  async fetch(req: Request) {
20
21
  const url = new URL(req.url);
21
22
  const pathname = url.pathname;
22
- const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
23
23
 
24
24
  const config = loadConfig();
25
- const apiSpecs = config.api?.length
26
- ? await loadApiSpecs(config.api).catch(() => [])
25
+ const route = resolveRoute(pathname, config);
26
+
27
+ if (route.type === RouteType.Redirect) {
28
+ // biome-ignore lint/correctness/useHookAtTopLevel: useNitroApp is a Nitro DI accessor, not a React hook
29
+ useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, route.status, 0);
30
+ return new Response(null, {
31
+ status: route.status,
32
+ headers: { Location: route.to },
33
+ });
34
+ }
35
+
36
+ const isApiRoute = route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
37
+ const pageSlug = route.type === RouteType.DocsPage ? route.slug : [];
38
+
39
+ const apiConfigs = isApiRoute
40
+ ? getApiConfigsForVersion(config, route.version.dir)
27
41
  : [];
42
+ const apiSpecs = apiConfigs.length ? await loadApiSpecs(apiConfigs) : [];
28
43
 
29
44
  const [tree, page] = await Promise.all([
30
45
  getPageTree(),
31
- getPage(slug),
46
+ route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
32
47
  ]);
48
+ const nav = page ? await getPageNav(pageSlug, tree) : { prev: null, next: null };
33
49
 
34
50
  const relativePath = page ? getRelativePath(page) : null;
35
51
  const originalPath = page ? getOriginalPath(page) : null;
@@ -37,22 +53,27 @@ export default {
37
53
 
38
54
  const pageData = page
39
55
  ? {
40
- slug,
41
- frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
56
+ slug: pageSlug,
57
+ frontmatter: extractFrontmatter(page, pageSlug[pageSlug.length - 1]),
42
58
  content: mdxModule?.default
43
59
  ? React.createElement(mdxModule.default, { components: mdxComponents })
44
60
  : null,
45
61
  toc: mdxModule?.toc ?? [],
62
+ prev: nav.prev,
63
+ next: nav.next,
46
64
  }
47
65
  : null;
48
66
 
49
67
  const embeddedData = {
50
68
  config,
51
69
  tree,
52
- slug,
70
+ slug: pageSlug,
71
+ version: route.version,
53
72
  frontmatter: pageData?.frontmatter ?? null,
54
73
  relativePath,
55
74
  originalPath,
75
+ prev: pageData?.prev ?? null,
76
+ next: pageData?.next ?? null,
56
77
  };
57
78
  const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
58
79
 
@@ -82,6 +103,7 @@ export default {
82
103
  initialTree={tree}
83
104
  initialPage={pageData}
84
105
  initialApiSpecs={apiSpecs}
106
+ initialVersion={route.version}
85
107
  loadMdx={async () => ({ content: null, toc: [] })}
86
108
  >
87
109
  <App />
@@ -95,9 +117,9 @@ export default {
95
117
 
96
118
  const renderDuration = performance.now() - renderStart;
97
119
 
98
- const isApiRoute = pathname.startsWith('/apis');
99
- const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
120
+ const status = route.type === RouteType.DocsPage && !page ? 404 : 200;
100
121
 
122
+ // biome-ignore lint/correctness/useHookAtTopLevel: useNitroApp is a Nitro DI accessor, not a React hook
101
123
  useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);
102
124
 
103
125
  return new Response(stream, {
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import matter from 'gray-matter';
3
3
  import { defineHandler, HTTPError } from 'nitro';
4
- import { loadConfig } from '@/lib/config';
5
4
  import { getPage, getOriginalPath } from '@/lib/source';
6
5
  import { safePath } from '@/server/utils/safe-path';
7
6
 
@@ -9,11 +8,6 @@ export default defineHandler(async event => {
9
8
  const pathname = event.path || event.req.url?.split('?')[0] || '';
10
9
  if (!pathname.endsWith('.md')) return;
11
10
 
12
- const config = loadConfig();
13
- if (!config.llms?.enabled) {
14
- throw new HTTPError({ status: 404, message: 'Not Found' });
15
- }
16
-
17
11
  const stripped = pathname.replace(/\.md$/, '');
18
12
  const parts = stripped === '/index' || stripped === '/'
19
13
  ? []
@@ -0,0 +1,26 @@
1
+ import { getRouterParam } from 'h3';
2
+ import { defineHandler, HTTPError } from 'nitro';
3
+ import { loadConfig } from '@/lib/config';
4
+ import { buildLlmsTxt } from '@/lib/llms';
5
+ import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
6
+
7
+ export default defineHandler(async event => {
8
+ const config = loadConfig();
9
+
10
+ const versionDir = getRouterParam(event, 'version');
11
+ const version = config.versions?.find(v => v.dir === versionDir);
12
+ if (!version) {
13
+ throw new HTTPError({ status: 404, message: 'Not Found' });
14
+ }
15
+
16
+ const ctx = { dir: version.dir, urlPrefix: `/${version.dir}` };
17
+ const pages = await getPagesForVersion(ctx);
18
+ const body = buildLlmsTxt(
19
+ config,
20
+ pages.map(p => ({ url: p.url, title: extractFrontmatter(p).title })),
21
+ ctx,
22
+ );
23
+
24
+ event.res.headers.set('Content-Type', 'text/plain');
25
+ return body;
26
+ });
@@ -1,21 +1,18 @@
1
- import { defineHandler, HTTPError } from 'nitro';
1
+ import { defineHandler } from 'nitro';
2
2
  import { loadConfig } from '@/lib/config';
3
- import { getPages, extractFrontmatter } from '@/lib/source';
3
+ import { buildLlmsTxt } from '@/lib/llms';
4
+ import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
5
+ import { LATEST_CONTEXT } from '@/lib/version-source';
4
6
 
5
7
  export default defineHandler(async event => {
6
8
  const config = loadConfig();
7
9
 
8
- if (!config.llms?.enabled) {
9
- throw new HTTPError({ status: 404, message: 'Not Found' });
10
- }
11
-
12
- const pages = await getPages();
13
- const index = pages.map(p => {
14
- const fm = extractFrontmatter(p);
15
- const mdUrl = p.url === '/' ? '/index.md' : `${p.url}.md`;
16
- return `- [${fm.title}](${mdUrl})`;
17
- }).join('\n');
18
- const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
10
+ const pages = await getPagesForVersion(LATEST_CONTEXT);
11
+ const body = buildLlmsTxt(
12
+ config,
13
+ pages.map(p => ({ url: p.url, title: extractFrontmatter(p).title })),
14
+ LATEST_CONTEXT,
15
+ );
19
16
 
20
17
  event.res.headers.set('Content-Type', 'text/plain');
21
18
  return body;
@@ -22,9 +22,9 @@ async function loadFont(): Promise<ArrayBuffer> {
22
22
 
23
23
  export default defineHandler(async event => {
24
24
  const config = loadConfig();
25
- const title = event.url.searchParams.get('title') ?? config.title;
25
+ const title = event.url.searchParams.get('title') ?? config.site.title;
26
26
  const description = event.url.searchParams.get('description') ?? '';
27
- const siteName = config.title;
27
+ const siteName = config.site.title;
28
28
 
29
29
  const font = await loadFont();
30
30
 
@@ -1,6 +1,6 @@
1
1
  import { defineHandler } from 'nitro';
2
2
  import { buildApiRoutes } from '@/lib/api-routes';
3
- import { loadConfig } from '@/lib/config';
3
+ import { getAllVersions, getApiConfigsForVersion, loadConfig } from '@/lib/config';
4
4
  import { loadApiSpecs } from '@/lib/openapi';
5
5
  import { getPages } from '@/lib/source';
6
6
 
@@ -23,11 +23,19 @@ export default defineHandler(async event => {
23
23
  return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
24
24
  });
25
25
 
26
- const apiPages = config.api?.length
27
- ? buildApiRoutes(await loadApiSpecs(config.api)).map(
28
- route => `<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
29
- )
30
- : [];
26
+ const apiPages: string[] = [];
27
+ for (const v of getAllVersions(config)) {
28
+ const versionDir = v.isLatest ? null : v.dir;
29
+ const apiConfigs = getApiConfigsForVersion(config, versionDir);
30
+ if (!apiConfigs.length) continue;
31
+ const prefix = versionDir ? `/${versionDir}` : '';
32
+ const routes = buildApiRoutes(await loadApiSpecs(apiConfigs));
33
+ for (const route of routes) {
34
+ apiPages.push(
35
+ `<url><loc>${baseUrl}${prefix}/apis/${route.slug.join('/')}</loc></url>`,
36
+ );
37
+ }
38
+ }
31
39
 
32
40
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
33
41
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -18,7 +18,6 @@ function resolveOutputDir(projectRoot: string, preset?: string): string {
18
18
  export interface ViteConfigOptions {
19
19
  packageRoot: string;
20
20
  projectRoot: string;
21
- contentDir: string;
22
21
  configPath?: string;
23
22
  preset?: string;
24
23
  }
@@ -41,8 +40,9 @@ async function readChronicleConfig(projectRoot: string, configPath?: string): Pr
41
40
  export async function createViteConfig(
42
41
  options: ViteConfigOptions
43
42
  ): Promise<InlineConfig> {
44
- const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
43
+ const { packageRoot, projectRoot, configPath, preset } = options;
45
44
  const rawConfig = await readChronicleConfig(projectRoot, configPath);
45
+ const contentMirror = path.resolve(packageRoot, '.content');
46
46
 
47
47
  return {
48
48
  root: packageRoot,
@@ -86,8 +86,8 @@ export async function createViteConfig(
86
86
  resolve: {
87
87
  alias: {
88
88
  '@': path.resolve(packageRoot, 'src'),
89
+ 'tslib': 'tslib/tslib.es6.js',
89
90
  },
90
- conditions: ['module-sync', 'import', 'node'],
91
91
  dedupe: [
92
92
  'react',
93
93
  'react-dom',
@@ -98,11 +98,11 @@ export async function createViteConfig(
98
98
  },
99
99
  server: {
100
100
  fs: {
101
- allow: [packageRoot, projectRoot, contentDir]
101
+ allow: [packageRoot, projectRoot, contentMirror]
102
102
  }
103
103
  },
104
104
  define: {
105
- __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
105
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
106
106
  __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
107
107
  __CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig),
108
108
  },
@@ -0,0 +1,66 @@
1
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import { Button, Menu, Flex } from '@raystack/apsara';
3
+ import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
4
+ import { getLandingEntries } from '@/lib/config';
5
+ import { getActiveContentDir, splitContentButtons } from '@/lib/navigation';
6
+ import { usePageContext } from '@/lib/page-context';
7
+
8
+ const MAX_VISIBLE = 3;
9
+
10
+ export function ContentDirButtons() {
11
+ const { config, version } = usePageContext();
12
+ const { pathname } = useLocation();
13
+ const navigate = useNavigate();
14
+
15
+ const entries = getLandingEntries(config, version.dir);
16
+ if (entries.length <= 1) return null;
17
+
18
+ const active = getActiveContentDir(pathname, config);
19
+ const { visible, overflow } = splitContentButtons(entries, MAX_VISIBLE);
20
+
21
+ return (
22
+ <Flex gap='small' align='center'>
23
+ {visible.map(entry => (
24
+ <RouterLink
25
+ key={entry.href}
26
+ to={entry.href}
27
+ style={{ textDecoration: 'none' }}
28
+ >
29
+ <Button
30
+ size='small'
31
+ variant={active === entry.contentDir ? 'solid' : 'outline'}
32
+ color='neutral'
33
+ >
34
+ {entry.label}
35
+ </Button>
36
+ </RouterLink>
37
+ ))}
38
+ {overflow.length > 0 ? (
39
+ <Menu>
40
+ <Menu.Trigger
41
+ render={
42
+ <Button
43
+ size='small'
44
+ variant='outline'
45
+ color='neutral'
46
+ trailingIcon={<ChevronDownIcon width={14} height={14} />}
47
+ />
48
+ }
49
+ >
50
+ More
51
+ </Menu.Trigger>
52
+ <Menu.Content>
53
+ {overflow.map(entry => (
54
+ <Menu.Item
55
+ key={entry.href}
56
+ onClick={() => navigate(entry.href)}
57
+ >
58
+ {entry.label}
59
+ </Menu.Item>
60
+ ))}
61
+ </Menu.Content>
62
+ </Menu>
63
+ ) : null}
64
+ </Flex>
65
+ );
66
+ }