Using webpack with Middleman

Table of contents

    Using webpack with Middleman

    Middleman(opens in a new tab) is the static site generator we use for building all of our websites here at PSPDFKit. It enables us to build sites, both large and small, using Ruby, while generating static output that’s easy to deploy.

    For a long time, Middleman has relied on the Rails Asset Pipeline(opens in a new tab) (aka sprockets(opens in a new tab)) for compiling and bundling frontend assets (CSS, JavaScript, etc.) However, as the frontend ecosystem has evolved, some of what sprockets provides is now native to the web platform (e.g. JavaScript’s module system), while other aspects (compilation, bundling) are better served by newer tools that take advantage of JavaScript’s latest features.

    Middleman v4 and :external_pipeline

    Middleman v4 embraces this ecosystem with its new :external_pipeline(opens in a new tab) feature, which, according to its website:

    “allows Middleman to run multiple subprocesses which output content to temporary folders which are then merged into the Middleman sitemap.”

    In other words, we can run Middleman side by side with whatever build tool we choose and it will take care of combining the results into a final file tree.

    We chose to use webpack(opens in a new tab) — a module bundler that can handle all types of frontend assets and compile the result into a bundle for serving to the browser — as our build tool because we already use it successfully in PSPDFKit for Web(opens in a new tab). This article will discuss webpack in more detail, but the approach should be similar for other build tools such as Broccoli(opens in a new tab) or gulp(opens in a new tab).

    Create a package.json

    For the purposes of this article, we’ll presume you already have a Middleman project you want to migrate to use :external_pipeline. (If not, see the Middleman Documentation(opens in a new tab) for help setting one up.)

    First we’ll need to install webpack. Similar to how we use bundler to manage Gems in Ruby, we’ll use npm(opens in a new tab) to manage our frontend packages. npm has a setup command that will run you through creating a package.json file:

    Terminal window
    npm init

    Check the contents of package.json, and you’ll see it’s fairly bare bones and just includes the information you entered during setup. This file is equivalent to bundler’s Gemfile and will list all of our frontend dependencies.

    Install webpack

    Let’s add webpack as a dependency:

    Terminal window
    npm install --save-dev webpack

    Check the contents of package.json again and you’ll notice that webpack is now listed under devDependencies:

    "devDependencies": {
    "webpack": "^3.10.0"
    }

    While we’re here, let’s install some additional webpack-related tools we’ll be needing later:

    Terminal window
    npm install --save-dev css-loader extract-text-webpack-plugin node-sass postcss-flexbugs-fixes postcss-loader sass-loader

    Also notice that a node_modules directory was created in your project. This is where all our installed packages live. As we have already specified them in our package.json, there’s no need to commit them, so we’ll ignore them from Git:

    Terminal window
    echo node_modules >> .gitignore

    Install Bootstrap

    For demonstration purposes, let’s say we want to use Bootstrap(opens in a new tab) in our project. Previously, we might have used the bootstrap-sass(opens in a new tab) gem or static files from the Bootstrap distribution. We can now use npm:

    Terminal window
    npm install --save [email protected] jquery popper.js

    Here we specified the 4.0.0 version of Bootstrap, along with its JavaScript dependencies, jQuery and Popper.js.

    Checking the contents of package.json, we see our new packages listed under dependencies:

    "dependencies": {
    "bootstrap": "^4.0.0",
    "jquery": "^3.2.1",
    "popper.js": "^1.12.9"
    }

    (For an explanation of the difference between dependencies and devDependencies, see the npm docs(opens in a new tab).)

    Configure webpack

    webpack is very much a blank slate and needs to be configured to our needs. By default, it looks for a webpack.config.js file in our project directory, so let’s create that now with the following contents (loosely based on the example webpack configuration from the Bootstrap docs(opens in a new tab)):

    webpack.config.js
    var path = require('path')
    var webpack = require('webpack')
    var ExtractTextPlugin = require('extract-text-webpack-plugin')
    module.exports = {
    entry: {
    site: ['./assets/javascripts/index.js', './assets/stylesheets/index.scss']
    },
    output: {
    filename: 'assets/javascripts/[name].js',
    path: path.resolve(__dirname, '.tmp/dist')
    },
    module: {
    rules: [
    {
    test: /\.scss$/,
    use: ExtractTextPlugin.extract({
    use: [
    'css-loader',
    {
    loader: 'postcss-loader',
    options: {
    plugins: function() {
    return [
    require('autoprefixer'),
    require('postcss-flexbugs-fixes')
    ]
    }
    }
    },
    'sass-loader'
    ]
    })
    }
    ]
    },
    plugins: [
    new ExtractTextPlugin({
    filename: 'assets/stylesheets/[name].css'
    })
    ]
    }

    An explanation of the entire configuration is beyond the scope of this article, but the first relevant lines to note are:

    entry: {
    application: [
    './assets/javascripts/index.js',
    './assets/stylesheets/index.scss'
    ]
    },

    Coming from the Rails Asset Pipeline, we can think of this as telling webpack that these are our “manifest” files.

    Let’s create those two files now with the following contents:

    assets/javascripts/index.js
    import 'jquery'
    import 'popper.js'
    import 'bootstrap'
    assets/stylesheets/index.scss
    @import '~bootstrap/scss/bootstrap.scss';

    Notice that these files live in their own assets directory at the top level of the project and are not in the source directory as usual. This will be important in the next step.

    The final line of note from webpack.config.js is:

    output: {
    ...
    path: path.resolve(__dirname, '.tmp/dist')
    },

    This instructs webpack to place its build output in .tmp/dist in our current project. Again, this will be important in the next step, but for now we should ignore these temporary files from version control:

    Terminal window
    echo .tmp >> .gitignore

    Configure Middleman

    If we run our Middleman site now, we might be rather disappointed to discover that not much happens. That’s because the final piece of the puzzle is to configure Middleman to talk to webpack.

    We add the following lines to our package.json:

    "scripts": {
    "start": "webpack --watch --progress --color",
    "build": "webpack --bail -p"
    },

    This defines a couple of npm shortcuts for starting the project in development and building the project for production. It’s a common npm convention and will help keep our Middleman configuration clean and easy to read.

    Now we add the following to our config.rb:

    activate :external_pipeline,
    name: :webpack,
    command: build? ? 'npm run build' : 'npm run start',
    source: '.tmp/dist',
    latency: 1
    config[:js_dir] = 'assets/javascripts'
    config[:css_dir] = 'assets/stylesheets'

    Notice that we’re telling Middleman both to use the npm scripts we defined earlier, and where to find the webpack build output (.tmp/dist).

    Also note that we’re overriding the default :js_dir and :css_dir configuration. Even when using the :external_pipeline, Middleman will attempt to process files in source/assets/* (e.g. uglify them), so we neatly sidestepped that issue without the need for complex ignore rules.

    Now if we run our site and view the logs, we should see webpack booting up alongside Middleman. If we open the site in the browser, we should also see that both the Bootstrap CSS and JavaScript are being loaded correctly! 🤗

    Conclusion

    This might seem like a lot of effort for not much benefit. If everything goes according to plan, there’s no perceivable difference between the resulting build output and that of sprockets.

    However, we have access to the entire npm ecosystem. We can piece together our own build system that includes all the goodies that make us more productive and happy, from using ES2015+(opens in a new tab) features or CSS variables(opens in a new tab) today, to adding type support to JavaScript(opens in a new tab) or developing in a completely different language(opens in a new tab) altogether.

    I hope this article has shown how easy it is to integrate an external build tool with Middleman thanks to :external_pipeline, in addition to giving you a taste of the opportunities this opens up.

    William Meleyal

    William Meleyal

    Explore related topics

    FREE TRIAL Ready to get started?