Sometimes there is a need to generate HTML files in Node.js - for example to create a report or HTML e-mail. This can be done with string concatenation and template literals, but it's not very convenient. Wouldn't it be great if could just use React to generate HTML? Let's see how to do that.
In this tutorial I will use TypeScript, but it's not required. I will also use npm, but other package managers can be used as well.
Setting up the project
We can use an existing project or create a new one. To create a new project, we can use npm init
. We will be using TypeScript, so let's run npm install typescript @types/node
. The next step is to create a tsconfig.json
file. This can be done by running npx tsc --init
. One of the options to change is "jsx": "react-jsx"
. Here's how my tsconfig.json
looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"compilerOptions": {
"incremental": true,
"target": "es2022",
"jsx": "react-jsx",
"module": "commonjs",
"rootDir": "./src/",
"baseUrl": "./src/",
"outDir": "./dist",
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"skipLibCheck": true,
},
}
Rendering React to HTML
The first thing we need to do is to install React and ReactDOM. We can do that by running npm i react react-dom @types/react @types/react-dom
. Now we need something to render. Let's create a few components:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export interface PageProps extends React.PropsWithChildren {
title: string;
}
export function Page(props: PageProps) {
return (
<html>
<head>
<title>{props.title}</title>
<meta charSet="utf-8" />
</head>
<body>
{props.children}
</body>
</html>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import * as types from "../Report";
import { ReportEntry } from "./ReportEntry";
export interface ReportProps {
report: types.Report;
}
export function Report(props: ReportProps) {
return (
<div>
<h1>Report: {props.report.title}</h1>
<p>Created: {new Date(props.report.createdTimestamp).toISOString()}</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{props.report.entries.map((entry) => (
<ReportEntry reportEntry={entry} key={entry.id} />
))}
</tbody>
</table>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as types from "../Report";
export interface ReportEntryProps {
reportEntry: types.ReportEntry;
}
export function ReportEntry(props: ReportEntryProps) {
return (
<tr>
<td>{props.reportEntry.name}</td>
<td>{props.reportEntry.status}</td>
<td>{props.reportEntry.message ?? ""}</td>
</tr>
);
}
We also need to create types for reports:
1
2
3
4
5
6
7
8
9
10
11
12
export interface Report {
createdTimestamp: number;
title: string;
entries: ReportEntry[];
}
export interface ReportEntry {
id: string;
name: string;
status: "ok" | "warning" | "error";
message?: string;
}
Now it's time to create a sample report and render it to HTML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { Page } from "./components/Page";
import { ReactRenderer } from "./ReactRenderer";
import * as types from "./Report";
import { Report } from "./components/Report";
const report: types.Report = {
createdTimestamp: 1604589425123,
title: "Hardware report",
entries: [
{
id: "webServer",
name: "Web Server",
status: "ok",
},
{
id: "oracleServer",
name: "Oracle Server",
status: "warning",
message: "Low disk space",
},
{
id: "backupServer1",
name: "Backup Server 1",
status: "ok",
message: "Last backup: 2020-11-05 12:00:00",
},
{
id: "backupServer2",
name: "Backup Server 2",
status: "error",
message: "Server not responding",
},
],
};
const renderer = new ReactRenderer();
const html = renderer.renderNodeToHtml(
<Page title="Report">
<Report report={report} />
</Page>
);
console.log(html);
The last (and the most important) step is to create ReactRenderer
. We can use renderToStaticMarkup
from react-dom/server
to render React to HTML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import type React from "react";
import { renderToStaticMarkup } from "react-dom/server";
export class ReactRenderer {
renderNodeToHtml(reactNode: React.ReactNode): string {
const html = renderToStaticMarkup(
<>
{reactNode}
</>
);
return html;
}
}
Now we can run src/main.tsx
; the result is:
1
<html><head><title>Report</title><meta charSet="utf-8"/></head><body><div><h1>Report: Hardware report</h1><p>Created: 2020-11-05T15:17:05.123Z</p><table><thead><tr><th>Name</th><th>Status</th><th>Message</th></tr></thead><tbody><tr><td>Web Server</td><td>ok</td><td></td></tr><tr><td>Oracle Server</td><td>warning</td><td>Low disk space</td></tr><tr><td>Backup Server 1</td><td>ok</td><td>Last backup: 2020-11-05 12:00:00</td></tr><tr><td>Backup Server 2</td><td>error</td><td>Server not responding</td></tr></tbody></table></div></body></html>
As we can see, the HTML is generated correctly. But what if we want to make it more readable?
Beautifying generated HTML
One of many ways to beautify HTML is to use js-beautify
. Let's install it by running npm i js-beautify @types/js-beautify
. Now we can update our ReactRenderer
, run src/main.tsx
and check the result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jsBeautify from "js-beautify";
import type React from "react";
import { renderToStaticMarkup } from "react-dom/server";
export class ReactRenderer {
renderNodeToHtml(reactNode: React.ReactNode): string {
const html = renderToStaticMarkup(
<>
{reactNode}
</>
);
return this.beautifyHtml(html);
}
private beautifyHtml(rawHtml: string): string {
const beautifiedHtml = jsBeautify.html(rawHtml, {
indent_inner_html: true,
indent_empty_lines: true,
end_with_newline: true,
extra_liners: [],
});
return beautifiedHtml;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<html>
<head>
<title>Report</title>
<meta charSet="utf-8" />
</head>
<body>
<div>
<h1>Report: Hardware report</h1>
<p>Created: 2020-11-05T15:17:05.123Z</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web Server</td>
<td>ok</td>
<td></td>
</tr>
<tr>
<td>Oracle Server</td>
<td>warning</td>
<td>Low disk space</td>
</tr>
<tr>
<td>Backup Server 1</td>
<td>ok</td>
<td>Last backup: 2020-11-05 12:00:00</td>
</tr>
<tr>
<td>Backup Server 2</td>
<td>error</td>
<td>Server not responding</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
Adding @emotion/styled
What if we wanted to add some styles to our report? We could use style
attribute, but it's not very convenient. Let's use @emotion/styled
instead. We will also keep our styles in the same HTML file to keep it portable. Let's start by installing required packages: npm i @emotion/styled @emotion/react @emotion/server
.
Now it's time to add some styles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import styled from "@emotion/styled";
import * as types from "../Report";
import { ReportEntry } from "./ReportEntry";
export interface ReportProps {
report: types.Report;
}
export function Report(props: ReportProps) {
return (
<div>
<h1>Report: {props.report.title}</h1>
<p>Created: {new Date(props.report.createdTimestamp).toISOString()}</p>
<Table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{props.report.entries.map((entry) => (
<ReportEntry reportEntry={entry} key={entry.id} />
))}
</tbody>
</Table>
</div>
);
}
const Table = styled.table`
border-collapse: collapse;
border: 1px solid #ccc;
td, th {
padding: 5px 10px;
}
`;
We are going to generate HTML and then we are going to use EmotionServer
to extract styles. Finally, we are going to inject styles into <head>
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import createEmotionServer from "@emotion/server/create-instance";
import type { EmotionServer } from "@emotion/server/types/create-instance";
import type { EmotionCache } from "@emotion/utils";
import jsBeautify from "js-beautify";
import type React from "react";
import { renderToStaticMarkup } from "react-dom/server";
export class ReactRenderer {
renderNodeToHtml(reactNode: React.ReactNode): string {
const { emotionServer, cache } = this.createEmotionServerAndCache();
const unstyledHtml = renderToStaticMarkup(
<CacheProvider value={cache}>
{reactNode}
</CacheProvider>
);
const styledHtml = this.injectStylesIntoHead(unstyledHtml, emotionServer);
return this.beautifyHtml(styledHtml);
}
private createEmotionServerAndCache(): { emotionServer: EmotionServer; cache: EmotionCache } {
const cache = createCache({ key: "app" });
const emotionServer = createEmotionServer(cache);
return { emotionServer, cache };
}
private injectStylesIntoHead(unstyledHtml: string, emotionServer: EmotionServer): string {
const chunks = emotionServer.extractCriticalToChunks(unstyledHtml);
const styles = emotionServer.constructStyleTagsFromChunks(chunks);
if (!unstyledHtml.includes("</head>")) {
throw new Error("Missing "</head>" string in the html code.");
}
const styledHtml = unstyledHtml.replace("</head>", `${styles}</head>`);
return styledHtml;
}
private beautifyHtml(rawHtml: string): string {
const beautifiedHtml = jsBeautify.html(rawHtml, {
indent_inner_html: true,
indent_empty_lines: true,
end_with_newline: true,
extra_liners: [],
});
return beautifiedHtml;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<html>
<head>
<title>Report</title>
<meta charSet="utf-8" />
<style data-emotion="app p9leoo">
.app-p9leoo {
border-collapse: collapse;
border: 1px solid #ccc;
}
.app-p9leoo td,
.app-p9leoo th {
padding: 5px 10px;
}
</style>
</head>
<body>
<div>
<h1>Report: Hardware report</h1>
<p>Created: 2020-11-05T15:17:05.123Z</p>
<table class="app-p9leoo">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web Server</td>
<td>ok</td>
<td></td>
</tr>
<tr>
<td>Oracle Server</td>
<td>warning</td>
<td>Low disk space</td>
</tr>
<tr>
<td>Backup Server 1</td>
<td>ok</td>
<td>Last backup: 2020-11-05 12:00:00</td>
</tr>
<tr>
<td>Backup Server 2</td>
<td>error</td>
<td>Server not responding</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
As we can see, the styles are inside the <head>
tag and our <table>
has a class
attribute. Classes are prefixed with app-
because we used key: "app"
when creating cache in createEmotionServerAndCache()
. Of course we can use a different key/prefix.
Conclusion
It's very easy to generate HTML files in Node.js with React. We can use this approach to generate reports, emails, etc. It's also very easy to add styles to our HTML and beautify the result to make it more readable/editable.