RSS
 

Posts Tagged ‘recipe’

Adding Markdown (mdx) Support to NextJS with App Router

04 Jul

Markdown allows text content to be written with implicit formatting that nearly matches how one would write text without thought of formatting. A lot of web content is also simply text content; a lot of web content is built on the ReactJS library; and NextJS has become a popular framework, extending ReactJS. With that, I was motivated to use the Markdown content of my NextJS web app, but I had trouble simply relying on their documentation, so I thought I would document what I got working.

  1. Use Markdown for a text-heavy portion of a page of my web app, and
  2. To be able to have an entire page of content come from a single Markdown page

The former is a feature of NextJS to allow markdown content to be imported as a “component,” which can be rendered like any component in ReactJs.

The latter would rely NextJS’s App Router that assumes many URL paths for the app parallel the file/directory hierarchy of the app itself. So, merely placing a “page” file in the app’s directory will create a new URL path for the website. That means that you can create a “page.md” or “page.mdx” file with Markdown text content to define the content for that URL. .mdx files also allow JSX syntax so that React/Next components can be embedded within the Markdown.

Setup

The first issue was getting things set up. Here were the steps that worked for me.

  1. Add packages. The highlighted line was missing from the documentation.
    npm install @mdx-js/loader @mdx-js/react @next/mdx
    npm install -D @types/mdx
    
  2. In next.config.*, add
    1. import withMDX from "@next/mdx";
    2. Add “md” (optional) and “mdx” extensions for App Router support: nextConfig.pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx']
    3. Create, then wrap the Next config with mdx-configwithMDX = createMDX({})export default withMDX(nextConfig)

    Note: I had to avoid using .ts to avoid syntax errors to support the “remark-gfm” plugin. I recommend using .js or .mjs extensions, I chose the latter.

    import type {NextConfig} from 'next';
    
    const nextConfig: NextConfig = {
      typescript: {
        ignoreBuildErrors: true,
      },
      eslint: {
        ignoreDuringBuilds: true,
      },
    };
    
    export default nextConfig;
    asdfasdf asdf asdf asd f
    import type {NextConfig} from 'next';
    import withMDX from "@next/mdx";
    
    const nextConfig: NextConfig = {
      pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
      typescript: {
        ignoreBuildErrors: true,
      },
      eslint: {
        ignoreDuringBuilds: true,
      },
    };
    
    const withMdxConfig = withMDX({
      options: {
        remarkPlugins: [],
        rehypePlugins: [],
      },
    });
    
    export default withMdxConfig(nextConfig);
  3. Add the mdx-components.tsx file to src/. This file allows overriding how tags and components will be rendered, providing a mapping from tag/component name to an overriding render function; see the official documentation, mdx-components.js, but I will elaborate on how we’ll use this in the following sections.
    import type { MDXComponents } from 'mdx/types'
    
    export function useMDXComponents(components: MDXComponents): MDXComponents {
       return {
          ...components,
       }
    }
    
  4. Help VSCode resolve imports
    1. Open settings
    2. Search for “file associations”
    3. Add item key = *.mdx, value = markdown. (Articles say, “react-markdown,” but that didn’t work for me).
  5. Styles will need to be defined for all the generated HTML tags. See the next sections. 

Defining Markdown Output Style

Markdown syntax is translated to HTML. By default, the HTML tags have had their formatting stripped, for some reason. That means that the tags need to have style/class definitions applied to render properly. See How to use markdown and MDX in Next.js, for the official word.

The following are tags that correspond to Markdown syntax:

  Markdown HTML
Headings # Level 1
## Level 2
### Level 3

###### Level 6
<h1>Level 1</h1>
<h2>Level 2</h2>
<h3>Level 3</h3>

<h6>Level 6</h6>
Bold, Italic, Bold Italic * and _ are interchangeable
**bold** or __bold__
*italic* or _italic_
***bold-italic***
<b>bold</b>
<i>italic</i>
<b><i>bold-italic</i></b> or <i><b>bold-italic</b></i>
Bullet list - Item 1
- Item 2
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
Ordered list 1. Item 1
2. Item 2
<ol>
<li>Item 1</li>
<li>Item 2</li>
</ol>
Unformatted 

```
unformatted 
   text
```

<pre>
unformatted
  text
</pre>

