Next.js + Markdown (marked) で作るブログサイト
- インストール
- sanitize(XSS攻撃対策)
- インストール
- 導入例
- シンタックスハイライト(Prism.js)
- インストール
- 導入例
- 目次(Table of Contents)
- 導入例
- 出力されるhtmlタグのカスタマイズ
- 導入例: link(aタグ)
- Reference 参照
当サイトでは記事は全てMarkdownファイルで作成・管理しています。Markdown ファイルをサイトで表示させるためにはHTMLに変換する必要があります。Markdown を解析してHTMLに変換する軽量のコンパイラ marked を使用しています。
marked を使用した Next.js での Markdown のカスタマイズ方法を紹介します。(記事内のコードはTypeScriptで記述しています。)
インストール
npm install marked
sanitize(XSS攻撃対策)
sanitize (サニタイズ・サニタイジング)とは
《消毒の意》インターネット上で、掲示板(BBS)などの入力フォームに入力されたテキストデータから、HTMLのタグやスクリプト言語などを検出し、別の安全な文字列に置き換えること。悪意あるプログラムなどが実行されるのを防ぐためのもの。無害化。サニタイズ。サニタイゼーション。
引用: サニタイジングとは何?わかりやすく解説 Weblio辞書
marked は出力するHTMLを sanitize しないため、sanitize 用のライブラリで別途対応する必要があります。公式ドキュメントでは sanitize 用のライブラリとして、DOMPurify が推奨されており、ほかに js-xss や sanitize-html 、 insane が紹介されています。
当サイトでは公式推奨の DOMPurify を使用しますが、Next.js の SSG や ISR を使用しているためか公式サイトの記述そのままでは機能しませんでした。
「DOMPurify.sanitize is not a function」 の issue を参考に isomorphic-dompurify を使用することでエラーが解消され、Markdownが表示されるようになりました。
インストール
npm i isomorphic-dompurify
導入例
import DOMPurify from "isomorphic-dompurify";
import { marked } from "marked";
const Post = ({ post }) => {
const dirty = marked(post);
return (
<article dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(dirty) }} />
);
};
シンタックスハイライト(Prism.js)
記事内のコードブロックのシンタックスハイライトには Prism.js を使用しています。
marked のThe parse function を使用して設定します。
インストール
npm i prismjs
導入例
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) }} />;
};
目次(Table of Contents)
parser に渡されるトークンの配列を構築する lexer
を使用します。
marked.lexer(post);
以下のようなMarkdown本文のトークン配列が取得できます。
[
{
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",
},
];
取得したトークン配列にフィルターをかけて、見出しに当たる heading
のみを取得して、 map()
でhtmlタグなどを適宜追加して整えます。
h2, h3 などの見出しのレベルは depth
で取得できるので、当サイトではカスタムデータ属性の data-*
に depth
を設定して見出しのレベルによってCSSを変えられるようにしています。
導入例
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 href="dummy">{heading.text}</a>
</Link>
</li>
)
});
return (
<ul>{toc}</ul>
);
}
出力されるhtmlタグのカスタマイズ
renderer
を使用して特定の html 出力を定義することができます。
カスタマイズできるタグ例
- 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) など
その他のオプション等は公式ドキュメントでご確認ください。
導入例: link(aタグ)
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 参照
- Better solution for TOC · Issue #1489 · markedjs/marked · GitHub https://github.com/markedjs/marked/issues/1489(2022-8-20参照)
- Use marked and prism.js to parse markdown and add syntax highlighting in Node.js https://gist.github.com/lightpohl/f7786afa86ff2901ef40b1b1febf14e0(2022-8-23参照)
- Add support for NextJS Image - bytemeta https://bytemeta.vip/repo/markedjs/marked/issues/2252(2022-8-23参照)