11import * as React from 'react'
22import { MarkdownLink } from '~/components/MarkdownLink'
33import type { HTMLProps } from 'react'
4- import { createHighlighter as shikiGetHighlighter } from 'shiki/bundle- web.mjs '
4+ import { createHighlighter , type HighlighterGeneric } from 'shiki/bundle/ web'
55import { transformerNotationDiff } from '@shikijs/transformers'
66import parse , {
77 attributesToProps ,
88 domToReact ,
99 Element ,
1010 HTMLReactParserOptions ,
1111} from 'html-react-parser'
12- import mermaid from 'mermaid'
12+ import type { Mermaid } from 'mermaid'
1313import { useToast } from '~/components/ToastProvider'
1414import { twMerge } from 'tailwind-merge'
1515import { useMarkdownHeadings } from '~/components/MarkdownHeadingContext'
@@ -92,17 +92,29 @@ const markdownComponents: Record<string, React.FC> = {
9292 < iframe { ...props } className = "w-full" title = "Embedded Content" />
9393 ) ,
9494 // eslint-disable-next-line @typescript-eslint/no-unused-vars
95- img : ( { children, alt, ...props } : HTMLProps < HTMLImageElement > ) => (
96- < img
97- { ...props }
98- alt = { alt ?? '' }
99- className = { `max-w-full h-auto rounded-lg shadow-md ${
100- props . className ?? ''
101- } `}
102- loading = "lazy"
103- decoding = "async"
104- />
105- ) ,
95+ img : ( { children, alt, src, ...props } : HTMLProps < HTMLImageElement > ) => {
96+ // Use Netlify Image CDN for local images
97+ const optimizedSrc =
98+ src &&
99+ ! src . startsWith ( 'http' ) &&
100+ ! src . startsWith ( 'data:' ) &&
101+ ! src . endsWith ( '.svg' )
102+ ? `/.netlify/images?url=${ encodeURIComponent ( src ) } &w=800&q=80`
103+ : src
104+
105+ return (
106+ < img
107+ { ...props }
108+ src = { optimizedSrc }
109+ alt = { alt ?? '' }
110+ className = { `max-w-full h-auto rounded-lg shadow-md ${
111+ props . className ?? ''
112+ } `}
113+ loading = "lazy"
114+ decoding = "async"
115+ />
116+ )
117+ } ,
106118}
107119
108120export function extractPreAttributes ( html : string ) : {
@@ -127,7 +139,16 @@ export function extractPreAttributes(html: string): {
127139
128140const genSvgMap = new Map < string , string > ( )
129141
130- mermaid . initialize ( { startOnLoad : true , securityLevel : 'loose' } )
142+ // Lazy load mermaid only when needed
143+ let mermaidInstance : Mermaid | null = null
144+ async function getMermaid ( ) : Promise < Mermaid > {
145+ if ( ! mermaidInstance ) {
146+ const { default : mermaid } = await import ( 'mermaid' )
147+ mermaid . initialize ( { startOnLoad : false , securityLevel : 'loose' } )
148+ mermaidInstance = mermaid
149+ }
150+ return mermaidInstance
151+ }
131152
132153export function CodeBlock ( {
133154 isEmbedded,
@@ -174,12 +195,17 @@ export function CodeBlock({
174195 ; ( async ( ) => {
175196 const themes = [ 'github-light' , 'tokyo-night' ]
176197
198+ // Normalize language name
199+ const normalizedLang = LANG_ALIASES [ lang ] || lang
200+ const effectiveLang =
201+ normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
202+
177203 const highlighter = await getHighlighter ( lang , themes )
178204
179205 const htmls = await Promise . all (
180206 themes . map ( async ( theme ) => {
181207 const output = highlighter . codeToHtml ( code , {
182- lang : lang === 'mermaid' ? 'plaintext' : lang ,
208+ lang : effectiveLang ,
183209 theme,
184210 transformers : [ transformerNotationDiff ( ) ] ,
185211 } )
@@ -188,6 +214,7 @@ export function CodeBlock({
188214 const preAttributes = extractPreAttributes ( output )
189215 let svgHtml = genSvgMap . get ( code || '' )
190216 if ( ! svgHtml ) {
217+ const mermaid = await getMermaid ( )
191218 const { svg } = await mermaid . render ( 'foo' , code || '' )
192219 genSvgMap . set ( code || '' , svg )
193220 svgHtml = svg
@@ -278,31 +305,66 @@ const cache = <T extends (...args: any[]) => any>(fn: T) => {
278305 }
279306}
280307
281- const highlighterPromise = shikiGetHighlighter ( { } as any )
308+ // Core languages to bundle (most commonly used in TanStack docs)
309+ const CORE_LANGS = [
310+ 'typescript' ,
311+ 'javascript' ,
312+ 'tsx' ,
313+ 'jsx' ,
314+ 'bash' ,
315+ 'json' ,
316+ 'html' ,
317+ 'css' ,
318+ 'markdown' ,
319+ 'plaintext' ,
320+ ] as const
321+
322+ // Language aliases mapping
323+ const LANG_ALIASES : Record < string , string > = {
324+ ts : 'typescript' ,
325+ js : 'javascript' ,
326+ sh : 'bash' ,
327+ shell : 'bash' ,
328+ console : 'bash' ,
329+ zsh : 'bash' ,
330+ md : 'markdown' ,
331+ txt : 'plaintext' ,
332+ text : 'plaintext' ,
333+ }
282334
283- const getHighlighter = cache ( async ( language : string , themes : string [ ] ) => {
284- const highlighter = await highlighterPromise
335+ // Lazy highlighter initialization
336+ let highlighterPromise : Promise < HighlighterGeneric < any , any > > | null = null
285337
286- const loadedLanguages = highlighter . getLoadedLanguages ( )
287- const loadedThemes = highlighter . getLoadedThemes ( )
288-
289- const promises = [ ]
290- if ( ! loadedLanguages . includes ( language as any ) ) {
291- promises . push (
292- highlighter . loadLanguage (
293- language === 'mermaid' ? 'plaintext' : ( language as any ) ,
294- ) ,
295- )
338+ async function getShikiHighlighter ( ) {
339+ if ( ! highlighterPromise ) {
340+ highlighterPromise = createHighlighter ( {
341+ themes : [ 'github-light' , 'tokyo-night' ] ,
342+ langs : CORE_LANGS as unknown as string [ ] ,
343+ } )
296344 }
345+ return highlighterPromise
346+ }
297347
298- for ( const theme of themes ) {
299- if ( ! loadedThemes . includes ( theme as any ) ) {
300- promises . push ( highlighter . loadTheme ( theme as any ) )
348+ const getHighlighter = cache ( async ( language : string , _themes : string [ ] ) => {
349+ const highlighter = await getShikiHighlighter ( )
350+
351+ // Normalize language name
352+ const normalizedLang = LANG_ALIASES [ language ] || language
353+ const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
354+
355+ const loadedLanguages = highlighter . getLoadedLanguages ( )
356+
357+ // Only load language if not already loaded
358+ if ( ! loadedLanguages . includes ( langToLoad as any ) ) {
359+ try {
360+ // Load language using shiki's built-in loader (works with bundle/web)
361+ await highlighter . loadLanguage ( langToLoad as any )
362+ } catch {
363+ // Fallback to plaintext if language not found
364+ console . warn ( `Shiki: Language "${ langToLoad } " not found, using plaintext` )
301365 }
302366 }
303367
304- await Promise . all ( promises )
305-
306368 return highlighter
307369} )
308370
0 commit comments