FRONT-END DEVELOPER ・ FREELANCE ・ BASED IN TOKYO

tips_nextjs

How to Create a Markdown Blog with Next.js

September 01, 2022

I created this Blog with Next.js using Markdown files. It needs to convert from markdown to HTML to display. I’m using marked which is a markdown parser and compiler.

marked Official Site

marked GitHub

Let’s see how to create and customize Blog with Next.js based on Markdown.

Install Markdown

npm install marked

Customize 1: sanitize(XSS: Cross-Site Scripting)

What is HTML sanitization?

In data sanitization, HTML sanitization is the process of examining an HTML document and producing a new HTML document that preserves only whatever tags are designated "safe" and desired. HTML sanitization can be used to protect against attacks such as cross-site scripting (XSS) by sanitizing any HTML code submitted by a user. Quote: HTML sanitization - Wikipedia

Since marked doesn’t support HTML sanitization, it needs to use a library. In the official doc, marked recommends using DOMPurify as HTML sanitization. Other options are js-xss , sanitize-html , and insane .

I chose DOMPurify which is recommended by marked.

But with Next.js, I needed to use isomorphic-dompurify instead of DOMPurify (reference: DOMPurify.sanitize is not a function ).

Step 1: Install isomorphic-dompurify

npm i isomorphic-dompurify

Step 2: Add to post detail file

import DOMPurify from 'isomorphic-dompurify';
import { marked } from 'marked';

const Post = ({ post }) => {
    const dirty = marked(post);
    return (
        <article dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(dirty) }} />
    );
}

Customize 2: Syntax Highlighting(Prism.js)

I added Prism.js to highlight source code in the article.

And also using The parse function of marked.

Step 1: Install prismjs

npm i prismjs

Step 2: Add to post detail file

import Prism from 'prismjs';
import { marked } from 'marked';

const Post = ({ post }) => {
    const renderer = new marked.Renderer();
    
    // Set options
    marked.setOptions({
      renderer,
      highlight: function (code, lang) {
        if (Prism.languages[lang]) {
          return Prism.highlight(code, Prism.languages[lang], lang)
        } else {
          return code
        }
      }
    });

    return (
        <article dangerouslySetInnerHTML={{ __html: marked.parse(post) }} />
    );
}

Customize 3: Table of Contents

Using lexer on marked, you can recieve the token array to parser.

marked.lexer(post)

Here is the sample of token array.

[
    {
        type: 'heading',
        raw: '## Headline Text',
        depth: 2,
        text: 'Headline Text',
        tokens: Array(1)
    },
    {
        type: 'paragraph',
        raw: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod te'
    },
    {
        type: 'heading',
        raw: '### Headline Text',
        depth: 3,
        text: 'Headline Text',
        tokens: Array(1)
    },
    {
        type: 'paragraph',
        raw: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod te'
    },
    {
        type: 'paragraph',
        raw: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod te'
    },
]

Filter to token array, get the headline ( heading ), and format data using map() method.

You can get the level of heading like h2, h3 with depth . In my blog, I added data-* attributes with depth and styled with css.

Example: Add to post detail file

import { marked } from 'marked';

const Post = ({ post }) => {
    let toc;
    // トークン配列を取得
    const tokens = marked.lexer(post);
    // フィルターをかけて見出しのみを取得
    const headings = tokens.filter(token => token.type === "heading");
    // map()で整える
    toc = headings.map((heading: any, index: any) => {
    let target = heading.text.replace(/ /g, "-").replace(".", "").toLowerCase()
    return (
      <li key={index} data-depth={heading.depth}>
                <Link href={`#${target}`}>
            <a>{heading.text}</a>
                </Link>
      </li>
    )
  });

    return (
        <ul>{toc}</ul>
    );
}

Customize 4: html tags

You can customize which HTML tag should be output using renderer .

Example tags you can customize

  • code(string code, string infostring, boolean escaped)
  • list(string body, boolean ordered, number start)
  • paragraph(string text)
  • link(string href, string title, string text)
  • image(string href, string title, string text) など

Please check other options on the official doc .

import { marked } from 'marked';

const Post = ({ post }) => {
    const renderer = new marked.Renderer();

    renderer.link = function (href: string, title: string, text: string) {
    return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`
  }

    marked.setOptions({
        renderer,
    });

    return (
        <article dangerouslySetInnerHTML={{ __html: marked.parse(post) }} />
    );
}

Reference