Using a static site generator to power the blog for a single-page application

In this post, I’ll explain how Sequiturs uses a static site generator to power this blog, which lives inside the larger Sequiturs single-page application.

Sequiturs uses Hugo as its static site generator and React as its application framework, but the approach described here could easily be adapted to support using other technologies like Jekyll and Angular.

Why

Before explaining how, I’ll explain why you would want to build a blog this way.

There is no shortage of tools for making a blog: static site generators like Hugo and Jekyll, content management systems like WordPress, and publishing platforms like Medium. Which should you choose?

We wanted our choice of blogging technology to optimize for SEO: we wanted the authority accrued by the blog to contribute to the authority of Sequiturs application pages. This ruled out using Medium, because a Medium blog would have to either be hosted on medium.com or on a subdomain like blog.sequiturs.com, neither of which would have the desired SEO outcome (see here for a discussion of why hosting your blog at a subdomain is not ideal for SEO).

The remaining options were a CMS like WordPress or a static site generator like Hugo. We had no need for the full feature suite of a CMS and were happy to avoid the associated hosting needs. A static site generator, on the other hand, promised:

  • simplicity: just write Markdown files and add some YAML metadata
  • extensibility: ample supply of themes and plugins
  • no need for new infrastructure, just an easy integration with our build process.

Between Jekyll and Hugo, the two most popular static site generators, we chose Hugo for its superior performance and default support for organizing posts by tag and by category (Jekyll only has built-in support for by category). There are also static site generators such as gatsby that use React, but as you’ll see below, our implementation is entirely agnostic to the choice of static site generator, so there’s no obvious reason why you should prefer a React-based static site generator just because your single-page app is made with React.

How

Having settled on a static site generator, the key issue in implementation was around how the generated blog files would relate to the Sequiturs single-page app.

The design we wanted was for blog posts to display inside the main container of the Sequiturs app–that is, between the navbar and footer, which are rendered via React. This seemed like the ideal user experience; users would be able to move seamlessly from reading the blog to actually using the Sequiturs app.

Desired design
Desired design, with blog content contained between navbar and footer

There are two possibilities for achieving this design, and they can be framed in terms of whether React (plus server-side templating) or Hugo is ultimately responsible for generating the final html that will be sent by the server in response to blog requests:

1. Hugo generates the final html - First we use React (specifically ReactDOMServer.renderToString()) to generate html for the navbar and footer that we want to surround the blog content, then we use these as partials in a hugo build step that outputs the full html for all blog pages.

2. React (plus server-side templating) generates the final html - First a hugo build step generates html partials for all the blog pages, then these are injected into the React application when we call ReactDOMServer.renderToString() to render the whole page at response time.

Crucially, approach 1 does not support customizing the server’s html response at response time; rather, all blog html has been pre-packaged before response time. This should offer superior performance to approach 2, which needs to generate a full html response at response time for each request, but makes approach 1 undesirable if anything about the React-generated html for the navbar and footer needs to be customized for a particular request. Although it is possible, technically, under approach 1 to push this customization of the page the user sees onto the client side, that would introduce undesirable complexity for the developer, who would now have to:

  • manage the fact that for blog-related routes, the html returned by the server should come from reading a file generated by Hugo, while for other routes, it should come from calling ReactDOMServer.renderToString(); and
  • manage the fact that blog-related routes might render differently to the user than the html returned by the server represents, instead of being able to assume that the html returned from the server always correctly represents the initial page the user interacts with; and
  • ensure their view logic supports an acceptable user experience in the case where such client-side customization occurs, since the page loaded in the browser would for a moment reflect the html sent by the server, and then immediately change to reflect the customization.

Approach 2, on the other hand, maintains the role of React as the sole server-side source of response html and preserves a consistency in user experience, that for all routes, the html the server responds with correctly represents the first page the user will interact with.

In the case of Sequiturs, the navbar needs to be customized depending on whether a user is signed in. If the user viewing the page is not signed in, we show a navbar that includes a Tour link and Sign in and Sign up buttons:

Navbar for user not signed in
Navbar for a user who is not signed in

If the user is signed in, we show a navbar that includes a link to the user’s profile and their Notifications:

Navbar for user signed in
Navbar for a user who is signed in

Because we need to support this customization, we chose approach 2.

