https://jkc.codes/John Kemp-Cruz (Atom feed)Where John writes about web development2023-02-05T15:00:15.000Zhttps://jkc.codes/img/site/icon-site-128x128.pngJohn Kemp-Cruzhttps://jkc.codeshttps://jkc.codes/blog/creating-drafts-in-eleventy/Creating Drafts In Eleventy2023-02-05T15:00:15.000ZJohn Kemp-Cruzhttps://jkc.codes<p><small>(About <time datetime="PT569S">9 minutes</time> to read)</small></p>
<p>Several static site generators and content management systems have built in functionality to mark posts as drafts. Eleventy (11ty) isn't one of these but fortunately it is possible to implement.</p>
<p>The core concept is to use two <a href="https://www.11ty.dev/docs/data-frontmatter/">front matter</a> <a href="https://www.11ty.dev/docs/data-configuration/">keys</a> — <a href="https://www.11ty.dev/docs/permalinks/"><code>permalink</code></a> and <a href="https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections"><code>eleventyExcludeFromCollections</code></a> — to hide pages from users and then <a href="https://www.11ty.dev/docs/data-computed/">computed data</a> with environment variables to automatically toggle visibility depending on the environment.</p>
<h2>What Functionality Is Needed?</h2>
<h3>The <code>permalink</code> key</h3>
<p>The <code>permalink</code> front matter key controls where a file is built to. More practically, it dictates what the URL will be for the page.</p>
<p>When we don't want to create files for drafts or give them a URL, <code>permalink</code> should be set to <code>false</code>. To quote the <a href="https://www.11ty.dev/docs/permalinks/#permalink-false">Eleventy permalink docs</a>: <q cite="https://www.11ty.dev/docs/permalinks/#permalink-false">If you set the <code>permalink</code> value to be <code>false</code>, this will disable writing the file to disk in your output folder. The file will still be processed normally (and present in collections, with its <a href="https://www.11ty.dev/docs/data-eleventy-supplied/">url and outputPath properties</a> set to <code>false</code>) but will not be available in your output directory as a standalone template.</q></p>
<p>We don't want the file to still be present in collections though, that's where <code>eleventyExcludeFromCollections</code> comes in.</p>
<h3>The <code>eleventyExcludeFromCollections</code> key</h3>
<p>The <code>eleventyExcludeFromCollections</code> front matter key does what it says — it excludes pages from <a href="https://www.11ty.dev/docs/collections/">collections</a>. Collections are sets of data from related content that can be used to create dynamic pages such as RSS feeds, site maps or blog post lists.</p>
<p><code>eleventyExcludeFromCollections</code> should be set to <code>true</code> to hide drafts from lists created from collections. To quote the <a href="https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections">Eleventy collection docs</a>: <q cite="https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections">In front matter (or further upstream in the data cascade), set the <code>eleventyExcludeFromCollections</code> option to <code>true</code> to opt out of specific pieces of content added to all collections (including <code>collections.all</code>, collections set using tags, or collections added from the Configuration API in your config file). Useful for your RSS feed, <code>sitemap.xml</code>, custom templated <code>.htaccess</code> files, et cetera.</q></p>
<p>Pages can be hidden on demand by combining <code>permalink</code> and <code>eleventyExcludeFromCollections</code> but what we really want is for them to only be hidden in live production sites so we can continue to test locally. Computed data lets us do that.</p>
<h3>Computed Data</h3>
<p><a href="https://www.11ty.dev/docs/data-computed/">Computed data</a> allows modification of front matter values dynamically based on passed in data from the page, front matter or elsewhere. Front matter is usually static but with computed data we can check whether the <code>permalink</code> and <code>eleventyExcludeFromCollections</code> keys need to be toggled based on whether we're working locally or building our site for production.</p>
<h3>Environment Variables</h3>
<p>Environment variables aren't an Eleventy specific feature so I won't be covering them in any detail but in short they provide a global variable which we can toggle depending on where your code is running.</p>
<p>We will be using the <a href="https://www.npmjs.com/package/dotenv">dotenv NPM package</a> because it provides a consistent and easy set up across operating systems.</p>
<h2>How To Create Drafts</h2>
<p>There are three ways popularised by Jekyll to handle drafts which I'll cover:</p>
<ul>
<li>Using draft keys in front matter</li>
<li>Setting a future date</li>
<li>Using a draft folder</li>
</ul>
<p>I'll be focusing on blog posts here but these methods can be used on any type of page as long as there's front matter somewhere in the <a href="https://www.11ty.dev/docs/data-cascade/">data cascade</a>.</p>
<h3>Set Up Your Environment</h3>
<p>The following steps assume that you're building your site on a server using a service like Netlify.</p>
<p>First, install dotenv using <code>npm i dotenv</code> in the command line.</p>
<p>Next, create a <code>.env</code> file in your root directory (the same folder as <code>package.json</code>) and add the following inside the file:</p>
<pre><code class="language-text">ELEVENTY_ENV=development</code></pre>
<p>This is gives us a global variable "ELEVENTY_ENV" with the value "development" we can import into files later.</p>
<p>Finally, we need to prevent sending the <code>.env</code> file to the server so that it doesn't think it's in a development environment. Do this by adding your <code>.env</code> file to your <code>.gitignore</code> file (create one in your root folder if it doesn't exist):</p>
<pre><code class="language-text">.env
node_modules</code></pre>
<p>With environment variables set up we can start using them in computed data.</p>
<h3>Using Draft Keys Or A Future Date In Front Matter</h3>
<p>The goal here is to either:</p>
<ul>
<li>Set a <code>draft</code> key to <code>true</code> or <code>false</code> in the front matter of a page to determine whether it's hidden or not; or</li>
<li>Set a <a href="https://www.11ty.dev/docs/dates/"><code>date</code> key</a> in the front matter of a page to a future date and only show that page if a build is triggered on or after that date</li>
</ul>
<pre><code class="language-yaml">---
date: 2150-12-31
draft: true
// Other front matter
---
// Page content</code></pre>
<p>To do so we'll need to add a <a href="https://www.11ty.dev/docs/data-template-dir/">directory specific data file</a> which allows us to add the same front matter to all files in a folder:</p>
<pre>
blog
|- blog.11tydata.js
|- first-post.md
|- second-post.md
|- third-post.md
</pre>
<p>I've used <code>blog.11tydata.js</code> here as it's in the blog folder. If you're using a different folder name replace "blog" in the file name for the folder's name.</p>
<p>Inside of the 11tydata.js file we'll export our front matter data:</p>
<pre><code class="language-js">require('dotenv').config();
const isDevEnv = process.env.ELEVENTY_ENV === 'development';
const todaysDate = new Date();
function showDraft(data) {
const isDraft = 'draft' in data && data.draft !== false;
const isFutureDate = data.page.date > todaysDate;
return isDevEnv || (!isDraft && !isFutureDate);
}
module.exports = function() {
return {
eleventyComputed: {
eleventyExcludeFromCollections: function(data) {
if(showDraft(data)) {
return data.eleventyExcludeFromCollections;
}
else {
return true;
}
},
permalink: function(data) {
if(showDraft(data)) {
return data.permalink
}
else {
return false;
}
}
}
}
}</code></pre>
<p>On <b>line 1</b> we're importing dotenv so we can read the environment variables set in our <code>.env</code> file from a <code>process.env</code> object.</p>
<p>On <b>line 3</b> we're reading the <code>ELEVENTY_ENV</code> environment variable from the <code>process.env</code> object and using it to assign a boolean value to an <code>isDevEnv</code> variable.</p>
<p>On <b>line 4</b> we're creating a new date object containing today's date.</p>
<p><b>Lines 6–10</b> contain the function which will return a boolean determining whether to show the draft page or not.</p>
<p>On <b>line 7</b> we're checking if a <code>draft</code> key has been set to <code>true</code> in the page's front matter. Note that using <code class="lang-js">const isDraft = data.draft === true;</code> would also work but because of type coercion any typos would assume that the page is not a draft. By explicitly checking for a draft key and that it isn't set to false we can be sure it's meant to be public.</p>
<p>On <b>line 8</b> we're checking the front matter date against today's date from line 4 to see if it's greater than today's date.</p>
<p>On <b>line 9</b> we're using the <code>isDevEnv</code> variable from line 3, the <code>isDraft</code> result from line 7 and the <code>isFutureDate</code> variable from line 8 to return a boolean result confirming whether the page should be shown or not.</p>
<p><b>Lines 12–33</b> are exporting our front matter data for Eleventy to use in its data cascade. The <code>eleventyComputed</code> key is the only key I'm including here but you can add other front matter keys such as <code>tags</code> in the return object if you need to.</p>
<p><b>Lines 15–22 and 23–30</b> are where we're determining the <code>eleventyExcludeFromCollections</code> and <code>permalink</code> values based on the result of the <code>showDraft</code> function being passed <a href="https://www.11ty.dev/docs/data-eleventy-supplied/">data supplied by Eleventy</a>. If the <code>showDraft</code> function from line 6 returns true, we're using the existing values but if it returns false we're overriding the existing values to true and false to exclude the page from collections and prevent the page being built respectively.</p>
<h3>Using A Draft Folder</h3>
<p>The goal here is to be able to move files in and out of a drafts folder to hide or show them. To do so we'll need to add a <a href="https://www.11ty.dev/docs/data-template-dir/">directory specific data file</a> to the drafts folder so the same front matter is added to all files in that folder:</p>
<pre>
blog
|- first-post.md
|- second-post.md
drafts
|- drafts.11tydata.js
|- third-post.md
</pre>
<p>I've used <code>drafts.11tydata.js</code> here as it's in the drafts folder. If you're using a different folder name replace "drafts" in the file name for the folder's name.</p>
<p>Inside of the 11tydata.js file we'll export our front matter data:</p>
<pre><code class="language-js">require('dotenv').config();
const isDevEnv = process.env.ELEVENTY_ENV === 'development';
module.exports = function() {
return {
eleventyComputed: {
eleventyExcludeFromCollections: function(data) {
if(isDevEnv) {
return data.eleventyExcludeFromCollections;
}
else {
return true;
}
},
permalink: function(data) {
if(!isDevEnv) {
return false;
}
else if(data.permalink !== '') {
return data.permalink;
}
else {
return data.page.filePathStem.replace('/drafts/', '/blog/') + '/';
}
}
}
}
}</code></pre>
<p>On <b>line 1</b> we're importing dotenv so we can read the environment variables set in our <code>.env</code> file from a <code>process.env</code> object.</p>
<p>On <b>line 3</b> we're reading the <code>ELEVENTY_ENV</code> environment variable from the <code>process.env</code> object and using it to assign a boolean value to an <code>isDevEnv</code> variable.</p>
<p><b>Lines 5–29</b> are exporting our front matter data for Eleventy to use in its data cascade. The <code>eleventyComputed</code> key is the only key I'm including here but you can add other front matter keys such as <code>tags</code> in the return object if you need to.</p>
<p><b>Lines 8–15 and 16–26</b> are where we're determining the <code>eleventyExcludeFromCollections</code> and <code>permalink</code> values based on <code>isDevEnv</code> from line 3.</p>
<p>On <b>lines 9–14</b> we're returning the existing <code>eleventyExcludeFromCollections</code> value if we're in a development environment or true if we're not.</p>
<p>On <b>lines 17–19</b> we're checking if we're in a development environment and setting the <code>permalink</code> key to <code>false</code> if we're not.</p>
<p>On <b>lines 20–22</b> we're checking if a permalink has been explicitly set in the front matter and using it if so.</p>
<p>On <b>lines 23–25</b> we're modifying the page's default permalink so that the "drafts" part of the path is replaced by "blog". Instead of "/drafts/third-post/" we'll output "/blog/third-post/". Note that <a href="https://www.11ty.dev/docs/permalinks/#remapping-output-(permalink)">adding the trailing slash on the end is important</a>.</p>
<h2>Summary</h2>
<p>The <code>permalink</code> and <code>eleventyExcludeFromCollections</code> front matter keys allow us to create drafts in Eleventy:</p>
<pre><code class="language-yaml">---
permalink: false
eleventyExcludeFromCollections: true
---</code></pre>
<p>By leveraging computed data and environment variables we can replicate Jekyll's draft behaviour:</p>
<pre>Command line
<code class="lang-shell">npm i dotenv</code></pre>
<pre>.env
<code class="lang-text">ELEVENTY_ENV=development</code></pre>
<pre>.gitignore
<code class="lang-text">.env
node_modules</code></pre>
<pre>blog.11tydata.js
<code class="lang-js">require('dotenv').config();
const isDevEnv = process.env.ELEVENTY_ENV === 'development';
const todaysDate = new Date();
function showDraft(data) {
const isDraft = 'draft' in data && data.draft !== false;
const isFutureDate = data.page.date > todaysDate;
return isDevEnv || (!isDraft && !isFutureDate);
}
module.exports = ()=> {
return {
eleventyComputed: {
eleventyExcludeFromCollections: data => showDraft(data) ? data.eleventyExcludeFromCollections : true,
permalink: data => showDraft(data) ? data.permalink : false,
}
}
}</code></pre>
<pre>drafts.11tydata.js
<code class="lang-js">require('dotenv').config();
const isDevEnv = process.env.ELEVENTY_ENV === 'development';
module.exports = ()=> {
return {
eleventyComputed: {
eleventyExcludeFromCollections: data => isDevEnv ? data.eleventyExcludeFromCollections : true,
permalink: data => {
if(!isDevEnv) { return false; }
return data.permalink !== '' ? data.permalink : data.page.filePathStem.replace('/drafts/', '/blog/') + '/';
}
}
}
}</code></pre>
<pre>post.md
<code class="lang-yaml">---
draft: true
---</code></pre>
<h2>Further Reading</h2>
<ul>
<li><a href="https://www.11ty.dev/docs/permalinks/">Permalinks documentation</a></li>
<li><a href="https://www.11ty.dev/docs/collections/">Collections documentation</a></li>
<li><a href="https://www.11ty.dev/docs/data-computed/">Computed data documentation</a></li>
<li><a href="https://www.11ty.dev/docs/data-cascade/">Data cascade documentation</a></li>
<li><a href="https://www.11ty.dev/docs/data-template-dir/">Directory specific data files documentation</a></li>
<li><a href="https://www.11ty.dev/docs/quicktips/draft-posts/">Draft posts using computed data</a></li>
</ul>
2021-08-06T13:39:37.000Zhttps://jkc.codes/blog/building-a-robust-toggle-menu/Building A Robust Toggle Menu2021-07-01T18:30:47.000ZJohn Kemp-Cruzhttps://jkc.codes<p><small>(About <time datetime="PT499S">8 minutes</time> to read)</small></p>
<p>I recently noticed that my site's speed index had shot up dramatically on the <a href="https://www.11ty.dev/speedlify/">Eleventy leaderboards</a> and managed to track the issue down to my mobile navigation menu. I was deliberately leaving the menu open until JavaScript kicked in to keep it accessible but this had the unfortunate side effect of it always being briefly visible at each page load.</p>
<p>Here's the thought process I went through when fixing it so that it doesn't affect my site's speed index or layout shift but still remains usable even if CSS and/or JavaScript fail.</p>
<h2>The Problem</h2>
<p>My website has always been built with progressive enhancement and graceful degradation in mind. The goal of these two concepts is to ensure that your content is always available no matter the device or connection viewers use.</p>
<p>Progressive enhancement uses a "bottom up" approach where you build a minimum viable experience and build enhancements on top of that, progressively adding more and more on top. If any of those enhancements fail you know there's a base experience to fall back to.</p>
<p>Graceful degradation uses a "top down" approach which starts with fully enhanced code and then asks what would happen if each piece failed and implements fall backs to handle those errors. This way, instead of your site outright failing, it gracefully degrades to a less enhanced version instead.</p>
<p>I had chosen to keep my mobile navigation menu open until JavaScript downloaded and executed to ensure it was always available. Otherwise, if the menu was initially closed and JavaScript failed to work then the open menu button would do nothing and users wouldn't be able to navigate my site. Unfortunately, the slower the connection the longer it took to close and this sometimes led to a brief flash of the menu on page load.</p>
<h2>Possible Solutions</h2>
<p>Here are the solutions I considered when trying to fix the problem. If you have your own that I didn't think of let me know!</p>
<h3>Do Nothing</h3>
<p>It may feel bad to leave a known bug in my code but if a solution causes side effects that outweigh this minor inconvenience then doing nothing is the better option.</p>
<h3>Closed By Default</h3>
<p>If the problem was the menu being open when it should be closed, having it closed by default would solve the problem, right? Unfortunately, that would make my site almost impossible to navigate if JavaScript failed to load because the menu would be permanently closed.</p>
<h3><code><noscript></code> Element</h3>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript"><code><noscript></code> element</a> allows you to insert HTML if JavaScript can't function. So I could close the menu by default but open it using <code><noscript></code> if JavaScript was unavailable. This sounded like the perfect solution until I read the description on MDN more carefully: <q cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript">The HTML <code><noscript></code> element defines a section of HTML to be inserted if a script type on the page is unsupported or if scripting is currently turned off in the browser</q>.</p>
<p>The description only specifies JavaScript being unsupported or turned off; <strong><code><noscript></code> doesn't work if JavaScript fails to load due to a connection error</strong>. Being realistic, hardly anyone is going to turn off JavaScript these days but plenty of people are going to have errors from a bad mobile connection.</p>
<h3>Inline The JavaScript</h3>
<p>Placing a <code><script></code> element inline with my HTML would ensure that JavaScript can't fail to load due to a connection error. I could combine it with closing the menu by default and a <code><noscript></code> element to apply open menu styles if JavaScript is unsupported. However, there are three trade offs with this approach:</p>
<ol>
<li>The JavaScript needs to be duplicated for each page, increasing build complexity.</li>
<li>Running the script delays page rendering.</li>
<li>The browser can no longer cache the script, increasing load times.</li>
</ol>
<p>The first issue is quite easily solved by <a href="https://www.11ty.dev/">Eleventy</a>, my static site generator, and the remaining issues are negligible. The JavaScript for my navigation menu is only around 2.5kb and not being able to cache 2.5kb of code is better than seeing an open menu on each page load.</p>
<h3>Checkbox Input</h3>
<p>Using an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox"><code><input></code> element with a <code>type="checkbox"</code> attribute</a> gives us access to the <code>checked</code> attribute whenever it's selected. Unlike a <code><button></code> element, this means that I could determine whether the menu is open without any JavaScript. The code would go something like this:</p>
<pre><code class="language-html"><nav>
<label for="nav-menu-checkbox" hidden>Open menu</label>
<input type="checkbox" id="nav-menu-checkbox" aria-controls="nav-menu-content" hidden>
<ul id="nav-menu-content">
<li>Nav item 1</li>
<li>Nav item 2</li>
<li>Nav item 3</li>
</ul>
</nav></code></pre>
<pre><code class="language-css">label[for="nav-menu-checkbox"],
input[type="checkbox"] {
display: inline-block;
}
button[aria-expanded="false"] + ul,
input + ul {
transition-delay: 0ms, 0.25s;
transform: translateY(-100%);
visibility: hidden;
}
button[aria-expanded="true"] + ul,
input:checked + ul {
transition-delay: 0ms;
transform: translateY(0);
visibility: visible;
}
button[aria-expanded] + ul,
input + ul {
transition: 0.25s ease-out;
transition-property: transform, visibility;
}</code></pre>
<p>Here's a brief explanation of the less obvious bits:</p>
<ul>
<li>The <code><label></code> and <code><input></code> elements have a <code>hidden</code> attribute which is overridden by <code>display: inline-block</code> in the CSS. This ensures that if the CSS isn't loaded there won't be a redundant checkbox that doesn't work. If you're wondering when this would be applicable, read <a href="https://www.sarasoueidan.com/blog/tips-for-reader-modes/">Sara Soueidan's article on optimising content for reader modes and reading apps</a>.</li>
<li>There's an <code>aria-controls</code> attribute on the <code><input></code> linked to the nav menu list. This tells assistive technology that supports it that the two are linked so enhanced functionality can be provided.</li>
<li>The <code>:checked</code> pseudo class is combined with an <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator"><code>adjacent sibling combinator</code></a> to toggle the menu. You could also use the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator"><code>general sibling combinator</code></a> if elements are farther apart.</li>
<li><code>visibility: hidden</code> is used instead of <code>display: none</code> or <code>opacity: 0</code> to keep transitions while still removing the menu from the DOM when closed. <code>display: none</code> would hide the menu before the transition could finish and <code>opacity: 0</code> would hide the menu after the transition but only visually, causing issues with assistive technology.</li>
<li><code>transition-delay: 0ms, 0.25s</code> is used when closing the menu to allow the transform to finish before <code>visibility</code> is set to <code>hidden</code>. <code>visibility</code> doesn't fade like <code>opacity</code> but it is still an <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties">animatable property</a>.</li>
<li>There are selectors targeting a <code><button></code> with an <code>aria-expanded</code> attribute because <strong>the <code><input></code> and <code><label></code> must be replaced with JavaScript as soon as possible</strong> to avoid accessibility issues. <code><input></code> doesn't have the same functionality as <code><button></code> such as being triggered by the enter key or being listed in shortcut menus.</li>
</ul>
<p>To be clear: <strong>using a checkbox to control a menu should only be used as a temporary measure until JavaScript replaces it with a button</strong>.</p>
<h3><code><details></code> And <code><summary></code> Elements</h3>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"><code><details></code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"><code><summary></code></a> elements provide an HTML native menu that can be toggled open and closed without any JavaScript. The ease of creating a navigation menu is why so many sites are now — <i>checks notes</i> — <em>not</em> using it?</p>
<p><code><details></code> and <code><summary></code> were first supported in 2011 but didn't receive full adoption from all the major browser engines until 2020 when Edge switched to Chromium. However, <a href="https://caniuse.com/details">current support for <code><details></code> and <code><summary></code></a> is excellent and it's only Internet Explorer that doesn't and will never recognise them, treating them each as a <code><div></code> instead.</p>
<p>Another reason why there isn't more widespread use is because the reveal of content within <code><details></code> isn't animatable — but there is a way around this. Usually, everything within a closed <code><details></code> element except <code><summary></code> is removed from the DOM which doesn't give any time for a closing animation to end before it's removed. This can be avoided by moving the content outside of the <code><details></code> element:</p>
<pre><code class="language-html"><nav>
<details>
<summary aria-controls="nav-menu-content" hidden>Open menu</summary>
</details>
<ul id="nav-menu-content">
<li>Nav item 1</li>
<li>Nav item 2</li>
<li>Nav item 3</li>
</ul>
</nav></code></pre>
<p>You can then use the checkbox solution from above to toggle the content by watching for an <code>open</code> attribute on <code><details></code>.</p>
<h3>No Collapsible Menu</h3>
<p>The rest of these solutions assume that a collapsible navigation menu is unavoidable but it's possible to not have a menu and eliminate the problem altogether. The only reason I have one in the first place is because the existing design wraps the text 2–3 lines deep. There's no reason I couldn't change the menu style to accommodate all of the links.</p>
<h2>My Decision</h2>
<p>After eliminating doing nothing, closing the menu by default and using a <code><noscript></code> element because there were better alternatives, I was left with 4 options:</p>
<ol>
<li>No collapsible menu would have been the ideal choice but I don't have the design skills to do this quickly and make it look good.</li>
<li>Using <code><details></code> and <code><summary></code> elements was my next favourite but supporting Internet Explorer meant this wasn't possible.</li>
<li>I decided against inline JavaScript simply because I hate making users download data they don't have to.</li>
<li>In the end I went with a temporary checkbox input that's replaced by a proper button later.</li>
</ol>
<p>My <a href="https://github.com/JKC-Codes/jkc-codes.github.io/blob/76f9b0ba3cec59a5d86b5256e494c9548c284ca0/site/Markup/_templates/_includes/header.html">HTML</a>, <a href="https://github.com/JKC-Codes/jkc-codes.github.io/blob/76f9b0ba3cec59a5d86b5256e494c9548c284ca0/site/Styles/site/_header.scss">CSS/SASS</a> and <a href="https://github.com/JKC-Codes/jkc-codes.github.io/blob/76f9b0ba3cec59a5d86b5256e494c9548c284ca0/site/Scripts/site.js">JavaScript</a> files are available on <a href="https://github.com/JKC-Codes/jkc-codes.github.io/tree/76f9b0ba3cec59a5d86b5256e494c9548c284ca0">GitHub</a> if you're interested but remember that I didn't choose the best method on paper because there were other constraints limiting me. The choice came down to the least worse out of downloading extra JavaScript for all users or the menu being less accessible (but still accessible) to specific users until JavaScript loaded.</p>
<p>With this change I managed to knock more than a second off my speed index, from 2.18 seconds to 1.16 and move from 29<sup>th</sup> place to 13<sup>th</sup> on the <a href="https://www.11ty.dev/speedlify/#site-7702f769">Eleventy leaderboards</a>!</p>
2021-07-01T18:30:47.000Zhttps://jkc.codes/blog/using-sass-with-eleventy/Using SASS With Eleventy2021-03-17T15:08:01.000ZJohn Kemp-Cruzhttps://jkc.codes<p><small>(About <time datetime="PT296S">5 minutes</time> to read)</small></p>
<p><a href="https://www.11ty.dev/">Eleventy (11ty)</a> is a super customisable static site generator that at its core transforms template language into HTML. However, template languages like Liquid and Nunjucks are designed to output HTML rather than CSS so how does Eleventy handle styling?</p>
<p>Let me show you how I compile SASS automatically and display the output on a local server without triggering a build from Eleventy or refreshing the browser.</p>
<p>If you haven't already, you'll need to install <a href="https://nodejs.org/">Node</a>, create a package.json file by typing <code>npm init</code> in the command line and then run <code>npm i @11ty/eleventy</code>.</p>
<h2>Transform SCSS Files</h2>
<p>I use the terminal and <a href="https://sass-lang.com/documentation/cli/dart-sass">SASS' CLI commands</a> to compile CSS but you can use any build system and skip this section if you want. The only Eleventy specific thing is placing the CSS in Eleventy's output folder — by default this is "_site".</p>
<p>First, install the <a href="https://www.npmjs.com/package/sass">SASS package</a>: <code>npm i sass</code>.</p>
<p>Then, tell SASS where the SCSS files are and where to output the CSS. I do this through NPM so I don't need to type out the command every time.</p>
<p>Assuming your file structure looks like this:</p>
<pre>
_site
|- index.html
|- css
|- styles.css
sass
|- styles.scss
index.html
</pre>
<p>You would use the following in <code>package.json</code>:</p>
<pre><code class="language-json">"scripts": {
"watch:sass": "npx sass sass:_site/css --watch"
},</code></pre>
<p>This allows you to enter <code>npm run watch:sass</code> in the command line to take any .scss files in the "sass" directory and put them in the "_site/css" directory as .css files any time a change is made.</p>
<p>You can rename the script from "watch:sass" to anything you like. Similarly, you can customise the input and output paths; the sass input goes before the : and the css output goes after it. There are also a number of flags you can customise other than the <code>--watch</code> flag, full details are in the <a href="https://sass-lang.com/documentation/cli/dart-sass">SASS documentation</a>.</p>
<h2>Refresh The Browser</h2>
<p>In order to show the newly converted CSS live in the browser I use Eleventy's <code>--serve</code> command.</p>
<h3>What Is <code>eleventy --serve</code>?</h3>
<p><code>eleventy --serve</code> is a command line instruction that can be broken down into 3 steps: build, watch and serve.</p>
<h4>Build</h4>
<p>The build step will tell Eleventy to take template files like Markdown, Nunjucks or Liquid and create HTML files from them. It is equivalent to running <code>eleventy</code> in the command line.</p>
<h4>Watch</h4>
<p>The watch step will run the above build step every time a change is made to any of the template files. It is equivalent to running <code>eleventy --watch</code> in the command line.</p>
<h4>Serve</h4>
<p>The serve step will start a local server to display your website and update it automatically whenever files are changed through the above watch step. It is similar to running <code>eleventy --watch & browser-sync start</code> in the command line.</p>
<p>Eleventy uses Browsersync under the hood to handle its live server. Notably this means that any options are detailed in the <a href="https://browsersync.io/docs/">Browsersync docs</a> and not the <a href="https://www.11ty.dev/docs/">Eleventy docs</a>.</p>
<h3>Configuring BrowserSync</h3>
<p>BrowserSync is automatically run when using <code>eleventy --serve</code> and its options are set via <a href="https://www.11ty.dev/docs/watch-serve/#override-browsersync-server-options">EleventyConfig's <code>setBrowserSyncConfig</code> method</a>.</p>
<p>In my <a href="https://www.11ty.dev/docs/config/">Eleventy config file</a> (.eleventy.js by default) I add this:</p>
<pre><code class="language-js">module.exports = function(eleventyConfig) {
eleventyConfig.setBrowserSyncConfig({
files: './_site/css/**/*.css'
});
};</code></pre>
<p>I'm using BrowserSync's <a href="https://browsersync.io/docs/options#option-files">files option</a> to watch any file in the <code>_site/css/</code> folder with a <code>.css</code> extension and add that CSS to the page whenever those files update. It doesn't use Eleventy's build command and therefore doesn't trigger a rebuild of the HTML.</p>
<h3>What about addPassthroughCopy?</h3>
<p>You may have seen other sites build their CSS files in the same folder as their SASS and then use Eleventy config's <a href="https://www.11ty.dev/docs/copy/">addPassthroughCopy</a> method to copy the CSS to Eleventy's output folder. The file structure looks something like this:</p>
<pre>
_site
|- index.html
|- css
|- styles.css
sass
|- styles.scss
css
|- styles.css
index.html
</pre>
<p>This works, but there are two reasons why I don't like it.</p>
<ol>
<li>The CSS folder is duplicated which makes unnecessary writes to my hard drive and uses additional space.</li>
<li>It increases build times because the SCSS file and the CSS file both trigger an Eleventy build. In the terminal you see a message like this: <samp>You saved while Eleventy was running, let’s run again. (1 remain)</samp>.</li>
</ol>
<h3>What about addWatchTarget?</h3>
<p>Eleventy config's <a href="https://www.11ty.dev/docs/watch-serve/">addWatchTarget</a> method allows you to specify a file or folder which will trigger an Eleventy build whenever it's updated.</p>
<p>In theory this means that we could watch our SASS folder but in practice it creates a race condition which hopes that SASS will create a CSS file faster than Eleventy can start its build. You could watch the CSS folder instead but this creates the same problems as addPassthroughCopy.</p>
<h2>Wrapping up</h2>
<p>With the configuration set up, the last step is to run Eleventy and SASS together. I use VS Code's Tasks to do this but you can update your <code>package.json</code> with this instead:</p>
<pre><code class="language-json">"scripts": {
"watch:eleventy": "npx @11ty/eleventy --serve",
"watch:sass": "npx sass sass:_site/css --watch",
"start": "npm run watch:eleventy & npm run watch:sass"
},</code></pre>
<p>And then run <code>npm start</code> in your terminal whenever you open your project.</p>
<p>If you're using Windows and/or Powershell, the start script's <code>&</code> syntax won't work so I recommend using <a href="https://www.npmjs.com/package/npm-run-all">npm-run-all</a> or <a href="https://www.npmjs.com/package/concurrently">concurrently</a> to run the two scripts at the same time instead.</p>
<h3>Advantages</h3>
<ol>
<li>Zero build time when CSS is updated</li>
<li>No duplicated CSS files</li>
<li>No additional dependencies</li>
</ol>
<h3>Disadvantages</h3>
<ol>
<li>Doesn't work with inline styles</li>
<li>Some configuration is outside of Eleventy</li>
</ol>
<h3>TL;DR</h3>
<pre>
package.json
<code class="lang-json">"scripts": {
"watch:eleventy": "npx @11ty/eleventy --serve",
"watch:sass": "npx sass sass:_site/css --watch",
"start": "npm run watch:eleventy & npm run watch:sass"
},</code>
</pre>
<pre>
.eleventy.js
<code class="lang-js">module.exports = function(eleventyConfig) {
eleventyConfig.setBrowserSyncConfig({
files: './_site/css/**/*.css'
});
};</code>
</pre>
<pre>
command line
<code class="lang-shell">npm start</code>
</pre>
<h3>Further Reading</h3>
<ul>
<li><a href="https://www.11ty.dev/docs/watch-serve/">11ty setBrowserSyncConfig documentation</a></li>
<li><a href="https://browsersync.io/docs/options#option-files">BrowserSync files option documentation</a></li>
<li><a href="https://sass-lang.com/documentation/cli/dart-sass">SASS CLI documentation</a></li>
<li><a href="https://www.11ty.dev/docs/quicktips/inline-css/">How to inline minified CSS</a></li>
</ul>
2021-03-17T15:08:01.000Z