Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/chronicle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@raystack/tools-config": "0.56.0",
"@types/hast": "^3.0.4",
"@types/lodash": "^4.17.23",
"@types/mdast": "^4.0.4",
"@types/mdx": "^2.0.13",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"@types/unist": "^3.0.3",
"mdast-util-mdx-jsx": "^3.2.0",
"semver": "^7.7.4",
"typescript": "5.9.3"
},
Expand Down
59 changes: 59 additions & 0 deletions packages/chronicle/src/lib/remark-resolve-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import path from 'node:path'
import { visit } from 'unist-util-visit'
import type { Plugin } from 'unified'
import type { Image, Html } from 'mdast'
import type { Element } from 'hast'
import type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute } from 'mdast-util-mdx-jsx'

function resolveUrl(src: string, dir: string): string {
if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
if (src.startsWith('//')) return src
if (src.startsWith('#')) return src
if (src.startsWith('/_content/')) return src

if (src.startsWith('/')) return `/_content${src}`
return `/_content/${path.posix.normalize(path.posix.join(dir, src))}`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const remarkResolveImages: Plugin = () => {
return (tree, file) => {
const filePath = file.path
if (!filePath) return

const contentIdx = filePath.lastIndexOf('/content/')
if (contentIdx === -1) return

const relative = filePath.slice(contentIdx + '/content/'.length)
const dir = path.posix.dirname(relative)
Comment thread
rsbh marked this conversation as resolved.

visit(tree, 'image', (node: Image) => {
if (!node.url) return
node.url = resolveUrl(node.url, dir)
})

visit(tree, 'html', (node: Html) => {
node.value = node.value.replace(
/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi,
(_, before, src, after) => `${before}${resolveUrl(src, dir)}${after}`
)
})

visit(tree, (node) => {
if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
const jsx = node as MdxJsxFlowElement | MdxJsxTextElement
if (jsx.name !== 'img') return
const srcAttr = jsx.attributes.find((a): a is MdxJsxAttribute => a.type === 'mdxJsxAttribute' && a.name === 'src')
if (!srcAttr?.value || typeof srcAttr.value !== 'string') return
srcAttr.value = resolveUrl(srcAttr.value, dir)
})

visit(tree, 'element', (node: Element) => {
if (node.tagName !== 'img') return
const src = node.properties?.src
if (typeof src !== 'string') return
node.properties.src = resolveUrl(src, dir)
})
}
}

export default remarkResolveImages
9 changes: 8 additions & 1 deletion packages/chronicle/src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const frontmatterGlob: Record<string, Record<string, unknown>> = import.meta.glo
{ eager: true, import: 'frontmatter' }
);

const readingTimeGlob: Record<string, { text: string; minutes: number; words: number; time: number } | undefined> = import.meta.glob(
'../../.content/**/*.{mdx,md}',
{ eager: true, import: 'readingTime' }
);

const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
'../../.content/**/meta.json',
{ eager: true }
Expand All @@ -38,10 +43,12 @@ function buildFiles() {
for (const [key, data] of Object.entries(frontmatterGlob)) {
const originalPath = key.slice(CONTENT_PREFIX.length);
const relativePath = originalPath.replace(/readme\.(mdx?)$/i, 'index.$1');
const rt = readingTimeGlob[key];
const _readingTime = rt?.minutes != null ? Math.max(1, Math.round(rt.minutes)) : undefined;
files.push({
type: 'page',
path: relativePath,
data: { ...data, _relativePath: relativePath, _originalPath: originalPath }
data: { ...data, _readingTime, _relativePath: relativePath, _originalPath: originalPath }
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/chronicle/src/server/api/apis-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export default defineHandler(async event => {
? await response.json()
: await response.text();

return {
return Response.json({
status: response.status,
statusText: response.statusText,
body: responseBody
};
});
} catch (error) {
const message =
error instanceof Error
Expand Down
2 changes: 1 addition & 1 deletion packages/chronicle/src/server/api/health.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineHandler } from 'nitro';

export default defineHandler(() => {
return { status: 'ok' };
return Response.json({ status: 'ok' });
});
18 changes: 6 additions & 12 deletions packages/chronicle/src/server/api/page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineHandler, HTTPError } from 'nitro';
import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, loadPageModule } from '@/lib/source';
import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';

export default defineHandler(async event => {
const slugParam = event.url.searchParams.get('slug') ?? '';
Expand All @@ -11,18 +11,12 @@ export default defineHandler(async event => {
}

const nav = await getPageNav(slug);
const originalPath = getOriginalPath(page);
const relativePath = getRelativePath(page);
const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath) : null;

return {
frontmatter: {
...extractFrontmatter(page, slug[slug.length - 1]),
_readingTime: mdxModule?._readingTime,
},
relativePath,
originalPath,
return Response.json({
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
relativePath: getRelativePath(page),
originalPath: getOriginalPath(page),
prev: nav.prev,
next: nav.next,
};
});
});
8 changes: 4 additions & 4 deletions packages/chronicle/src/server/api/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,21 @@ export default defineHandler(async event => {

if (!query) {
const docs = await getDocs(ctx);
return docs
return Response.json(docs
.filter(d => d.type === 'page')
.slice(0, 8)
.map(d => ({
id: d.id,
url: d.url,
type: d.type,
content: d.title
}));
})));
}

return index.search(query).map(r => ({
return Response.json(index.search(query).map(r => ({
id: r.id,
url: r.url,
type: r.type,
content: r.title
}));
})));
});
4 changes: 2 additions & 2 deletions packages/chronicle/src/server/api/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineHandler(async event => {
}

const apiConfigs = getApiConfigsForVersion(config, versionDir);
if (!apiConfigs.length) return [];
if (!apiConfigs.length) return Response.json([]);

return loadApiSpecs(apiConfigs);
return Response.json(await loadApiSpecs(apiConfigs));
});
3 changes: 1 addition & 2 deletions packages/chronicle/src/server/routes/[...slug].md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,5 @@ export default defineHandler(async event => {
throw new HTTPError({ status: 404, message: 'Not Found' });
}

event.res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
return matter(raw).content;
return new Response(matter(raw).content, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } });
});
3 changes: 1 addition & 2 deletions packages/chronicle/src/server/routes/[version]/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ export default defineHandler(async event => {
ctx,
);

event.res.headers.set('Content-Type', 'text/plain');
return body;
return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
});
40 changes: 40 additions & 0 deletions packages/chronicle/src/server/routes/_content/[...path].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { defineHandler, HTTPError } from 'nitro';
import { safePath } from '@/server/utils/safe-path';

