wassfila
wassfila

Reputation: 1901

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

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

Upvotes: 12

Views: 2552

Answers (1)

wassfila
wassfila

Reputation: 1901

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

Upvotes: 2

Related Questions