Implementing approach 2: detailed instructions

  1. Modify the template files used by the static site generator, so that the files it generates are html partials suitable for inclusion in a larger html document.
    • For instance, you’ll remove the <html>, <head>, and <body> tags from the templates, since those tags will be coming from the html generated by React (or else from server-side templates).
  2. Create a build step that will run the static site generator.

    • Example build step, if you’re using Gulp as your build system:

      gulp.task('buildBlog', function(cb){
        // 1. Clear the destination dir
        var destinationDir = 'public/blog';
        del([ destinationDir ], function(err) {
          if (err) cb(err);
      
      
          // 2. Build the blog in the destination dir
          var spawn = require('child_process').spawn;
          var hugo = spawn('hugo', [ '--destination=../' + destinationDir ], {
            stdio: 'inherit',
            cwd: process.cwd() + '/blog'
          });
      
      
          hugo.on('exit', function(code) {
            cb(code === 0 ? null : 'ERROR: Hugo process exited with code: ' + code);
          });
        });
      });
      
  3. Implement server-side route handling that covers the blog-related routes.

    • Example routes:
      • /blog
      • /blog/page/:pageNum
      • /blog/tags/:tag
      • /blog/post/:slug
    • As you can see, you’ll need to maintain a route for every link taxonomy that your static site generator outputs.
    • Example route handling for these routes, for an Express.js server:

      'use strict';
      
      
      var fs = require('fs');
      var express = require('express');
      var router = express.Router();
      
      
      var PATH_TO_BLOG_DIR = 'public/blog';
      var NO_SUCH_FILE_OR_DIR_MESSAGE = 'ENOENT: no such file or directory';
      
      
      router.get('/blog/post/:slug', function(req, res) {
        var pathToBlogHtmlFile = PATH_TO_BLOG_DIR + '/post/' + req.params.slug + '/index.html';
        fs.readFile(pathToBlogHtmlFile, 'utf8', function(err, data) {
          if (err) {
            if (err.message.slice(0, NO_SUCH_FILE_OR_DIR_MESSAGE.length) === NO_SUCH_FILE_OR_DIR_MESSAGE) {
              res.status(404).send();
            } else {
              res.status(500).send();
            }
          } else {
            // Do server rendering here.
            // e.g. Call `ReactDOMServer.renderToString(...)`, then `res.status(200).send(yourHtml)`
      
      
            // Note for optimal SEO, you'll want to parse `data` to get your blog post's
            // title, and then use that value to populate the <title> element in
            // your html response.
          }
        });
      });
      
      
      module.exports = router;
      
  4. Implement corresponding routes in your React routing, which mount a Blog component that renders blog html:

    • Example routing using React Router:

      'use strict';
      
      
      var React = require('react');
      var ReactRouter = require('react-router');
      var Route = ReactRouter.Route;
      
      
      var App = require('./components/app/App.js');
      var Blog = require('./components/blog/Blog.js');
      
      
      var routes =
        <Route path="/" component={App}>
          <Route path="blog" component={Blog} />
          <Route path="blog/page/:pageNum" component={Blog} />
          <Route path="blog/tags/:tag" component={Blog} />
          <Route path="blog/post/:slug" component={Blog} />
          {// other application routes here...}
        </Route>;
      
    • Example Blog component:

      'use strict';
      
      
      var React = require('react');
      
      
      module.exports = React.createClass({
        displayName: 'Blog',
        propTypes: {
          blogHtml: React.PropTypes.string.isRequired
        },
        createMarkup: function() {
          return { __html: this.props.blogHtml };
        },
        render: function() {
          return <div dangerouslySetInnerHTML={this.createMarkup()}></div>;
        }
      });
      
    • If you want to support navigating to blog routes without a full page refresh, you’ll also need to implement client-side logic (as well as corresponding server-side support) for AJAX fetching of the html partials that you’ll use to populate this.props.blogHtml. We decided that user experience was unnecessary for Sequiturs, so we avoided this unnecessary complexity.

      • Note that to ensure blog routes are served via a full page refresh, you’ll want to link to them within your application via a regular <a> element, rather than the history-manipulating element your client-side router uses (e.g. <Link> for React Router).
    • An important word about security:

    There’s nothing inherently bad about using dangerouslySetInnerHTML here. The same XSS risk that the name dangerouslySetInnerHTML is meant to warn about would be present if we used approach 1. In both approaches, we’re rendering html, generated from the blog’s Markdown files, in the same page where we run our single-page application. Thus, in both approaches, we need to prevent the introduction of malicious Javascript–which could, for example, execute the single-page app’s Javascript, make requests to our application server, or even make requests to other servers if they allow CORS.

    Avoiding such an XSS vulnerability in our Markdown is fairly straightforward: the key thing is not to include dynamically generated (e.g. user-generated) values which could be maliciously formulated. Ultimately, it’s the responsibility of the blogger, and the developer who deploys the blogger’s html, to avoid introducing this vulnerability.

    To reduce the likelihood of mistakes in this regard, you might consider committing the generated blog html to source control, so that you can easily keep track of the final html that is being rendered.

    If you’re using a third-party system to power comments on your blog, you will want to give additional scrutiny to XSS concerns. (If those comments are loaded via an iframe that points to a different domain, you may be okay, as browsers won’t give a cross-domain iframe access to the parent page.) The Sequiturs blog doesn’t use such a comment system, so that wasn’t an issue.

That’s it! With this implementation, a blog powered by a static site generator can live inside of a single-page application, and its search engine authority will apply to the single-page application’s domain. It’s not as simple as hosting your blog on a third-party platform or even on a subdomain, but if SEO is a priority, this is the best you can do.