さようなら Gatsby
JavaScriptGatsby をやめて Next.js に移行してた。
経緯
ここはもともとポートフォリオを兼ねて作成したブログで、当時の技術スタックを反映していたつもりだった。
その実態は闇のカオス、 Dark-Chaos だった。それも強力な技術同士を悪魔合体させ、オーバーフローした結果できるレベル 1 桁のむしろレアなやつ。よくわからん例え。
WordPress + Nginx 時代
他所からコピペしてきた何もわからん docker-compose.yml で環境を立ち上げて、SimpleNote に初稿を書き WYSIWYG 画面へ貼り付ける。
アップロードは FTP クライアントを使って、さくらサーバーのスタンダード(年 5,000 円)に手動で上げてた。
得体の知れない SEO 最適化プラグインマシマシで、Lighthouse のパフォーマンススコアが 30 ぐらいだった。
Gatsby 時代
いわゆる WordPress 反抗期。
JAMStack(現在は Jamstack) という強そうなワードに惹かれ、型も秩序もない JavaScript を書いて、何やってるか知らんけどいい感じのプラグインを突っ込んで、FLOCSS な設計の CSS を CSS Prop で縦横無尽に設置して、天も地もないコンポーネント郡に GraphQL クエリを流し込んでた。
CD は自動化したけど雰囲気で生きてきたし、思考を止めて「モダン JavaScript 万歳!」してた。
Lighthouse のスコアは 100 を出していたけど、何か大切なものを見失っていた。
反省
WordPress や Gatsby のような「いい感じにやってくれるやつ」は、ビジネス要件に合致してて、使い手も思想を受け入れられれば存分に力を発揮できる。
もちろん「いい感じにやってくれるやつ」を使うこと自体は悪くないし、どちらかと言えば多くのページで使われていて、ノウハウの共有に価値がある分野だと思う。
ただ、自分のブログには必要ない機能が多いし、雰囲気だけで技術を組み合わせても、いずれ自分の足を撃ち抜かれてしまうし、ブログのために固有の知識をストックする気力もなかった。
案の定メンテが不可能な状態になっていたし、もう一度作り直すことにした。
というか上記は建前で、詳細は伏せるものの、ここ最近の Gatsby コミュニティが不穏だったのでやめたかった。
次
SSG ライブラリはやめて、 SSG もできる Next.js にした。
当初はいっそ jsx も引き剥がしたくて、何かと評判の 11ty を考えていた。
でも、11ty は蓋を開けてみると zero-config が結構 Easy 寄りで、余計なテンプレートエンジンが Core に集まっていることがとても気になってしまった。
ESLint が Sindre Sorhus と並んで投資するぐらい有望で、それ単体で見ればかなり優秀だけど、今のままだと運用しても WP や Gatsby 時代の二の舞を踏みそうだと思い断念。
- テンプレートが外側、ひいてはライブラリ特有の知識を知らなくていい
- 記事の Interface はこちらで用意して、それを内に流し込むだけ
- ↑について IDE で型補完が効く
上記の要件が満たせれば、少なくとも自分のブログで迷子にはならないと思った。
TypeScript + TSX が使えて、特有の知識は Page 層で完結する Next.js を採用した。
やったこと
ローカルの md ファイルから Sitemap, RSS, index を生成する。
記事個別ページは Remark で HTML に変換し、それを Next.js の Page に流し込む。
あとは React コンポーネントを書いていくだけ。Gatsby 時代に書いたそれはなかったことにした。
これまでとの違いは、雰囲気でやってる部分が消えて、コードの是非はともかく説明できるようになったこと。
補完が効くようになったのと、md 単位でバリデーション的なテストが書けるようになったのも嬉しい。
依存関係のメンテナンスが辛そうなのと、CSS は依然負債を抱えてるのがネック。
散々偉そうなことを言いつつ、魔がさして AMP も入れた。
Web Components だし、気に入らなかったら差し替えればいいかな、という軽い気持ち。
AMP の制約は https://amp.dev/ja/documentation/guides-and-tutorials/learn/spec/amphtml/ にある。
Next.js の AMP 設定については割愛。
引っかかったところだけ書き残した。
amp-img
AMP ページでは img
要素でなく、縦幅と横幅を指定した amp-img
を用いる。
Remark に amp-img
のプラグインはないので作る。
MDAST から img
の Node を漁り hName
を amp-img
に置き換えるもの。
ついでに、imageSize
で画像にアクセスし、縦横のサイズも書き出した。
convertImgToAmpImg.tsx
import appRoot from 'app-root-path'
import { imageSize } from 'image-size'
import visit from 'unist-util-visit'
export const convertImgToAmpImg = (options: {
rootPathName: string
}) => {
const appRootPath = appRoot.path
const { rootPathName } = options
// remark の img は p 要素でラップされる。消すプラグインもあるけど、デフォルトはこれ
const wrapper = 'paragraph'
return transformer
function transformer(ast) {
visit(ast, wrapper, visitor)
function visitor(node) {
if (!node.children || !node.children.length) return
node.children.forEach((data, index) => {
// img 要素の type は image
if (data.type === 'image') {
// imageSize ライブラリで、ルートフォルダから画像のあるパスへアクセスし、寸法を取得
const dimension = imageSize(
`${appRootPath}/${rootPathName}${data.url}`,
)
node.children[index].data = {
// 要素と属性を書き換える。
// layout が responsive に設定されているとレスポンシブになる
hName: 'amp-img',
hProperties: {
layout: 'responsive',
height: `${dimension.height}`,
width: `${dimension.width}`,
},
}
}
})
}
}
}
これを Remark に噛ませる。
markdownToHtml.tsx
import { convertImgToAmpImg } from './convertImgToAmpImg'
import html from 'remark-html'
import parser from 'remark-parse'
import unified from 'unified'
export const markdownToHtml = async (markdown: string): Promise<string> => {
const result = await unified()
.use(parser)
// parser の後に噛ませる。
// rootPathName では Next.js の静的画像を置く public を指定
.use(convertImgToAmpImg, {
rootPathName: 'public',
})
.use(html)
.process(markdown)
return result.toString()
}
下記の md だと "/public/images/foo.jpg" が amp-img
として読み込まれる。
markdown
![fooの画像](/images/foo.jpg)
amp-script
書き出したページに React のランタイムはない。
また AMP ページの script
要素は text/plain か JSON-LD しか許されてないので通常 JavaScript は使えない。
ただ、amp-script
で紐づけた script
は実行できる。150kb までの制限がある。
amp-script.tsx
const AMPSandBox = () => {
return (
<div>
{/* @ts-ignore */}
<amp-script script="localScript">
<input
id="buttonUb"
name="ButtonUb"
type="button"
value="サンプルコードを実行するボタン"
/>
{/* @ts-ignore */}
</amp-script>
<div
dangerouslySetInnerHTML={{
__html: `
<script id="localScript" type="text/plain" target="amp-script">/* ここにコードが入る */</script>
`,
}}
/>
</div>
)
}
export default AMPSandBox
これも amp-img
も、 d.ts
で型定義できる。
この記事を書いた 2020/10/05 時点では、公式の型定義が用意されてないので、自分で書く必要あり。
https://nextjs.org/docs/advanced-features/amp-support/typescript
next-env.d.ts
declare namespace JSX {
interface AmpScript {
script: string
children: ReactElement
}
interface IntrinsicElements {
'amp-script': AmpScript
}
}
window.alert
はないので途中で終わってしまうけど、動かせることはわかった。
デモページ
結局使わなかった。
コードタイトル
Gatsby でコードブロックに表示していたタイトル部分の再現。
gatsby-remark-prismjs-title のような Remark プラグインは公式にも存在しないし、ラベル付けするためにユニーク ID を持ったコードブロックを置きたかったので、これも作ることにした。
remark-prism に挟めば良さそうな気がしたけど、このプラグインは HTML Node を書き出し ID 属性などの情報は拾わない(それはそう)。
悲しいけどここは宿題ということで、いったん fork してオレオレ処理を挿した。完全敗亡。
remark-prism/index.js
const parseLang = (str) => {
/* 省略 */
const title =
// esLint に引っかかるのでこう
str.indexOf('title=') === -1
? null
: str.replace(/.+:title=(.[^{]+).*$/, '$1');
/* 省略 */
return {
/* 省略 */
title,
};
ユニーク ID は offset などから取った。
記事
自分が書いた記事の中で、ライブラリをなぞるだけの記事、体系的な解説記事がノイズに思えてきたので全て消した。
前者は公式ドキュメントをアップデートした方が健全だし、後者は鮮度を保ち続けるモチベがないため。
とは言え手抜きをしたいわけではなくて、執筆当時の Why と What を書き残しておくことに重きを置きたい。
あと、曖昧な口調を統一するため、analyze-desumasu-dearu で「です・ます」口調の記事を調べ修正した。
終わり
ありがとう Gatsby さようなら。