Next で、入稿された生HTMLテンプレートをSSRで動的に機能追加して出力する

React
2024-06-16 18:28 (5週間前) ytyng
View in English

クライアント入稿されたHTMLに、一部サーバーサイドで動的に変更する要件を、Next で対応するチュートリアルです。

今回のプロジェクトの結果は Github で公開しています。

https://github.com/ytyng/my-customer-submission

デプロイしたサイト: https://my-customer-submission.ytyng.com/

Next の環境構築

npx create-next-app@latest --typescript

次のように選択していきます。

画像

動作確認

cd my-customer-submission
npm run dev

画像

入稿HTML の追加

src/templates フォルダを作って、入稿されたHTMLを入れます。

CSS は Bootstrap CDN と手書きの CSS を使っているものとします。

src/templates/index.html

<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>

src/templates/css/site.css

.site-header {
  background-color: #2b538f;
  color: white;
}

.site-nav {
  background-color: #eee;
  width: 300px;
}

.site-footer {
  background-color: #333;
  color: white;
}

src/templates/js/site.js

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-react-parser のインストール

HTMLをパースして使うにあたり、html-react-parser を使います。

npm install html-react-parser -D

コンポーネントの追加

JSONPlaceholderposts を取得して、 Bootstrap の Card コンポーネントで表示する React コンポーネントを作ります。

src/app/interfaces/posts.ts

export interface PostData {
  userId: number
  id: number
  title: string
  body: string
}

src/app/components/PostCard.tsx

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>
  )
}

src/app/components/PostCards.tsx

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 の修正

layout.tsxhtml, head, body の HTML エレメントを返すようにします。 テンプレートの css はここで import しています。

今回は、link タグなどは 入稿HTMLからパースせず、tsx にハードコーディングしました。

head 内の link タグや body のクラス名は、二重管理になってしまっていますのであまり良くありませんが、 どのような方針が良いかは入稿 HTML の修正頻度やプロジェクトの方針次第になると思います。

src/app/layout.tsx

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>
  );
}

page.tsx の修正

入稿HTML を読み込み、正規表現で body の中だけを抽出して html-react-parser でパースします。

そして、 <div id="main-content"></div> を、PostCards コンポーネントに差し替えます。

src/app/page.tsx

'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ファイルのコピー

入稿された ブラウザJSは、public/js/ 内にコピーします。

動作確認

できました。

画像

デプロイしたサイト: https://my-customer-submission.ytyng.com/

生成されたHTMLソースを見ると、SSRできていることが確認できます。

画像

現在未評価
タイトルとURLをコピー

コメント

アーカイブ

2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
2013
2012
2011