Link [description](http://…) <a href="http://…">description</a>
Plain or paragraph text text … <p>text …</p>
Checkbox (an extended feature supported by the remark-gfm plugin) 

- [ ]Incomplete task
- [x]Completed task

<ul class="contains-task-list">
  <li class="task-list-item"><input disabled="" type="checkbox">Incomplete task</li>
  <input disabled="" type="checkbox"><li class="task-list-item" checked>Completed task</li>
</ul>

By default, NextJS removes the styling for the Markdown HTML tags, so we have to define them. 

Wrapper

Styles and classnames can be defined for any tag and component by redefining it to render with a style or className property, as desired. Instead, we will take advantage of a special Component named “wrapper” that wraps any .mdx content and define tag styles within that container context. Then we define all tag’s styles in an importable CSS module. 

import type { MDXComponents } from 'mdx/types'
import styles from './mdx-components.module.css'

export function useMDXComponents(components: MDXComponents): MDXComponents { 
   return { 
      ...components, 
      wrapper: ({ children }) => <div className={styles.mdxWrapper}>{children}</div>,
   } 
}

The file name and the wrapper class names are irrelevant so long as they match. Here is a good starting point for the “mdx-components.module.css” that I use. Note the class name, “_mdxWrapper” parent style definition. 

/**
 * Style definitions for tags corresponding to Markdown for MDX components
 * This file provides a reset for common HTML elements used in MDX content
 * to ensure consistent styling.
 */
._mdxWrapper {
  h1, h2, h3, h4, h5, h6, p, blockquote, pre, table {
    display: block;
    margin-block: initial;
    margin-inline: initial;
  }

  h1, h2, h3, h4, h5, h6 { font-weight: bold; }

  h1 {
    border-top: 1px solid lightgray;
    margin-block-start: 0.67em;
    font-size: 2em;
  }
  h2 { margin-block-start: 0.83em; font-size: 1.5em; }
  h3 { margin-block-start: 1em;    font-size: 1.17em; }
  h4 { margin-block-start: 1.67em; font-size: 1em; }
  h5 { margin-block-start: 1.33em; font-size: 0.83em; }
  h6 { margin-block-start: 2.33em; font-size: 0.67em; }

  p { margin-block: 0.4em; }

  a {
    color: -webkit-link;
    text-decoration: underline;
    cursor: pointer;
  }

  ul, ol {
    display: block;
    padding-inline-start: 1.5625rem;
  }
  ul { list-style-type: disc; }
  ul ul { list-style-type: circle; }
  ul ul ul { list-style-type: square; }
  ol { list-style-type: decimal; }
  
  blockquote {
    margin-block: 1em;
    margin-inline: 40px;
  }
  code, pre {
    font-family: monospace;
  }
  pre {
    display: block;
    margin-block: 1em;
    white-space: pre;
  }

  img {
    display: inline-block;
    max-width: 100%;
    height: auto;
  }

  table {
    /* Table styling variables */
    /* --table-border-color: blue; */
    /* --table-border-width: 2px;  */    /* Enable to non-0 to surround table with border */
    /* --table-header-background-color: #f2f2f2; */ /* Enable for distinct header background	color */
    --table-header-border-color: #BBB;   /* Defaults to cell border color */
    --table-header-border-width: 2px;    /* Defaults to row border width */
    --table-body-row-border-width: 1px;  /* Enable to non-0 show row dividers */
    --table-body-col-border-width: var(--table-body-row-border-width,1px);  /* Enable to non-0 show col dividers */
    --table-body-cell-border-color: lightgray;  /* Override border color */
    --table-body-background-color: white;/* Even row background color */
    --table-body-background-color-alt: #f9f9f9; /* Odd row background color */
    margin-left: var(--table-body-row-border-width,0);
    border-collapse: collapse;
    text-indent: initial;
  }
  thead, tbody, tfoot {
    vertical-align: middle;
  }
  thead {
    border-color: var(--table-border-color);
    border-width: var(--table-border-width,0) var(--table-border-width,0) 0 var(--table-border-width,0);
  }
  tbody {
    border-color: var(--table-border-color);
    border-width: 0 var(--table-border-width,0) var(--table-border-width,0) var(--table-border-width,0);
  }
  thead th {
    background-color: var(--table-header-background-color, inherit);
    /* border-style: solid; */
    border-color: var(--table-header-border-color, var(--table-body-cell-border-color));
    border-bottom-width: var(--table-header-border-width,var(--table-body-row-border-width,0));
  }
  tr {
    vertical-align: inherit;
    border: none; /* Disable row borders */
    border-color: inherit;
  }
  td, th {
    /* No top border, conditional side/bottom borders */
    border-width: 0 var(--table-body-col-border-width, 0px) var(--table-body-row-border-width, 0px) var(--table-body-col-border-width, 0px);
    border-color: var(--table-body-cell-border-color);
    padding-inline: 8px;
  }
  /* No side borders */
  td:last-child, th:last-child { border-right: none; }
  td:first-child, th:first-child { border-left: none; }

  /* Alternating background colors for table rows */
  tbody tr:nth-child(odd)  { background-color: var(--table-body-background-color); }
  tbody tr:nth-child(even) { background-color: var(--table-body-background-color-alt); }
} /* ._mdxWrapper */

Checkbox Support via “remark-gfm”

Github markup is supported via the remark-gfm plugin, however I was never able to get it to work as documented (i.e., import and add  remarkGfm  to the plugins array. Instead I had to use the string name of the plugin and add an array to the plugins array:

const withMdxConfig = withMDX({
  options: {
    remarkPlugins: [['remark-gfm']],
    rehypePlugins: [],
  },
});

export default withMdxConfig(nextConfig);
import withMDX from "@next/mdx"
import remarkGfm from 'remark-gfm'

const nextConfig: NextConfig = {
  pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
}

const withMdxConfig = withMDX({
  options: {
    // If you use remark-gfm, you'll need to use next.config.mjs
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

export default withMdxConfig(nextConfig);

The styling for task/checkboxes needs to be set. Unfortunately, remark-gfm uses class names to identify the list and list item tags. Since importing a CSS modules mangles names, we have to import the CSS definitions directly. We can create a CSS file, randomly named, “remark-gfm.css”

/* remarkGfm MDX Checkbox support for remarkGfm */
ul.contains-task-list li.task-list-item {
  list-style-type: none; /* Remove list style for task list items */
  text-indent: -18px;    /* ...and outdent it so it aligns        */
}
/* Undo <li> inherited styling within nested chckbox lists */
ul.contains-task-list li:not(.task-list-item) {
  list-style-type: initial;
  text-indent: initial;
}

Then import this directly into “mdx-components.tsx.” Alternatively you can add these style definitions in global.css.

import type { MDXComponents } from 'mdx/types'
import styles from './mdx-components.module.css'
import './remark-gfm.css'

export function useMDXComponents(components: MDXComponents): MDXComponents {
   return {
      ...components,
      wrapper: ({ children }) => <div className={styles._mdxWrapper}>{children}</div>,
   }
}

Note: This style definition will affect all content on the page, even outside an MDX component. 

Summary

I was led down a bunch of rabbit holes due to NextJS’s Markdown support not matching the documented steps. With the steps described in this post, others should be able to get it working without the headaches I had to go through.

  1. Npm-install mdx support packages
  2. Update VSCode file association settings (optionally)
  3. Update “next.config.mjs”
  4. Create src/mdx-components.tsx and import a CSS module to define and wrap tag style definitions
  5. If Github markdown support is desired, add the plugin and import CSS definitions into mdx-components.tsx

Then you can use Markdown content for App Router pages as page.mdx and React components, implicitly.

# Page Title

This is an example a NextJS App Router page, written with Markdown syntax. 

 

import UpdateContent from "./updates.mdx" // Markdown content

export default async function UpdatesPage() { // App Router page component

return (
  <>
     <h1>Updated News</h1>
     <UpdateContent/> {/* Render Markdown component */}
  </>
)

 

 
 

Bash recipe: Script call-stack

25 Mar
Bash logoHere’s a little recipe for printing a “call-stack” for Bash scripts. A call-stack is normally the list of nested function calls. This recipe will utilize the BASH_SOURCE, BASH_LINENO, and FUNCNAME environment variables, set by Bash during script executions, to display a call-stack of one script to another, excluding function calls; this will show only the invocations of one script to another. Read the rest of this entry »
 

ChatGPT: What is the common way to parse command line parameters with Rust?

04 Mar

Rust Programming Language logoIn Rust, the most common way to parse command-line parameters is by using the std::env::args function, which returns an iterator over the command-line arguments passed to the program. Read the rest of this entry »

 

PHP Recipe to Show Source-code

19 Nov

PHP source-code display recipeHere is a simple Web 1.5 (static HTML with a little bit of styling and JavaScript) recipe to allow a viewer of your web page to see the PHP source-code, behind it, with a minimal amount of JavaScript and a little CSS manipulation—good for showing the work you’ve done to others. Or for embedding in your own source, in debug mode, so that teammates can see each others’ work.

See the example code at: http://cachecrew.com/sample_code/highlight_file.php and http://cachecrew.com//sample_code/highlight_file2.php.

The PHP and HTML Read the rest of this entry »

 

Self-documenting Bash Scripts Using Heredoc

22 Jul

It is surprising how quickly we forget how to use scripts we had written, let alone, what they were for. So it is even more difficult for others to know how and whether to use your script. You can avoid wasting the efforts you put into the problems you solved with your script by simply adding some friendly “help” output.

Here is some basic information you should include:

  • Summary description
  • Form of use—basic syntactic usage
  • Description of usage syntax; i.e., input parameters
  • More detailed information, if necessary. Read the rest of this entry »