Qiming Weng/Writing

Creating Static, Isomorphic Websites with React and Webpack

Using webpack to render a website statically, and react-router to switch between pages dynamically. AKA, how this website was made.

The Single Page Application

Many web applications today are written with a single index.html page. This file is served irregardless of the requested url, whether that be domain.com/this/deep/path or domain.com. This single html file loads javascript which parses the the url location and renders the correct views.

This is great for web applications. You get fast switching between routes (since no html is reloaded) but still preserve the ability to share URLs that link to a deep part of the application.

However, when building more traditional websites that consist of many static pages, this presents a few problems.

1. SEO

Search engine bots don't load javascript. Thus, because all content is loaded by javascript, the bots that index your website don't actually see anything, and can't provide accurate search results. It's a bad idea to index applications, because they are different for each user, but you want static content to be indexed so people can find you!

2. Github Pages

Suppose you want to use the awesome github pages tool, like I do. You can't configure the server on github pages to always serve the root index.html. You'll need to create a static website instead.

3. Download Speed

Realistically, download speeds will be fast with either method, but statically loaded sites are still faster than loading in content through javascript. The HTML is downloaded right away, and the javascript is loaded on top of it after. When your application reaches millions of users, like Facebook or Instagram, this can matter a lot.

Rendering Static Websites

Webpack was made for single page applications, so it doesn't render static websites out of the box. However, webpack is highly extensible, so I wrote a minimal plugin that will get this job done, static-render-webpack-plugin.

The goal is to create an output like this

/project
  /build
    /about
      index.html
    /projects
      index.html
    index.html
    404.html
    bundle.js
    style.css

Setup

First, let's install some dependencies.

npm i --save-dev webpack webpack-dev-server react react-router static-render-webpack-plugin babel-loader

Let's take a look at how I've set up my source files. This will be good reference for later.

The /build folder is dynamically generated and served to clients, the /src folder is where we keep our source files.

/project
  /build
  /src
    /pages
      RootPage.js
      HomePage.js
      AboutPage.js
      ProjectsPage.js
      404Page.js
    entry.js
  webpack.config.js

Let's take a look at our webpack config.

// webpack.config.js

// Import the plugin
var StaticSiteGeneratorPlugin = require('static-render-webpack-plugin');

// Define the routes we want in this project
var routes = [
  '/',
  '/about',
  '/projects',

  /**
   * The 404 page doesn't render as index.html on a regular folder, 
   * so we use a special object to describe more accurately the 
   * behaviour we would like
   */

  {
    path: '/not-found', // this path will be passed to react-router
    output: '/404.html' // this is the output file
  }
];

module.exports = {
  entry: 'src/entry.js', // our main javascript file
  output: {
    filename: 'bundle.js', // the output js file
    path: './build', // build directory
    /** 
     * This is really important, the plugin expects the 
     * bundle output to export a function
     */
    libraryTarget: 'umd'
  },
  module: {
    loaders: [
      // I'm using babel to load ES6 code
      {text: /\.js$/, exclude: /node_modules/, loader: 'babel?stage=0'}
    ]
  },
  plugins: [
    new StaticSiteGeneratorPlugin('bundle.js', routes)
  ]
};

To illustrate how simple this is, create a project/src/entry.js file with just this.

// src/entry.js

export default function(path, props, callback) {
  callback('<!doctype html><html><head></head><body>HTML!</body></html>');
}

When you run the project, for example, with $: webpack-dev-server --progress, you'll generate 3 pages that all say the same thing at the routes /, /about, /projects and /404.html.

Let's now use react-router to build the application.

// src/entry.js

import React from 'react';
import Router, {Route, DefaultRoute} from 'react-router';
import RootPage from './pages/RootPage';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ProjectsPage from './pages/ProjectsPage';
import 404Page from './pages/404Page';

const routes = (
  <Route path="/" handler={RootPage}>
    <DefaultRoute handler={HomePage}/>
    <Route path="/about" handler={AboutPage}/>
    <Route path="/projects" handler={ProjectsPage}/>
    {/* The following route will return when we inject /not-found with static-render-webpack-plugin. */}
    <NotFoundRoute handler={404Page}/>
  </Route>
)

/**
 * This function is the global export that static-render-webpack-plugin uses
 */
module.exports = function(path, props, callback) {
  /**
   * Each route that is provided will execute the following function once,
   * the callback is called with the desired html
   */
  Router.run(routes, path, (Root) => {
    /**
     * React.renderToStaticMarkup converts your react elements 
     * into regular html as a string.
     * 
     * Please note that we add the doctype, which react isn't
     * able to generate on its own. If we don't add the doctype
     * some browsers will render the page in quirks mode which is
     * extremely hard to debug.
     */
    const html = React.renderToStaticMarkup(<Root/>);
    callback('<!doctype html>' + html);
  });
}

