はじめに
microcmsとJavaScriptフレームワークを使ったJAMSTACKブログの記事はめちゃめちゃあります。
でも..........
microcmsのリッチテキストを使ったシンタックスハイライト実装に関する記事が全くない😂
しかもnext.js microcmsでググると
oh.......
Nuxtの記事が多すぎて「もしかして:」が出てしまう。。。。。
なので記事にしました。
シンタックスハイライトとは
こんな感じでコードに色を持たせて強調するものです。
とてもみやすくなりますし、技術ブログではほぼほぼ必須です。
const hoge = 'hogehoge'
const fuga = 'fugafuga'
実装方法
大体この記事を見ている人は他の方のブログ実装の記事を見ていてある程度は一緒のディレクトリ構成になっていると思います。なので1から作りたい人は以下の記事を参考にしながら途中まで実装してみてください。
Next.js + microCMS + NetlifyでJAMstackな世界に入門する
Vercel + Next.js + micro CMSで作る、jamstackなCMS構築
実装するにあたって参考にしたのは
microcmsのこちらの記事です。
こちらの記事はNuxtでの実装方法が書かれていますがNextに転用して使います。
記事の中でこのように書かれています。
microCMSではリッチエディタでソースコードの入力を行う場合、以下のようなHTMLが出力されます。
<pre>
<code>
// ソースコード
</code>
</pre>
リッチエディタで作成されたデータをjson形式で取り出す時には以下のように取り出されます。
(1行で申し訳ない)
blog:{
"body":"<pre><code>const fruits = ['apple','grapes','pomme']\n\nconst cake = fruits.map((fruit:string) => {\n fruit + 'cake'\n})\n\nconsole.log(cake)</code></pre><p><br>これはどうだい?<br><code>hogehoge</code></p><h1 id=\"ha53b8ea4b7\">ほい!</h1><p><br><span style=\"font-size: 0.75em\">hogehoge</span><br><strong style=\"font-size: 0.75em\">ううおお</strong><br><br><img src=\"https://images.microcms-assets.io/assets/c2ccfec8448e4d2abdaad5969c2ba309/1d60bef563904e789c4891d7644a0305/kumon.jpg\" alt><br><br></p><blockquote>こんにちは</blockquote><p><br></p>"
}
ただこのまま
<div dangerouslySetInnerHTML={{ __html: blog.body }}></div>
としてしまうと
こんな感じで味気なくなってしまいます。
ここで,microcms公式の
以下のライブラリを使います。
cheerio,highlight.js
cheerioはHTMLパーサーで、highlight.jsはシンタックスハイライト用のライブラリです。
highlight.jsには様々なAPIが用意されています。
今回は highlightAuto()
を使います。
highlightAuto()
は引数にソースコードの文字列を与えると、自動で言語を推測して良い感じにハイライトしてくれます。
microCMSではリッチエディタでソースコードの入力を行う場合、以下のようなHTMLが出力されます。
<pre>
<code>
// ソースコード
</code>
</pre>
cheerioを使って該当のソースコード部分を抜き出し、そこにハイライトを当てるには次のように記述します。
import cheerio from 'cheerio';
import hljs from 'highlight.js'
// 略
const $ = cheerio.load(data.body); // data.bodyはmicroCMSから返されるリッチエディタ部分
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text());
$(elm).html(result.value);
$(elm).addClass('hljs');
});
console.log($.html()); // ハイライト済みのHTML
まずこのcheerioとhljsをインストールした後に
pages/{ページディレクトリ}/[id].tsx
例)pages/blogs/[id].tsx
に上記のコードを追記します。
またこの
const $ = cheerio.load(data.body); // data.bodyはmicroCMSから返されるリッチエディタ部分
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text());
$(elm).html(result.value);
$(elm).addClass('hljs');
});
を記述する場所なのですがこちらはgetStaticPropsのなかに書いていきます。
export const getStaticProps = async (context: any) => {
const id = context.params.id
const key: any = {
headers: { 'X-API-KEY': process.env.API_KEY },
}
const res = await fetch(
`https://hogehoge.microcms.io/api/v1/blogs/${id}`,
key,
)
const blog = await res.json()
const $ = cheerio.load(blog.body)
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text())
$(elm).html(result.value)
$(elm).addClass('hljs')
})
return {
props: {
blog,
highlightedBody:$.html()
},
}
}
getStaticPropsはgetInitialPropsが行っていた処理をビルド時に行い、静的なファイルを事前に生成するためのAPIです。next exportと異なりnext linkでルーティングした場合でも静的なファイルを利用します。
このメソッドはクライアント側で実行されることはありません。必ず事前にサーバーサイドで実行されます。
引用:Next.js 9.3新API getStaticProps と getStaticPaths と getServerSideProps の概要解説
これをすることで事前にサーバーサイドレンダリングが行われ、propsとして渡されます
highlightedBody:$.html()
とすることで記事本体にシンタックスハイライトがかかったhtmlコードがpropsとして渡されます。
return {
props: {
blog,
highlightedBody:$.html()
},
またこの$.html()を出力してみると
$.html(は"<html><head></head><body><pre><code class=\"hljs\"><span class=\"hljs-keyword\">const</span> fruits = [<span class=\"hljs-string\">'apple'</span>,<span class=\"hljs-string\">'grapes'</span>,<span class=\"hljs-string\">'pomme'</span>]\n\n<span class=\"hljs-keyword\">const</span> cake = fruits.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">fruit:<span class=\"hljs-built_in\">string</span></span>) =></span> {\n fruit + <span class=\"hljs-string\">'cake'</span>\n})\n\n<span class=\"hljs-built_in\">console</span>.log(cake)</code></pre><p><br>これはどうだい?<br><code>hogehoge</code></p><h1 id=\"ha53b8ea4b7\">ほい!</h1><p><br><span style=\"font-size: 0.75em\">hogehoge</span><br><strong style=\"font-size: 0.75em\">ううおお</strong><br><br><img src=\"https://images.microcms-assets.io/assets/c2ccfec8448e4d2abdaad5969c2ba309/1d60bef563904e789c4891d7644a0305/kumon.jpg\" alt=\"\"><br><br></p><blockquote>こんにちは</blockquote><p><br></p></body></html>"
こんな感じで加工されたhtmlが返されます。
これを
<div dangerouslySetInnerHTML={{ __html: highlightedBody }}></div>
として渡すと、
こんな感じでいい感じで色がついてみやすくなりました
ちなみにテーマは
night-owlを使っています
テーマの使い方は
import 'highlight.js/styles/night-owl.css';
コード冒頭でimportすることで切り替えてくれます。
コード全体
page/blogs/[id].tsx
import React from 'react';
import cheerio from 'cheerio';
import hljs from 'highlight.js'
import 'highlight.js/styles/night-owl.css';
import Layout from '../../components/Layout'
type Props = {
blog: {
id: string,
createdAt: string,
updatedAt: string,
publishedAt: string,
revisedAt: string,
title: string,
tags: any[],
body: any
},
highlightedBody: any
}
type Tag = {
id: string,
createdAt: string,
updatedAt: string,
publishedAt: string,
revisedAt: string,
name: string
}
const BlogId: React.FC<Props> = ({ blog,highlightedBody }) => {
return (
<Layout>
<h1>{blog.title}</h1>
<div>
{blog.tags.map((tag: Tag) => (
<React.Fragment key={tag.id}>
<span>{tag.name}</span>
</React.Fragment>
))}
</div>
<div dangerouslySetInnerHTML={{ __html: highlightedBody }}></div>
</Layout>
)
}
export const getStaticPaths = async () => {
const key: any = {
headers: { 'X-API-KEY': process.env.API_KEY },
}
const res = await fetch('https://hogehoge.microcms.io/api/v1/blogs', key)
const repos = await res.json()
const paths = repos.contents.map((repo: any) => `/blogs/${repo.id}`)
return { paths, fallback: false }
}
export const getStaticProps = async (context: any) => {
const id = context.params.id
const key: any = {
headers: { 'X-API-KEY': process.env.API_KEY },
}
const res = await fetch(
`https://hogehoge.microcms.io/api/v1/blogs/${id}`,
key,
)
const blog = await res.json()
const $ = cheerio.load(blog.body)
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text())
$(elm).html(result.value)
$(elm).addClass('hljs')
})
return {
props: {
blog,
highlightedBody:$.html()
},
}
}
export default BlogId