Search code examples
markdownviteastrojs

How to customize markdown with Astro components?


md vs mdx

md import pipeline renders to html, mdx import pipeline renders to .js/.ts/.jsx... which allows to customize html tags with Astro components.

goal

I would like to take advantage of the mdx power in .md files with Astro

what I tried

  • tried to configure mdx integration in Astro but it is excluding .md extension unfortunately to allow default md rehype pipeline

  • My workaround of renaming all .md files to .mdx is very intrusive (changes files meta data) I would like to find a different approach

  • forking mdx integration is hard to maintain

  • I started a vite plugin that changes .md ids to add an x as .mdx, then I had to write my own loader, then it got too complex

  • astro-remote only takes some default components and does allow to replace any custom component

examples

I would like to avoid

and rather

Any ideas of the finest approach to achieve this, it feels like this last step is missing to unleash Astro's power over Markdow !!!

references


Solution

  • So after a long search I answer my own question, this is indeed possible in an elegant way.

    Concept

    • Parse the Markdown and get the Abstract Syntax Tree. The AST already provides all nodes in json format that can be used as properties for Astro components

    example, it is also possible to use extensions optionally, but let's keep it simple

    import {fromMarkdown} from 'mdast-util-from-markdown'
    
    const tree = fromMarkdown(content)
    
    • Pass the tree to a recursive component that handles all custom nodes that need to be rendered with a particular component.
    • handle the default non custom nodes with a generic Markdown renderer e.g. toHtml(toHast(node))

    Implementation

    here's how the full custom rendering recursive component looks like

    ---
    import Heading from '../nodes/Heading.astro';
    import Image from '../nodes/image/Image.astro'
    import Code from '../nodes/code/Code.astro'
    import {toHast} from 'mdast-util-to-hast'
    import {toHtml} from 'hast-util-to-html'
    export interface Props {
        node: object;
        data: object;
    }
    
    const {node, data} = Astro.props;
    const handled_types = ["root","heading","paragraph","image","code"]
    const other_type = !handled_types.includes(node.type)
    ---
    {(node.type == "root") &&
        node.children.map((node)=>(
            <Astro.self node={node} data={data} />
        ))
    }
    {(node.type == "paragraph") &&
    <p>
        {node.children.map((node)=>(
            <Astro.self node={node} data={data}/>
        ))}
    </p>
    }
    {(node.type == "heading") &&
        <Heading node={node} headings={data.headings}/>
    }
    {(node.type == "image") &&
        <Image node={node}  filepath={data.path}/>
    }
    {(node.type == "code") &&
        <Code node={node}  filepath={data.path}/>
    }
    {other_type &&
        <Fragment set:html={toHtml(toHast(node))}></Fragment>
    }
    

    I tried this on a template project and it is working great, this is an example usage e.g. from a dynmaic route file such as [...sid].astro

    ---
    const {sid} = Astro.params;
    import AstroMarkdown from '@/components/renderers/AstroMarkdown.astro'
    
    const data_content = await load_json(`gen/documents/${sid}/content.json`)
    const tree = await load_json(`gen/documents/${sid}/tree.json`)
    ---
    <AstroMarkdown node={tree} data={data_content} />
    

    References