4 |
5 |
6 | {{ title }}
7 |
8 |
9 |
10 | {% if not eleventyExcludeFromCollections %}
11 |
12 | {% endif %}
13 |
14 |
15 | {{ content | safe }}
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/CNAME.11ty.js:
--------------------------------------------------------------------------------
1 | // @see https://www.11ty.io/docs/languages/javascript/
2 |
3 | // Standard lib.
4 | import { join as joinPath } from 'path';
5 | import { URL as NodeURL } from 'url';
6 |
7 | // Package modules.
8 | import {
9 | OUTPUT_DIRECTORY,
10 | PRODUCTION,
11 | } from '~/lib/constants';
12 | import { homepage } from '~/package.json';
13 |
14 | // Constants.
15 | const CNAME_FILE = joinPath(OUTPUT_DIRECTORY, 'CNAME');
16 |
17 | // Exports.
18 | module.exports = class CNameRecord {
19 | #hostname = null;
20 |
21 | data = {
22 | permalink: PRODUCTION && CNAME_FILE, // Enable only in production.
23 | permalinkBypassOutputDir: true,
24 | }
25 |
26 | constructor() {
27 | // Extract hostname from homepage.
28 | // If not a valid URL, disable CNAME generation altogether.
29 | try {
30 | this.#hostname = new NodeURL(homepage).hostname;
31 | } catch (e) {
32 | this.data.permalink = false;
33 | }
34 | }
35 |
36 | render() {
37 | return this.#hostname;
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // @see https://tailwindcss.com/docs/configuration
2 |
3 | // Standard lib.
4 | import { join as joinPath } from 'path';
5 |
6 | // Local modules.
7 | import {
8 | INPUT_DIRECTORY,
9 | PRODUCTION,
10 | } from './lib/constants';
11 |
12 | // Constants.
13 | // @see https://www.11ty.io/docs/languages/
14 | const ELEVENTY_TEMPLATE_LANGUAGES = [
15 | 'html', 'md', '11ty.js', 'liquid', 'njk', 'hbs', 'mustache', 'ejs', 'haml', 'pug', 'jstl',
16 | ];
17 |
18 | // Exports.
19 | module.exports = {
20 | // @see https://tailwindcss.com/docs/optimizing-for-production#writing-purgeable-html
21 | purge: {
22 | content: [joinPath(INPUT_DIRECTORY, `**/*.{${ELEVENTY_TEMPLATE_LANGUAGES}}`)],
23 | enabled: PRODUCTION,
24 | mode: 'all', // Remove all unused styles, not just Tailwinds'.
25 | options: {
26 | fontFace: true,
27 | keyframes: true,
28 | safelist: [],
29 | variables: true,
30 | },
31 | },
32 |
33 | darkMode: false, // or 'media' or 'class'
34 | theme: {
35 | extend: {},
36 | },
37 | variants: {
38 | extend: {},
39 | },
40 | plugins: [],
41 | };
42 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Mark van Seventer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/_shortcodes/image.js:
--------------------------------------------------------------------------------
1 | // Package modules.
2 | import render from 'posthtml-render';
3 |
4 | // Helpers.
5 | const generateSrcSet = (entries) => (
6 | entries
7 | .map((entry) => entry.srcset)
8 | .join(', ')
9 | );
10 |
11 | // Exports.
12 | export default (stats, options = {}) => {
13 | const {
14 | alt = '',
15 | sizes = '100vw',
16 | } = options;
17 |
18 | const formats = Object.keys(stats);
19 | const [firstFormat] = formats;
20 |
21 | // Use lowest-quality src as base img element.
22 | const [{ url, width, height }] = stats[firstFormat];
23 | const lowestSrc = {
24 | tag: 'img',
25 | attrs: {
26 | src: url,
27 | width,
28 | height,
29 | alt,
30 | },
31 | };
32 |
33 | // Use single img element if there's only one format.
34 | if (formats.length === 1) {
35 | const entries = stats[firstFormat];
36 |
37 | // Append srcset only if there's more than one entry.
38 | if (entries.length > 1) {
39 | lowestSrc.attrs = {
40 | ...lowestSrc.attrs,
41 | sizes,
42 | srcset: generateSrcSet(entries),
43 | };
44 | }
45 |
46 | return render(lowestSrc);
47 | }
48 |
49 | // Use picture element otherwise.
50 | return render({
51 | tag: 'picture',
52 | content: [
53 | ...formats.map((format) => ({
54 | tag: 'source',
55 | attrs: {
56 | sizes,
57 | srcset: generateSrcSet(stats[format]),
58 | type: `image/${format}`,
59 | },
60 | })),
61 | lowestSrc,
62 | ],
63 | });
64 | };
65 |
--------------------------------------------------------------------------------
/lib/nunjucks/tags/link.js:
--------------------------------------------------------------------------------
1 | // @see https://mozilla.github.io/nunjucks/api.html#custom-tags
2 |
3 | // Standard lib.
4 | import { resolve as resolvePath } from 'path';
5 | import { inspect } from 'util';
6 |
7 | // Local modules.
8 | import { INPUT_DIRECTORY } from '~/lib/constants';
9 |
10 | // Exports.
11 | export default class LinkExtension {
12 | #memo = { };
13 |
14 | #nunjucksEngine;
15 |
16 | tags = ['link'];
17 |
18 | static #instance = null;
19 |
20 | constructor(nunjucksEngine) {
21 | this.#nunjucksEngine = nunjucksEngine;
22 | }
23 |
24 | parse(parser, nodes /* , lexer */) {
25 | // Get the tag token.
26 | const tok = parser.nextToken();
27 |
28 | // Parse the args and move after the block end.
29 | const args = parser.parseSignature(null, true);
30 | parser.advanceAfterBlockEnd(tok.value);
31 |
32 | return new nodes.CallExtension(this, 'run', args);
33 | }
34 |
35 | run({ ctx }, rawInputPath) {
36 | // Memoize result.
37 | const search = resolvePath(INPUT_DIRECTORY, rawInputPath);
38 | if (!Object.prototype.hasOwnProperty.call(this.#memo, search)) {
39 | const page = ctx.collections.all.find(({ inputPath }) => resolvePath(inputPath) === search);
40 | this.#memo[search] = page ? page.url : null;
41 | }
42 |
43 | // Return the result, or fail if no such page was found.
44 | const result = this.#memo[search];
45 | if (result === null) {
46 | throw new Error(`Invalid link: no page for ${inspect(rawInputPath)}`);
47 | }
48 | return new this.#nunjucksEngine.runtime.SafeString(result);
49 | }
50 |
51 | static singleton(nunjucksEngine) {
52 | if (LinkExtension.#instance === null) {
53 | LinkExtension.#instance = new LinkExtension(nunjucksEngine);
54 | }
55 | return LinkExtension.#instance;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/eleventy.config.js:
--------------------------------------------------------------------------------
1 | // @see https://www.11ty.io/docs/config/
2 |
3 | // Standard lib.
4 | import {
5 | basename,
6 | extname,
7 | join as joinPath,
8 | relative as relativePath,
9 | } from 'path';
10 |
11 | // Package modules.
12 | import EleventyImage from '@11ty/eleventy-img';
13 |
14 | // Local modules.
15 | import {
16 | INPUT_DIRECTORY,
17 | INTERMEDIATE_DIRECTORY,
18 | } from './lib/constants';
19 | import inspect from './lib/filters';
20 | import NunjucksLinkExtension from './lib/nunjucks/tags/link';
21 | import imageShortcode from './src/_shortcodes/image';
22 |
23 | // Constants.
24 | const ELEVENTY_IMAGE_DEFAULT_URL_PATH = '/images/';
25 |
26 | // Helpers.
27 | const formatImageFilename = (id, src, width, format /* , options */) => {
28 | const filename = basename(src, extname(src));
29 | return `${filename}.${width}w.${format}`;
30 | };
31 |
32 | // Exports.
33 | module.exports = (eleventyConfig) => {
34 | // Add universal filters.
35 | // @see https://www.11ty.io/docs/filters/
36 | eleventyConfig.addFilter('debug', inspect);
37 | eleventyConfig.addFilter('pageURL', ({ outputPath, url }) => {
38 | if (outputPath) {
39 | return joinPath('/', outputPath);
40 | }
41 | return eleventyConfig.getFilter('url')(url);
42 | });
43 |
44 | // Add utility to perform build-time image manipulation.
45 | eleventyConfig.addNunjucksAsyncShortcode('image', async (src, options) => {
46 | const urlPath = options?.urlPath || ELEVENTY_IMAGE_DEFAULT_URL_PATH;
47 | const stats = await EleventyImage(src, {
48 | filenameFormat: formatImageFilename,
49 | formats: [extname(src).substring(1)], // Use input format.
50 | ...options,
51 | outputDir: joinPath(INTERMEDIATE_DIRECTORY, urlPath),
52 | urlPath: joinPath('~', INTERMEDIATE_DIRECTORY, urlPath),
53 | });
54 | return imageShortcode(stats, options);
55 | });
56 |
57 | // Add custom tags.
58 | // @see https://www.11ty.io/docs/shortcodes/
59 | eleventyConfig.addNunjucksTag('link', NunjucksLinkExtension.singleton);
60 |
61 | // Copy static assets.
62 | eleventyConfig.addPassthroughCopy(joinPath(INPUT_DIRECTORY, '*.txt'));
63 |
64 | // Return configuration options.
65 | // @see https://www.11ty.io/docs/config/
66 | return {
67 | // @see https://www.11ty.io/docs/config/#input-directory
68 | dir: {
69 | layouts: relativePath(INPUT_DIRECTORY, joinPath(INPUT_DIRECTORY, '_layouts/')),
70 | },
71 |
72 | // @see https://www.11ty.io/docs/config/#default-template-engine-for-markdown-files
73 | markdownTemplateEngine: 'njk',
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eleventy-parcel-boilerplate",
3 | "version": "0.1.0",
4 | "description": "Starter kit for using Eleventy with Parcel, backed by Forestry.",
5 | "author": "Mark van Seventer ",
6 | "license": "MIT",
7 | "browserslist": "last 2 years and > 0.5%, ie 11",
8 | "homepage": "http://localhost:8080",
9 | "config": {
10 | "input": "src/",
11 | "intermediate": "tmp/",
12 | "output": "dist/"
13 | },
14 | "sideEffects": [
15 | "src/scripts/main.js"
16 | ],
17 | "targets": {
18 | "browser": {}
19 | },
20 | "scripts": {
21 | "lint": "cross-env eslint '*.js' lib/ $npm_package_config_input --ignore-pattern $npm_package_config_input'scripts/'",
22 |
23 | "11ty": "cross-env eleventy --config=./eleventy.config.js --input=$npm_package_config_input --output=$npm_package_config_intermediate",
24 | "parcel:build": "cross-env parcel build $npm_package_config_intermediate'*.*' --dist-dir $npm_package_config_output --no-cache --no-source-maps --public-url $npm_package_homepage",
25 | "parcel:watch": "cross-env parcel serve $npm_package_config_intermediate'*.*' --no-autoinstall --no-content-hash",
26 |
27 | "clean": "cross-env rimraf $npm_package_config_intermediate $npm_package_config_output",
28 |
29 | "watch:11ty": "run-s '11ty -- --watch'",
30 | "watch:parcel": "run-s parcel:watch",
31 | "prewatch": "run-p clean lint",
32 | "watch": "run-p watch:*",
33 |
34 | "prebuild": "run-p clean lint",
35 | "build": "BUILD_ENV=production run-s 11ty parcel:build",
36 |
37 | "start": "run-s watch"
38 | },
39 | "dependencies": {
40 | "@11ty/eleventy-img": "^0.5",
41 | "postcss-preset-env": "^6.7",
42 | "postcss-reporter": "^7",
43 | "posthtml-render": "^1.4",
44 | "stylelint": "^13.8",
45 | "tailwindcss": "^2",
46 | "~": "file:."
47 | },
48 | "devDependencies": {
49 | "@11ty/eleventy": "^0.11",
50 | "@babel/core": "^7.12",
51 | "@babel/plugin-proposal-class-properties": "^7.12",
52 | "@babel/plugin-proposal-private-methods": "^7.12",
53 | "@babel/plugin-transform-runtime": "^7.12",
54 | "@babel/preset-env": "^7.12",
55 | "@babel/register": "^7.12",
56 | "@babel/runtime-corejs3": "^7.12",
57 | "@parcel/validator-eslint": "2.0.0-nightly.464",
58 | "babel-eslint": "^10",
59 | "cross-env": "^7",
60 | "eslint": "^7.12",
61 | "eslint-config-airbnb-base": "^14.2",
62 | "eslint-plugin-import": "^2.20",
63 | "npm-run-all": "^4.1",
64 | "parcel": "2.0.0-nightly.462",
65 | "parcel-namer-custom": "^0.2",
66 | "parcel-optimizer-friendly-urls": "^0.2",
67 | "parcel-optimizer-imagemin": "^1",
68 | "postcss": "^8.1",
69 | "rimraf": "^3",
70 | "sharp": "^0.26",
71 | "stylelint-config-recommended": "^3",
72 | "stylelint-no-unsupported-browser-features": "^4.1"
73 | },
74 | "engines": {
75 | "node": ">=12.13"
76 | },
77 | "parcel-namer-custom": {
78 | ".css$": "[folder]/[name].[hash].[type]",
79 | ".jsx?$": "[folder]/[name].[hash].[type]",
80 | ".txt$": "[base]",
81 | "(tmp/)(.*).(gif|jpe?g|png|svg|webp)$": "[2].[hash].[type]"
82 | },
83 | "parcel-optimizer-imagemin": {
84 | "imagemin-pngquant": {
85 | "quality": [
86 | 0.6,
87 | 0.8
88 | ],
89 | "speed": 1,
90 | "strip": true
91 | }
92 | },
93 | "private": true
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eleventy-parcel-boilerplate
2 | > Starter kit for using [Eleventy] with [Parcel], backed by [Forestry].
3 |
4 | [Eleventy] is _a simpler static site generator_, which does a beautiful job of scaffolding your static site. However, a web application is so much more; what about images, stylesheets, or scripts? This is where [Parcel], a _zero configuration web application bundler_, comes in. By combining [Eleventy] with [Parcel], you can take your static site to the next level with minimal effort.
5 |
6 | As a bonus, this project is preconfigured to work out of the box with [Forestry], in case you use [Forestry] to edit your site content.
7 |
8 | ## Installation
9 | **Recommended**
10 |
11 | This project is set-up as a [Template Repository][1]. Click the "Use this template" button to create your new static site from this repository.
12 |
13 | **Others**
14 |
15 | 1. Clone the repository using `git clone https://github.com/vseventer/eleventy-parcel-boilerplate`.
16 | 2. Navigate to your project directory using `cd eleventy-parcel-boilerplate`.
17 | 3. Install the dependencies using `npm install`.
18 |
19 | _This project supports both `npm` and `yarn`, feel free to use whichever package manager you're most comfortable with._
20 |
21 | ## Getting Started
22 | Please familiarize yourself with [Eleventy] and [Parcel], and you will recognize the source directory contains all you need to get started with your new static site.
23 |
24 | By default, [Parcel] will mark all files in your top-level source directory as entry points of your static site. Typically, these are your `index.html`, `404.html`, `robots.txt`, or `CNAME`. This project assumes all other pages of your static site are referenced by any of these entry points, and Parcel will pick them up automatically.
25 |
26 | ## Development
27 | * To start the development server, run `npm start` or `npm run watch` and navigate to `http://localhost:8080`.
28 | * To build your site just once (for production), run `npm run build`.
29 |
30 | _The development server, [browser-sync], is provided by [Eleventy] and set-up to work in sync with [Parcel]._
31 |
32 | ## Configuration
33 | This project predefines a set of configuration files, which can be tweaked depending on your preferences.
34 |
35 | ### `package.json`
36 | The `browserslist` property reflects the browsers your static website supports, per [browserslist].
37 |
38 | The `homepage` property should reflect the URL of your production site. If you prefer to use absolute URLs, remove the `--public-url $npm_package_homepage` flag from the `parcel:build` npm script.
39 |
40 | The `config` block in `package.json` enumerates three directories:
41 | * `input`: the source of your web application.
42 | * `intermediate`: the output directory for [Eleventy], and input directory for [Parcel]. You should never directly modify contents in this directory.
43 | * `output`: the final build of your web application.
44 |
45 | ### `.babelrc`
46 | The [Babel] smart preset is used allowing you to use the latest JavaScript. Two separate plugins supporting (private) class methods and properties are added by default as well.
47 |
48 | ### `.eslintrc` and `src/.eslintrc`
49 | This project follows [Airbnb] configuration for [ESLint]. The source directory extends the base configuration, and makes sure you can use `process` in your JavaScript, as this is [supported][2] by [Parcel].
50 |
51 | Linting is ran on your configuration files, as well as the scripts in the source directory of your static site.
52 |
53 | ### `.stylelintrc`
54 | This project follows the recommended configuration for [stylelint], with support for SCSS-syntax. Linting is ran as part of [PostCSS] as explained below.
55 |
56 | ### `eleventy.config.js`
57 | The [Eleventy] configuration file adds support for running a staging environment, useful for [Forestry] integration. The development server is also updated to redirect 404 routes to your `404.html` page (if present in your project).
58 |
59 | In addition, it sets some sane defaults, as well as provide a boilerplate for how to add custom filters and tags. This project comes with two, a `debug` filter, and `link` custom Nunjucks tag.
60 |
61 | ### `postcss.config.js`
62 | The [PostCSS] configuration adds a number of plugins. Your stylesheets are linted with [stylelint], before being optimized with [PurgeCSS] (production only), and [autoprefixer].
63 |
64 | ### `posthtml.config.js`
65 | The [PostHTML] configuration adds a custom plugin to your pipeline, required to make [Eleventy] and [Parcel] play nice together. Do not remove this plugin unless you know what you are doing, or want your build to break.
66 |
67 | ### Parcel
68 | [Parcel] does not have a separate configuration file, but does pick-up on packages named `parcel-plugin-*`. Included with this project are:
69 | * `parcel-plugin-eslint`: required to run linting before building with [Parcel].
70 | * `parcel-plugin-keep-asset-folders`: recommended if you want to keep your assets source directory structure rather than storing them all in the top-level output folder.
71 | * `parcel-plugin-remove-index-html`: highly recommended if you want your final build to have nice URLs (`https://example.com/` vs `https://example.com/index.html`).
72 |
73 | ## Content Management
74 | Content of your site lives in the `src/` directory by default.
75 |
76 | If you are using [Forestry] to manage your content, import your site by following the steps in the [Forestry] Dashboard. This project is set-up so that the [Instant Preview][3] functionality of Forestry will work out of the box.
77 |
78 | ## Alternatives
79 | * [parceleventy] (not actively maintained)
80 |
81 | ## License
82 | The MIT License (MIT)
83 |
84 | Copyright (c) 2019 Mark van Seventer
85 |
86 | Permission is hereby granted, free of charge, to any person obtaining a copy of
87 | this software and associated documentation files (the "Software"), to deal in
88 | the Software without restriction, including without limitation the rights to
89 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
90 | the Software, and to permit persons to whom the Software is furnished to do so,
91 | subject to the following conditions:
92 |
93 | The above copyright notice and this permission notice shall be included in all
94 | copies or substantial portions of the Software.
95 |
96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
98 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
99 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
100 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
101 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
102 |
103 | [eleventy]: https://www.11ty.io/
104 | [airbnb]: https://github.com/airbnb/javascript
105 | [autoprefixer]: https://github.com/postcss/autoprefixer
106 | [babel]: https://babeljs.io/
107 | [browser-sync]: https://www.browsersync.io/
108 | [browserslist]: https://github.com/browserslist/browserslist
109 | [eslint]: https://eslint.org/
110 | [forestry]: https://forestry.io/
111 | [parcel]: https://parceljs.org/
112 | [parceleventy]: https://github.com/chrisdmacrae/parceleventy
113 | [postcss]: https://postcss.org/
114 | [posthtml]: https://github.com/posthtml/posthtml
115 | [purgecss]: https://www.purgecss.com/
116 | [stylelint]: https://stylelint.io/
117 | [1]: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template
118 | [2]: https://parceljs.org/env.html
119 | [3]: https://forestry.io/docs/previews/instant-previews/
120 |
--------------------------------------------------------------------------------