A static HTML website starter template using Bridgetown
posted by Ayush Newatia
23 November, 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.
postcss-easy-import
processes@import
directives in your CSS code.postcss-url
inlines any SVG images referenced in your CSS so there’s fewer files to load.@csstools/postcss-sass
is the SASS plugin we’re using.postcss-flexbugs-fixes
andpostcss-preset-env
were taken as-is from the Rails 6 template. They’re used to enhance browser compatibility. The latter includes the aforementionedautoprefixer
plugin.cssnano
is the last step and it compresses the CSS to reduce its file size as much as possible.
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 require
d 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 require
s 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 require
ing 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!