|
| 1 | +--- |
| 2 | +title: "Next.jsでブログを作った" |
| 3 | +date: "2021-04-04-00-00" |
| 4 | +tags: [nextjs, mdx, blog] |
| 5 | +--- |
| 6 | + |
| 7 | +Next.jsで自分用のブログを作った(このブログ)ので、開発時に工夫した事を書こうと思います。 |
| 8 | + |
| 9 | +ブログとしての基本的な機能はだいぶ前に出来ていたものの、CSSとかOGP辺りの見た目の部分が面倒で後回しにしていたら年明けるどころか桜咲いてました。 |
| 10 | +何か明確な用途があったから作ったわけではなくReact周りの技術で何か作りたくて作った感じなので、継続して更新するかは分かりませんが、せっかく作ったからには色々書きたい気持ちです。 |
| 11 | + |
| 12 | + |
| 13 | +## 特徴 |
| 14 | +- Next.jsでSSG |
| 15 | +- MDXで記事を書ける |
| 16 | +- YAML Front Matter 対応 |
| 17 | +- OGP画像の生成 |
| 18 | + |
| 19 | + |
| 20 | +## MDXで記事を書きたい |
| 21 | +MDX詳細: https://mdxjs.com/ |
| 22 | + |
| 23 | +ブログを作ろうと思った当初は microCMS でコンテンツ管理をするつもりだったんですが、自分だけが更新するブログ程度の規模ならMarkdownファイルをリポジトリに直置きで十分という結論になった。 |
| 24 | +その後調べているとMarkdown中にJSXが書けるMDXというものを知り、ただのMarkdownよりは面白そうだったので採用。 |
| 25 | + |
| 26 | +Next.jsでMDXファイルに記述したコンテンツを表示したい場合、一番簡単なのはNext.jsのルーティングをそのまま利用して`pages`ディレクトリ以下にMDXファイルを配置してしまうことだと思います。 |
| 27 | + |
| 28 | +例えば、`next.config.js`に[MDX用の設定](https://mdxjs.com/getting-started/next)を記述してから以下のようなMDXファイルを`pages`以下に配置すれば、js/tsファイル等を置いた時と同様にアクセス可能です。 |
| 29 | + |
| 30 | +``` |
| 31 | +// hoge.mdx |
| 32 | +
|
| 33 | +import Layout from '../components/Layout.tsx'; |
| 34 | +
|
| 35 | +# MDX Document |
| 36 | +
|
| 37 | +- hogehoge |
| 38 | +- some text |
| 39 | +
|
| 40 | +export default ({children}) => { |
| 41 | + <Layout> |
| 42 | + {children} |
| 43 | + </Layout> |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +ただし、今回は以下の理由により`pages`以下にMDXファイルを直接置くのは避けました。 |
| 48 | + |
| 49 | +- MDXファイルには実際に表示するコンテンツ以外の記述(ヘッダーやフッター等の共通部品の記述等)はしたくない |
| 50 | +- 記事数が増えた時に`pages`ディレクトリ以下が肥大化するのが何となく気持ち悪かった |
| 51 | +- ビルド時にファイル名やFront Matterから記事のメタデータを拾ったりしたかったので、そういう対象のファイルが`pages`以下にあるのは何か違う気がした |
| 52 | + |
| 53 | + |
| 54 | +回避策として`pages/articles/[slug].ts`で記事ページへのリクエストを受けて、対応する記事を [Dynamic import](https://nextjs.org/docs/advanced-features/dynamic-import) して表示するようにしてます。 |
| 55 | + |
| 56 | +MDXの文書そのままでは当然import出来ないのでwebpackのLoaderを通す必要があります。 |
| 57 | +`next.config.js`を以下の様に編集し、拡張子が`.mdx`のファイルは`@mdx-js/loader`を通します。 |
| 58 | + |
| 59 | +```js |
| 60 | +// next.config.js |
| 61 | + |
| 62 | +const path = require("path") |
| 63 | +const rehypePrism = require('@mapbox/rehype-prism') |
| 64 | + |
| 65 | +module.exports = { |
| 66 | + pageExtensions: ['js', 'jsx', 'ts', 'tsx'], |
| 67 | + webpack: (config, options) => { |
| 68 | + config.module.rules.push({ |
| 69 | + test: /\.mdx?$/, |
| 70 | + use: [ |
| 71 | + options.defaultLoaders.babel, |
| 72 | + { |
| 73 | + loader: '@mdx-js/loader', |
| 74 | + options: { |
| 75 | + rehypePlugins: [rehypePrism] |
| 76 | + } |
| 77 | + }, |
| 78 | + path.join(__dirname, "./lib/fm-loader"), |
| 79 | + ], |
| 80 | + }) |
| 81 | + return config |
| 82 | + }, |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +これでMDXで書いた各記事がReactコンポーネントとしてimport出来るようになったので、`pages/articles/[slug].ts`でDynamic importして使うだけです。 |
| 87 | + |
| 88 | + |
| 89 | +```jsx |
| 90 | +// 例 |
| 91 | + |
| 92 | +import Layout from '../../components/Layout' |
| 93 | +import dynamic from 'next/dynamic' |
| 94 | +import { GetStaticProps, GetStaticPaths } from 'next' |
| 95 | + |
| 96 | +const Article = (props: Props) => { |
| 97 | + // Dynamic import |
| 98 | + const MDX = dynamic(() => import(`../../articles/${props.fileName}`)) |
| 99 | + |
| 100 | + return ( |
| 101 | + <Layout> |
| 102 | + <MDX /> |
| 103 | + </Layout> |
| 104 | + ) |
| 105 | +} |
| 106 | + |
| 107 | +export const getStaticPaths: GetStaticPaths = async () => { |
| 108 | + // ...省略 |
| 109 | +} |
| 110 | + |
| 111 | +export const getStaticProps: GetStaticProps = async ({ params }) => { |
| 112 | + // ...省略 |
| 113 | +} |
| 114 | + |
| 115 | +export default Article |
| 116 | +``` |
| 117 | + |
| 118 | +### コードのシンタックスハイライト |
| 119 | +シンタックスハイライターには [Prism.js](https://prismjs.com/) を使いました。 |
| 120 | + |
| 121 | +先程の`next.config.js`を見ると`@mdx-js/loader`のoptionsに [rehype-prism](https://github.com/mapbox/rehype-prism) が指定されていますが、これはMDXのLoaderを実行したタイミングでコードブロックのHTMLを Prism.js の形式に変換するためです。 |
| 122 | + |
| 123 | +Prism.jsを使う場合ブラウザ側でスクリプトを読み込んで変換するのが一般的な使い方だと思いますが、ビルド時に変換してしまったほうが効率的です。 |
| 124 | + |
| 125 | +そうすれば後は好きなPrism.jsのテーマCSSを読み込めば色付けされます。 |
| 126 | + |
| 127 | +(テーマは公式サイトで見れるものの他に[GitHub](https://github.com/PrismJS/prism-themes)にもいくつかある) |
| 128 | + |
| 129 | + |
| 130 | +## 記事のメタ情報をFront Matterで書きたい |
| 131 | + |
| 132 | +タグや作成日、タイトル等の各記事のメタデータをYAML Front Matterで書きたかった。 |
| 133 | + |
| 134 | +YAML Front Matterってこういうやつです。 |
| 135 | + |
| 136 | +``` |
| 137 | +// some.md |
| 138 | +
|
| 139 | +--- |
| 140 | +title: "タイトル" |
| 141 | +date: "2021-04-05-12-37" |
| 142 | +tags: [blog, markdown, diary] |
| 143 | +--- |
| 144 | +
|
| 145 | +# Markdown document |
| 146 | +- some text |
| 147 | +- some text 2 |
| 148 | +``` |
| 149 | + |
| 150 | +MDXでFront Matterを使う方法は色々あると思うんですが、今回は記事ページ以外の色々なページで記事のメタ情報を使いたかったので、Next.jsのビルド直前に以下のようなスクリプトを実行して記事のメタ情報をまとめたJSONファイルを生成し、その後のNext.jsのSSGのプロセスでメタ情報が欲しい時は生成したJSONファイルから取ってくるようにしました。 |
| 151 | + |
| 152 | + |
| 153 | +```js |
| 154 | +// generate-json.mjs (一例) |
| 155 | + |
| 156 | +import matter from 'gray-matter' |
| 157 | + |
| 158 | +// fileNamesには記事のMDXファイル名が配列で入っている想定 |
| 159 | +const articleList = fileNames.map(fileName => { |
| 160 | + const fullPath = path.join(articlesDir, fileName) |
| 161 | + const doc = fs.readFileSync(fullPath, 'utf8') |
| 162 | + const frontMatter = matter(doc).data |
| 163 | + return { ...frontMatter } |
| 164 | +}) |
| 165 | +const json = JSON.stringify(articleList, undefined, 2) |
| 166 | +fs.writeFileSync(path.join(process.cwd(), 'gen/articles.json'), json) |
| 167 | +``` |
| 168 | + |
| 169 | +(`package.json`を、Next.jsのビルドコマンド等を実行する前に特定のスクリプトを割り込ませるように書き換えた) |
| 170 | + |
| 171 | +```json |
| 172 | +// package.json |
| 173 | + |
| 174 | +{ |
| 175 | + "name": "blog", |
| 176 | + "version": "0.1.0", |
| 177 | + "private": true, |
| 178 | + "scripts": { |
| 179 | + "dev": "node script/generate-json.mjs && next dev", |
| 180 | + "build": "node script/generate-json.mjs && next build && next export", |
| 181 | + "start": "next start" |
| 182 | + }, |
| 183 | + // ...省略 |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +若干力技な感じもしていて、もう少しいい方法無いかなーとも思ったんですが、とりあえずJSONに吐き出しておけば再利用もしやすいのでまあ良いかという気持ちです。 |
| 188 | + |
| 189 | +余談ですがこの記事書く時に Front Matter の正しい名称が分からなくて困った。 |
| 190 | +人によって「YAML Front Matter」だったり「Frontmatter」だったり「Front-matter」だったりと表記ゆれが激しくて、結局今もどれが正しいのか分かってないです(特に統一されてない...?)。 |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | +## OGP画像を自動生成したい |
| 195 | + |
| 196 | +node-canvasを使って記事タイトルから上のようなOGP画像を生成してます。 |
| 197 | + |
| 198 | +本当はビルド時に自動で画像生成スクリプトを実行出来れば良かったんですが、デプロイ先のVercelで node-canvas が動かなかったので、とりあえずは都度ローカルでスクリプトを実行して画像生成してからpushする運用をしています。 |
| 199 | + |
| 200 | +参考: https://mizchi.dev/202006211925-support-ogp |
| 201 | + |
| 202 | + |
| 203 | + |
| 204 | +# まとめ |
| 205 | +- 個人的にはそれなりに使いやすいものが出来たと思ってる |
| 206 | +- Next.jsを触ってみて結構楽しかったので、もっと掘り下げて触ってみたい |
| 207 | +- よく分かってなかった`webpack`の雰囲気を知れたのは良かった |
| 208 | +- CSSはあまり気に入ってないので改善必須 |
| 209 | +- 冗長な処理が多い気がするので見直したい |
0 commit comments