I'll show you the root page and the home page, you should be able to build AboutPage, ProjectsPage and 404Page on your own.

// src/pages/RootPage.js

import React, {PropTypes} from 'react';
import {RouteHandler} from 'react-router';

export default class RootPage extends React.Component {
  render() {
    return (
      <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
          <RouteHandler/>
        </body>
      </html>
    )
  }
}
// src/pages/HomePage.js

import React, {PropTypes} from 'react';

export default class HomePage extends React.Component {
  render() {
    return (
      <div>
        <h1>Home Page</h1>
        <h2>All About Me</h2>
        <TheRestOfMyApplication/>
      </div>
    )
  }
}

Isomorphic Javascript

The idea of isomorphic javascript is that you run javascript on both the client (browser) and server side. This way, we can use the same code in two environments (as opposed to writing JS for front-end and something like rails for back-end).

In our case, we want to serve a static page when the user first loads the website, but then inject javascript and "rehydrate" the page to become dynamic and respond to user events. I like to imagine dried potatoes that astronauts bring to space and fill with water.

React makes this really easy. There's actually two different top level functions that React provides which generates a string from a root element. React.renderToStaticMarkup and React.renderToString. The difference is that React.renderToString will produce an html output that has extra attributes, like <body data-reactid="...">. When React.run is applied to an html page that has these special attributes, the React engine will know not to replace it, and simply "rehydrates" the page.

First, we need to modify RootPage.js to load our bundled javascript.

// src/pages/RootPage.js

import React, {PropTypes} from 'react';
import {RouteHandler} from 'react-router';

export default class RootPage extends React.Component {
  render() {
    return (
      <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
          <RouteHandler/>
          // Add a reference to the bundle, which webpack outputs to our build folder anyway
          <script src="/bundle.js"/>
        </body>
      </html>
    )
  }
}

The real magic is going to be in our bundled javascript output. Let's go back to entry.js

// src/entry.js

// This export is what static-render-webpack-plugin uses to render the static html files
export default function(path, props, callback) {
  Router.run(routes, path, (Root) => {
    // Notice that we are using renderToString
    const html = React.renderToString(<Root/>);
    callback('<!doctype html>' + html);
  });
}

// This code will only run on the browser, where there is a document object
if (typeof document != 'undefined') {
  Router.run(routes, path, (Root) => {
    React.render(<Root/>, document);
  });
}

That's it! We just tell react to re-render the root on the document!

Adding SCSS

SCSS is a little tricky. Most webpack tutorials will tell you to use the style!css!sass loaders to load scss into your webpage. Here, the sass loader first compiles to css, then the css loader turns the required css file into a string, which the style loader injects into the <head> element at runtime. This doesn't work because we are calling our bundle during compile time, when there is no document or elements to inject css in.

Instead, we want to compile all our css into a text file with a handy native plugin called extract-text-webpack-plugin. This will output a css file to our /build directory that we simply include in the html with a <link> tag.

// webpack.config.js

// import the Extract Text Plugin
var ExtractTextPlugin = require('extract-text-webpack-plugin');

...

module.exports = {
  ...
  module: {
    loaders: [
      {test: /\.scss$/, loader: ExtractTextPlugin.extract('css?sourceMap!sass?sourceMap')}
    ]
  }
  ...
  plugins: [
    new ExtractTextPlugin('styles.css'),
    new StaticRenderWebpackPlugin('bundle.js', routes)
  ]
  ...
}

This config will output all of our css into a nice packaged file at project/build/style.css. We just have to import it.

// src/pages/RootPage.js

export default class RootPage extends React.Component {
  render() {
    return (
      <html>
        <head>
          <title>Hello World</title>
          // Import the stylesheet here
          <link rel="stylesheet" type="text/css" href="/style.css"/>
        </head>
        <body>
          <RouteHandler/>
          <script src="/bundle.js"/>
        </body>
      </html>
    )
  }
}

Fin

Thanks for reading. In a follow up article, I'll show you how to blog with static rendering and markdown files.

Sources

This post and plugin was inspired by a post by Brent Jackson. I decided to emphasize parts that were more confusing for me and how to solve CSS problems which were core to my usage.

Feedback

I'd be happy to hear your feedback on this tutorial and any suggestions for improving it. Tweet Me.