Binary Solo

A static HTML website starter template using Bridgetown

posted by Ayush Newatia on November 23rd, 2020
Static HTML websites are incredibly popular these days. They're unparalleled in performance as you can serve them via CDNs which are blazing fast. Also they're incredibly easy to deploy thanks to services like Netlify.

To build static websites with changeable content and without repeating code requires the use of a static site generator.

There's quite a few popular generators; but being a Rubyist, I've used Jekyll for a number of websites. While it's awesome; I thought it was a bit out of date as it does not integrate Webpack by default. Anyone who's looked at a webpack.config.js knows that hitting your head against the nearest wall is usually a more pleasurable activity than configuring Webpack; so I went looking for a solution that gives me Webpack out of the box.

The solution I found is Bridgetown. It's been forked from Jekyll 4.1 and was released in March, 2020. Webpack is included as a first class citizen. Even though the project is very new; it's built on the very mature foundation of Jekyll. The documentation is also amazing. For a project so new, I was very impressed with the quality and level of documentation on the official website. As such, I'm confident of using it in production.

The only problem was I wasn't very happy with the default site structure generated by Bridgetown. Due to this I decided to go down the rabbit hole of configuring Webpack manually. 

In this post, I'm going to share the starter template I made for Bridgetown; describe the changes I've made and some of the thinking behind my decisions.

The starter template


My starter template is available on GitHub. Please keep this open while reading the rest of the post so you can refer back to the directory and file structure as needed.

Problems I wanted to address


  • Swap SASS with PostCSS for CSS processing
  • Compress CSS files
  • Autoload CSS files into the bundle instead of importing each one manually
  • Add the CSS Reset
  • Add a conventional location for custom fonts
  • Allow images to be referenced in CSS
  • Improve search paths for JavaScript so modules in the javascript and _components directories can be referenced easily

Swapping SASS with PostCSS


PostCSS is all the rage these days. It's a different take on CSS processing as it involves assembling plugins like Lego blocks rather than using a monolithic processor like SASS or Less. Plugins like Autoprefixer have become essential in my opinion.

As such I was keen on using PostCSS but I still wanted to write SASS syntax for my CSS. Luckily there is a SASS plugin for PostCSS we can use.

To make this change; we need to remove the sass-loader from webpack.config.js and add the postcss-loader. You can look in the file for the exact config details.

After this, we need to specify the plugins we want to use with PostCSS. That is specified in postcss.config.js. I used the default PostCSS config in Rails 6 as a starting point.

The contents of the file are:

module.exports = {
  plugins: [
    require('postcss-easy-import'),
    require("postcss-url")([
      { filter: 'src/_frontend/assets/*.svg', url: 'inline' }
    ]),
    require('@csstools/postcss-sass'),
    require('postcss-flexbugs-fixes'),
    require('postcss-preset-env')({
      autoprefixer: {
        flexbox: 'no-2009'
      },
      stage: 3
    }),
    require('cssnano')({ 
      preset: 'default' 
    })
  ]
}

Let's look at the plugins attribute line by line. 


With this configuration we've addressed the first two problems in my list. SASS is now being used via PostCSS and the final output is being compressed with cssnano.

Autoloading CSS files


Being a Rubyist, I hate having to manually import code. To autoload all CSS files, I created an index.js in the src/_frontend/styles directory. This file is required in the main application.js file which is the entry point for Webpack.

We can use a Webpack method called require.context to require files based on a regex pattern. So in index.js, we can put:

require.context('../../_components/', true, /(^[^_].*)(\.(s[ac]|c)ss$)/)
require.context('.', true, /(^[^_].*)(\.(s[ac]|c)ss$)/)

These two lines recursively search for .scss, .sass and .css files that don't start with a _ in src/_frontend/styles and src/_components. It then automatically requires them so they're included in the bundle. No need to manually import every file!

If you need to control the order some CSS files are imported in, you can prefix them with a _ and import them manually. This approach is great for files containing global properties. We'll use this idea in the next step to add a CSS Reset file.

Adding the CSS Reset file


The CSS reset disposes of a number of browser defaults that can make it quite hard to track weird layout bugs. I can't recommend it highly enough for every website.

