Slide 1

Slide 1 text

React Server Componentで 生のHTMLを扱う技術 id:nakataki / @nakataki17 2025/02/25 Hatena Engineer Seminar #32 1

Slide 2

Slide 2 text

自己紹介 id:nakataki (@nakataki17) 株式会社はてな 23年新卒入社 → ブログチーム Webアプリケーションエンジニア 得意分野は React + Next.js 副業で謎解きクリエイターもやっています 2

Slide 3

Slide 3 text

はてなCMS ノーコードでカンタンにWebサイトの構築・公開・運用ができるCMS Next.js製 2024/2/3 リリース はてなCMSのLPも、はてなCMS製です! 3

Slide 4

Slide 4 text

LPもはてなCMS製! 4

Slide 5

Slide 5 text

プロダクトのバックボーン Unlayer はてなCMSが採用した、プロプライエタリなノーコードエディタ React ComponentとしてのAPIも提供するので要望に合致 ページを作ると、HTMLとCSSを生成 大事なのは、これが生のHTMLとCSSを出力するということ これをどうやってNext.jsでSSRしようか? 5

Slide 6

Slide 6 text

はてなCMSのしくみ ページ管理とページのレンダリングを同一 のNext.jsアプリケーションで実施 ノーコードエディタが出力するHTMLに CMSならではの設定項目を反映して、 React Server Componentに変換して Server-Side Rendering 6

Slide 7

Slide 7 text

こういう流れになる 7

Slide 8

Slide 8 text

CMSならではの設定項目 ざっくり3つ ブログの記事埋め込み カスタム 自由HTML埋め込み これらをApp Routerで各個撃破! 8

Slide 9

Slide 9 text

各ステップに対応する 9

Slide 10

Slide 10 text

ブログの記事埋め込み 10

Slide 11

Slide 11 text

ブログの記事埋め込み レンダリング時にブログ本体と通信して記事データをもらってくる 単一のReact Componentとして開発 では、HTMLの一部をどうやってReact Componentにするのか? 11

Slide 12

Slide 12 text

ブログの記事埋め込み 2 まず、Unlayerを拡張してこういうHTMLを吐き出してもらう(例)
これをReactコンポーネントに変換 Unlayer(HTML) → JSDOM(パース) → html-react-parser(Component生成) 12

Slide 13

Slide 13 text

html-react-parser 文字列からReactコンポーネントを組み立ててくれる。 import parse from "html-react-parser"; const Hello = parse("
Hello, World!
"); ↑同じ!↓ const Hello =
Hello,World!
; 13

Slide 14

Slide 14 text

html-react-parser 2 操作もできる(コードはnpmを参照) parse('

text

', { replace(domNode) { if (domNode.attribs && domNode.attribs.id === "replace") { return replaced; } }, }); 14

Slide 15

Slide 15 text

ブログの記事埋め込み 3 できたReactコンポーネントをServer Componentとしてレンダリング Next.js App Routerならではポイント 記事データの取得もサーバ側で行う(= キャッシュが効く) 15

Slide 16

Slide 16 text

余談: App Routerの強み 今回の要件: HTMLからReact Componentを生成して配信したい 16

Slide 17

Slide 17 text

余談: App Routerの強み Pages Routerを使うと… getServerSideProps ではJSONにSerializeできるものしか返せない JSX Element(Reactコンポーネント)は渡せない つまり、Componentの生成を Page() 以下でやるしかない そして Page() の処理は、クライアントにコードが露出してしまう セキュリティ、パフォーマンスの両面で懸念 17

Slide 18

Slide 18 text

余談: App Routerの強み 2 App Routerだと… React Server Compoentを生成できる Component生成のロジックは隠蔽される つまり… React Componentを生成してSSRしたかったら、App Routerを使おう! 18

Slide 19

Slide 19 text

カスタム 19

Slide 20

Slide 20 text

カスタム App Routerでは、もはやを触らせてくれない。 なんてない 各種メタデータを generateMetadata() 関数でエクスポート export function generateMetadata(): Metadata { return { title: "Hello, World!", description: "Hatena Engineer Seminar #32", }; } ユーザが入力した文字列から Metadata オブジェクトを組み立てるのは 非現実的… 20

Slide 21

Slide 21 text

カスタム 2 React 19の新機能を使おう React がこのコンポーネントをレンダーする際、 , <link> , <meta> タグを認識し、自動的にドキュメントの <head> セクションに移動させます。 React v19 – React Reactから直接ドキュメントにメタデータを追加できるようになっている! 21

Slide 22

Slide 22 text

カスタム 3 Easy (簡略化しています) document.querySelectorAll("script, meta, link").forEach((elem, index) => { if (elem.tagName.toLowerCase() === "style") { elem.setAttribute("precedence", "high"); elem.setAttribute("href", `user_style_${index}`); } if (elem.tagName.toLowerCase() === "link") { elem.setAttribute("precedence", "high"); } headContent += elem.outerHTML; }); 22

Slide 23

Slide 23 text

余談: styleとlink と <link> は… precedence 属性(React独自)をつけないと並び替えてくれない headに挿入する際に、どこに挿入するのか優先度 ( precedence ) を指定する必要がある 23

Slide 24

Slide 24 text

自由HTML埋め込み 24

Slide 25

Slide 25 text

自由HTML埋め込み ユーザ入力のHTML を Next/Script に置き換えて生成 → レンダリング時に展開 してやればよさそう…? 25

Slide 26

Slide 26 text

ここで問題発生 26

Slide 27

Slide 27 text

LPの資料請求フォームが出ない! 27

Slide 28

Slide 28 text

LPの資料請求フォームが出ない! 28

Slide 29

Slide 29 text

自由HTML埋め込み: 落とし穴 LPの資料請求フォームが出ない! でBIツールを埋め込んでいた このツールは、実行時にまず <div> を生成して、そこにFormを展開す る挙動をする 29

Slide 30

Slide 30 text

自由HTML埋め込み: 調べる Twitterカード なども動かない DOMをその場に生成する が動いていなさそう Hydration Error が起きていたのが原因 30

Slide 31

Slide 31 text

Hydration Errorとは? Next.js はサーバ側でHTMLを作成し、クライアントへ送る クライアントのHTML構造がサーバで準備したものと異なるとエラーに なる 今回はクライアント側でフォームやTwitterカードの挿入が発生し、 HTML構造が変わったため発生 31

Slide 32

Slide 32 text

今回のケースではこう 32

Slide 33

Slide 33 text

自由HTML埋め込み: 直す const isServerSide = typeof window === "undefined"; export function SSROnlyBlock({ html }: { html: string }) { return (
); } Next.jsは dangerouslySetInnerHTML で指定したScriptをHydrationの 対象にしない suppressHydrationWarning でエラーを回避 33

Slide 34

Slide 34 text

余談: dangerous すぎないか? はてなブログ/CMSは、その特性上、ユーザが任意のJSを実行できる。 そういう意味で、 dangerous であることは避けられない そこで記事編集と閲覧/プレビューをドメインごと分離し、JSの実行は 閲覧用ドメインに限るようにしている hatena.ne.jp = 記事編集画面 hatena.com(など) = 記事閲覧画面 記事編集画面での任意コード実行を防ぐ 詳しくはnanto_viさんの発表にて 34

Slide 35

Slide 35 text

解決まとめ ブログの記事埋め込み → JSDOM + html-react-parser → React 19の新機能(巻き上げ) 自由HTML埋め込み → suppressHydrationWarning 35

Slide 36

Slide 36 text

まとめ 最新のWebフレームワークを使って、CMSの未来を切り開いていきます 36