どうもこんにちわ。
コイバナ恋愛 ミニファンディスク アフターフェスティバル 攻略しました。
FD で FD 言及するのねw
かわい~
おもろい!!!!!!!!
いちいちおもろいのすごい
サブカプも報われててよかったよかった。
まぶしすぎるので(?)ぜひ
みじかい!!!
本題
修正したい点がまた増えてきた、、いい加減直します!
優先度高め順で
- 静的(意味深)書き出し
GitHub Actions
が遅すぎる
Markdown → HTML
が何回も何回も呼ばれているのが原因ぽい
Markdown → HTML
の変換結果をキャッシュする方法が必要
- 静的サイトでも使える全文検索があるらしいので使ってみたい→
pagefind
- OGP 画像を作って共有したときにおしゃれにしたい
- シンタックスハイライトを
rehype-pretty-code
にしたい
Google Analytics 4
のみ。UA
の方を消したい
- ついでにページ遷移イベントを自前で送るのもやめたい
- コードブロックにコピーボタンほしい
- スマホでも目次
- ダークモード切り替えを
localStorage + useSyncExternalStore
にしたい
- 環境変数じゃなくてベタ書きのが残ってる
- Mastodon / Misskey の認証マーク付けれるように
やったこと
シンタックスハイライト
highlight.js
からShiki(を使っている rehype-pretty-code )
にしました。
JSX
とTypeScript
の色つけが良くなった気がする。やった~~~
どうやらVSCode
のシンタックスハイライトがこのShiki
みたい?
依存関係の更新
Next.js
とかUnified
とかの更新もした。
rehype-pretty-code
とかは11
系にしないといけないので
Google Analytics の UA を消す
https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries#google-analytics
GA4 / UA
の両方書いてたんですけど、いつからか、なんか重複されてる気がする?記録されるようになったので消しました。
ついでに、Next.js
側でGoogle アナリティクス
の使い方が言及されたので、そっちに乗り換えました。
ブラウザの履歴を検知する設定を有効にする必要があります。
私の環境ではjsdom
の型がおかしくなってしまったので、next/third-parties
は使わず、引き続き<script>
を仕込むことにしました。
ただ、hooks(useEffect)
でページ切り替えイベントを送るのはもうやめようと思いました。
GA4
にブラウザの履歴イベントに基づくページの変更
ってやつがあるので、それにページ遷移イベントを送るのをお任せしようと思います。
今まではuseEffect
で送ってましたが、GA4
におまかせできるならお任せしようと思う。
Mastodon / Misskey のチェックマークが付くように rel に me を入れる
自分のWebページ
(静的サイトでいい)に、自分のMastodon / Misskey
のURL
を<a>
タグで追加して、
href
と共にrel="me"
を付与します。
そのあと、Mastodon / Misskey
の補足情報にサイトのURL
を入れると、チェックマークがつく機能があります。
そのrel="me"
を付与できるように修正しました。
これ、SPA
だと無理でSSG
とかSSR
みたいにHTML
をプリレンダリングした状態でホスティングしないといけないハズなので、ちょっと注意です。
https://takusan.negitoro.dev/posts/nuxt_universal/
Markdown から HTML の変換が何度も走って遅い
改修したかった一番の理由これ。
SSG
するGitHub Actions
が遅い。2024
年になってから?GitHub Actions
のマシンスペックが向上したらしく、ずいぶん早くなりましたが。それでも15分くらいかかってる。
(ちなみにスペックが上る前は30~40分くらいかかってた。小声。やばい)
理由はわかっていて、Markdown
からHTML
の変換が何度も何度も走っているから。
変換したらメモリに乗せておくとか、あると思うんですけど、ファイル更新の際にメモリに乗せた変換結果を破棄するのもめんどいなあ思ってやらなかった。
でもやります。やっぱ遅いんで。
シングルトン案
最初に考えたのはシングルトンにしてパース結果を使い回す案。
Next.js
のホットリードにシングルトンにしたインスタンスまで巻き込まれて、インスタンスが作り直されてしまうらしい。調べたらglobalThis
とかいうのに入れるといいらしい。
https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
これ合ってる?、これでいいのか怪しくなってきた。暫定対応感あるけど。
ちなみにこの技を使っても、constructor()
でconsole.log
してみると何故か2回出力される。シングルトンってなんだ?
Next.js のキャッシュ案
これやるならNext.js
のcache()
(どっちかと言うとunstable_cache
)とかを使うべきな気がしてきた...
この辺で言及しているやつ。。
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries
unstable_cache
の方だと、revalidateTag()
を使うことでunstable_cache
のキャッシュを消すことが出来るみたい。
cache()
の方はちょっと消し方見つからなかった。。。
というわけで仕込んでみました!
まずはキャッシュを保持しておくストアです。キャッシュがあれば返すやつ(なければパースする)と、消すやつ。
あんまりNext.js
に依存したコードを増やしたくなかったので今回はインターフェースを切りました。剥がしやすいようにしてみた。
それを仕込みました。
parseMarkdown
でマークダウンをパースする前にキャッシュが存在するか問い合わせて、あれば返す。無いならパースする。
また、ファイルの変更を監視して、変化があったらキャッシュから消すようにした。これで変更があった際には再度パースする様になっているはず。
キャッシュのキーはファイルパス。
これでページの表示も早くなる(マークダウンを毎回パースする手間が減る)、かつ、マークダウンに変更があったら再度パーサーにかかるようにキャッシュを消すようにしています。
これで勝った!!!と思ってたんですが。
Next.js
を静的書き出しモードで使っている場合は、たとえ開発モードでもrevalidateTag
が呼べない。
もちろん静的書き出しではバックエンド側の機能(ブラウザではなくNode.js
が必要な機能)は使えないので、revalidateTag
が使えないというのは分かるんですが、ちょっと期待していたので残念。
静的書き出し時に使えない機能はこちらです:https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features
Next.js のキャッシュ案2
よく見たらnext.config.js
、これ開発中・静的書き出し中それぞれ別に設定を変更できるらしい。
https://nextjs.org/docs/app/api-reference/next-config-js
これで、開発中はoutput: 'export'
を消すようにすればいいのでは・・・?
というわけで試してみた、動いてます!!!
パース結果を使い回すようにしたため、超速くなりました。
開発中も静的書き出し時も超高速です(てか今までが毎回パースしてたからくっっっそ遅かった)
あと、これもシングルトンのときと同じくwatchFolder
が何度も呼ばれてしまうため、やっぱりglobalThis
しないとだめかも。。。。
Next.js のキャッシュ案3
watchFolder
が何回も呼ばれちゃうので、globalThis
等で制御する必要があり、気持ち悪いというかそれならシングルトンで良かった。
あとはrevalidateTag
のためだけにnext.config.js
いじって開発時のみSSR
とかやりたくないので。。。
のと、unstable_cache
はどうやら(開発時だけかも)ブラウザのスーパーリロードでキャッシュが消えることが判明したので、
もう無理にキャッシュを消すためにrevalidateTag
なんかしないで、手元でスーパーリロードすればいいじゃん。ってなった。
このキャッシュやっかいなことに、これ開発サーバーを再起動してもキャッシュ(unstable_cache の結果)は残り続けるらしく、知らないとガチ沼にはまりそう。
というかキャッシュがスーパーリロードで消えるとかどこに書いてある?、、書いてないけどこれまぐれで動いてるんか?
まとめ
- シングルトン案
globalThis
とかいうオブジェクトに入れることで、開発時でもインスタンスを一つにできる
- でも試した限りなんか
constructor()
が2回呼ばれている(2ついる?)
unstable_cache
案
- マークダウンが変化したら
revalidateTag
を呼び出してキャッシュを消したいところだが、静的書き出しモードでは利用できない
unstable_cache
+ 開発時のみ サーバーサイドレンダリング
案
- 開発時のみ SSR にしているのが引っかかる
- ファイル監視用の関数がホットリードのたびに呼ばれるので、シングルトン同様制御が必要
unstable_cache
で変更したら自分でスーパーリロードする案
- 普通のリロードだとキャッシュが返ってくる
- でも開発時だけ
SSR
と比べると一番マシな気がする、、、
どれがベスト?今のところ一番最後かなあ、、、
でもマークダウンのパースする部分にNext.js
依存を持ち込むかと言われるとシングルトンが最有力になる。まあいいや。
あと調べると、マークダウンパース結果をメモリ(シングルトンで配列を持つ)ではなく、ストレージに書き込む案もありましたが、
Next.js
のキャッシュがまさにそれな気がする。自前で書くかNext.js
にお任せするか。
テーマ設定を localStorage に、あと useSyncExternalStore
テーマ設定をlocalStorage
で持つようにしました。
2回目開いた時にテーマ設定が引き継がれます。
また、localStorage
書き込みイベントを投げて、useSyncExternalStore
を使い、React
側でも購読できるようにしました。
https://ja.react.dev/reference/react/useSyncExternalStore
useSyncExternalStore + dispatchEvent
を使ってlocalStorage
の変更を通知できるようにすると、
地味に複数タブを開いたときもテーマ設定が反映されるのでちょっと感動。
そういえば、端末がダークモード設定でも初回時はライトテーマになる問題、<head>
にJavaScript
を差し込めば良さそうだけど、
Next.js
だと出来なさそう・・・?
FOUC 対策に<head>
に書いてねって書いてある。。。
https://tailwindcss.com/docs/dark-mode
スマホ用目次を Tailwind CSS で作る
とりあえず記事の一番上に。目次を開くコンポーネントを置きました。
なんとJavaScript
無しで再現できます。GitHub
のMarkdown
とかでも使えるハズ。
↓こんなの
次に、Tailwind CSS
で見た目を調整したい(展開ボタンとか付けたい)
というわけでこちら→ https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
どうやら、親要素の状態(例えばこの場合は展開した時に<details>
にopen
属性がつく)に応じて CSS を付ける方法があります。
group
を使う方法です。
展開アイコンも出したい?
これでどうだろう?
どうだろう!?!?。
React Server Component
だとJavaScript
をギリギリまで使いたくないと思うから便利だと思う!!!
コードブロックにコピーボタンを置いた
こんな感じに、コードブロックをマウスオーバーするとコピーボタンが出てくるようになりました。
欲しかったけどunified
のプラグインとか絶対難しそうで、useEffect
で動的に差し込むか~どうするかな~思ってたところで。
仕組みとしては、Markdown
にあるコードを色付けするrehype-pretty-code
は、shiki
で色付けしているわけですが、
これ任意のコードを差し込むAPI
が用意されているんですね。Transformers
ってやつ。
https://shiki.matsu.io/guide/transformers
というか今回はこれをほぼパクっただけです。
https://github.com/rehype-pretty/rehype-pretty-code/blob/master/packages/transformers/examples/copy-button.ts
transformShikiCodeBlockCopyButton.ts
がこちらです。
ボタン要素を作って、押した時にクリップボードにコピーされるようにします。
pre(node) { }
が<pre>
要素に対してDOM 操作
が出来るやつです。今回は<pre>
に<button>
を差し込んでいるので。<code>
要素をいじるcode(node) { }
とかもあります。
ここでもTailwind CSS
のgroup
が大活躍です。親のコードブロックがマウスオーバーしたら、ボタンが表示されるよう、親にはgroup
、ボタンにはhidden group-hover:flex
?を付けています。JavaScript
なしでここまで出来るんだ。
あ、もし私みたいにTailwind CSS
を使う場合、tailwind.config.js
で、transformShikiCodeBlockCopyButton.ts
もユーティリティ名走査対象に追加する必要があります。
もしくは./app
とかのjsx
があるフォルダ内に書いた場合はいらないです。私は./src
の中に書いたので走査対象じゃない。
作ったアプリコンポーネントを作り直した
メニューを押して見ないと何があるか分からないので、選択中以外の項目も下に表示するようにしてみた。
マルチカラムです。
OGP 画像
リンクを共有したときに表示される、あの画像。
OGP画像
とかOpenGraph Image
とか言われてる?
Next.js
ではhtml(JSX)
を組み立てる感覚で画像を作ることが出来ます。すごい時代ですね。
https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#generate-images-using-code-js-ts-tsx
というわけで使いたいのですが、静的書き出しモードで動かすにはひと手間必要みたい。
といってもRoute Handlers
としてOpenGraph Image
を生成するといいらしい。
https://github.com/vercel/next.js/issues/51147
Route Handlers 機能
これはpage.tsx
ならhtml
をレスポンスとして返しますが、
スマホ向けにjson
とかRSS
用にxml
とか、html
以外をレスポンスで返したいときが多々あると思います。
このような場合は、このRoute Handlers
機能を使うことで、html
以外を生成してレスポンスとして返してあげることが出来ます。
page.tsx
が静的書き出し出来るのと同じ用に、このRoute Handlers
機能もレスポンスを静的書き出し時に生成することが出来ます。
ただ、生成するためには条件があり、GET
リクエストののみ + リクエストのたびに同じデータを返すようなRoute Handlers
でなければいけません。
静的サイトなので、同じデータを配信することしか出来ない。逆に言えばこれさえ守ればxml
とかjson
とかも静的書き出し時に生成できるので面白いことが出来そう(今回のOGP 画像
がこれ)
何言ってるかよくわからない場合は公式見て:
https://nextjs.org/docs/app/building-your-application/deploying/static-exports#route-handlers
app/posts/[blog]/example.json/route.ts
レスポンスはこんな感じになります
Route Handlers で OGP 画像を生成する
app/posts/[blog]/opengraph-image.png/route.tsx
を作ります。
そして雑ですがこんな感じにしてみると。。。
こんな感じのOGP 画像
が出来ます。
結構良さそう。すげ~~~
使えるCSS
とかはこの辺が多分そう。
flex
で作っていけば良さそう。長い文字入れても乱れないかは確認しておいたほうが良さそう。
https://github.com/vercel/satori
というわけで作ってみました。
ちゃんと静的書き出し時に記事毎に生成されてますね!
これで共有したときイケてるサイトみたいに画像が出ます!
正規ルート?ではなくRoute Handlers
で作ったので自動では<head>
にog:image
を追加してくれません。
自分でURL
を指定して追加する必要があります。
メモ
画像はbase64
にしたあと<img>
に入れるとデータの取り出し?操作の回数が減るからおすすめとのこと。
https://github.com/vercel/satori?tab=readme-ov-file#images
フォントもBuffer
(バイナリ)のまま渡せば使える。
https://github.com/vercel/satori?tab=readme-ov-file#fonts
せっかくなのでコード貼ります
ファイルを読み出すユーティリティクラスを作ってみたけどわざわざ作るまででもないと言われれればそう。
静的サイトで検索機能
この手の検索機能、全文検索
とかいう名前がついているらしい。
静的サイト書き出しという性質上、よくあるブログ記事検索みたいなのはかなりめんどくさいんですよね。
静的書き出し時にサーバーに配置する(ユーザーに配信する)html とか
を生成するので、検索ワードに応じた検索結果のhtml
を作ることが出来ないんですよね、、
(それこそサーバーサイドレンダリング
するサーバーが必要)
というわけで、静的書き出し時でも検索機能を付けたい場合、思いつくのが、
検索用に全部の記事を一つにした.json
を作って、検索をしたときにjson
ファイルをリクエストして、クライアント側で検索ワードを元にフィルタリングしていく。
ただ、全部の記事を一つのJSON
にして配信すると、多分とんでもない通信量になってしまう。。。
全部詰め込んだJSON
(1MB
超え)配信するのは、、、その辺の小さくした画像よりもずっと大きいJSON
できついわ。
というわけで静的書き出しサイトだと検索機能付けるのは厳しそうに見えたのですが、pagefind
を見つけた。
https://pagefind.app/
転送量を抑えながら全文検索ができる模様。静的サイト向けの全文検索機能みたい!!
Next.js 向けの記事もあった
https://www.petemillspaugh.com/nextjs-search-with-pagefind
pagefind 導入
https://pagefind.app/docs/running-pagefind/
pagefind
をいれて、
静的書き出し後に、pagefind
の処理を追加します。
out
は静的書き出しフォルダです。dist
とかの場合もある?
pagefind 検索 UI
https://pagefind.app/docs/api/
pagefind
の標準検索UI
もありますが、Next.js
のルーティングで動くか怪しいので、今回は1から作ります。
(next/link
の画面遷移でもDOMContentLoaded
呼ばれる?)
というわけでまず検索するためのJavaScript
をインポートしたいのですが、
どうやら、npx pagefind
した後に生成されるpagefind.js
をロードする必要があるそう。。
何が難しいか言うと、静的書き出し時までpagefind.js
が存在しないんですよね。開発時は無い。
というわけで先駆け者さんのをまるまるパクった。
window
オブジェクト(static
にあたるやつ)にpagefind
を差し込む。ページ表示時に。
ついでに、開発時にも適当な値が帰ってきてほしいので、開発中のみpagefindInDevMock
に向けるようにしました。
pagefind.search()
で検索が出来ます。
各検索結果はasync data()
関数を呼び出すことでタイトルとか本文とかが取り出せます。
今回はPromise.all
で10件取り出してロードした状態でJSX
に渡してますが、ロード前の状態でJSX
に渡して、useEffect()
で表示されたらロードみたいなことも出来ると思います。
タイトルはmeta['title']
で取れるらしいです。html
の中から<h1>
を探してきてそれをタイトルとして使うみたいなので、複数<h1>
がある場合は注意ですね。
動くかまでは見てないけどこんな感じ。開発中はハードコートした値が帰ってきます。
あとpagefind
、型がないから自前で適当に作ってas
でキャストしてるけど正攻法なのかな、これ。
pagefind インデックス対象の調整
https://pagefind.app/docs/indexing/
おそらくそのままでは、本文以外の、投稿日時とかタグとかも検索結果に出てきてしまいます。
検索結果には本文だけでてきてほしいと思います。というわけでMarkdown
をhtml
にして表示している箇所にdata-pagefind-body
をつけました。
一点、注意点があり、data-pagefind-body
を付けると、ついていない画面は検索結果の対象にはなりません。
data-pagefind-body
を一回でも使った場合、検索結果に出てきて欲しいすべてのページで同様に付ける必要があります。
今回は記事本文だけ検索結果に表示されればいいので、data-pagefind-body
をブログ記事本文page.tsx
に付けて終わりです。
組み込んだソースコード
pagefind
検索コンポーネント。クライアント(Node.js
ではなくブラウザ側のJavaScript
)で動きます。
https://github.com/takusan23/ziyuutyou-next/blob/main/app/search/PagefindSearch.tsx
実際に動かしてみた結果
英語だけかと思ったら日本語も結構出てきて感動した。すごい。なんだこれ???
文字数カウントからコードブロックの分を消す
はい。
本番(意味深)に入れる
一応人がいなさそうな深夜とかに本番環境へ入れようと思います。。。
(そもそも見てる人おらんやろ)
今回もPR
を作りました。
https://github.com/takusan23/ziyuutyou-next/pull/3
Markdown
のパース回数が減ったので、かかる時間もかなり短くなった(てか本当に今まで長すぎた)
Windows Update
すら環境に配慮する時代やぞ
本番(意味深)のAmazon CloudFront
から見ていますがちゃんと反映されました。
2024/05/05 の午前3時くらいのことです。おはよう!朝4時に何してるんだい?
↑検索ボタンが出ていますねっ
終わりに
JavaScript
でクラスってあんまり使わないらしい?のであんまり気にならないのかもしれないけど、
this
を付けないといけないの、明らかに冗長だと思う。
!!!!
JavaScript
のクラスで思い出したんですけど、JavaScript
のクラスのコンストラクタってreturn
で値返せるらしいんですよね。。
(コンストラクタでreturn
したいことあるんかな?)
(てかnew
したクラスとは関係ない値が返せるってこと??)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/new#構文
https://effectivetypescript.com/2024/04/16/inferring-a-type-predicate/
終わりに2
JSX
の中でIIFE(即時実行関数式)
を使うのってアリなのかな。
Jetpack Compose
と違ってJSX
内ではif
が使えない(正確にはJS
のif
は文なので、値を返せない。三項演算子を使う)。
早期 return したいときにIIFE
を使ったけどどうなの?あり?
この程度ならlet component: ReactNode
でJSX
外で条件分岐すれば良い気もしてきた。。。let
が嫌ならこれ?
終わりに3
Google Analytics
のUA
がGA4
に取って代わったため、UA
がサービス終了になるわけですが、
7月1日
より前にUA
で集めたデータをダウンロードしておく必要があります。UA
の集計結果を見ることが出来なくなってしまいます。
https://support.google.com/analytics/answer/11583528?hl=ja#export
以上です。お疲れ様でした。8888888888