BlogReactJS

Creating a static website with ReactJS and renderToStaticMarkup()

Written by Codemzy on November 17th, 2021

Do you want to build a static website without learning another javascript framework? If you already know ReactJS, you can use it to create your static HTML content. It's quick and easy, and you won't even need to configure Webpack to do it! Use renderToStaticMarkup().

Static websites are safe, speedy and reliable. And static sites aren't just for small one page websites either. Massive companies like Nike, and huge websites like Smashing Magazine use static sites.

If you're providing mostly static content, like a marketing website or a blog, or even product pages on an e-commerce store, you might want to build a static site.

Why waste money running a server when you can stick your HTML files on a CDN and have them delivered at speed to your visitors?

Let's build a static site!

Why would you need a framework?

So you’re creating static content, and you know how to write HTML. Why do you need a framework or library for that? You can just… write the HTML, right?

Yep, you can do that. For a single landing page, that makes sense. And for a few pages, it’s easy enough. But what about when you add more content? More blog posts, more landing pages, or more products?

There are going to be certain common elements that you repeat on each page. Like the navigation menu at the top, and the footer. Oh and don’t forget the copyright notice!

That’s a lot of duplicate code. Never a good sign.

Now… say your a few months or years in. Your website has taken off, and you've added hundreds of pages. You add a new category and you need to update the footer. Or it’s a new year and you need to update the date on your copyright.

Do you really want to go through the hundreds of pages on your highly successful website, updating the same thing on each one? No!

That's going to take... a long time.

Template engines to the rescue

This is the problem that template engines fix. Some popular JavaScript template engines include Pug, EJS, and Handlebars.

These are mostly used to generate content on the fly. Someone hits a route on your NodeJS server and it uses the template engine to compile the HTML and return it to the user.

If your content is highly dynamic and each user gets unique content back, that makes sense. But if every user on that route gets the same HTML, it's better to build it once than build the same thing on each and every request.

The good news is, since these template engines generate HTML, you can also use the output to create static content. Instead of creating the HTML on-demand and returning it in response to the HTTP request, you can write it to a static file.

The bad news is, you need to learn another framework. Because it doesn't really look like HTML. Here's some Pug.

doctype html
html
  include includes/header.pug
  body
    include includes/nav.pug
    h1(class="text-6xl") My Site
    p Welcome to my website.
    include includes/footer.pug

I’ve used EJS before it looks the most like HTML. The same code as the Pug example above could look like this:

<!DOCTYPE html>
<html lang="en">
     <%- include('../partials/header'); %>
    <body>
        <%- include('../partials/nav'); %>
        <h1 class="text-6xl">My Site</h1>
        <p>Welcome to my website.</p>
        <%- include('../partials/footer'); %>
    </body>
</html>

I've been really happy with EJS, but there were a few drawbacks.

  • You still need to learn how to pass variables and partials around
  • Can be difficult to read once you start adding if/else statements
  • Doesn't support blocks (default/ replaceable content)
  • Extra package needed for layouts

As my static site got bigger, the code started getting a little tricky to read, mostly because I'm used to working with ReactJS and switching between the two was hard.

Consider this example in EJS:

