クライアント入稿されたHTMLに、一部サーバーサイドで動的に変更する要件を、Next で対応するチュートリアルです。
今回のプロジェクトの結果は Github で公開しています。
https://github.com/ytyng/my-customer-submission
デプロイしたサイト: https://my-customer-submission.ytyng.com/
npx create-next-app@latest --typescript
次のように選択していきます。
src/
directory?: Yescd my-customer-submission
npm run dev
src/templates
フォルダを作って、入稿されたHTMLを入れます。
CSS は Bootstrap CDN と手書きの CSS を使っているものとします。
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>My customer submission</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
<link rel="stylesheet" href="./css/site.css">
</head>
<body class="d-flex flex-column h-100">
<header class="site-header d-flex align-items-center">
<h1 class="flex-grow-1 m-0 p-2">My customer submission</h1>
<div>
<button class="btn btn-primary m-2" id="register-button">Register</button>
</div>
</header>
<div id="center-row" class="flex-grow-1 d-flex">
<nav class="site-nav p-2">
nav
</nav>
<main class="flex-grow-1">
<div id="main-content" class="p-3">
main content
</div>
</main>
</div>
<footer class="site-footer p-2">
Site footer
</footer>
<script src="./js/site.js"></script>
</body>
</html>
.site-header {
background-color: #2b538f;
color: white;
}
.site-nav {
background-color: #eee;
width: 300px;
}
.site-footer {
background-color: #333;
color: white;
}
document.querySelector('#register-button').addEventListener('click', function() {
alert('Hello World!');
})
入稿 HTML ファイルから、 body
の中身だけを抜き出して React のコンポーネントにし、id="main-content"
のHTMLエレメントを別の React エレメントに差し替えます。
入稿 HTML ファイルには head
タグがありますが、今回はこれは使わずに独自で head の内容を作ります。
head の内容をパースして使う方法もあると思いますが、今回は行わず、例えば BootStrap を CDN から読み込んでいる箇所は、コピペで ``` タグを作ります。
ブラウザJSがあります。右上の「Register」ボタンを押した時のアクションが登録されています。 これは public 内にそのままコピーしてクライアントに返します。
HTMLをパースして使うにあたり、html-react-parser
を使います。
npm install html-react-parser -D
JSONPlaceholder の posts を取得して、 Bootstrap の Card コンポーネントで表示する React コンポーネントを作ります。
export interface PostData {
userId: number
id: number
title: string
body: string
}
import {PostData} from '@/app/interfaces/posts'
/**
* ポストのカード1枚を表すコンポーネント
*/
export default async function Component({postData}: {postData: PostData}) {
return (
<div className={'card my-3 mx-3'}>
<div className={'card-header'}>
#{postData.id} {postData.title}
</div>
<div className={'card-body p-2'}>
<div>{postData.body}</div>
</div>
</div>
)
}
import {PostData} from '@/app/interfaces/posts'
import PostCard from './PostCard'
async function getPosts(): Promise<PostData[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
return await res.json()
}
/**
* ポストのカード複数枚を fetch してから表示するコンポーネント
*/
export default async function Component() {
const posts = await getPosts()
return posts.map((post: any) => (
<PostCard postData={post} key={post.id}/>
));
}
layout.tsx
で html
, head
, body
の HTML エレメントを返すようにします。
テンプレートの css はここで import しています。
今回は、link タグなどは 入稿HTMLからパースせず、tsx にハードコーディングしました。
head
内の link
タグや body
のクラス名は、二重管理になってしまっていますのであまり良くありませんが、
どのような方針が良いかは入稿 HTML の修正頻度やプロジェクトの方針次第になると思います。
import type { Metadata } from "next";
import "../templates/css/site.css"
export const metadata: Metadata = {
title: "My customer submission",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja" className={'h-100'}>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossOrigin="anonymous"/>
</head>
<body className={'d-flex flex-column h-100'}>{children}</body>
</html>
);
}
入稿HTML を読み込み、正規表現で body
の中だけを抽出して html-react-parser でパースします。
そして、 <div id="main-content"></div>
を、PostCards
コンポーネントに差し替えます。
'use server'
import fs from 'fs'
import parse from 'html-react-parser'
import PostCards from '@/app/components/PostCards'
/**
* ファイルの内容をテキストとして取得
*/
async function loadHtmlFile(filePath: string) {
return fs.promises.readFile(filePath, 'utf8')
}
/**
* HTMLのbodyタグの中身を取得
*/
function extractBodyContent(html: string) : string {
const match = /<body[^>]*?>([\s\S]*)<\/body>/.exec(html)
return match ? match[1] : html
}
/**
* テンプレート内の <div id="main-content"> を PostCards に置き換える
*/
function replaceElement(domNode: any, index: number) {
if (domNode.type === "tag" && domNode.name === "div" && domNode.attribs.id === "main-content") {
return (
<PostCards />
)
}
}
/**
* テンプレートHTML の Body を JSX.Element として取得
*/
async function getInnerBodyElement() {
const htmlTemplatePath = "src/templates/index.html"
const htmlTemplate = await loadHtmlFile(htmlTemplatePath)
const innerBodyHTML = extractBodyContent(htmlTemplate)
return parse(innerBodyHTML, {replace: replaceElement})
}
export default async function Home() {
return await getInnerBodyElement()
}
入稿された ブラウザJSは、public/js/
内にコピーします。
できました。
デプロイしたサイト: https://my-customer-submission.ytyng.com/
生成されたHTMLソースを見ると、SSRできていることが確認できます。
コメント