Next.jsでMarkdownを表示
はじめに
react-markdown
を使い、GFM、TeX を含み、Markdown を Next.js で表示する方法を紹介します。
ソースは以下を参照ください。
Markdownを表示する方法
Markdown を HTML に変換して表示するにはいくつか方法がありますが、本記事では、react-markdown
を利用し Markdown を Next.js で表示する方法を紹介します。
以下、Markdown を HTML に変換するライブラリの一覧です。
- https://github.com/markedjs/marked
- https://github.com/markdown-it/markdown-it
- https://github.com/remarkjs/remark
以下の記事で上記のライブラリの比較がされています。
GFMとは
- GFM とは GitHub Flavored Markdown の略です。
- GFM とは GitHub が独自に拡張した Markdown のことです。
- 自動リンクや、テーブルなどが GFM の拡張です。
以下が GFM の仕様です。
以降で、GFM の仕様を含めた、Markdown を Next.js で表示する方法を紹介します。
Next.jsのプロジェクト作成
Next.js のプロジェクトを作成します。
$ yarn create next-app nextjs-tex --typescript --eslint --src-dir --import-alias "@/*"
実施結果
yarn create v1.22.19
warning package.json: No license field
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Installed "create-next-app@13.1.6" with binaries:
- create-next-app
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
Creating a new Next.js app in /Users/hayato94087/Work/nextjs-react-markdown.
Using yarn.
Installing dependencies:
- react
- react-dom
- next
- @next/font
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
yarn add v1.22.19
warning ../package.json: No license field
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 198 new dependencies.
info Direct dependencies
├─ @next/font@13.1.6
├─ @types/node@18.13.0
├─ @types/react-dom@18.0.10
├─ @types/react@18.0.27
├─ eslint-config-next@13.1.6
├─ eslint@8.33.0
├─ next@13.1.6
├─ react-dom@18.2.0
├─ react@18.2.0
└─ typescript@4.9.5
info All dependencies
├─ @babel/runtime@7.20.13
├─ @eslint/eslintrc@1.4.1
├─ @humanwhocodes/config-array@0.11.8
├─ @humanwhocodes/module-importer@1.0.1
├─ @humanwhocodes/object-schema@1.2.1
├─ @next/env@13.1.6
├─ @next/eslint-plugin-next@13.1.6
├─ @next/font@13.1.6
├─ @next/swc-darwin-x64@13.1.6
├─ @nodelib/fs.scandir@2.1.5
├─ @nodelib/fs.stat@2.0.5
├─ @nodelib/fs.walk@1.2.8
├─ @pkgr/utils@2.3.1
├─ @rushstack/eslint-patch@1.2.0
├─ @swc/helpers@0.4.14
├─ @types/json5@0.0.29
├─ @types/node@18.13.0
├─ @types/prop-types@15.7.5
├─ @types/react-dom@18.0.10
├─ @types/react@18.0.27
├─ @types/scheduler@0.16.2
├─ @typescript-eslint/parser@5.51.0
├─ @typescript-eslint/scope-manager@5.51.0
├─ @typescript-eslint/typescript-estree@5.51.0
├─ acorn-jsx@5.3.2
├─ acorn@8.8.2
├─ ajv@6.12.6
├─ ansi-regex@5.0.1
├─ ansi-styles@4.3.0
├─ argparse@2.0.1
├─ aria-query@5.1.3
├─ array-includes@3.1.6
├─ array-union@2.1.0
├─ array.prototype.flat@1.3.1
├─ array.prototype.tosorted@1.1.1
├─ ast-types-flow@0.0.7
├─ axe-core@4.6.3
├─ axobject-query@3.1.1
├─ balanced-match@1.0.2
├─ brace-expansion@1.1.11
├─ braces@3.0.2
├─ callsites@3.1.0
├─ caniuse-lite@1.0.30001451
├─ chalk@4.1.2
├─ client-only@0.0.1
├─ color-convert@2.0.1
├─ color-name@1.1.4
├─ concat-map@0.0.1
├─ cross-spawn@7.0.3
├─ csstype@3.1.1
├─ damerau-levenshtein@1.0.8
├─ deep-is@0.1.4
├─ define-lazy-prop@2.0.0
├─ emoji-regex@9.2.2
├─ enhanced-resolve@5.12.0
├─ es-get-iterator@1.1.3
├─ es-set-tostringtag@2.0.1
├─ es-to-primitive@1.2.1
├─ escape-string-regexp@4.0.0
├─ eslint-config-next@13.1.6
├─ eslint-import-resolver-node@0.3.7
├─ eslint-import-resolver-typescript@3.5.3
├─ eslint-module-utils@2.7.4
├─ eslint-plugin-import@2.27.5
├─ eslint-plugin-jsx-a11y@6.7.1
├─ eslint-plugin-react-hooks@4.6.0
├─ eslint-plugin-react@7.32.2
├─ eslint-scope@7.1.1
├─ eslint-utils@3.0.0
├─ eslint@8.33.0
├─ esquery@1.4.0
├─ esrecurse@4.3.0
├─ estraverse@5.3.0
├─ fast-deep-equal@3.1.3
├─ fast-glob@3.2.12
├─ fast-json-stable-stringify@2.1.0
├─ fast-levenshtein@2.0.6
├─ fastq@1.15.0
├─ file-entry-cache@6.0.1
├─ fill-range@7.0.1
├─ find-up@5.0.0
├─ flat-cache@3.0.4
├─ flatted@3.2.7
├─ function.prototype.name@1.1.5
├─ get-symbol-description@1.0.0
├─ get-tsconfig@4.4.0
├─ glob-parent@6.0.2
├─ glob@7.1.7
├─ globalthis@1.0.3
├─ globalyzer@0.1.0
├─ globby@13.1.3
├─ globrex@0.1.2
├─ graceful-fs@4.2.10
├─ grapheme-splitter@1.0.4
├─ has-bigints@1.0.2
├─ has-flag@4.0.0
├─ has-proto@1.0.1
├─ import-fresh@3.3.0
├─ imurmurhash@0.1.4
├─ is-bigint@1.0.4
├─ is-boolean-object@1.1.2
├─ is-callable@1.2.7
├─ is-core-module@2.11.0
├─ is-date-object@1.0.5
├─ is-docker@2.2.1
├─ is-extglob@2.1.1
├─ is-map@2.0.2
├─ is-negative-zero@2.0.2
├─ is-number-object@1.0.7
├─ is-number@7.0.0
├─ is-path-inside@3.0.3
├─ is-set@2.0.2
├─ is-string@1.0.7
├─ is-symbol@1.0.4
├─ is-typed-array@1.1.10
├─ is-weakmap@2.0.1
├─ is-weakref@1.0.2
├─ is-weakset@2.0.2
├─ is-wsl@2.2.0
├─ isexe@2.0.0
├─ js-sdsl@4.3.0
├─ js-tokens@4.0.0
├─ json-schema-traverse@0.4.1
├─ json-stable-stringify-without-jsonify@1.0.1
├─ json5@1.0.2
├─ jsx-ast-utils@3.3.3
├─ language-subtag-registry@0.3.22
├─ language-tags@1.0.5
├─ locate-path@6.0.0
├─ lodash.merge@4.6.2
├─ loose-envify@1.4.0
├─ lru-cache@6.0.0
├─ micromatch@4.0.5
├─ minimatch@3.1.2
├─ minimist@1.2.7
├─ ms@2.1.2
├─ nanoid@3.3.4
├─ natural-compare@1.4.0
├─ next@13.1.6
├─ object-assign@4.1.1
├─ object-inspect@1.12.3
├─ object-is@1.1.5
├─ object.hasown@1.1.2
├─ open@8.4.0
├─ optionator@0.9.1
├─ p-limit@3.1.0
├─ p-locate@5.0.0
├─ parent-module@1.0.1
├─ path-exists@4.0.0
├─ path-key@3.1.1
├─ path-type@4.0.0
├─ picomatch@2.3.1
├─ postcss@8.4.14
├─ prop-types@15.8.1
├─ punycode@2.3.0
├─ queue-microtask@1.2.3
├─ react-dom@18.2.0
├─ react-is@16.13.1
├─ react@18.2.0
├─ regenerator-runtime@0.13.11
├─ regexpp@3.2.0
├─ resolve-from@4.0.0
├─ reusify@1.0.4
├─ rimraf@3.0.2
├─ run-parallel@1.2.0
├─ safe-regex-test@1.0.0
├─ scheduler@0.23.0
├─ shebang-command@2.0.0
├─ shebang-regex@3.0.0
├─ slash@4.0.0
├─ source-map-js@1.0.2
├─ stop-iteration-iterator@1.0.0
├─ string.prototype.matchall@4.0.8
├─ string.prototype.trimend@1.0.6
├─ string.prototype.trimstart@1.0.6
├─ strip-ansi@6.0.1
├─ strip-bom@3.0.0
├─ strip-json-comments@3.1.1
├─ styled-jsx@5.1.1
├─ supports-color@7.2.0
├─ synckit@0.8.5
├─ tapable@2.2.1
├─ text-table@0.2.0
├─ tiny-glob@0.2.9
├─ to-regex-range@5.0.1
├─ tsconfig-paths@3.14.1
├─ tsutils@3.21.0
├─ type-check@0.4.0
├─ type-fest@0.20.2
├─ typed-array-length@1.0.4
├─ typescript@4.9.5
├─ unbox-primitive@1.0.2
├─ uri-js@4.4.1
├─ which-collection@1.0.1
├─ which@2.0.2
├─ word-wrap@1.2.3
├─ yallist@4.0.0
└─ yocto-queue@0.1.0
✨ Done in 8.00s.
Initializing project with template: default
Initialized a git repository.
Success! Created nextjs-react-markdown at /Users/hayato94087/Work/nextjs-react-markdown
✨ Done in 12.22s.
react-markdown
react-markdown は、Markdown を React コンポーネントに変換するライブラリです。
インストール
react-markdown
をインストールします。
$ yarn add react-markdown
実施結果
yarn add v1.22.19
warning ../package.json: No license field
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning " > react-markdown@8.0.5" has unmet peer dependency "@types/react@>=16".
warning " > react-markdown@8.0.5" has unmet peer dependency "react@>=16".
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 15 new dependencies.
info Direct dependencies
└─ remark-gfm@3.0.1
info All dependencies
├─ markdown-table@3.0.3
├─ mdast-util-gfm-footnote@1.0.2
├─ mdast-util-gfm-strikethrough@1.0.3
├─ mdast-util-gfm-table@1.0.7
├─ mdast-util-gfm-task-list-item@1.0.2
├─ mdast-util-gfm@2.0.2
├─ mdast-util-phrasing@3.0.1
├─ micromark-extension-gfm-autolink-literal@1.0.3
├─ micromark-extension-gfm-footnote@1.0.4
├─ micromark-extension-gfm-strikethrough@1.0.4
├─ micromark-extension-gfm-table@1.0.5
├─ micromark-extension-gfm-tagfilter@1.0.1
├─ micromark-extension-gfm-task-list-item@1.0.3
├─ micromark-extension-gfm@2.0.1
└─ remark-gfm@3.0.1
✨ Done in 3.47s.
ページの作成
以下の通り、src/pages/markdown-test-1.tsx
を修正します。
import ReactMarkdown from 'react-markdown';
const markdownString = `
# Markdown
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Magni, nemo!
`;
const Home = () => {
return <ReactMarkdown>{markdownString}</ReactMarkdown>;
};
export default Home;
動作を確認
ローカルサーバを起動します。
$ yarn dev
無事、Markdown が表示されています。
remark-gfm
react-gfm
は、GFM をサポートするためのライブラリです。
インストール
remark-gfm
をインストールします。
$ yarn add remark-gfm
実施結果
yarn add v1.22.19
warning ../package.json: No license field
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 42 new dependencies.
info Direct dependencies
└─ react-markdown@8.0.5
info All dependencies
├─ @types/debug@4.1.7
├─ @types/ms@0.7.31
├─ bail@2.0.2
├─ character-entities@2.0.2
├─ comma-separated-tokens@2.0.3
├─ dequal@2.0.3
├─ diff@5.1.0
├─ extend@3.0.2
├─ hast-util-whitespace@2.0.1
├─ inline-style-parser@0.1.1
├─ is-plain-obj@4.1.0
├─ kleur@4.1.5
├─ mdast-util-definitions@5.1.2
├─ mdast-util-from-markdown@1.3.0
├─ mdast-util-to-hast@12.3.0
├─ mdast-util-to-string@3.1.1
├─ micromark-core-commonmark@1.0.6
├─ micromark-factory-destination@1.0.0
├─ micromark-factory-label@1.0.2
├─ micromark-factory-title@1.0.2
├─ micromark-factory-whitespace@1.0.0
├─ micromark-util-classify-character@1.0.0
├─ micromark-util-combine-extensions@1.0.0
├─ micromark-util-decode-string@1.0.2
├─ micromark-util-html-tag-name@1.1.0
├─ micromark-util-sanitize-uri@1.1.0
├─ micromark@3.1.0
├─ mri@1.2.0
├─ property-information@6.2.0
├─ react-is@18.2.0
├─ react-markdown@8.0.5
├─ remark-parse@10.0.1
├─ remark-rehype@10.1.0
├─ sade@1.8.1
├─ space-separated-tokens@2.0.2
├─ style-to-object@0.4.1
├─ trim-lines@3.0.1
├─ trough@2.1.0
├─ unist-util-generated@2.0.1
├─ unist-util-position@4.0.4
├─ unist-util-visit-parents@5.1.3
└─ vfile-message@3.1.4
✨ Done in 3.77s.
ページの作成
以下の通り、src/pages/markdown-test-2.tsx
を修正します。
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const markdownString = `
# GFM
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Magni, nemo!
## Autolink literals
www.example.com, https://example.com, and contact@example.com.
## Footnote
A note[^1]
[^1]: Big note.
## Strikethrough
~one~ or ~~two~~ tildes.
## Table
| a | b | c | d |
| - | :- | -: | :-: |
## Tasklist
* [ ] to do
* [x] done
`;
const Home = () => {
return (
<ReactMarkdown remarkPlugins={[remarkGfm]}>{markdownString}</ReactMarkdown>
);
};
export default Home;
動作を確認
ローカルサーバを起動します。
$ yarn dev
無事、GFM が表示されています。
remark-math, rehype-katex
-
remark-math
は、Markdown の数式を処理するためのライブラリです。 -
rehype-katex
は、数式をレンダリングするためのライブラリです。
インストール
remark-math
、rehype-katex
をインストールします。
$ yarn add remark-math rehype-katex
実施結果
yarn add remark-math rehype-katex
yarn add v1.22.19
warning ../package.json: No license field
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 16 new dependencies.
info Direct dependencies
├─ rehype-katex@6.0.2
└─ remark-math@5.1.1
info All dependencies
├─ hast-util-from-parse5@7.1.1
├─ hast-util-is-element@2.1.3
├─ hast-util-parse-selector@3.1.1
├─ hast-util-to-text@3.1.2
├─ hastscript@7.2.0
├─ katex@0.15.6
├─ mdast-util-math@2.0.2
├─ micromark-extension-math@2.0.2
├─ parse5@6.0.1
├─ rehype-katex@6.0.2
├─ rehype-parse@8.0.4
├─ remark-math@5.1.1
├─ unist-util-find-after@4.0.1
├─ unist-util-remove-position@4.0.2
├─ vfile-location@4.1.0
└─ web-namespaces@2.0.1
✨ Done in 4.33s.
ページの作成
以下の通り、src/pages/markdown-test-3.tsx
を修正します。
import ReactMarkdown from 'react-markdown';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
const markdownString = `
# TeX
x^2 + y^2 = 1 をインライン表示すると $x^2 + y^2 = 1$ になります。
$$ S=\\sum_{n=1}^\\infty a_n $$
$$\\frac{1}{2} $$
`;
const Home = () => {
return (
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{markdownString}
</ReactMarkdown>
);
};
export default Home;
動作を確認
ローカルサーバを起動します。
$ yarn dev
無事、TeX が表示されています。
まとめ
Next.js で react-markdown
を使い Markdown、GFM、TeX を表示する方法を紹介しました。
参考
react-markdown のリポジトリです。
react-markdown の npm です。
React における Markdown の比較がされている記事です。
remark-gfm のリポジトリです。
marked のリポジトリです。
markdown-it のリポジトリです。
remark のリポジトリです。
Discussion