I built a doubly static site using React (inc react-router) and Webpack. You can see the current demo here on GitHub or continue reading the following post that explains the steps I took during this experiment. This post proves the basic concept and there will be a followup post covering the fine tuning needed to put this into production.
Why
My blog currently uses Jekyll. Its a great way to build a static site but for a while now I have been wanting to migrate off Jekyll onto something more familiar. I don’t use Jekyll for anything other than my blog so each time I go back to it there is a small learning curve. I don’t feel the need to join the WordPress cult and Javascript is where my heart is so some sort of custom node setup was the likely winner.
Options
Having narrowed down my search to Javascript there were still plenty of options available. I like the simple approach @code_barbarian took, rendering jade templates from a list of posts as mentioned here. Again though I don’t use jade that often. I have also looked at Harp several times but it never quite got me hooked. Remy Sharp has an interesting article on using Ghost or Harp. I also discovered morpheus while I was working on this. It looks very interesting but I have ruled it out as I don’t want to run anything on a server. And just this week I read Presenting The Most Over Engineered Blog Ever which I’ll be keeping an interested eye on.
Obvious Choice
For me there was an obvious alternative. Recently I have really been getting into React. I love it and have yet to encounter any major hurdles. I am totally addicted to Hot Module Reloading as provided by Webpack and Dan Abramovs React Hot Loader plugin. I am also really interested in exploring the idea of moving css into javascript (or JSS).
Eventually it clicked. React has the renderToStaticMarkup
method. I could develop the entire site as a React app and render the result to static html.
Requirements
Time to set myself some requirements then.
- Simple. One of the main goals is to replace Jekyll which is very simple to use. I am prepared to play around a bit to get the initial setup right as this is an experiment but ongoing use must be easy.
- Flexible. The solution must not restrict what I can do with my blog (e.g. url formats, content).
- A pleasurable build experience. Primarily my blog is written for me by me. I should enjoy building it as much as using it. Development must be simple but still have features that help (hot module reloading I’m looking at you).
- Doubly Static. The end result of the build step must be doubly static meaning no further rendering on the server or the client. This is a simple blog and I want static html files for each route that could be served from anywhere.
Chosen Approach
- A single page app built from React components
- React Router to handle all possible routes for the site
- Compiled by Webpack
- All pages and posts listed in a javascript object along with their meta data
Getting Started
Time to start putting these ideas into practice and to start with I just want to create and view a basic index page. For those playing along at home here is the first commit.
elements/Layout.jsx
is my starting point. Its a basic React component that renders a full html page. There are some caveats to rendering full pages in React but as long as its first rendered server side its OK (see this discussion).
So next I need a script to render and serve the page. I’m using the WebpackDevServer for this so I can take advantage of hot module replacement. I create my webpack.config.js
passing jsx files through the jsx-loader and react-hot-loader transforms and pointing webpack to dev/entry.jsx
as an entry point for the bundle it will build. dev/entry.jsx
simply renders a Layout component. server.js
uses React.renderToString
to write the result of creating a Layout component to file in dev/index.html
and then starts the WebpackDevServer on localhost port 3000 to serve that file and handle live updates.
So now if run npm start
the following will happen:
- an
elements/Layout.jsx
component will be rendered to string and saved indev/index.html
- Webpack will create
dev/bundle.js
from thedev/entry.jsx
starting point - Webpack will start an express server on port 3000
- Webpack has setup hot module replacement so any changes I make are updated with out the page having to reload
Another Page
Thats a good development environment to start with but now its time to get this working for multiple pages.
At this point I’m going to setup some basic css styling. I’ll explain what I’ve done but this approach will definitely be changed later on before this is ready for production. First I update webpack.config.js
to pass css files though the css-loader and style-loaders. I can then just require elements/style.css
and /bower_components/pure/pure.css
in entry.jsx to have them injected into the page by Webpack.
My strategy for creating the entire site from a single page will revolve around React Router.
First I create elements/Routes.jsx
as my main router. It uses elements/Layout.jsx
as the handler at the top level and then has home and about routes pointing to new elements/Home.jsx
and elements/About.jsx
elements as handlers. At this stage the new elements only render simple headings but are enough to see the router working.
elements/Layout.jsx
gets updated now to render a new elements/LayoutNav.jsx
element so we can move between the home and about pages (with the help of react-routers Link element). It also renders react-routers RouteHandler element as its main content which will be either elements/Home.jsx
or elements/About.jsx
.
I also update dev/entry.jsx
to run the elements/Routes.jsx
router (passing in the current history location as the route) and rendering the handler return from it instead of rendering elements/Layout.jsx
. Similarly I update server.js
write the handler resulting from running the elements/Routes.jsx
router with the current route set to ‘/‘ into dev/index.html
.
So now running npm start
will serve our multi route single page app.
A Little Fix
At this point the app works but only if you load localhost:3000. Reloading the browser on localhost:3000/about will fail because Webpacks express server doesn’t know about other routes. This is quickly fixed with the following update to server.js
server.use('/', function(req, res) {
Router.run(Routes, req.path, function (Handler) {
res.send(React.renderToString(React.createElement(Handler, null)));
});
});
Dynamic Page Titles
Its all well and good having two pages we can move between but the html page title doesn’t update. I need a strategy to store data for each page and dynamically display it. The key to this is paths.js
. It contains an object with keys for each route and some helper methods for extracting data from this object (at this point just titleForPath()
). elements/Layout.jsx
is now updated to lookup the title for the current route which it determines thanks to the react-router Router.State
mixin.
Rendering Rethink
The elements/About.jsx
and elements/Home.jsx
components are pretty rubbish and I am likely to already have existing page contents as html that I would like to reuse without rewriting it as a component. To do that I want to create Page.jsx that pulls content from a html file listed in paths.js. Thats easy enough to do but how do I load the content in a way that works in both node and the browser? I can use Webpack loaders like raw-loader and html-loader but they present a problem. They work fine within the Webpack context but they don’t work outside of it. My current strategy for the intial server side render would no longer work. Time for a rethink.
Dual Webpack Configs
The solution was actually quite simple and cleaned up server.js
a lot. First I updated webpack.config.js
to return an array of two configs. The first named browser is unchanged, so still points to the dev/entry.jsx
entry point and compiles to dev/bundle.js
. The second named server and targeting node points to a new entry point dev/page.jsx
and compiles to dev/bundlePage.js
. The function exported from dev/page.jsx
returns the html for the path of a given request and is callable from node. server.js
no longer needs any knowledge of my components (or React for that matter) and can use dev/page.jsx
to get the html for any path it needs.
On The Same Page
Now thats sorted I can get back to making a reusable page element to pull in html content. I remove elements/Home.jsx
and elements/About.jsx
and replace them with elements/Page.jsx
(also updating elements/Routes.jsx
). This new component again makes use of react-routers Router.State
mixin to pull the page title (heading) and html content via paths.js
. The html content is inserted into the page using the dangerouslySetInnerHTML
method. Note that the pageForPath()
method of paths.js
uses require.context('./pages', false, /^\.\/.*\.html$/);
so that Webpack nows to transform html files in pages/
directory.
At this point I can easily add any additional pages I want without the need to create new components.
Whats A Blog Without Posts
Pages are done so now its time to add posts and a blog index page to list all posts.
The first thing I did was to create elements/PathsMixin.js
to make it easier to access data from paths.js
and remove the need to repeat logic. It depends on getCurrentPathname
and getCurrentParams
from Router.State
so it sets them as required in the contextTypes property (meaning React will throw a warning if you try to use the PathsMinixin without Router.State). This cool because things like var title = paths.titleForPath(this.getPathname());
can now become var title = this.getPathMeta('title’);
within components using this mixin. Mixins for the win.
paths.js
was updated to list the posts in a similar way to how it lists pages but with posts having some extra data such as md, published and preview. It also gains a postforPath
method that utilises a new require context to load and transform markdown files is the posts/
directory.
elements/Post.jsx
is very similar to elements/Page.jsx
but displays the transformed markdown for a post as well as the date it was published. The display of the date was a great opportunity to create a reusable component elements/Moment.jsx
to format and render the date. This is where components really shine.
elements/Blog.jsx
is a custom component that grabs data via the elements/PathsMixin.js
and loops of each post to create a list. Nothing too exciting (and please ignore how I have done the styling) but it shows just how quickly a new feature can be added. elements/Routes.jsx
gets updated for the blog and post routes as does elements/LayoutNav.jsx
.
Building For Production
Up until this point the index.html pages being created still use React to render any changes to the page after it is initially loaded using dev/bundle.js
. For production I want a doubly static site. That is the job of build.js
. All the files required to serve every path of the blog statically will end up in the public/
directory. build.js
is pretty simple. It first copies the style sheets into public/assets/
. It then loops through each page and post in paths.js
and writes a html file.
Because I am not using React to update pages in production I created new, third entry point dev/staticPage.jsx
that uses React.renderToStaticMarkup
rather than React.renderToString
. I am sure there is a way to just add this entry point to my node targeted “server” config in Thanks to Eric Eldredge who submiited a pull request my webpack.config.js
but I couldn’t make it work. Instead I added a third config named “static” with dev/staticPage.jsx
that compiles to dev/bundleStaticPage.js
. If someone nows how to avoid this third config and still get dev/bundleStaticPage.js
I’d love to hear from you.webpack.config.js
now only has two configs again. The error I made was that I had left my entry points as a flat array when they needed to be an object as the object keys set the [name]
variable for the output files.
For convenience I also created publicServer.js
which just a very simple express server for the public/
directory. I can now use npm run-script build-static
to build the production version into public/
and the view it at localhost:4000.
Conclusion
I’m calling this one a success. While I still need to sort out my CSS strategy and there are a bunch of features I still want to implement, my initial goals have been met. React, React Router and Webpack have proven to be a great combo for building static sites. I will followup with another post as I fine tune this approach and move bradenver.com onto it. Any feedback is appreciated so feel free to use the comments below or log an issue on GitHub.