We need to include this file before any other styles are loaded so the "cascade" effect isn't broken. As such we call the file _reset.css and place it under src/_frontend/styles. Then at the top of index.js we manually import it.

import "_reset.css"

require.context('../../_components/', true, /(^[^_].*)(\.(s[ac]|c)ss$)/)
require.context('.', true, /(^[^_].*)(\.(s[ac]|c)ss$)/)

This way it's included before all other styles and we're free to override any attributes in out custom CSS! 

A conventional location for custom fonts


A nice custom font can completely change the look of a website. Personally, I'm against relying on external services to host my assets so a conventional location for fonts from where they're autoloaded is essential.

To facilitate this I created a directory called fonts under src/_frontend. Each custom font should be placed in its own subdirectory under fonts. The subdirectory should contain all the font files and a stylesheet containing its @font-family declarations. I'm a big fan of Transfonter to generate .woff2 files and its stylesheet for my websites.

For autoloading these fonts, I added a declaration to the top of application.js. Looking inside that file we can see:

require.context('fonts/', true, /\.(s[ac]|c)ss$/)

Similar to the way we autoloaded all our custom CSS, we're looking in the fonts directory and automatically requireing all CSS files found under it and its subdirectories. The CSS files reference the font files so Webpack will include them as well!

Referencing images in CSS


Making direct references to images in CSS should generally be avoided. But sometimes it's an awesome tool especially for use in :before and :after pseudo elements.

Ideally you should reference SVG images so they're automatically inlined in the CSS (as explained in the PostCSS configuration) rather than included as an external file. However sometimes there's no getting around including an image. The default configuration supports .jpg or .png images. They should live in the src/_frontend/assets directory.

To include these image files in our build, we have to make a change to our Webpack configuration so it knows what to do when it encounters these files. Towards the bottom of webpack.config.js, you can see the lines:

{
  test: /\.png$|\.jpg$/,
  loader: "file-loader",
  options: {
    name: "[name]-[contenthash].[ext]",
    outputPath: "../assets",
    publicPath: "../assets",
  },
},

This configuration entry specifies that Webpack should use the file-loader for .png and .jpg files. It will copy them as is into an assets directory along with the CSS and JavaScript. It will also rename the file as per the pattern specified in name. Having the content hash in the name ensures that if the file changes, the filename will always change ensuring the cache is automatically invalidated.

Improve JavaScript search paths


I'm a little OCD with my code so I hate having to write something like:

require("./my-module")

I don't think it should be necessary to specify the current directory. I'd much rather write: 

require("my-module")

To keep with Bridgetown conventions, according to which JavaScript can be placed in src/_frontend/javascript or along with individual components in src/_components, we need to tell Webpack to look for modules in these directories. That way we can require modules without specifying the current directory.

Adding the following lines under the resolve attribute in webpack.config.js adds the above paths to the search directories:

modules: [
  path.resolve(__dirname, 'src', '_frontend', 'javascript'),
  path.resolve(__dirname, 'src', '_frontend', 'styles'),
  path.resolve(__dirname, 'src', '_frontend'),
  path.resolve(__dirname, 'src', '_components'),
  path.resolve('./node_modules')
]

Now we can require modules in our  src/_frontend/javascript/index.js without specifying the directory. That means we can do something like this:

// Module located in src/_frontend/javascript
require("controllers")

// Module located in src/_components
require("dropdown")

No additional configuration required!

Moving frontend code within src


The code under frontend forms part of the web application so I believe it should live under src with the markup and not in the root.

In my template I've moved frontend into src and renamed it _frontend. This approach unifies all app code under one directory and all config files outside it.

Conclusion


That brings us to the end of my list of desired improvements. If you stuck with me until the end, thanks and well done! I can safely say I never want to look at a webpack.config.js ever again in my life.

I believe this template modernises Bridgetown even further. CSS frameworks that are written as PostCSS plugins like TailwindCSS can easily be integrated into this setup.

If you have any ideas, suggestions, criticisms; please open an issue in the GitHub repo or leave a comment below! 

Subscribe to Binary Solo


We'll send you an email every time a new post is published. We'll never spam you or sell your email address to third parties. Check out our privacy policy for details.