Remixing MDX
to Improve Content Accessibility and Usability
Monica Powell • Remix Conf 2023
Hello, 👋🏾
I’m Monica Powell
- Senior Software Engineer in EdTech at Newsela 👩🏾🏫
- Let's chat about: Open Source, MDX, Creative Coding 👩🏾🎨
- React Robins Organizer (FKA React Ladies)
- GitHub Star 🌟
monica.dev • @indigitalcolor
Hello, 👋🏾
monica.dev • @indigitalcolor
https://links.monica.dev
Hello, 👋🏾
monica.dev • @indigitalcolor
I’m Monica Powell
- Senior Software Engineer in EdTech at Newsela 👩🏾🏫
- Let's chat about: Open Source, MDX, Creative Coding 👩🏾🎨
- React Robins Organizer (FKA React Ladies)
- GitHub Star 🌟
What is ?
Hint: 👩🏾💻 + MDX = 👩🏾🏫🪄📄
monica.dev • @indigitalcolor
What is MDX?
MDX is Markdown supercharged with JSX,
monica.dev • @indigitalcolor
What is MDX?
monica.dev • @indigitalcolor
MDX is Markdown supercharged with JSX, like TypeScript is JavaScript supercharged with types.
Developers can leverage MDX to create interactive technical documentation.
👩🏾💻 + MDX = 👩🏾🏫🪄📄
monica.dev • @indigitalcolor
<TabbedContent tabs={[<FileWithIcon text="TypeScript" icon="code"/>,
<FileWithIcon text="Javascript" icon="code"/>]}>
<tab>
```ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```
</tab>
<tab>
```js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
```
</tab>
</TabbedContent>
monica.dev • @indigitalcolor
Prisma Docs + MDX
monica.dev • @indigitalcolor
React Docs + MDX
monica.dev • @indigitalcolor
React Docs + MDX
monica.dev • @indigitalcolor
unified is two things:
monica.dev • @indigitalcolor
Unified Ecosystem
ASTs
Abstract Syntax Trees are more verbose version of human-readable code
💡 remark plugins transform markdown and rehype plugins transform HTML.
MDX, remark and rehype
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
Using MDX with
Built-In MDX Support | Advanced Setup |
---|---|
Add .mdx files to routes | Use a tool like mdx-bundler |
Build MDX files at build-time | Build MDX files on demand |
Quick Start ⚡️ | More Customizable 🪄 |
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
+ MDX-Bundler
Install MDX-Bundler + esbuild
monica.dev • @indigitalcolor
+ MDX-Bundler
monica.dev • @indigitalcolor
Write MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
+ MDX-Bundler
monica.dev • @indigitalcolor
Write MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
- Out of the
📦 with
Remix without a tool likemdx-bundler
we can use MDX by:- creating a
.mdx
route module - importing
.mdx
into route modules - export route functions like
loader
,action
, andhandle
- creating a
monica.dev • @indigitalcolor
+ MDX-Bundler
Write MDX
use Loader function fetch transformed MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
const loader = async ({params}) => {
...
return json({post})
}
app/routes/blog.$.tsx
Write MDX
use Loader function fetch transformed MDX
Render MDX Component
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
const loader = async ({params}) => {
...
return json({post})
}
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
+ MDX-Bundler
import { bundleMDX } from "mdx-bundler";
export async function parseMdx(mdx: string) {
const { frontmatter, code } = await bundleMDX({
source: mdx,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? [])];
options.rehypePlugins = [...(options.rehypePlugins ?? [])];
return options;
},
});
return {
frontmatter,
code
};
}
Parsing MDX
mdx-bundler.server.ts
monica.dev • @indigitalcolor
import { parseMdx } from "~/utils/mdx-bundler.server";
import { getContent } from "~/utils/blog.server";
export const loader = async ({params}: LoaderArgs) => {
let path = params["*"];
invariant(path, "BlogPost: path is required");
const files = await getContent(`docs/${path}`);
let post = files && (await parseMdx(files[0].content));
if (!post) {
throw json({}, {status: 404})
}
return json({post})
}
Loading MDX
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
import { useLoaderData } from '@remix-run/react'
import { getMDXComponent } from 'mdx-bundler/client';
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
const { code } = post;
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
<Component/>
</article>
);
}
Rendering MDX
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
<Component components={{
a: CustomLink,
blockquote: CustomBlockQuote,
br: CustomBr,
code: CustomCode,
em: CustomEm,
h1: CustomHOne
h2: CustomHTwo
h3: CustomHThree,
h4: CustomHFour,
h5: CustomH5,
h6: CustomH6,
hr: CustomHr,
img: CustomImg,
li: CustomListItem,
ol: CustomOrderedList,
p: CustomParagraph,
pre: CustomPre,
strong: CustomStrong,
ul: CustomUnorderedList}} />
monica.dev • @indigitalcolor
Component shadowing
<Component
components={{
a: CustomLink,
p: CustomParagraph
}}
/>
monica.dev • @indigitalcolor
Component shadowing
const CustomParagraph = (props: Props): JSX.Element => {
if (typeof props.children !== 'string') {
return <>{props.children}</>
}
return <> <p {...props} /><p>✨Sparkles Powered By: MDX✨</p></>
}
monica.dev • @indigitalcolor
Component shadowing
Wrap links in additional markup to identify external links will open in a new window and use Remix Links for internal links
monica.dev • @indigitalcolor
import { Link } from "@remix-run/react";
export const CustomLink = (props: {
href: string;
children?: React.ReactNode;
}) => {
const {href} = props;
const isInternalLink = href && (href.startsWith('/') || href.startsWith('#'))
return isInternalLink ? <Link to={href} prefetch="intent" {...props} /> : <>
<a {...props} target="_blank" rel="noopener noreferrer" />
<span className="sr-only">(opens in a new tab)</span>
</>
}
Component shadowing
monica.dev • @indigitalcolor
[blog](/blog)
/* [blog](/blog) */
<a href="/blog">blog</a>
# on hover
<a href="/blog">blog</a><link rel="prefetch" as="fetch" href="/blog? data=routes%2Fblog">
<link rel="modulepreload" href="/build/routes/blog-QLALKNZQ_.js"><link rel="modulepreload" href=
"/build/ shared/chunk-JLXQF5V2.js">
Component shadowing
Markdown HTML in Browser
monica.dev • @indigitalcolor
/*[Remix Conf](https://remix.run/conf)*/
<a href="https://remix.run/conf" target="_blank" rel="noopener noreferrer">Remix Conf</a>
<span class="sr-only">(opens in a new tab)</span>
[Remix Conf](https://remix.run/conf)
Component shadowing
Markdown HTML in Browser
Semantic HTML
-
Use the HTML element that best reflects the markup. Be specific!
-
Optimize for "human-readable"
-
<header> and <section> to group elements conveys more meaning than <span> and <div>
-
Use text when possible instead of images of text
monica.dev • @indigitalcolor
Using Remark + Rehype Plugins
monica.dev • @indigitalcolor
💡Remember
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
- remark-sectionize
- remark-a11y-emoji
- rehype-autolink-headings
- rehype-slug
Using Remark + Rehype Plugins
- remark-sectionize
- remark-a11y-emoji
- rehype-autolink-headings
- rehype-slug
monica.dev • @indigitalcolor
💡Remember
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
Using Remark + Rehype Plugins
- remark-sectionize
- remark-a11y-emoji
- rehype-autolink-headings
- rehype-slug
Bonus:
- mdx-embed
- prism-react-renderer
- Sandpack
monica.dev • @indigitalcolor
💡Remember
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
How Users Read on the Web Summary:
They don't. People rarely read Web pages word by word; instead, they scan the page, picking out individual words and sentences.
- Jakob Nielsen, Nielsen Norman Group
monica.dev • @indigitalcolor
<TableOfContents/>
Text
export function TableOfContents({ headings }) {
if (!headings) return <></>
return (
<>
<h2> Table of Contents</h2>
<ul>{headings.map(({ id, text }) =>
id && text ?
<li key={id}>
<a href={`#${id}`}>{text}</a>
</li> : <></>
)}
</ul>
</>)
}
monica.dev • @indigitalcolor
Creating a Rehype Plugin
Let's write a Rehype plugin to return all of the H2 headings in our MDX file
# Table of Contents 101
<TableOfContents/>
The above Table of Contents was generated by using MDX to parse headings using the below custom Remark plugin. A table of contents on a website is helpful for many of the same reasons that it is helpful in a document or book.
## Navigation
A table of contents can help website visitors to navigate the site quickly and easily. It provides an overview of the content and enables visitors to find the information they need without having to scroll through the entire page or site.
## Accessibility
A table of contents can improve website accessibility by providing an overview of the content that is organized in a clear and logical way. This can make the site more accessible to visitors with visual or cognitive impairments who may have difficulty navigating the site.
## User Experience
User experience: A table of contents can enhance the user experience by making it easier for visitors to find what they are looking for. This can increase visitor satisfaction and improve the chances that visitors will return to the site in the future.
monica.dev • @indigitalcolor
Parsing HTML for Headings
Text
export default function rehypeHeadings(options) {
return async function transform(tree: M.Root) {
const { visit } = await import('unist-util-visit');
visit(
tree, { type: 'element', tagName: "h2" },
function visitor(node) {
const { properties, children } = node;
const anchor = children.find((child) => child.tagName === "a")
const anchorText = anchor.children.find((child) => child.type === 'text').value
if (anchorText && properties?.id)
options.exportRef.push({
id: properties?.id,
text: anchorText
})
})
}
}
app/utils/parse-headings.ts
monica.dev • @indigitalcolor
Text
# Table of Contents 101
<TableOfContents/>
The above Table of Contents was generated by using MDX to parse headings using the below custom Remark plugin. A table of contents on a website is helpful for many of the same reasons that it is helpful in a document or book.
## Navigation
A table of contents can help website visitors to navigate the site quickly and easily. It provides an overview of the content and enables visitors to find the information they need without having to scroll through the entire page or site.
## Accessibility
A table of contents can improve website accessibility by providing an overview of the content that is organized in a clear and logical way. This can make the site more accessible to visitors with visual or cognitive impairments who may have difficulty navigating the site.
## User Experience
User experience: A table of contents can enhance the user experience by making it easier for visitors to find what they are looking for. This can increase visitor satisfaction and improve the chances that visitors will return to the site in the future.
[
{
"id": "accessibility",
"text": "Accessibility"
},
{
"id": "navigation",
"text": "Navigation"
},
{
"id": "user-experience",
"text": "User Experience"
}
]
monica.dev • @indigitalcolor
Using GFM
Text
import rehypeHeadings from "./parseHeadings";
export async function parseMdx(mdx: string) {
let headings = []
const { frontmatter, code } = await bundleMDX({
source: mdx,
mdxOptions(options) {
options.remarkPlugins = [ ...(options.remarkPlugins ?? [])];
options.rehypePlugins = [...(options.rehypePlugins ?? []),
rehypeSlug, [rehypeAutolinkHeadings, { behavior: "wrap"}], [rehypeHeadings, {exportRef: headings}],
];
return options;
},
});
return {
headings,
frontmatter,
code,
};
}
Rendering Table of Contents
Text
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
const { code, headings } = post;
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
<Component components={{TableOfContents: () => <TableOfContents headings={headings}/>}}/>
</article>
);
}
monica.dev • @indigitalcolor
Accessible Footnote Navigation
monica.dev • @indigitalcolor
Accessible Footnote Navigation
monica.dev • @indigitalcolor
<p><a
id="the-world-wide-web-ref"
href="#the-world-wide-web-note"
role="doc-noteref"
aria-describedby="footnotes-label">
The World Wide Web</a>, commonly known as the web, is an integral part of modern life...
</p>
Accessible Footnote Markup
Further reading: Accessible footnotes and a bit of React by Kitty Giraudel
monica.dev • @indigitalcolor
<footer role="doc-endnotes">
<h2 id="footnotes-label">Footnotes</h2>
<ol> <li id="the-world-wide-web-note">
"World Wide Web." Wikipedia, Wikimedia Foundation, 23 Apr. 2022"<a href="#the-world-wide-web-ref" aria-label="Back to reference 1" role="doc-backlink">↩</a></li>
</ol>
</footer>
Footnote Ref
Footnote
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Implementing Footnotes
Write HTML a11y markup for every individual footnote + ref
A
monica.dev • @indigitalcolor
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Implementing Footnotes
Write HTML a11y markup for every individual footnote + ref
Create a custom <Footnotes/> component to use as needed
A
B
monica.dev • @indigitalcolor
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Implementing Footnotes
Write HTML a11y markup for every individual footnote + ref
Create a custom <Footnotes/> component to use as needed
Extend our version of Markdown to use GitHub Flavored Markdown (b.k.a GFM)
A
B
C
monica.dev • @indigitalcolor
Using react-a11y-footnotes
Text
import { FootnotesProvider, FootnoteRef, Footnotes } from 'react-a11y-footnotes'
const ourComponent = () => {
return (
<FootnotesProvider>
<p> Maintaining <FootnoteRef description='additional information that is not directly related to the main text but is still relevant or useful to the reader.'>
footnotes </FootnoteRef> manually can be a pain. </p>
<Footnotes />
</FootnotesProvider>
)
}
monica.dev • @indigitalcolor
<Footnotes/> component
Access Footnotes in Component
return (
<article>
<FootnotesProvider>
<Component components={{
FootnoteRef,
Footnotes }} />
</FootnotesProvider>
</article>
);
monica.dev • @indigitalcolor
Access Footnotes in Component
<FootnoteRef description={<>"World Wide Web." Wikipedia, Wikimedia Foundation, 23 Apr. 2022, <a href="https://en.wikipedia.org/wiki/World_Wide_Web">en.wikipedia.org/wiki/World_Wide_Web</a>.'</>}>The World Wide Web</FootnoteRef>, commonly known as the web, is an integral part of modern life. It is a global network of information and resources accessible through the Internet. The web was invented in 1989 by British computer scientist <FootnoteRef description={<>'Berners-Lee, Tim. "Information Management: A Proposal." CERN, 12 Mar. 1989, <a href="https://info.cern.ch/Proposal.html">info.cern.ch/Proposal.html</a>.'</>}>Tim Berners-Lee</FootnoteRef>, who was working at the European Organization for Nuclear Research (CERN) at the time.
<Footnotes />
monica.dev • @indigitalcolor
Using gfm-remark
[unified] remark plugin to support GFM (autolink literals, footnotes, strikethrough, tables, tasklists)
/*autolink literals*/
(www.x.com)
/*footnotes*/
([^1])
/*strikethrough*/
(~~stuff~~)
/*tables*/
(| cell |…)
/*tasklists*/
(* [x])
monica.dev • @indigitalcolor
options.remarkPlugins = [
...(options.remarkPlugins ?? []), gfm-remark
];
Summary
monica.dev • @indigitalcolor
MDX is a powerful tool to enhance and customize the user experience of educational content
- Replace native HTML elements with component shadowing
- Import custom components
- Transform MDX with unist-util-visit, remark and rehype plugins
Configuring GFM Plugin
Text
export async function parseMarkdown(markdown: string) {
const {default: remarkGfm} = await import("remark-gfm")
const { frontmatter, code } = await bundleMDX({
source: markdown,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGFM];
options.rehypePlugins = [...(options.rehypePlugins ?? [])];
return options;
},
});
THANK YOU !
🐦 @indigitalcolor
☁️ @monica.dev
👩🏾💻 M0nica/remix-mdx-docs
monica.dev • @indigitalcolor
Remixing MDX
By m0nica
Remixing MDX
- 317