├── .browserslistrc ├── .eleventyignore ├── src ├── images │ ├── favicon.png │ └── logo.svg ├── assets │ ├── css │ │ └── index.scss │ └── js │ │ └── index.js ├── _pages │ ├── _pages.json │ └── index.md ├── _data │ └── cacheBust.js └── _layouts │ └── default.ejs ├── .gitignore ├── postcss.config.js ├── babel.config.json ├── webpack.config.dev.js ├── .editorconfig ├── .eslintrc.json ├── webpack.config.prod.js ├── LICENSE ├── .eleventy.js ├── package.json ├── webpack.config.common.js ├── README.md └── tutorial.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | 4 | .DS_Store 5 | Thumbs.db -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stowball/elf/HEAD/src/images/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | /src/compiled-assets/ 4 | 5 | .DS_Store 6 | Thumbs.db -------------------------------------------------------------------------------- /src/assets/css/index.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | background: #cbe3f5; 4 | } 5 | -------------------------------------------------------------------------------- /src/_pages/_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "permalink": "<%- page.filePathStem.replace('/_pages', '').replace('/index', '') %>/index.html" 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | // eslint-disable-next-line global-require 4 | require('autoprefixer'), 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": 3, 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.config.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | // Allow watching and live reloading of assets 7 | watch: true, 8 | }); 9 | -------------------------------------------------------------------------------- /src/assets/js/index.js: -------------------------------------------------------------------------------- 1 | import '../css/index.scss'; 2 | 3 | // eslint-disable-next-line no-console 4 | console.log('Hello again'); 5 | 6 | Array.from(document.getElementsByTagName('p')).forEach((p, index) => { 7 | // eslint-disable-next-line no-console 8 | console.log(`p ${index}, startsWith('W')`, p, p.innerHTML.startsWith('W')); 9 | }); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{ejs,js,scss}] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_size = 2 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | 9 | [*.json] 10 | end_of_line = lf 11 | insert_final_newline = true 12 | indent_size = 2 13 | indent_style = space 14 | trim_trailing_whitespace = true 15 | 16 | [*.md] 17 | insert_final_newline = true 18 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/_pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: My cool website 3 | layout: default.ejs 4 | --- 5 | 6 | # Hello, world 7 | 8 | Welcome to my website. 9 | 10 | A random number is <%- Math.random() %> 11 | 12 | This project was built with **[Elf](https://github.com/stowball/elf)**, a simple & magical **[Eleventy](https://www.11ty.dev/)** starter project. 13 | 14 |  15 | 16 | Created by [Matt Stow](https://twitter.com/stowball). 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 12, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | }, 15 | "overrides": [{ 16 | "files": ["./*.js", "./src/_data/**/*.js"], 17 | "rules": { 18 | "import/no-extraneous-dependencies": ["error", { 19 | "devDependencies": true 20 | }] 21 | } 22 | }] 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const { merge } = require('webpack-merge'); 4 | const common = require('./webpack.config.common.js'); 5 | 6 | module.exports = merge(common, { 7 | // Enable minification and tree-shaking 8 | mode: 'production', 9 | optimization: { 10 | minimizer: [ 11 | new OptimizeCssAssetsPlugin({}), 12 | new TerserPlugin({ 13 | extractComments: false, 14 | }), 15 | ], 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/_data/cacheBust.js: -------------------------------------------------------------------------------- 1 | const md5File = require('md5-file'); 2 | 3 | const cacheBust = () => { 4 | // A "map" of files to cache bust 5 | const files = { 6 | mainCss: './src/compiled-assets/main.css', 7 | mainJs: './src/compiled-assets/main.js', 8 | vendorJs: './src/compiled-assets/vendor.js', 9 | }; 10 | 11 | return Object.entries(files).reduce((acc, [key, path]) => { 12 | const now = Date.now(); 13 | const bust = process.env.ELEVENTY_ENV === 'production' ? md5File.sync(path, (_err, hash) => hash) : now; 14 | 15 | acc[key] = bust; 16 | 17 | return acc; 18 | }, {}); 19 | }; 20 | 21 | module.exports = cacheBust; 22 | -------------------------------------------------------------------------------- /src/_layouts/default.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |`s, and demonstrating that you can also use EJS features (and JavaScript) within markdown itself. 160 | 161 | ## Serving our site 162 | 163 | With our page created, how do we serve it up locally and see changes as we update the content and create new pages? 164 | 165 | Let’s head over to `package.json`, and update our `"scripts"` with this: 166 | 167 | ``` 168 | "scripts": { 169 | "build:site": "ELEVENTY_ENV=production npx eleventy", 170 | "dev:site": "ELEVENTY_ENV=development npx eleventy --serve" 171 | }, 172 | ``` 173 | 174 | Now, from a terminal, you can run `npm run dev:site` and browse to [http://localhost:8080/_pages/](http://localhost:8080/_pages/) to see your HTML page fully rendered. 175 | 176 | We’ve also added a command to perform a production build, but we won’t need to use that just yet. Also note the `ELEVENTY_ENV=production|development`. This provides us the ability to do different things with our Eleventy process, like minifying HTML, depending on the build type. 177 | 178 | ## Re-writing URLs 179 | 180 | But wait up, we don’t want users (or us) to have to browse to `/_pages` in every URL; that home page should be available at the root domain! 181 | 182 | Thankfully, Eleventy has a feature called [permalinks](https://www.11ty.dev/docs/permalinks/), which allows you to set what the URL for each page will be. Now, while we can manually add this to every page’s front matter to remove `_pages`, we can go one better by automating that. 183 | 184 | Let’s create a `_pages.json` in `src/_pages`, with the following: 185 | 186 | ```json 187 | { 188 | "permalink": "<%- page.filePathStem.replace('/_pages', '').replace('/index', '') %>/index.html" 189 | } 190 | ``` 191 | 192 | “What is this madness?”, I hear you cry. Well, since we can use JavaScript within EJS, and use EJS within JSON, we can use the `page.filePathStem` which Eleventy provides to construct a new, better permalink path. 193 | 194 | As an example, for the following files: 195 | 196 | ``` 197 | /_pages/index.md 198 | /_pages/foo/index.ejs 199 | /_pages/foo/bar.md 200 | /_pages/foo/baz/index.ejs 201 | /_pages/foo/baz/qux.md 202 | ``` 203 | 204 | Eleventy will provide the following `filePathStem`s: 205 | 206 | ``` 207 | /_pages/index 208 | /_pages/foo/index 209 | /_pages/foo/bar 210 | /_pages/foo/baz/index 211 | /_pages/foo/baz/qux 212 | ``` 213 | 214 | So, this “script” first removes the `/_pages` from the `filePathStem` string, then removes any trailing `/index` so every page is “equal”, and finally appends `/index.html`, which, for the above path examples, results in: 215 | 216 | ``` 217 | /index.html 218 | /foo/index.html 219 | /foo/bar/index.html 220 | /foo/baz/index.html 221 | /foo/baz/qux/index.html 222 | ``` 223 | 224 | Now, you should be able to browse directly to [http://localhost:8080/](http://localhost:8080/) to see your “Hello, world” file, and the correct path for any other file you create later on. 225 | 226 | While this is kinda cool, it’s completely unstyled, so let’s see how we can set up webpack to compile CSS using Sass. 227 | 228 | ## Setting up webpack 229 | 230 | ### Creating our assets 231 | 232 | Before we do any webpack configuration, let’s first scaffold our assets so we have something to configure. 233 | 234 | We’re going to use Sass, which, while it may be going out of favour in some circles, still does an excellent job at allowing us to write more maintainable CSS. 235 | 236 | *As a side note, I still like Sass so much that I’ve used it to create an atomic CSS library called [Hucssley](https://github.com/stowball/hucssley), which, in my humble opinion, is excellent!* 237 | 238 | Anyway, back to this project… 239 | 240 | Create `src/assets/css/index.scss` with the following code: 241 | 242 | ```css 243 | html { 244 | font-family: sans-serif; 245 | background: #cbe3f5; 246 | } 247 | ``` 248 | 249 | In this directory, you would add all of your project’s Sass partials and `@import` them from `index.scss`. 250 | 251 | *If you prefer to co-locate your CSS and components, you could just as easily store them in specific template folders within `_components` and `@import` from there as well.* 252 | 253 | For webpack to handle our CSS, it must be `import`ed into a JavaScript file, so let’s create `src/assets/js/index.js` with the following: 254 | 255 | ```js 256 | import '../css/index.scss'; 257 | 258 | console.log('Hello again'); 259 | ``` 260 | 261 | Although we `import` the CSS within in JavaScript, this is not CSS-in-JS; it’s purely so webpack can do its thing™. 262 | 263 | ### Installing our dependencies 264 | 265 | Now that we have our asset files, let’s begin with the setup. 266 | 267 | First, we’re going to need to install quite a few dependencies now, so kill your `dev:site` process, and run this in the terminal. 268 | 269 | ```sh 270 | npm install css-loader fibers mini-css-extract-plugin optimize-css-assets-webpack-plugin sass sass-loader terser-webpack-plugin webpack webpack-cli webpack-merge --save-dev --save-exact 271 | ``` 272 | 273 | ### Creating our configs 274 | 275 | In the previous step, we installed a dependency called [webpack-merge](https://www.npmjs.com/package/webpack-merge). This will allow us to have separate development and production configurations which share the same, common configuration. 276 | 277 | In the project root, create `webpack.config.common.js` with the following: 278 | 279 | ```js 280 | // Makes Sass faster! 281 | const Fiber = require('fibers'); 282 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 283 | const path = require('path'); 284 | 285 | module.exports = { 286 | // Our "entry" point 287 | entry: './src/assets/js/index.js', 288 | output: { 289 | // The global variable name any `exports` from `index.js` will be available at 290 | library: 'SITE', 291 | // Where webpack will compile the assets 292 | path: path.resolve(__dirname, 'src/compiled-assets'), 293 | }, 294 | module: { 295 | rules: [ 296 | { 297 | // Setting up compiling our Sass 298 | test: /\.scss$/, 299 | use: [ 300 | { 301 | loader: MiniCssExtractPlugin.loader, 302 | }, 303 | { 304 | loader: 'css-loader', 305 | options: { 306 | url: false, 307 | }, 308 | }, 309 | { 310 | loader: 'sass-loader', 311 | options: { 312 | implementation: require('sass'), 313 | sassOptions: { 314 | fiber: Fiber, 315 | outputStyle: 'expanded', 316 | }, 317 | }, 318 | }, 319 | ], 320 | }, 321 | ], 322 | }, 323 | // Any `import`s from `node_modules` will compiled in to a `vendor.js` file. 324 | optimization: { 325 | splitChunks: { 326 | cacheGroups: { 327 | commons: { 328 | test: /[\\/]node_modules[\\/]/, 329 | name: 'vendor', 330 | chunks: 'all', 331 | }, 332 | }, 333 | }, 334 | }, 335 | plugins: [ 336 | new MiniCssExtractPlugin({ 337 | filename: '[name].css', 338 | }), 339 | ], 340 | }; 341 | ``` 342 | 343 | Now, let’s create our development config, at `webpack.config.dev.js`: 344 | 345 | ```js 346 | const { merge } = require('webpack-merge'); 347 | const common = require('./webpack.config.common.js'); 348 | 349 | module.exports = merge(common, { 350 | mode: 'development', 351 | // Allow watching and live reloading of assets 352 | watch: true, 353 | }); 354 | ``` 355 | 356 | And finally, our production config, at `webpack.config.prod.js`: 357 | 358 | ```js 359 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 360 | const TerserPlugin = require('terser-webpack-plugin'); 361 | const { merge } = require('webpack-merge'); 362 | const common = require('./webpack.config.common.js'); 363 | 364 | module.exports = merge(common, { 365 | // Enable minification and tree-shaking 366 | mode: 'production', 367 | optimization: { 368 | minimizer: [ 369 | new OptimizeCssAssetsPlugin({}), 370 | new TerserPlugin({ 371 | extractComments: false, 372 | }), 373 | ], 374 | }, 375 | }); 376 | ``` 377 | 378 | Now we have our configs, we need scripts to run them. Head over to `package.json`, and add 2 new scripts: 379 | 380 | ``` 381 | "build:assets": "webpack --config webpack.config.prod.js", 382 | "dev:assets": "webpack --config webpack.config.dev.js", 383 | ``` 384 | 385 | so it should look like this: 386 | 387 | ```json 388 | "scripts": { 389 | "build:assets": "webpack --config webpack.config.prod.js", 390 | "build:site": "ELEVENTY_ENV=production npx eleventy", 391 | "dev:assets": "webpack --config webpack.config.dev.js", 392 | "dev:site": "ELEVENTY_ENV=development npx eleventy --serve" 393 | }, 394 | ``` 395 | 396 | *Note: JSON doesn’t allow trailing commas on the last line of an object, so all of the updates I suggest are correct if adding them alphabetically.* 397 | 398 | If you run `npm run build:assets` in your terminal, you should now have 2, minified files generated at: 399 | 400 | ``` 401 | src/compiled-assets/main.css 402 | src/compiled-assets/main.js 403 | ``` 404 | 405 | ### Adding the assets to our Eleventy site 406 | 407 | Let’s open up our `src/_layouts/default.ejs`, and in the `
`, add a reference to the stylesheet, and before the closing ``, add a reference to our JavaScript files. 408 | 409 | ```html 410 | 411 | … existing tags 412 | 413 | 414 | ``` 415 | 416 | ```html 417 | 418 | 419 |