<% if (user) %> <h2><%= user.name %></h2><% } else { %><%- include('user/login' %><% } %>

Against the same logic in ReactJS:

{ user ? <h2>{user.name}</h2> : <LoginLink /> }

I find the ReactJS code easier to write and quicker to read.

Using ReactJS to generate static content

I’m used to working with ReactJS, and I want to make my life easier. If you already know ReactJS and JSX, then you already know how to put HTML together using components. Why learn another framework?

You don’t have to!

First of all, you don't even have to build any of this yourself.

You can use frameworks like NextJS and GatsbyJS to write your static site with ReactJS. They are both popular, fully-featured frameworks, and pretty awesome.

However, you're still going to need to spend some time in the docs learning these frameworks if they are new to you. It's well worth the investment if you're starting a new project, but to spin up something quickly, or to keep your site really lightweight, maybe there's a simpler way?

Did you know ReactJS can also create static content out of the box? And you don’t even need to configure Webpack to use it.

Instead, you can use a function provided by ReactJS called ReactDOMServer.renderToStaticMarkup().

Similar to renderToString, except this doesn’t create extra DOM attributes that React uses internally, such as data-reactroot. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save some bytes.

- React renderToStaticMarkup()

How to use ReactDOMServer.renderToStaticMarkup()

Like with any other use of React, you're going to want to install it. You can use the CDN to use ReactJS, but since we are building a static site here, your build process happens before you hit the browser.

For your React apps and using ReactJS in the browser as a single page app (SPA), you would install react with react-dom. And you will install these same two packages for building your static website.

npm install react react-dom --save-dev

And while you don't need to configure Webpack, you will need babel so that your build process can understand ReactJS.

npm install @babel/core @babel/register @babel/preset-env @babel/preset-react --save-dev

And configure babel in the root of your project in a .babelrc file.

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

Now you can go ahead and write your ReactJS components, just like you normally would. For example, here's what your main page template might look like.

import React from 'react';
import Nav from './Nav';
import Footer from './Footer';

function Main({ title, description, ...props }) {
    return (
        <html lang="en">
            <head>
                <title>{ title }</title>
                <meta charSet="utf-8" />
                <meta name="description" content={ description } />
                <meta name='viewport' content='initial-scale=1.0, width=device-width' />
                <link href="/css/styles.min.css" rel="stylesheet" />
                { props.head }
            </head>
            <body>
                <Nav />
                { props.children }
                <Footer />
            </body>
        </html>
    );
};

export default Main;

And then you can use that for creating your pages. For example, your homepage at /pages/Index.js might look like this.

import React from 'react';
import Main from '../Main';

function Index(props) {
    return (
        <Main {...props}>
            <h1>My Site</h1>
            <p>Welcome to my website.</p>
        </Main>
    );
};

export default Index;

Pretty boring, but you get the idea!

Once you have created your page components, it's time to build them as static content, and that's where ReactDOMServer.renderToStaticMarkup() comes in handy. We can get ReactDOMServer from the react-dom package we installed earlier.

The ReactDOMServer object enables you to render components to static markup. Typically, it’s used on a Node server.

- ReactJS ReactDOMServer

You need to pass a ReactJS component to ReactDOMServer.renderToStaticMarkup(). Create a file called pages.js where you can import your pages components, and here you'll also pass title and description props for the head info. I kind of view this like a router file, as it is where I decide all of the static routes for my pages. and return the static content.

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { buildPage } from './build';

// get the component
import Home from '../components/pages/Index';
import Contact from '../components/pages/Contact';

// build the page
buildPage('index', ReactDOMServer.renderToStaticMarkup(<Home title="My Website" description="Welcome to my website, it's static and splendid!" />));
buildPage('contact', ReactDOMServer.renderToStaticMarkup(<Contact title="Contact Me" description="Get in touch, I'd love to hear from you!" />));

Now we need that buildPage function, and to trigger the build. Let's create our final file build.js.

const fs = require('fs');
require("@babel/register"); // to compile React

// build function writes the static html to a file in the dist directory
exports.buildPage = function(path, content) {
    fs.writeFile(__dirname + '/../dist/' + path + '.html', '<!DOCTYPE html>\n' + content, function(error) {
        if (error) { 
            console.error(err); 
            return false;
        }
        console.log('Build of ' + path + '.html successful');
    });
};

// build the pages
require("./pages.js");

A few gotchas

  1. @babel/register doesn't compile within the file it is called from, which is why we need it in our build file (so it compiles all our other files), but any ReactJS code is kept out of that file. It's also why I'm using require in that file and import in the other files.
  2. JSX doesn't like non-closing tags, which is why I add <!DOCTYPE html>\n during the build process rather than in the ReactJS Main component.
  3. This is for simple static content, so you can't use ReactJS hooks or javascript logic. If you want to do that, use renderToString(), or a framework like NextJS instead.