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
4 changes: 4 additions & 0 deletions packages/chronicle/src/lib/mdx-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const MdxNodeType = {
JsxFlow: 'mdxJsxFlowElement',
JsxText: 'mdxJsxTextElement',
} as const
7 changes: 7 additions & 0 deletions packages/chronicle/src/lib/page-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export function PageProvider({
frontmatter: Frontmatter;
relativePath: string;
originalPath?: string;
images?: string[];
prev?: PageNavLink | null;
next?: PageNavLink | null;
}
Expand All @@ -132,6 +133,12 @@ export function PageProvider({
try {
const data = await fetchPageData(slug);
if (cancelled.current) return;
if (data.images?.length) {
for (const src of data.images) {
const img = new Image();
img.src = src;
}
}
const { content, toc } = await loadMdx(data.originalPath || data.relativePath);
if (cancelled.current) return;
setErrorStatus(null);
Expand Down
23 changes: 21 additions & 2 deletions packages/chronicle/src/lib/remark-resolve-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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'
import { MdxNodeType } from './mdx-utils'

function resolveUrl(src: string, dir: string): string {
if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
Expand All @@ -26,33 +27,51 @@ const remarkResolveImages: Plugin = () => {
const relative = filePath.slice(contentIdx + '/content/'.length)
const dir = path.posix.dirname(relative)

const seen = new Set<string>()
const images: string[] = []

function collect(src: string) {
if (!src || seen.has(src) || /^data:/i.test(src)) return
seen.add(src)
images.push(src)
}

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

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

visit(tree, (node) => {
if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
if (node.type !== MdxNodeType.JsxFlow && node.type !== MdxNodeType.JsxText) 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)
collect(srcAttr.value)
})

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)
collect(node.properties.src as string)
})

file.data.images = images
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/chronicle/src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ const readingTimeGlob: Record<string, { text: string; minutes: number; words: nu
{ eager: true, import: 'readingTime' }
);

const imagesGlob: Record<string, string[] | undefined> = import.meta.glob(
'../../.content/**/*.{mdx,md}',
{ eager: true, import: 'images' }
);

const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
'../../.content/**/meta.json',
{ eager: true }
Expand All @@ -54,10 +59,11 @@ function buildFiles() {
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;
const _images = imagesGlob[key] ?? [];
files.push({
type: 'page',
path: relativePath,
data: { ...data, _readingTime, _relativePath: relativePath, _originalPath: originalPath }
data: { ...data, _readingTime, _images, _relativePath: relativePath, _originalPath: originalPath }
});
}

Expand Down Expand Up @@ -285,6 +291,10 @@ export function getOriginalPath(page: { data: unknown }): string {
return ((page.data as Record<string, unknown>)._originalPath as string) ?? '';
}

export function getPageImages(page: { data: unknown }): string[] {
return ((page.data as Record<string, unknown>)._images as string[]) ?? [];
}

export async function getPageSearchContent(page: { data: unknown }): Promise<{ headings: string; body: string }> {
const originalPath = getOriginalPath(page);
if (!originalPath) return { headings: '', body: '' };
Expand Down
3 changes: 2 additions & 1 deletion 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, isDraft } from '@/lib/source';
import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';

export default defineHandler(async event => {
const slugParam = event.url.searchParams.get('slug') ?? '';
Expand All @@ -16,6 +16,7 @@ export default defineHandler(async event => {
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
relativePath: getRelativePath(page),
originalPath: getOriginalPath(page),
images: getPageImages(page),
prev: nav.prev,
next: nav.next,
});
Expand Down
6 changes: 5 additions & 1 deletion packages/chronicle/src/server/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
import { loadApiSpecs } from '@/lib/openapi';
import { PageProvider } from '@/lib/page-context';
import { resolveRoute, RouteType } from '@/lib/route-resolver';
import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, isDraft } from '@/lib/source';
import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';
import { getFirstApiUrl } from '@/lib/api-routes';
import { StatusCodes } from 'http-status-codes';
import { resolveDocsRedirect } from '@/lib/tree-utils';
Expand Down Expand Up @@ -79,6 +79,7 @@ export default {
const relativePath = page ? getRelativePath(page) : null;
const originalPath = page ? getOriginalPath(page) : null;
const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath!) : null;
const pageImages = page ? getPageImages(page) : [];

const pageData = page
? {
Expand Down Expand Up @@ -125,6 +126,9 @@ export default {
{assets.js.map((attr: { href: string }) => (
<link key={attr.href} rel="modulepreload" {...attr} />
))}
{pageImages.map((src: string) => (
<link key={src} rel="preload" as="image" href={src} />
))}
<script type="module" src={assets.entry} />
<script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
</head>
Expand Down
2 changes: 1 addition & 1 deletion packages/chronicle/src/server/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function createViteConfig(
default: defineFumadocsConfig({
mdxOptions: {
remarkImageOptions: false,
valueToExport: ['readingTime'],
valueToExport: ['readingTime', 'images'],
remarkPlugins: [
remarkDirective,
[remarkDirectiveAdmonition, {
Expand Down
Loading