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 🌟 
Animated version of Monica holding a laptop
A pen plotter plotting Animonica is sparkly purple ink

monica.dev • @indigitalcolor

 Hello,              👋🏾

Animated picture of Monica holding a laptop
A pen plotter plotting Animonica is sparkly purple ink

monica.dev • @indigitalcolor

QR code to links.monica.dev

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

MDX logo
purple magic wand

What is MDX?

MDX is Markdown supercharged with JSX,

monica.dev • @indigitalcolor

purple magic wand

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.

purple magic wand

👩🏾‍💻 + 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>
gif of prisma docs toggle between typescript and javascript language

monica.dev • @indigitalcolor

Prisma Docs + MDX

monica.dev • @indigitalcolor

Screenshot of @reactjs tweet that reads: "Today, we’re thrilled to launch https://react.dev, the new home for React and its documentation. It teaches modern React with function components and Hooks, and we’ve included diagrams, illustrations, challenges, and over 600 interactive examples. Check it out!"

React Docs + MDX

monica.dev • @indigitalcolor

image of a codesandbox in the react docs

React Docs + MDX

unified's github homepage

monica.dev • @indigitalcolor

unified's github homepage

unified is two things:

  • unified is a collective of 500+ free and open source packages that work with content as structured data (ASTs)
  • unified is the core package to inspect and transform content with plugins

monica.dev • @indigitalcolor

Unified Ecosystem

video comparing js function to its verbose abstract syntax tree output

ASTs

Abstract Syntax Trees are more verbose version of human-readable code

unified's github homepage

💡 remark plugins transform markdown and rehype plugins  transform HTML.

 

rehypejs/rehype

remarkjs/remark

MDX, remark and rehype

monica.dev • @indigitalcolor

Gallery of projects that use unified, including Prettier, Gatsby, freeCodeCamp, ESLint, GitHub and MDX

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

woman holding a magnifying glass
mdx-bundler's page on github

monica.dev • @indigitalcolor

 + MDX-Bundler

npm installing mdx-bundler and esbuild

 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 like  mdx-bundler we can use MDX by:
    • creating a .mdx  route module
    •  importing .mdx into route modules
    • export route  functions like loader, action, and handle

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})
}
Woman saying "TADA" with her hands

       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></>
  }

a series of paragraphs followed by the paragraph "✨ Powered By: MDX ✨"

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


 

https://web.dev/learn/html/semantic-html/

monica.dev • @indigitalcolor

a woman leaning back in chair to say "Tell me everything!"

Using Remark + Rehype Plugins

monica.dev • @indigitalcolor

Bart Simpson cutting through hedge maze and exclaiming "Hey, I found a shortcut through your hedge maze"

💡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

A table of Contents displaying on a page written with MDX

Accessible Footnote Navigation

monica.dev • @indigitalcolor

A man holding up a sign which reads "That was too much information"
GIF showing navigation back and forth between footnotes and footnote refs

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>
a footnote ref example

Accessible Footnote Markup

Further reading: Accessible footnotes and a bit of React by Kitty Giraudel

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>
a footnote example

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

an example of a paragraph with multiple footnote refs
an example of a footnotes section with multiple footnotes

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

QR code that links to links.monica.dev

Remixing MDX

By m0nica