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/
npx create-next-app@latest --typescript
Make the following selections:
src/
directory?: Yescd my-customer-submission
npm run dev
Create a src/templates
folder and place the submitted HTML in it.
Assume that the CSS uses Bootstrap CDN and handwritten 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!');
})
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.
To parse and use HTML, we will use html-react-parser
.
npm install html-react-parser -D
Fetch posts from JSONPlaceholder and create a React component that displays them using Bootstrap's Card component.
export interface PostData {
userId: number
id: number
title: string
body: string
}
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>
)
}
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}/>
));
}
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.
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>
);
}
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.
'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()
}
Copy the submitted browser JavaScript into public/js/
.
It works.
Deployed site: https://my-customer-submission.ytyng.com/
When viewing the generated HTML source, you can confirm that it is server-side rendered (SSR).
Comments