Middleman

Adding React to a Middleman project with Webpack

Middleman is a static site generator written in Ruby. It's a great way to produce rich static content sites without the need for a server. Despite this, it doesn't come with internal support for modern JavaScript frameworks like React. Luckily, Middleman 4 ships with a feature called External Pipeline which allows wiring in your own external build tool like Gulp or Webpack.

Let's look at how to integrate Webpack with Middleman for the purpose of using React on our site.

Install development dependencies

First, let's install the dependencies we'll need to build our React code. Note that we're using the --save-dev flag to indicate to npm that we should append these libraries to the devDependencies section of our package.json.

$ npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react webpack uglifyjs-webpack-plugin
  • babel-core is the core Babel package for transpiling ES6 and JSX into browser-friendly ES5 JavaScript.
  • babel-loader is a Webpack loader which loads files from our path into Babel.
  • babel-preset-es2015 is a preset for Babel to transpile ES6 code.
  • babel-preset-react is a preset for Babel to transpile JSX code.
  • webpack is Webpack itself.
  • uglifyjs-webpack-plugin is a Webpack plugin to uglify and compress our code for production.

Install React

Next, install the React packages, this time using --save to indicate these are runtime dependencies:

$ npm install --save react react-dom

Set up your Babel configuration file

Babel has its own configuration file inside .babelrc. Create this file in the root of your Middleman project with the following contents:

{
  "presets": [
    "es2015", "react"
  ]
}

This file tells Babel to use the es2015 and react presets we installed in the first step.

Configure Webpack

Next we'll configure Webpack with a basic configuration file that supports both development and production environments. Place the following inside the file webpack.config.js in the root of your project:

// webpack.config.js
var webpack = require('webpack');

const isProduction = process.env.NODE_ENV === 'production';

const productionPlugins = [
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': '"production"'
  }),
  isProduction ? new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: false,
    },
  }) : null,
];

module.exports = {
  entry: './assets/javascripts/index.js',
  devtool: isProduction ? false : 'source-map',
  output: {
    library: 'MyApp',
    path: __dirname + '/tmp/dist',
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: isProduction ? productionPlugins : []
};

Notice that we use the isProduction flag to toggle the use of the UglifyJsPlugin as well as whether we use a devtool to provide us with source maps for debugging.

Configure Middleman to build Webpack

Now that we've configured Webpack, let's tell Middleman to execute it whenever it rebuilds. To do this, we'll activate the external pipeline plugin in our config.rb and point it at the Webpack executable:

# config.rb

activate :external_pipeline,
  name: :webpack,
  command: build? ?
  "NODE_ENV=production ./node_modules/webpack/bin/webpack.js --bail -p" :
  "./node_modules/webpack/bin/webpack.js --watch -d --progress --color",
  source: "tmp/dist",
  latency: 1

When we run middleman build, we run Webpack with NODE_ENV=production. This triggers our production build options in our Webpack configuration.

When using middleman server though, we tell Webpack to --watch for changes and automatically rebuild.

Middleman will look for the result of our built assets inside tmp/dist. Let's go ahead and create that directory now:

$ mkdir -p tmp/dist

Build an example React component

Now that we've got our tooling configured, let's create a simple React component to test that everything works. First, create a directory to store your Webpack JavaScript assets. I place these files in assets/javascripts since Sprockets looks inside source/javascripts, and I don't want Sprockets to attempt to build my React code.

$ mkdir -p assets/javascripts

Next, let's sketch out a React component. Note that from index.js I'm exporting a function renderHello which renders the HelloWorld component to a DOM element specified by ID. This allows us to call upon fragments of React code from within existing pages.

// assets/javascripts/index.js

import React from 'react';
import ReactDOM from 'react-dom';

const HelloReact = props => (
  <div>Hello, React!</div>
);

function renderHello(id) {
  const el = document.getElementById(selector);
  ReactDOM.render(<HelloReact />, el);
}

export default {
  renderHello
}

Mount the component

Finally, mount the component onto an existing DOM element on a page. First include the built bundle.js file. Then, call renderHello:

  <body>
    <div id="hello">
    </div>

    <%= javascript_include_tag  "bundle" %>
    <script type="text/javascript">
      MyApp.default.renderHello('hello');
    </script>
  </body>

Other resources

Using Gulp to generate image thumbnails in a Middleman app

var gulp = require('gulp');
var imageResize = require('gulp-image-resize');

var paths = {
  images: "source/images/**/*"
}

gulp.task('images', function() {

    gulp.src(['source/images/**/*.png', 'source/images/**/*.jpg'])
        .pipe(imageResize({
            width: 538,
            height: 538
        }))
        .pipe(gulp.dest('tmp/dist/assets/images/538x538'));

    gulp.src(['source/images/**/*.png', 'source/images/**/*.jpg'])
        .pipe(imageResize({
            width: 1076,
            height: 1076
        }))
        .pipe(gulp.dest('tmp/dist/assets/images/1076x1076'));

});

gulp.task('watch', function() {
  gulp.watch(paths.images, ['images']);
});

gulp.task('default', ['watch', 'images']);
gulp.task('build', ['images']);

Fixing a Ruby crash using Middleman 4.1x with Ruby 2.3.0

I went to update a Middleman project's gems today. After I did, I noticed middleman server exited with the following error:

Assertion failed: (!STR_EMBED_P(shared)), function str_new_frozen, file string.c, line 1075.

This is an internal Ruby error generated from within its C source. I found a discussion about the topic in the Ruby bug tracker, but that wasn't much help.

Once I modified my Gemfile to use Ruby 2.3.1 and re-bundled my gems, middleman server worked just fine.

If you're running into this problem, try upgrading to a more recent Ruby and running Middleman on it. There's a chance this bug has nothing to do with Middleman and could be due to another gem, but I figured I'd mention it here in case someone else is having the same problem.