Next.jsでUIをそんなにこだわらない静的ページ(例えばヘルプページやプライバシーポリシーなど)やブログのような記事を開発者以外が編集するような場合、基本的にはReactで書くよりマークダウンを使って記述し、それを表示した方が早いことがあります。
Reactで作られたメインのコンテンツとは別にマークダウンをベースにした静的ページをNext.js上につくりたい場合以下のやり方で実装することができます。(今回のケースはNext.js内にReactで作られたコンテンツと同居させる場合のやり方なので、マークダウンがメインになる場合はGatsbyやそれに類する静的サイトジェネレータを使った方が効率はよいかもしれません)
利用するのは以下の2つのライブラリ
getStaticProps
を利用し、ビルド時にマークダウンファイルから静的なHTMLページを生成するためのものです。---
で区切られたメタ情報(タイトルや日付など)をパースして取得することができるライブラリそれらをインストールします
npm i next-mdx-remote gray-matter
まずは元のデータとなるマークダウンを作成します。今回はとりあえず contents/hello.mdx
というファイルを作成します。ディレクトリは以下のようになります。
└── src
├── components
├── contents
│ └── hello.mdx
└── pages
└── index.tsx
mdx
はマークダウンの記述の中にReactのJSXの記法を混ぜて書き込むことができるファイルの形式です。
hello.mdxの内容は以下のようにします。
---
title: Hello
slug: home
---
# Hello world
foo bar.
## Hello world2
* aaa
* bbb
* ccc
<div style={{ padding: '20px', color: 'red' }}>
<h3>This is JSX</h3>
</div>
表示させたいページの数やパスが未定の場合、存在するマークダウンファイルに対してのみアクセスをできるようにします。Next.js のDynamic Routeを使って static/[id].tsx
というページを作成します。
└── src
├── components
├── contents
│ └── hello.mdx
└── pages
├── index.tsx
└── static
└── [id].tsx
ファイル全体は以下のようになります
import fs from 'fs';
import matter from 'gray-matter';
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import Head from 'next/head';
import path from 'path';
import * as React from 'react';
const contentDirectory = path.join(process.cwd(), 'src/contents');
const getAllPostSlugs = () => {
const fileNames = fs.readdirSync(contentDirectory);
return fileNames.map(filename => {
return {
params: {
id: filename.replace('.mdx', '')
}
};
});
};
interface StaticProps {
source: MDXRemoteSerializeResult;
frontMatter: { [key: string]: any };
}
const components = {
h1: props => <h1 {...props} />,
h2: props => <h2 {...props} />,
h3: props => <h3 {...props} />,
h4: props => <h4 {...props} />,
p: props => <p {...props} />,
a: props => <a {...props} />,
li: props => <li {...props} />
};
const Static: NextPage<StaticProps> = ({ source, frontMatter }) => {
return (
<div>
<Head>
<title>{frontMatter.title}</title>
</Head>
<div>
<MDXRemote {...source} components={components} />
</div>
</div>
);
};
// 静的ページとして取得するパスを指定
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostSlugs();
return {
paths,
fallback: false // 見つからない場合は404とする
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const fullPath = path.join(contentDirectory, `${params.id}.mdx`);
const pageContent = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(pageContent);
const mdxSource = await serialize(content);
return {
props: {
source: mdxSource,
frontMatter: data
},
revalidate: 600 // sec
};
};
export default Static;
コンテンツ(マークダウン)があるディレクトリの位置を指定します。
const contentDirectory = path.join(process.cwd(), 'src/contents');
後述のgetStaticPathsで利用する静的ページとなるファイルがいくつ存在するかを定義します。paramsのidはファイル名が[id].tsxとなっているためこのようになります。
const getAllPostSlugs = () => {
const fileNames = fs.readdirSync(contentDirectory);
return fileNames.map(filename => {
return {
params: {
id: filename.replace('.mdx', '')
}
};
});
};
マークダウンをHTMLに変換された際に利用するcomponentを定義します。スタイルや振る舞いを定義することができます。
const components = {
h1: props => <h1 {...props} />,
...
};
実際にレンダリングされるコンポーネントはこちらになります。MDXRemoteにマークダウン(mdx)の文字列を渡すとそれを解釈しHTMLとして表示されます。
const Static: NextPage<StaticProps> = ({ source, frontMatter }) => {
return (
<div>
<Head>
<title>{frontMatter.title}</title>
</Head>
<div>
<MDXRemote {...source} components={components} />
</div>
</div>
);
};
Next.jsのgetStaticPathsです。静的なものとして表示したいページを一覧として paths
に定義します。ビルド時にそれらを評価し静的なページとして生成します。
つまり、ビルド時にsrc/contentsディレクトリに配置されているすべてのマークダウンファイルを静的ページとして生成します。
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostSlugs();
return {
paths,
fallback: false // 見つからない場合は404とする
};
};
Next.jsのgetStaticPropsです。静的ページを生成するためparams(パス)に指定されたマークダウンファイルを読み込み、gray-matterによってfront matterを抽出し、コンテンツをコンポーネント側に返します。
serialize
関数によりmdxを解釈することができます。
export const getStaticProps: GetStaticProps = async ({ params }) => {
const fullPath = path.join(contentDirectory, `${params.id}.mdx`);
const pageContent = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(pageContent);
const mdxSource = await serialize(content);
return {
props: {
source: mdxSource,
frontMatter: data
},
revalidate: 600 // sec
};
};
localhost:3000/static/helloにアクセスすると以下のようなページが表示され、マークダウンが解釈されページが表示されたことがわかります。
npm run buildすると作成したマークダウンが生成されていることがわかります。
running command with prefix "build"
> next build
...
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
info - Generating static pages (5/5)
info - Finalizing page optimization
Page Size First Load JS
┌ ○ / 256 B 64 kB
├ ○ /404 3.17 kB 67 kB
└ ● /static/[id] 3.67 kB 67.5 kB
├ /static/foo
└ /static/hello
+ First Load JS shared by all 63.8 kB
├ chunks/framework.64eb71.js 42 kB
├ chunks/main.c03421.js 20.2 kB
├ chunks/pages/_app.a8eaee.js 801 B
└ chunks/webpack.672781.js 766 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
foo.mdxというファイルを追加し、実行してみました。●(SSG)の部分に静的なHTMLとして生成されていることが表示されています。
上記のやり方でマークダウンから静的なページを生成することができました。
一つのWebサイト上でReactと静的なページを分けて管理する場合に有効だと思います。
/staticを付けたくない場合、next.jsのconfigを書くことでルート直下のパスとしてページを返すことができます。
※ ただしこの場合、静的なページ名を正規表現で指定することになります。ワイルドカードでやると関係ないページ名なども拾ってしまい、別の箇所で不具合がでることがあります。
const config = {
async rewrites() {
return [
{
source: '/:id(foo|hello)',
destination: '/statics/:id'
}
];
}
}
module.exports = config;