はじめに
microCMSで記事を作っているが、もうちょっと細かいことがしたい。そんなことはありませんか?
僕は細かい実装をしつつ再利用性高いComponentを作ってそれをCMSから呼びたい。
microCMSとNext.jsで記事サイトを作るにあたって、
もうちょっと手が届けば良いなと思う細かい要件をクリアしたく、
Next.jsとmicroCMSで連携を上げられる方法を考えてみました。
microCMSから受け取ったHTMLをパースしてNext.jsで使いやすい形にしよう!という記事です。
microCMSの特徴
軽く今回の記事の前提としてmicroCMSの紹介です。
API Schemaを自由に設計
作りたいものに合わせたデータ構造を簡単に作成できるAPIベースのheadlessCMSです。
テキストフィールド、数字、繰り返しのデータ等を設定し、データを入れてAPIを作ることができます。
リッチエディタ
API Schemaにはリッチエディタで入れられるデータ型もあります。
よくあるCMSと同様に、画像や見出し、段落等が入れられます。
出力の形はHTMLで固定です。
Next.jsとの連携力を高めて細かい実装を
上記に書きましたがリッチエディタを使いたいとき、microCMSの出力はHTML固定です。
受け取ったHTMLからフロントエンドでデザインを作るためには、
- 受け取ったHTMLに対してCSSを当てる
- HTMLをパースしてNext.jsで使いやすい形にパースする
ということをしなければいけませんが、受け取ったHTMLをそのまま出力すると、
再利用性が無くReactのComponentの恩恵が受けられなかったり、
細かい実装ができなかったりして辛いポイントがあります。
例えば、一部画像だけlazy loadにしたり、特定の条件化のみスタイルを変えたりするときには辛いことが多いです。
今回は細かい実装をしていきたいので、パースする方法で進めます。
また、変換するだけでは細かい処理ができない場合、
Viewに近いロジックが重くなるため、パースするときに変換します。
HTLM to JSX
今回は細かい仕様に対応したいので、
HTMLをパースしてNext.jsが受け取りやすい形(JSX)に変換します。
パース処理には、unifiedというパッケージを使います。
unified
これは様々な言語から様々な言語に変換する際に使えるnpmパッケージで、
コンテンツを構文木に変換し、構文木をコンテンツに変換するサポートをしてくれるライブラリです。
例えば今回は、HTMLをReactに変換したいので、
- HTMLを構造木に変換してHASTにする(HTMLを構造木にしたものをHASTといいます)
- HASTを操作して好きな形に変換する
- HASTをJSXに変換する
という順番で進めます。
ちなみに、それぞれの言語の構造木には呼び名があり、HTMLなら、HAST
、Markdownなら、MDAST
、Text
なら、nlcst
、JavaScriptなら、ESTress
といいます。
rehype
unifiedを使おうと書きましたが、unifiedだけでは動きません。
unifiedはインターフェイスを提供してくれていて、それぞれ変換したい処理のプラグインを作るか、公開されているプラグインを使わないといけません。
rehypeはHTMLをunifiedで操作するためのエコシステムです。
HTMLをパースしたり、操作したりするためにそれぞれの処理がひとつひとつパッケージになっています。
実際に変換する
上記で簡単に説明を書きましたが、
unifiedとrehypeを使って、HTMLを変換していきます。
HTMLをパースしてくれるrehype-parseとHTMLからJSXに変換を行ってくれるrehype-reactも使います。
import * as runtime from "react/jsx-runtime";
import rehypeParse from "rehype-parse";
import rehypeReact, { type Components, type Options } from "rehype-react";
import { unified } from "unified";
type Props = {
source: string;
components: Partial<Components>;
};
export const htmlToJsx = ({ source, components }: Props) => {
const { jsx, jsxs, Fragment } = runtime;
const rehypeOptions: Options = {
Fragment,
jsx,
jsxs,
components,
};
const pipeline = unified()
.use(rehypeParse, { fragment: true }) //HTMLをHASTに変換する
.use(rehypeReact, rehypeOptions) //HASTをJSXに変換する
.processSync(source);
return pipeline.result;
};
上記のコードのようにunified()
に各プラグインをくっつけていく形で実装ができます。
コメントで書いたようにHTMLからJSXに変換を行っています。
以下のようにHTMLと変換するComponentのオブジェクトを渡すことによって、Reactで出力することができます。
htmlToJsx({
source: '<p>サンプルテキスト</p><img src="/image.png" alt="サンプルイメージ" />',
components: {
p: ({ children }) => <TypographyP>{children}</TypographyP>,
strong: ({ children }) => <strong>{children}</strong>,
img: ({ src, alt }) => {
if (!src || !alt) return null;
return <Image src={src} alt={alt} />;
},
},
});
==>//出力イメージ
return (
<TypographyP>サンプルテキスト</TypographyP>
<Image src={'/image.png'} alt={'サンプルイメージ'} />
)
細かい仕様を実装する
記事等のWebサイトの開発をしていると、CMSからは入れにくい要件の実装が必要になることも多いと思います。
例えば、Tableの中のスタイルの操作や細かいデザインの装飾等についてです。
簡単なスタイルはmicroCMSのカスタムClassという機能があるので実装できますが、複雑なものはできないので、変換処理をプラグインとして追加します。
Tableのカラムが今何カラム目かを取得する
Tableを使うとき、n行目に対して特殊な処理をしたいことばあるかもしれません。
3行目だけ大きくスタイルを変えたくなるかもしれません。(このパターンだと、CSSだけでできるかもしれませんが)
こんな要件があるかわかりませんが、例として進めてみます。
記事の前半でも書いたように、プラグインを自作することもできます。
以下のように作ったり持ってきたプラグインは差し込むことができ、テキストの内容を自由に形を変えたり、差し込んだりすることができます。
const pipeline = unified()
.use(rehypeParse, { fragment: true })
.use(plugin1) //操作したいプラグイン1
.use(plugin2) //操作したいプラグイン2
.use(rehypeReact, rehypeOptions)
.processSync(source);
今回は、Tableの行数をカウントするcountTableRows
を作ります。
export const countTableRows: Plugin<[], Root> = () => {
return (tree) => {
visit(tree, "element", (node) => {
if (node.tagName === "tbody") {
return node.children.reduce((count, child) => {
if (child.type === "element" && child.tagName === "tr") {
child.properties = { ...child.properties, count };
return count + 1;
}
return count;
}, 1);
}
});
};
};
//htmlToJsx
...
const pipeline = unified()
.use(rehypeParse, { fragment: true })
.use(countTableRows)
.use(rehypeReact, rehypeOptions)
.processSync(source);
return pipeline.result;
中を見るとわかるように、構造化になったHTMLがtree
、node
にわたってくるので、
一つ一つ条件にあった操作したいtagに対して変換したり追加をしています。
あとは受け取る部分で、他の属性といっしょにprops.count
のように受け取ることができるのでComponentで処理することができると思います。
終わりに
いかがでしたか。
僕はhtmlToJsxでパースして記事を書いていますが、
Tableに限らず他の要件でもパース処理に責任を任せたほうが良いロジックは出てきます。
細かい処理を対応するかどうかは、メンテナンスの重さと要件を対応したい重要度によって変わると思いますが、
Next.jsとmicroCMSを使ってWebサイトを作るに当たってパース処理で進めること自体は個人的には好きです。
Componentの再利用性が上げられたり、細かいデザイン要件等を実装することができるようになったのではないでしょうか。
よりよい方法を模索中なので、もっと良い方法があれば教えてください。