const MIME: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf',
};

export default defineHandler(async event => {
const pathname = event.path?.replace(/^\/_content/, '') || '';
if (!pathname || pathname.endsWith('.md') || pathname.endsWith('.mdx')) {
throw new HTTPError({ status: 404, message: 'Not Found' });
}

const contentDir = __CHRONICLE_CONTENT_DIR__;
let filePath: string | null = null;
try { filePath = safePath(contentDir, pathname); } catch { /* malformed URL encoding */ }
if (!filePath) throw new HTTPError({ status: 404, message: 'Not Found' });

const data = await fs.readFile(filePath).catch(() => null);
if (!data) throw new HTTPError({ status: 404, message: 'Not Found' });

const ext = path.extname(filePath).toLowerCase();
const contentType = MIME[ext] ?? 'application/octet-stream';

return new Response(data, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
});
});
5 changes: 2 additions & 3 deletions packages/chronicle/src/server/routes/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { buildLlmsTxt } from '@/lib/llms';
import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
import { LATEST_CONTEXT } from '@/lib/version-source';

export default defineHandler(async event => {
export default defineHandler(async () => {
const config = loadConfig();

const pages = await getPagesForVersion(LATEST_CONTEXT);
Expand All @@ -14,6 +14,5 @@ export default defineHandler(async event => {
LATEST_CONTEXT,
);

event.res.headers.set('Content-Type', 'text/plain');
return body;
return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
});
4 changes: 1 addition & 3 deletions packages/chronicle/src/server/routes/og.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,5 @@ export default defineHandler(async event => {
},
);

event.res.headers.set('Content-Type', 'image/svg+xml');
event.res.headers.set('Cache-Control', 'public, max-age=86400');
return svg;
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } });
});
5 changes: 2 additions & 3 deletions packages/chronicle/src/server/routes/robots.txt.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { defineHandler } from 'nitro';
import { loadConfig } from '@/lib/config';

export default defineHandler(event => {
export default defineHandler(() => {
const config = loadConfig();
const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
const body = `User-agent: *\nAllow: /${sitemap}`;

event.res.headers.set('Content-Type', 'text/plain');
return body;
return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
});
8 changes: 3 additions & 5 deletions packages/chronicle/src/server/routes/sitemap.xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import { getAllVersions, getApiConfigsForVersion, loadConfig } from '@/lib/confi
import { loadApiSpecs } from '@/lib/openapi';
import { getPages } from '@/lib/source';

export default defineHandler(async event => {
export default defineHandler(async () => {
const config = loadConfig();

if (!config.url) {
event.res.headers.set('Content-Type', 'application/xml');
return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
return new Response('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>', { headers: { 'Content-Type': 'application/xml' } });
}

const baseUrl = config.url.replace(/\/$/, '');
Expand Down Expand Up @@ -43,6 +42,5 @@ export default defineHandler(async event => {
${[...docPages, ...apiPages].join('\n')}
</urlset>`;

event.res.headers.set('Content-Type', 'application/xml');
return xml;
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
});
3 changes: 3 additions & 0 deletions packages/chronicle/src/server/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import remarkDirective from 'remark-directive';
import { type InlineConfig } from 'vite';
import remarkResolveImages from '../lib/remark-resolve-images';
import remarkResolveLinks from '../lib/remark-resolve-links';
import remarkReadingTime from 'remark-reading-time';
import remarkUnusedDirectives from '../lib/remark-unused-directives';
Expand Down Expand Up @@ -56,6 +57,7 @@ export async function createViteConfig(
mdx({
default: defineFumadocsConfig({
mdxOptions: {
remarkImageOptions: false,
valueToExport: ['readingTime'],
remarkPlugins: [
remarkDirective,
Expand All @@ -78,6 +80,7 @@ export async function createViteConfig(
}],
remarkUnusedDirectives,
remarkResolveLinks,
remarkResolveImages,
remarkMdxMermaid,
remarkReadingTime,
],
Expand Down
Loading