@raystack/chronicle 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -46,21 +46,97 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
48
 
49
- // src/lib/remark-strip-md-extensions.ts
49
+ // src/lib/remark-resolve-images.ts
50
+ import path4 from "node:path";
50
51
  import { visit } from "unist-util-visit";
51
- var remarkStripMdExtensions = () => {
52
- return (tree) => {
53
- visit(tree, "link", (node) => {
52
+ function resolveUrl(src, dir) {
53
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(src))
54
+ return src;
55
+ if (src.startsWith("//"))
56
+ return src;
57
+ if (src.startsWith("#"))
58
+ return src;
59
+ if (src.startsWith("/_content/"))
60
+ return src;
61
+ if (src.startsWith("/"))
62
+ return `/_content${src}`;
63
+ return `/_content/${path4.posix.normalize(path4.posix.join(dir, src))}`;
64
+ }
65
+ var remarkResolveImages = () => {
66
+ return (tree, file) => {
67
+ const filePath = file.path;
68
+ if (!filePath)
69
+ return;
70
+ const contentIdx = filePath.lastIndexOf("/content/");
71
+ if (contentIdx === -1)
72
+ return;
73
+ const relative = filePath.slice(contentIdx + "/content/".length);
74
+ const dir = path4.posix.dirname(relative);
75
+ visit(tree, "image", (node) => {
76
+ if (!node.url)
77
+ return;
78
+ node.url = resolveUrl(node.url, dir);
79
+ });
80
+ visit(tree, "html", (node) => {
81
+ node.value = node.value.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (_, before, src, after) => `${before}${resolveUrl(src, dir)}${after}`);
82
+ });
83
+ visit(tree, (node) => {
84
+ if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement")
85
+ return;
86
+ const jsx = node;
87
+ if (jsx.name !== "img")
88
+ return;
89
+ const srcAttr = jsx.attributes.find((a) => a.type === "mdxJsxAttribute" && a.name === "src");
90
+ if (!srcAttr?.value || typeof srcAttr.value !== "string")
91
+ return;
92
+ srcAttr.value = resolveUrl(srcAttr.value, dir);
93
+ });
94
+ visit(tree, "element", (node) => {
95
+ if (node.tagName !== "img")
96
+ return;
97
+ const src = node.properties?.src;
98
+ if (typeof src !== "string")
99
+ return;
100
+ node.properties.src = resolveUrl(src, dir);
101
+ });
102
+ };
103
+ }, remark_resolve_images_default;
104
+ var init_remark_resolve_images = __esm(() => {
105
+ remark_resolve_images_default = remarkResolveImages;
106
+ });
107
+
108
+ // src/lib/remark-resolve-links.ts
109
+ import path5 from "node:path";
110
+ import { visit as visit2 } from "unist-util-visit";
111
+ var remarkResolveLinks = () => {
112
+ return (tree, file) => {
113
+ const filePath = file.path;
114
+ if (!filePath)
115
+ return;
116
+ const contentIdx = filePath.lastIndexOf("/content/");
117
+ if (contentIdx === -1)
118
+ return;
119
+ const relative = filePath.slice(contentIdx + "/content/".length);
120
+ const dir = path5.posix.dirname(relative);
121
+ visit2(tree, "link", (node) => {
54
122
  if (!node.url)
55
123
  return;
56
- if (node.url.startsWith("http://") || node.url.startsWith("https://"))
124
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(node.url))
57
125
  return;
58
- node.url = node.url.replace(/\.mdx?(#|$)/, "$1");
126
+ if (node.url.startsWith("#"))
127
+ return;
128
+ if (node.url.startsWith("/"))
129
+ return;
130
+ const [rawPath, hash] = node.url.split("#");
131
+ const stripped = rawPath.replace(/\.mdx?$/, "");
132
+ let resolved = path5.posix.normalize(path5.posix.join(dir, stripped));
133
+ resolved = resolved.replace(/\/(index|readme)$/i, "") || ".";
134
+ node.url = `/${resolved}${hash ? `#${hash}` : ""}`;
59
135
  });
60
136
  };
61
- }, remark_strip_md_extensions_default;
62
- var init_remark_strip_md_extensions = __esm(() => {
63
- remark_strip_md_extensions_default = remarkStripMdExtensions;
137
+ }, remark_resolve_links_default;
138
+ var init_remark_resolve_links = __esm(() => {
139
+ remark_resolve_links_default = remarkResolveLinks;
64
140
  });
65
141
 
66
142
  // ../../node_modules/.bun/reading-time@1.5.0/node_modules/reading-time/lib/reading-time.js
@@ -181,13 +257,13 @@ var require_reading_time2 = __commonJS((exports, module) => {
181
257
  });
182
258
 
183
259
  // ../../node_modules/.bun/remark-reading-time@2.1.0/node_modules/remark-reading-time/index.js
184
- import { visit as visit2 } from "unist-util-visit";
260
+ import { visit as visit3 } from "unist-util-visit";
185
261
  function readingTime({
186
262
  attribute = "readingTime"
187
263
  } = {}) {
188
264
  return function(info, file) {
189
265
  let text = "";
190
- visit2(info, ["text", "code"], (node) => {
266
+ visit3(info, ["text", "code"], (node) => {
191
267
  text += node.value;
192
268
  });
193
269
  file.data[attribute] = import_reading_time.default(text);
@@ -199,10 +275,10 @@ var init_remark_reading_time = __esm(() => {
199
275
  });
200
276
 
201
277
  // src/lib/remark-unused-directives.ts
202
- import { visit as visit3 } from "unist-util-visit";
278
+ import { visit as visit4 } from "unist-util-visit";
203
279
  var remarkUnusedDirectives = () => {
204
280
  return (tree) => {
205
- visit3(tree, ["textDirective"], (node) => {
281
+ visit4(tree, ["textDirective"], (node) => {
206
282
  const directive = node;
207
283
  if (!directive.data) {
208
284
  const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0;
@@ -234,12 +310,12 @@ import { defineConfig as defineFumadocsConfig } from "fumadocs-mdx/config";
234
310
  import mdx from "fumadocs-mdx/vite";
235
311
  import { nitro } from "nitro/vite";
236
312
  import fs3 from "node:fs/promises";
237
- import path4 from "node:path";
313
+ import path6 from "node:path";
238
314
  import remarkDirective from "remark-directive";
239
315
  function resolveOutputDir(projectRoot, preset) {
240
316
  if (preset === "vercel" || preset === "vercel-static")
241
- return path4.resolve(projectRoot, ".vercel/output");
242
- return path4.resolve(projectRoot, ".output");
317
+ return path6.resolve(projectRoot, ".vercel/output");
318
+ return path6.resolve(projectRoot, ".output");
243
319
  }
244
320
  async function readChronicleConfig(projectRoot, configPath) {
245
321
  if (configPath) {
@@ -250,7 +326,7 @@ async function readChronicleConfig(projectRoot, configPath) {
250
326
  }
251
327
  }
252
328
  try {
253
- return await fs3.readFile(path4.join(projectRoot, "chronicle.yaml"), "utf-8");
329
+ return await fs3.readFile(path6.join(projectRoot, "chronicle.yaml"), "utf-8");
254
330
  } catch {
255
331
  return null;
256
332
  }
@@ -258,18 +334,19 @@ async function readChronicleConfig(projectRoot, configPath) {
258
334
  async function createViteConfig(options) {
259
335
  const { packageRoot, projectRoot, configPath, preset } = options;
260
336
  const rawConfig = await readChronicleConfig(projectRoot, configPath);
261
- const contentMirror = path4.resolve(packageRoot, ".content");
337
+ const contentMirror = path6.resolve(packageRoot, ".content");
262
338
  return {
263
339
  root: packageRoot,
264
340
  configFile: false,
265
341
  plugins: [
266
342
  nitro({
267
- serverDir: path4.resolve(packageRoot, "src/server"),
343
+ serverDir: path6.resolve(packageRoot, "src/server"),
268
344
  ...preset && { preset }
269
345
  }),
270
346
  mdx({
271
347
  default: defineFumadocsConfig({
272
348
  mdxOptions: {
349
+ remarkImageOptions: false,
273
350
  valueToExport: ["readingTime"],
274
351
  remarkPlugins: [
275
352
  remarkDirective,
@@ -291,7 +368,8 @@ async function createViteConfig(options) {
291
368
  }
292
369
  }],
293
370
  remark_unused_directives_default,
294
- remark_strip_md_extensions_default,
371
+ remark_resolve_links_default,
372
+ remark_resolve_images_default,
295
373
  remarkMdxMermaid,
296
374
  readingTime
297
375
  ]
@@ -302,7 +380,7 @@ async function createViteConfig(options) {
302
380
  ],
303
381
  resolve: {
304
382
  alias: {
305
- "@": path4.resolve(packageRoot, "src"),
383
+ "@": path6.resolve(packageRoot, "src"),
306
384
  tslib: "tslib/tslib.es6.js"
307
385
  },
308
386
  dedupe: [
@@ -335,7 +413,7 @@ async function createViteConfig(options) {
335
413
  client: {
336
414
  build: {
337
415
  rollupOptions: {
338
- input: path4.resolve(packageRoot, "src/server/entry-client.tsx")
416
+ input: path6.resolve(packageRoot, "src/server/entry-client.tsx")
339
417
  }
340
418
  }
341
419
  }
@@ -349,7 +427,8 @@ async function createViteConfig(options) {
349
427
  };
350
428
  }
351
429
  var init_vite_config = __esm(() => {
352
- init_remark_strip_md_extensions();
430
+ init_remark_resolve_images();
431
+ init_remark_resolve_links();
353
432
  init_remark_reading_time();
354
433
  init_remark_unused_directives();
355
434
  });
@@ -720,7 +799,7 @@ var devCommand = new Command2("dev").description("Start development server").opt
720
799
 
721
800
  // src/cli/commands/init.ts
722
801
  import fs4 from "node:fs";
723
- import path5 from "node:path";
802
+ import path7 from "node:path";
724
803
  import chalk4 from "chalk";
725
804
  import { Command as Command3 } from "commander";
726
805
  import { stringify } from "yaml";
@@ -747,12 +826,12 @@ var GITIGNORE_ENTRIES = ["node_modules", "dist", ".output"];
747
826
  function runInit(projectDir) {
748
827
  const events = [];
749
828
  const defaultDir = defaultInitConfig.content[0].dir;
750
- const contentDir = path5.join(projectDir, "content", defaultDir);
829
+ const contentDir = path7.join(projectDir, "content", defaultDir);
751
830
  if (!fs4.existsSync(contentDir)) {
752
831
  fs4.mkdirSync(contentDir, { recursive: true });
753
832
  events.push({ type: "created", path: contentDir });
754
833
  }
755
- const configPath = path5.join(projectDir, "chronicle.yaml");
834
+ const configPath = path7.join(projectDir, "chronicle.yaml");
756
835
  if (!fs4.existsSync(configPath)) {
757
836
  fs4.writeFileSync(configPath, stringify(defaultInitConfig));
758
837
  events.push({ type: "created", path: configPath });
@@ -761,11 +840,11 @@ function runInit(projectDir) {
761
840
  }
762
841
  const contentFiles = fs4.readdirSync(contentDir);
763
842
  if (contentFiles.length === 0) {
764
- const indexPath = path5.join(contentDir, "index.mdx");
843
+ const indexPath = path7.join(contentDir, "index.mdx");
765
844
  fs4.writeFileSync(indexPath, sampleMdx);
766
845
  events.push({ type: "created", path: indexPath });
767
846
  }
768
- const gitignorePath = path5.join(projectDir, ".gitignore");
847
+ const gitignorePath = path7.join(projectDir, ".gitignore");
769
848
  if (fs4.existsSync(gitignorePath)) {
770
849
  const existing = fs4.readFileSync(gitignorePath, "utf-8");
771
850
  const existingLines = new Set(existing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -22,12 +22,16 @@
22
22
  "devDependencies": {
23
23
  "@biomejs/biome": "^2.3.13",
24
24
  "@raystack/tools-config": "0.56.0",
25
+ "@types/hast": "^3.0.4",
25
26
  "@types/lodash": "^4.17.23",
27
+ "@types/mdast": "^4.0.4",
26
28
  "@types/mdx": "^2.0.13",
27
29
  "@types/node": "^25.1.0",
28
30
  "@types/react": "^19.2.10",
29
31
  "@types/react-dom": "^19.2.3",
30
32
  "@types/semver": "^7.7.1",
33
+ "@types/unist": "^3.0.3",
34
+ "mdast-util-mdx-jsx": "^3.2.0",
31
35
  "semver": "^7.7.4",
32
36
  "typescript": "5.9.3"
33
37
  },
@@ -13,10 +13,10 @@ import { usePageContext } from '@/lib/page-context';
13
13
  import styles from './search.module.css';
14
14
 
15
15
  interface SearchProps {
16
- className?: string;
16
+ classNames?: { trigger?: string };
17
17
  }
18
18
 
19
- export function Search({ className }: SearchProps) {
19
+ export function Search({ classNames }: SearchProps) {
20
20
  const [open, setOpen] = useState(false);
21
21
  const navigate = useNavigate();
22
22
  const { version } = usePageContext();
@@ -60,7 +60,7 @@ export function Search({ className }: SearchProps) {
60
60
  aria-label='Search'
61
61
  title='Search (Ctrl/⌘K)'
62
62
  onClick={() => setOpen(true)}
63
- className={className}
63
+ className={classNames?.trigger}
64
64
  >
65
65
  <MagnifyingGlassIcon width={16} height={16} />
66
66
  </IconButton>
@@ -0,0 +1,59 @@
1
+ import path from 'node:path'
2
+ import { visit } from 'unist-util-visit'
3
+ import type { Plugin } from 'unified'
4
+ import type { Image, Html } from 'mdast'
5
+ import type { Element } from 'hast'
6
+ import type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute } from 'mdast-util-mdx-jsx'
7
+
8
+ function resolveUrl(src: string, dir: string): string {
9
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
10
+ if (src.startsWith('//')) return src
11
+ if (src.startsWith('#')) return src
12
+ if (src.startsWith('/_content/')) return src
13
+
14
+ if (src.startsWith('/')) return `/_content${src}`
15
+ return `/_content/${path.posix.normalize(path.posix.join(dir, src))}`
16
+ }
17
+
18
+ const remarkResolveImages: Plugin = () => {
19
+ return (tree, file) => {
20
+ const filePath = file.path
21
+ if (!filePath) return
22
+
23
+ const contentIdx = filePath.lastIndexOf('/content/')
24
+ if (contentIdx === -1) return
25
+
26
+ const relative = filePath.slice(contentIdx + '/content/'.length)
27
+ const dir = path.posix.dirname(relative)
28
+
29
+ visit(tree, 'image', (node: Image) => {
30
+ if (!node.url) return
31
+ node.url = resolveUrl(node.url, dir)
32
+ })
33
+
34
+ visit(tree, 'html', (node: Html) => {
35
+ node.value = node.value.replace(
36
+ /(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi,
37
+ (_, before, src, after) => `${before}${resolveUrl(src, dir)}${after}`
38
+ )
39
+ })
40
+
41
+ visit(tree, (node) => {
42
+ if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
43
+ const jsx = node as MdxJsxFlowElement | MdxJsxTextElement
44
+ if (jsx.name !== 'img') return
45
+ const srcAttr = jsx.attributes.find((a): a is MdxJsxAttribute => a.type === 'mdxJsxAttribute' && a.name === 'src')
46
+ if (!srcAttr?.value || typeof srcAttr.value !== 'string') return
47
+ srcAttr.value = resolveUrl(srcAttr.value, dir)
48
+ })
49
+
50
+ visit(tree, 'element', (node: Element) => {
51
+ if (node.tagName !== 'img') return
52
+ const src = node.properties?.src
53
+ if (typeof src !== 'string') return
54
+ node.properties.src = resolveUrl(src, dir)
55
+ })
56
+ }
57
+ }
58
+
59
+ export default remarkResolveImages
@@ -0,0 +1,32 @@
1
+ import path from 'node:path'
2
+ import { visit } from 'unist-util-visit'
3
+ import type { Plugin } from 'unified'
4
+ import type { Link } from 'mdast'
5
+
6
+ const remarkResolveLinks: Plugin = () => {
7
+ return (tree, file) => {
8
+ const filePath = file.path
9
+ if (!filePath) return
10
+
11
+ const contentIdx = filePath.lastIndexOf('/content/')
12
+ if (contentIdx === -1) return
13
+
14
+ const relative = filePath.slice(contentIdx + '/content/'.length)
15
+ const dir = path.posix.dirname(relative)
16
+
17
+ visit(tree, 'link', (node: Link) => {
18
+ if (!node.url) return
19
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(node.url)) return
20
+ if (node.url.startsWith('#')) return
21
+ if (node.url.startsWith('/')) return
22
+
23
+ const [rawPath, hash] = node.url.split('#')
24
+ const stripped = rawPath.replace(/\.mdx?$/, '')
25
+ let resolved = path.posix.normalize(path.posix.join(dir, stripped))
26
+ resolved = resolved.replace(/\/(index|readme)$/i, '') || '.'
27
+ node.url = `/${resolved}${hash ? `#${hash}` : ''}`
28
+ })
29
+ }
30
+ }
31
+
32
+ export default remarkResolveLinks
package/src/lib/source.ts CHANGED
@@ -23,6 +23,11 @@ const frontmatterGlob: Record<string, Record<string, unknown>> = import.meta.glo
23
23
  { eager: true, import: 'frontmatter' }
24
24
  );
25
25
 
26
+ const readingTimeGlob: Record<string, { text: string; minutes: number; words: number; time: number } | undefined> = import.meta.glob(
27
+ '../../.content/**/*.{mdx,md}',
28
+ { eager: true, import: 'readingTime' }
29
+ );
30
+
26
31
  const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
27
32
  '../../.content/**/meta.json',
28
33
  { eager: true }
@@ -38,10 +43,12 @@ function buildFiles() {
38
43
  for (const [key, data] of Object.entries(frontmatterGlob)) {
39
44
  const originalPath = key.slice(CONTENT_PREFIX.length);
40
45
  const relativePath = originalPath.replace(/readme\.(mdx?)$/i, 'index.$1');
46
+ const rt = readingTimeGlob[key];
47
+ const _readingTime = rt?.minutes != null ? Math.max(1, Math.round(rt.minutes)) : undefined;
41
48
  files.push({
42
49
  type: 'page',
43
50
  path: relativePath,
44
- data: { ...data, _relativePath: relativePath, _originalPath: originalPath }
51
+ data: { ...data, _readingTime, _relativePath: relativePath, _originalPath: originalPath }
45
52
  });
46
53
  }
47
54
 
@@ -1,6 +1,5 @@
1
1
  import { cx } from 'class-variance-authority';
2
2
  import type { ReactNode } from 'react';
3
- import { Search } from '@/components/ui/search';
4
3
  import { buildApiPageTree } from '@/lib/api-routes';
5
4
  import { usePageContext } from '@/lib/page-context';
6
5
  import { getTheme } from '@/themes/registry';
@@ -26,7 +25,6 @@ export function ApiLayout({ children }: ApiLayoutProps) {
26
25
  content: styles.content
27
26
  }}
28
27
  >
29
- <Search className={styles.hiddenSearch} />
30
28
  {children}
31
29
  </Layout>
32
30
  );
@@ -40,7 +40,7 @@ export function App() {
40
40
  <ApiPage slug={apiSlug} />
41
41
  </ApiLayout>
42
42
  ) : (
43
- <DocsLayout>
43
+ <DocsLayout hideSidebar={isLanding}>
44
44
  {isLanding ? <LandingPage /> : <DocsPage slug={docsSlug} />}
45
45
  </DocsLayout>
46
46
  )}
@@ -51,11 +51,11 @@ export default defineHandler(async event => {
51
51
  ? await response.json()
52
52
  : await response.text();
53
53
 
54
- return {
54
+ return Response.json({
55
55
  status: response.status,
56
56
  statusText: response.statusText,
57
57
  body: responseBody
58
- };
58
+ });
59
59
  } catch (error) {
60
60
  const message =
61
61
  error instanceof Error
@@ -1,5 +1,5 @@
1
1
  import { defineHandler } from 'nitro';
2
2
 
3
3
  export default defineHandler(() => {
4
- return { status: 'ok' };
4
+ return Response.json({ status: 'ok' });
5
5
  });
@@ -12,11 +12,11 @@ export default defineHandler(async event => {
12
12
 
13
13
  const nav = await getPageNav(slug);
14
14
 
15
- return {
15
+ return Response.json({
16
16
  frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
17
17
  relativePath: getRelativePath(page),
18
18
  originalPath: getOriginalPath(page),
19
19
  prev: nav.prev,
20
20
  next: nav.next,
21
- };
21
+ });
22
22
  });
@@ -125,7 +125,7 @@ export default defineHandler(async event => {
125
125
 
126
126
  if (!query) {
127
127
  const docs = await getDocs(ctx);
128
- return docs
128
+ return Response.json(docs
129
129
  .filter(d => d.type === 'page')
130
130
  .slice(0, 8)
131
131
  .map(d => ({
@@ -133,13 +133,13 @@ export default defineHandler(async event => {
133
133
  url: d.url,
134
134
  type: d.type,
135
135
  content: d.title
136
- }));
136
+ })));
137
137
  }
138
138
 
139
- return index.search(query).map(r => ({
139
+ return Response.json(index.search(query).map(r => ({
140
140
  id: r.id,
141
141
  url: r.url,
142
142
  type: r.type,
143
143
  content: r.title
144
- }));
144
+ })));
145
145
  });
@@ -15,7 +15,7 @@ export default defineHandler(async event => {
15
15
  }
16
16
 
17
17
  const apiConfigs = getApiConfigsForVersion(config, versionDir);
18
- if (!apiConfigs.length) return [];
18
+ if (!apiConfigs.length) return Response.json([]);
19
19
 
20
- return loadApiSpecs(apiConfigs);
20
+ return Response.json(await loadApiSpecs(apiConfigs));
21
21
  });
@@ -34,6 +34,5 @@ export default defineHandler(async event => {
34
34
  throw new HTTPError({ status: 404, message: 'Not Found' });
35
35
  }
36
36
 
37
- event.res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
38
- return matter(raw).content;
37
+ return new Response(matter(raw).content, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } });
39
38
  });
@@ -21,6 +21,5 @@ export default defineHandler(async event => {
21
21
  ctx,
22
22
  );
23
23
 
24
- event.res.headers.set('Content-Type', 'text/plain');
25
- return body;
24
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
26
25
  });
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { defineHandler, HTTPError } from 'nitro';
4
+ import { safePath } from '@/server/utils/safe-path';
5
+
6
+ const MIME: Record<string, string> = {
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.gif': 'image/gif',
11
+ '.svg': 'image/svg+xml',
12
+ '.webp': 'image/webp',
13
+ '.ico': 'image/x-icon',
14
+ '.pdf': 'application/pdf',
15
+ };
16
+
17
+ export default defineHandler(async event => {
18
+ const pathname = event.path?.replace(/^\/_content/, '') || '';
19
+ if (!pathname || pathname.endsWith('.md') || pathname.endsWith('.mdx')) {
20
+ throw new HTTPError({ status: 404, message: 'Not Found' });
21
+ }
22
+
23
+ const contentDir = __CHRONICLE_CONTENT_DIR__;
24
+ let filePath: string | null = null;
25
+ try { filePath = safePath(contentDir, pathname); } catch { /* malformed URL encoding */ }
26
+ if (!filePath) throw new HTTPError({ status: 404, message: 'Not Found' });
27
+
28
+ const data = await fs.readFile(filePath).catch(() => null);
29
+ if (!data) throw new HTTPError({ status: 404, message: 'Not Found' });
30
+
31
+ const ext = path.extname(filePath).toLowerCase();
32
+ const contentType = MIME[ext] ?? 'application/octet-stream';
33
+
34
+ return new Response(data, {
35
+ headers: {
36
+ 'Content-Type': contentType,
37
+ 'Cache-Control': 'public, max-age=86400',
38
+ },
39
+ });
40
+ });
@@ -4,7 +4,7 @@ import { buildLlmsTxt } from '@/lib/llms';
4
4
  import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
5
5
  import { LATEST_CONTEXT } from '@/lib/version-source';
6
6
 
7
- export default defineHandler(async event => {
7
+ export default defineHandler(async () => {
8
8
  const config = loadConfig();
9
9
 
10
10
  const pages = await getPagesForVersion(LATEST_CONTEXT);
@@ -14,6 +14,5 @@ export default defineHandler(async event => {
14
14
  LATEST_CONTEXT,
15
15
  );
16
16
 
17
- event.res.headers.set('Content-Type', 'text/plain');
18
- return body;
17
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
19
18
  });
@@ -69,7 +69,5 @@ export default defineHandler(async event => {
69
69
  },
70
70
  );
71
71
 
72
- event.res.headers.set('Content-Type', 'image/svg+xml');
73
- event.res.headers.set('Cache-Control', 'public, max-age=86400');
74
- return svg;
72
+ return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } });
75
73
  });
@@ -1,11 +1,10 @@
1
1
  import { defineHandler } from 'nitro';
2
2
  import { loadConfig } from '@/lib/config';
3
3
 
4
- export default defineHandler(event => {
4
+ export default defineHandler(() => {
5
5
  const config = loadConfig();
6
6
  const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
7
7
  const body = `User-agent: *\nAllow: /${sitemap}`;
8
8
 
9
- event.res.headers.set('Content-Type', 'text/plain');
10
- return body;
9
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
11
10
  });
@@ -4,12 +4,11 @@ import { getAllVersions, getApiConfigsForVersion, loadConfig } from '@/lib/confi
4
4
  import { loadApiSpecs } from '@/lib/openapi';
5
5
  import { getPages } from '@/lib/source';
6
6
 
7
- export default defineHandler(async event => {
7
+ export default defineHandler(async () => {
8
8
  const config = loadConfig();
9
9
 
10
10
  if (!config.url) {
11
- event.res.headers.set('Content-Type', 'application/xml');
12
- return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
11
+ return new Response('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>', { headers: { 'Content-Type': 'application/xml' } });
13
12
  }
14
13
 
15
14
  const baseUrl = config.url.replace(/\/$/, '');
@@ -43,6 +42,5 @@ export default defineHandler(async event => {
43
42
  ${[...docPages, ...apiPages].join('\n')}
44
43
  </urlset>`;
45
44
 
46
- event.res.headers.set('Content-Type', 'application/xml');
47
- return xml;
45
+ return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
48
46
  });
@@ -7,7 +7,8 @@ import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import remarkDirective from 'remark-directive';
9
9
  import { type InlineConfig } from 'vite';
10
- import remarkStripMdExtensions from '../lib/remark-strip-md-extensions';
10
+ import remarkResolveImages from '../lib/remark-resolve-images';
11
+ import remarkResolveLinks from '../lib/remark-resolve-links';
11
12
  import remarkReadingTime from 'remark-reading-time';
12
13
  import remarkUnusedDirectives from '../lib/remark-unused-directives';
13
14
 
@@ -56,6 +57,7 @@ export async function createViteConfig(
56
57
  mdx({
57
58
  default: defineFumadocsConfig({
58
59
  mdxOptions: {
60
+ remarkImageOptions: false,
59
61
  valueToExport: ['readingTime'],
60
62
  remarkPlugins: [
61
63
  remarkDirective,
@@ -77,7 +79,8 @@ export async function createViteConfig(
77
79
  },
78
80
  }],
79
81
  remarkUnusedDirectives,
80
- remarkStripMdExtensions,
82
+ remarkResolveLinks,
83
+ remarkResolveImages,
81
84
  remarkMdxMermaid,
82
85
  remarkReadingTime,
83
86
  ],
@@ -79,3 +79,7 @@
79
79
  flex: 1;
80
80
  background: var(--rs-color-background-neutral-primary);
81
81
  }
82
+
83
+ .hiddenTrigger {
84
+ display: none;
85
+ }
@@ -6,6 +6,7 @@ import { useLocation, useNavigate } from 'react-router';
6
6
  import { getLandingEntries } from '@/lib/config';
7
7
  import { getActiveContentDir } from '@/lib/navigation';
8
8
  import { usePageContext } from '@/lib/page-context';
9
+ import { Search } from '@/components/ui/search';
9
10
  import type { ThemeLayoutProps } from '@/types';
10
11
  import { ChapterNav } from './ChapterNav';
11
12
  import styles from './Layout.module.css';
@@ -82,6 +83,7 @@ function LayoutInner({
82
83
  </aside>
83
84
  ) : null}
84
85
  <div className={cx(styles.content, classNames?.content)}>
86
+ {config.search?.enabled && <Search classNames={{ trigger: styles.hiddenTrigger }} />}
85
87
  {children}
86
88
  </div>
87
89
  </Flex>
@@ -1,14 +0,0 @@
1
- import { visit } from 'unist-util-visit'
2
- import type { Plugin } from 'unified'
3
-
4
- const remarkStripMdExtensions: Plugin = () => {
5
- return (tree) => {
6
- visit(tree, 'link', (node: any) => {
7
- if (!node.url) return
8
- if (node.url.startsWith('http://') || node.url.startsWith('https://')) return
9
- node.url = node.url.replace(/\.mdx?(#|$)/, '$1')
10
- })
11
- }
12
- }
13
-
14
- export default remarkStripMdExtensions