Adding Dynamic Functionality to Submitted Raw HTML Templates with SSR in Next.js

React
2024-06-16 18:28 (6 months ago) ytyng

This is a tutorial on how to handle the requirement of dynamically changing parts of the client-submitted HTML on the server side using Next.js.

The results of this project are published on Github:

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

Deployed site: https://my-customer-submission.ytyng.com/

Setting Up the Next.js Environment

npx create-next-app@latest --typescript

Make the following selections:

Image

Running the App

cd my-customer-submission
npm run dev

Image

Adding the Submitted HTML

Create a src/templates folder and place the submitted HTML in it.

Assume that the CSS uses Bootstrap CDN and handwritten 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!');
})

Image

Strategy

Extract only the contents of the body from the submitted HTML file and convert it into a React component. Then, replace the HTML element with id="main-content" with another React element.

Although the submitted HTML file contains head tags, we will not use them this time, and instead, create the head contents ourselves. While there might be ways to parse and use the contents of the head, we will skip that for now and manually create the <link> tags, for example, to load Bootstrap from the CDN.

There is browser JavaScript. The action of pressing the "Register" button on the top right is registered. This will be copied as is into the public directory and returned to the client.

Preparing Libraries

Installing html-react-parser

To parse and use HTML, we will use html-react-parser.

npm install html-react-parser -D

Adding Components

Fetch posts from JSONPlaceholder and create a React component that displays them using Bootstrap's Card component.

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'

/**
 * Component representing a single post card
 */
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()
}

/**
 * Component that fetches and displays multiple post cards
 */
export default async function Component() {
  const posts = await getPosts()
  return posts.map((post: any) => (
    <PostCard postData={post} key={post.id}/>
  ));
}

Updating layout.tsx

In layout.tsx, return the html, head, and body HTML elements. Import the template's CSS here.

This time, the link tags, etc., were hardcoded in the tsx instead of parsing them from the submitted HTML.

While it is not ideal to have duplicated management of the link tags and the class names in the body, what approach is best depends on the frequency of modifications to the submitted HTML and the project's policies.

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

Updating page.tsx

Load the submitted HTML, extract only the contents of the body using a regular expression, and parse it with html-react-parser.

Then, replace the <div id="main-content"></div> with the PostCards component.

src/app/page.tsx

'use server'

import fs from 'fs'
import parse from 'html-react-parser'
import PostCards from '@/app/components/PostCards'

/**
 * Get the contents of a file as text
 */
async function loadHtmlFile(filePath: string) {
  return fs.promises.readFile(filePath, 'utf8')
}

/**
 * Extract the contents of the HTML body tag
 */
function extractBodyContent(html: string) : string {
  const match = /<body[^>]*?>([\s\S]*)<\/body>/.exec(html)
  return match ? match[1] : html
}

/**
 * Replace the <div id="main-content"> within the template with PostCards
 */
function replaceElement(domNode: any, index: number) {
  if (domNode.type === "tag" && domNode.name === "div" && domNode.attribs.id === "main-content") {
    return (
      <PostCards />
    )
  }
}

/**
 * Get the Body of the template HTML as a 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()
}

Copying JS Files

Copy the submitted browser JavaScript into public/js/.

Running the App

It works.

Image

Deployed site: https://my-customer-submission.ytyng.com/

When viewing the generated HTML source, you can confirm that it is server-side rendered (SSR).

Image

Currently unrated

Comments

Archive

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