Sia Karamalegos | BlogPerformance engineer and web developer, speaker, teacher, community organizer, and Google Developers Expert in Web Technologies based in Durham, North Carolina2024-02-08T00:00:00Zhttps://sia.codes/https://sia.codes/img/favicons/rss_144x144.pngSia Karamalegos | Blog144144Sia Karamalegossiakaramalegos@gmail.comRails folder tree explained2015-05-12T00:00:00Zhttps://sia.codes/posts/rails-folder-tree/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/folders_b2nyci.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/folders_b2nyci.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/folders_b2nyci.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/folders_b2nyci.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/folders_b2nyci.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Shelves of multi-colored folders" importance="high" width="4898" height="3265" />
<figcaption>Photo by <a href="https://unsplash.com/@mvdheuvel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Maarten van den Heuvel</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>Sometimes you just don’t like any one particular reference out there. This happened to me when I was trying to build lesson content for our first lesson on Rails. Oh, by the way, I’m teaching a coding bootcamp here in New Orleans right now (fun stuff!).</p>
<p>To summarize, I found a lot of different content explaining the folder tree and files for Rails, but no one particular source spoke to me above all others. So, I decided to mash them all together and only leave the bits I liked, modifying other bits, and adding some new bits.</p>
<p>First, I want to thank these sources for their initial great explanations - you should check them out also in case they speak more to you than the one I generated here.</p>
<ul>
<li><a href="http://guides.rubyonrails.org/getting_started.html#creating-the-blog-application">RailsGuides: Getting started with Rails</a></li>
<li><a href="http://www.tutorialspoint.com/ruby-on-rails/rails-directory-structure.htm">TutorialsPoint: Ruby on Rails directory structure</a></li>
<li><a href="http://www.pragtob.info/rails-beginner-cheatsheet/">Rails Beginner Cheat Sheet</a></li>
</ul>
<p>And now for something completely different…</p>
<h2 id="sia%E2%80%99s-guide-to-rails%E2%80%99-folders">Sia’s Guide to Rails’ Folders <a class="direct-link" href="https://sia.codes/posts/rails-folder-tree/#sia%E2%80%99s-guide-to-rails%E2%80%99-folders">#</a></h2>
<p>I would love to hear any feedback on this content - I’d love to make it a living document.</p>
<div class="scroll-table">
<table>
<thead>
<tr>
<th>Folder or File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>app/</code></td>
<td>Organizes your application components. It's got subdirectories that hold the view (views, assets, helpers), controller (controllers), the backend business logic (models), and more.</td>
</tr>
<tr>
<td><code>app/assets</code></td>
<td>Basically your front-end stuff. This folder contains images, javascripts, and stylesheets. These use the Rails asset pipeline which means that Rails is going to generate all this for us on deployment.</td>
</tr>
<tr>
<td><code>app/controllers</code></td>
<td>The controllers subdirectory contains the controllers, which handle the requests from the users. It is often responsible for a single resource type, such as places, users or attendees. Controllers also tie together the models and the views.</td>
</tr>
<tr>
<td><code>app/helpers</code></td>
<td>For the most part, helpers are going to be used to generate code for our views, though they can also be used to assist the models and controllers. Rails encourages “creating custom helpers to extract complicated logic or reusable functionality.” This helps keep our code small, focused, and uncluttered.</td>
</tr>
<tr>
<td><code>app/mailers</code></td>
<td>Functionality to send emails goes here.</td>
</tr>
<tr>
<td><code>app/models</code></td>
<td>Holds the classes that model the business logic of our application and wrap the data stored in our application's database. In most frameworks, this part of the application can grow pretty messy, tedious, verbose, and error-prone. Rails makes it much more simple.</td>
</tr>
<tr>
<td><code>app/views</code></td>
<td>The views subdirectory contains the display templates that will be displayed to the user after a successful request. By default they are written in HTML with embedded ruby (.html.erb). The embedded ruby is used to insert data from the application. It is then converted to HTML and sent to the browser of the user. It has subdirectories for every resource of our application. These subdirectories contain the associated view files.</td>
</tr>
<tr>
<td><code>app/views/_filename</code></td>
<td>Files starting with an underscore (_) are called partials. Those are parts of a view which are reused in other views. A common example is _form.html.erb which contains the basic form for a given resource. It is used in the new and in the edit view since creating something and editing something looks pretty similar.</td>
</tr>
<tr>
<td><code>app/views/layouts</code></td>
<td>Holds the template files for layouts to be used with views. This models the common header/footer method of wrapping views.</td>
</tr>
<tr>
<td><code>bin/</code></td>
<td>Contains the rails scripts, or executables, that start your app and perform other functions to setup, deploy, test, or run your application. Can be directly read by your computer, but not by humans. (The files we write need to be compiled in order to be read by computers.) Ex: bundle, rake, rails, gem.</td>
</tr>
<tr>
<td><code>config/</code></td>
<td>Configure your application's routes, database, and more., including your database configuration (in database.yml), your Rails environment structure (environment.rb), and routing of incoming web requests (routes.rb). You can also tailor the behavior of the three Rails environments for test, development, and deployment with files found in the environments directory.</td>
</tr>
<tr>
<td><code>db/</code></td>
<td>Contains your current database schema, as well as the database migrations. Usually, your Rails application will have model objects that access relational database tables. Migrations set up your database structure, including the attributes of your models. With migrations you can add new attributes to existing models or create new models. It also initially contains seeds.rb which is used to populate the database with default data. Schema.rb shows the current state of your app’s database</td>
</tr>
<tr>
<td><code>Gemfile</code></td>
<td>A file that specifies a list of gems that are required to run your application. Rails itself is a gem you will find listed in the Gemfile. Ruby gems are self-contained packages of code, more generally called libraries, that add functionality or features to your application.</td>
</tr>
<tr>
<td><code>Gemfile.lock</code></td>
<td>This file specifies the exact versions of all gems you use. Because some gems depend on other gems, Ruby will install all you need automatically. The file also contains specific version numbers. It can be used to make sure that everyone within a team is working with the same versions of gems. The file is auto-generated. <strong>Do not edit this file.</strong></td>
</tr>
<tr>
<td><code>lib/</code></td>
<td>Extended modules for your application. You'll put libraries here, unless they explicitly belong elsewhere (such as vendor libraries). For example, this might be code used to get specific information from Facebook.</td>
</tr>
<tr>
<td><code>log/</code></td>
<td>Application log files. See all the funny stuff that is written in the console where you started the Rails server? It is written to your development.log. Logs contain runtime information of your application. If an error happens, it will be recorded here.</td>
</tr>
<tr>
<td><code>public/</code></td>
<td>The only folder seen by the world as-is. Like the public directory for a web server, this directory has web files that don't change, such as JavaScript files (public/javascripts), graphics (public/images), stylesheets (public/stylesheets), and HTML files (public). You should store your assets in the app/assets/images directory locally. In production, Rails precompiles these files to public/assets by default.</td>
</tr>
<tr>
<td><code>Rakefile</code></td>
<td>This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.</td>
</tr>
<tr>
<td><code>README.rdoc</code></td>
<td>This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on. To make it visible on GitHub, change it to a markdown file type (.md).</td>
</tr>
<tr>
<td><code>test/</code></td>
<td>Contains the tests for your application. With tests you make sure that your application actually does what you think it does.</td>
</tr>
<tr>
<td><code>temp/</code></td>
<td>Temporary files (like cache, pid, and session files). Caching involves temporarily storing recently used information, which can improve application performance.</td>
</tr>
<tr>
<td><code>vendor/</code></td>
<td>A place for all third-party code. In a typical Rails application this includes vendored gems (such as security libraries).</td>
</tr>
</tbody>
</table>
</div>
What finally pushed me to learn to code2017-05-17T00:00:00Zhttps://sia.codes/posts/what-pushed-me-to-code/<p><img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/passports_zwgffj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/passports_zwgffj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/passports_zwgffj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/passports_zwgffj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/passports_zwgffj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="My dad's passport photo from the 1970's" importance="high" width="2400" height="1800" /></p>
<p>The UPS guy just knocked. Pandemonium ensues. After corralling the dogs, I fetch the brown envelope from my mailbox. I had been waiting for this package from my stepmom but had forgotten about it. I bring it inside and open it. My father’s old passports slip out and into my hands. It has been almost three years since we lost him. Enough time has passed that I can look on pictures with fondness and without too much pain. But, I am not prepared for the smell that hits me as I hold his passports, opening them, gently thumbing through the pages. It is like he is right here with me.</p>
<blockquote>
<p>How can a smell remind you so much more poignantly than a picture of what you have lost?</p>
</blockquote>
<p>My dad was a larger-than-life character. I suppose most daughters say that about their dads. He was the embodiment of Greek machismo, the American dream, and equal parts charisma and asshole. You couldn’t tell him nothing. He loved life. He loved love (sometimes a little too freely). He was difficult. He was not the world’s greatest dad, but we loved him, he loved us, and that is what family is about.</p>
<blockquote>
<p>Cologne, nightclubs, musk, cigarettes…</p>
</blockquote>
<p>I close my eyes and it feels like he’s right here. The tears rolling down my face are part grief but also part joy at being able to have this high-fidelity sensory experience, awakening my memories once again.</p>
<p>In February of 2014, my dad called me to tell me he had lung cancer. It was a short call. He was driving with my stepmom. It was so short I couldn’t process it other than feeling this immediate sense of dread like a weight pressing down on my chest. I don’t know what I said. I spent the next four months splitting my time between Houston and New Orleans as my father went in and out of hospitals. Once he splurged and got what I can only call a hospital penthouse suite. That’s my dad.</p>
<figure class="portrait-image">
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/soccer_wgxqft.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/soccer_wgxqft.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/soccer_wgxqft.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/soccer_wgxqft.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/soccer_wgxqft.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="692" height="1004" />
<figcaption>In his soccer uniform in his earlier years</figcaption>
</figure>
<p>He played soccer in the hospital hallways with my sister and stepbrother. I am not lying. He later laid down to take a nap in his hospital bed, and I went to the recliner next to him so he wouldn’t be alone. We looked into each other’s eyes while we held hands and silent tears rolled down our faces. This was my macho, proud, Greek dad. His seemingly moments of weakness were actually moments of strength. Strength in love and family.</p>
<p>The disease progressed despite treatment. Relationships are tough, especially complicated ones with those we love. I am part my father (including the difficult parts), and our similar personalities often led to conflict. To this day I am still grateful to the counselor at MD Anderson who told us not to wait to say our last words. So, we had “the talk” while he was still healthy enough.</p>
<blockquote>
<p>I cannot imagine how much regret I would have today if I hadn’t taken her advice.</p>
</blockquote>
<p>In the fourth month of his disease, my father went into <a href="https://en.wikipedia.org/wiki/Palliative_care">palliative care</a> where he soon was on increasing doses of opioids to manage not just the pain but the sense of drowning that lung cancer patients feel as the disease slowly robs them of breath. Everything was not going to be okay. But no matter what happened I was going to support him through it. We watched the World Cup matches together, and eventually, he could no longer communicate. One morning my siblings and I were still at the apartment and we got the call from my stepmom. He was gone.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/grave_fvhtns.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/grave_fvhtns.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/grave_fvhtns.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/grave_fvhtns.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/grave_fvhtns.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Two graves on a mountain overlooking the Aegean Sea, one fresh" width="3360" height="1972" />
<figcaption>My father Spiros is on the left, and my great uncle Ioannis is on the right</figcaption>
</figure>
<p>My father was from Piraeus, the port city near Athens, but he always loved Ikaria, the island where my yia-yia (grandmother) was from and where he spent much of his youth. That is where he is resting now, on the side of a mountain overlooking the Aegean Sea.</p>
<blockquote>
<p>This is not a sad story.</p>
</blockquote>
<p>A sad story would be never living your life. Never taking risks. Never experiencing all the good and bad that life and love have to offer. This is a happy story.</p>
<p>My father lived life to the fullest. I could say that he told us to do the same, but his delivery was always a bit unorthodox and it usually came out as boasting. I miss his boasting. I heard him, but it became more even more real and urgent when confronted with his loss. Life is so short. So entirely too short. After returning from Greece, I went to China, then I went to Hong Kong. Then I was forced to come back because I was a registered companion for my friend at American Airlines and she quit. It was time to face life again.</p>
<p>Luckily I still had contract consulting work, but I needed to figure out what was next. I wasn’t sure what I wanted to do yet, so I reflected on my interests and behavior over time and started noticing a pattern. My undergraduate degree was in chemical engineering, but my only perfect-score classes were in Fortran 90 and <a href="https://en.wikipedia.org/wiki/Discrete_mathematics">discrete mathematics</a> (I almost switched my major to math). With the advent of <a href="https://en.wikipedia.org/wiki/Massive_open_online_course">MOOCs</a>, I kept starting new computer science and coding courses, but only as a hobby.</p>
<p>There comes a time when you just have to kick yourself in the ass and say “Stop being dumb, you clearly like this, just do it already!” The loss of my dad reminded me to be brave, to take the risk, and to do something that I clearly loved more than my current work. If you’re a guy reading this and wondering why this was such a difficult step, realize that as young women, <a href="https://www.ted.com/talks/sheryl_sandberg_why_we_have_too_few_women_leaders">we internalize what society defines as “appropriate” behavior for women</a>, which does <em>not</em> include risk-taking, leadership, or being ambitious. It is difficult to break that mold.</p>
<p>In a month, I go to Ikaria for my father’s <a href="https://en.wikipedia.org/wiki/Memorial_service_(Orthodox)">three-year memorial service</a>. So naturally, this is a time of reflection for me. Here I am, three years, two coding boot camps, one apprenticeship, one full-time job, a handful of open-source projects, and several contract gigs later. I love and miss my dad fiercely. I’m also grateful to him every day for living his life to the fullest and pushing us to do the same, even with his loss.</p>
<p>This is not a sad story. Though I write in both grief and joy, more importantly, I write this story in the hopes that it inspires others to seek out what they truly love. Don’t wait for loss to open your eyes to our limited time. Be with who you love now. Do what you love now. Don’t wait. Leave no time left for regrets.</p>
<blockquote>
<p>Thanks, Dad. I love you.</p>
</blockquote>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/toast_igmcal.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/toast_igmcal.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/toast_igmcal.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/toast_igmcal.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/toast_igmcal.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Older man and daughter hugging and holding up a wine glass to the camera" width="3360" height="1972" />
<figcaption>My dad and I celebrating life with an amazing dinner and wine in New Orleans</figcaption>
</figure>
Google Analytics + caniuse = *MAGIC*2017-10-27T00:00:00Zhttps://sia.codes/posts/google-analytics-caniuse-magic/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/levitation_ec7gyj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/levitation_ec7gyj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/levitation_ec7gyj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/levitation_ec7gyj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/levitation_ec7gyj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Woman in woods levitating" importance="high" width="1360" height="765" />
<figcaption>How I feel right now. Photo by <a href="https://unsplash.com/photos/GR2uZmp7mUo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Rob Potter</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>Today I learned that you can import your Google Analytics data into <a href="https://caniuse.com/">caniuse</a> to determine how many of your users would be able to use a particular front-end technology in their browser of choice. Maybe the rest of you have been holding out on this and just didn’t tell me. Maybe I had my head in the sand. Well, today I learned about it, and it’s amazing.</p>
<p>At <a href="https://www.clioandcalliope.com/">Clio + Calliope</a>, we are really excited about trying out CSS grid layouts, but we know it won’t cover all users. Now we can make informed decisions for our clients to use tools that will work in their browsers or provide fallbacks when our first pick does not work. Here is the output of caniuse using a year’s worth of Google Analytics data from one client:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/caniuse-grid_ogcthi.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/caniuse-grid_ogcthi.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/caniuse-grid_ogcthi.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/caniuse-grid_ogcthi.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/caniuse-grid_ogcthi.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Screenshot of caniuse support for CSS Grid" loading="lazy" width="1360" height="1188" />
<figcaption>Can I use CSS Grid with my website’s users? Clearly not without fallbacks.</figcaption>
</figure>
<p>You can see in the top right-hand corner, that only 55.95% of our client’s users would be able to properly see a CSS grid layout (if also prefixed). So, clearly we cannot use grid without fallbacks that cover the rest of the browsers. Luckily, all I have to do to check for fallbacks like flexbox is to type “flexbox” in the <em>Can I use _______ ?</em> input box. Awesome sauce.</p>
<h2 id="how-to-invoke-this-incantation">How to invoke this incantation <a class="direct-link" href="https://sia.codes/posts/google-analytics-caniuse-magic/#how-to-invoke-this-incantation">#</a></h2>
<p>Go to <a href="https://caniuse.com/">caniuse</a>, and in the red <em>Can I use _______ ?</em> area, click on the Settings link to the right:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/caniuse-zoom_gjnrgu.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/caniuse-zoom_gjnrgu.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/caniuse-zoom_gjnrgu.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/caniuse-zoom_gjnrgu.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/caniuse-zoom_gjnrgu.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Close-up screenshot of main input with settings link to the right" loading="lazy" width="2066" height="254" />
<figcaption>Click on the gear/settings link to the right</figcaption>
</figure>
<p>Then, the left menu sidebar will include a button to <em>Import...</em> from Google Analytics:</p>
<figure class="portrait-image">
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/caniuse-sidebar_kxnqq9.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/caniuse-sidebar_kxnqq9.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/caniuse-sidebar_kxnqq9.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/caniuse-sidebar_kxnqq9.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/caniuse-sidebar_kxnqq9.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Close-up screenshot of sidebar with Import button for Google Analytics" loading="lazy" width="556" height="1066" />
<figcaption>Click on Import...</figcaption>
</figure>
<p>Click on that, and follow the steps to give caniuse access permission to the Google account with your analytics information. Then, select a profile (website) that you want to import from and a date range.</p>
<p>After that, you should be ready to roll. Search for a particular technology, and you will see the stats for your website plus global usage in the top right.</p>
<h2 id="give-thanks-where-thanks-is-due">Give thanks where thanks is due <a class="direct-link" href="https://sia.codes/posts/google-analytics-caniuse-magic/#give-thanks-where-thanks-is-due">#</a></h2>
<p>I have to send a special shout out to <a href="https://medium.com/@mshwery">Matt Shwery</a> for telling me about this today! You’re the best!</p>
My adventures in turning a Pixelbook into a developer machine2018-12-07T00:00:00Zhttps://sia.codes/posts/adventures-pixelbook-dev-machine/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/pixelbook_vkhrbc.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/pixelbook_vkhrbc.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/pixelbook_vkhrbc.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/pixelbook_vkhrbc.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/pixelbook_vkhrbc.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Side-view of Pixelbook showing how thin it is" importance="high" width="1360" height="776" />
<figcaption>My favorite view of the ultra-thin Pixelbook by Google</figcaption>
</figure>
<p>Today the FedEx man delivered my brand new Pixelbook. So much fun! I got this little tool to attempt to get off of the Mac platform without going full Linux distro. For the most part, I do web development and only need (1) a terminal, (2) an editor, preferably VS Code, and (3) a browser. Since Chrome OS deployed the new Linux beta, all these things are now possible with fewer install headaches than before. These are my adventures in trying to turn it into a dev machine.</p>
<h2 id="step-1%3A-give-it-a-chance-to-update-before-panicking">Step 1: Give it a chance to update before panicking <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-1%3A-give-it-a-chance-to-update-before-panicking">#</a></h2>
<p>Don’t fret that the Linux beta option doesn’t show up in your settings. First, check to see which Chrome OS you are on.</p>
<ol>
<li>Click on the clock.</li>
<li>Open Settings</li>
<li>Click on the hamburger menu</li>
<li>Select “About Chrome OS”</li>
</ol>
<p>You should then see your current version and whether it is checking for updates. Version 70 was the latest stable for me, though the Linux beta came out in version 69. After updating, Chrome OS will require a restart.</p>
<h2 id="step-2%3A-turn-on-linux">Step 2: Turn on Linux <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-2%3A-turn-on-linux">#</a></h2>
<p>Open your Settings again. Scroll down, and you will find a setting for the Linux beta:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/linux-flag_p9lyoy.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/linux-flag_p9lyoy.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/linux-flag_p9lyoy.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/linux-flag_p9lyoy.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/linux-flag_p9lyoy.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="" loading="lazy" width="1360" height="907" />
<figcaption>Linux (Beta) setting in Chrome OS. Oh hai, Momentum!</figcaption>
</figure>
<p>After you turn on the flag, it will download and open a terminal!! 🤯🤯🤯</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/terminal_t0fcej.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/terminal_t0fcej.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/terminal_t0fcej.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/terminal_t0fcej.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/terminal_t0fcej.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Terminal window view" loading="lazy" width="1360" height="907" />
<figcaption>Terminal systems are go!</figcaption>
</figure>
<aside><strong>Note</strong>: You may have struggled like me to copy-paste things into this new terminal. Try ctrl-shift-V instead of just ctrl-V.</aside>
<p>I like tools that make me faster, so I immediately attempted to install <code>zsh</code> and <code>oh-my-zsh</code>, and that’s when I came across my next problem…</p>
<h2 id="step-3.-wtf-is-the-password-in-terminal%3F">Step 3. WTF is the password in terminal? <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-3.-wtf-is-the-password-in-terminal%3F">#</a></h2>
<p>It’s <a href="https://www.reddit.com/r/chromeos/comments/9hzrzm/linux_beta_user_password/">a little unclear</a>, but you can update it as follows:</p>
<ol>
<li>Switch to root with <code>$ sudo su</code></li>
<li>Run <code>passwd your-username-here</code></li>
<li>It will prompt you twice for a new password. To exit, type <code>exit</code></li>
</ol>
<p>Boom! Now you can install and update things in your terminal.</p>
<h2 id="step-4.-oh-my-zsh!">Step 4. Oh My Zsh! <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-4.-oh-my-zsh!">#</a></h2>
<p>I debated on adding this section, but this article really is about my adventures in setting up my machine, so here goes. <a href="https://github.com/robbyrussell/oh-my-zsh">Oh My Zsh</a> is totally an easy cop-out I’ve used for setting up my terminal on every machine:</p>
<aside><strong>Oh My Zsh</strong> is an open source, community-driven framework for managing your <a href="https://www.zsh.org/">zsh</a> configuration.</aside>
<p>It gives me a custom terminal prompt that I like and Git aliases. You just need to install Zsh and then Oh My Zsh:</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">apt</span> <span class="token function">install</span> <span class="token function">zsh</span></span><br /><span class="highlight-line">$ <span class="token function">sh</span> <span class="token parameter variable">-c</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh<span class="token variable">)</span></span>"</span></span></code></pre>
<p>This worked like a breeze once I knew my password. 🤦</p>
<h2 id="step-5.-%EF%B8%8Fvs-code">Step 5. ️VS Code <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-5.-%EF%B8%8Fvs-code">#</a></h2>
<p>By now you should know that I like things that make my life easy. As I type these words, I have not yet attempted to install VS Code. I hope I don’t have to delete this section. Hold on while I Google some stuff…</p>
<p>Success! Okay, it looks like we might not have access to the file system for files downloaded using Chrome. So instead, we need to download the .deb installer using <code>curl</code> and then install it:</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">curl</span> <span class="token parameter variable">-L</span> https://go.microsoft.com/fwlink/<span class="token punctuation">\</span>?LinkID<span class="token punctuation">\</span><span class="token operator">=</span><span class="token number">760868</span> <span class="token operator">></span> code.deb</span><br /><span class="highlight-line">$ <span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> ./code.deb</span></code></pre>
<p>For the final test, see if it runs:</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ code <span class="token builtin class-name">.</span></span></code></pre>
<p>Success! 🙌</p>
<p>Thanks to <a href="https://www.reddit.com/r/Crostini/comments/8f5zu8/from_scratch_to_vs_code/">this Reddit post</a> for the VS Code installation clues. It suggests a few other libs that they had to install because of bugs, but I have not come across any bugs yet. I’ll update this post if I come across those bugs later.</p>
<h2 id="step-6.-installing-node-and-npm-via-nvm">Step 6. Installing Node and NPM via NVM <a class="direct-link" href="https://sia.codes/posts/adventures-pixelbook-dev-machine/#step-6.-installing-node-and-npm-via-nvm">#</a></h2>
<p>I chose to install Node and NPM via <a href="https://github.com/creationix/nvm">Node Version Manager</a>. Install via <code>curl</code>:</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">curl</span> -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh <span class="token operator">|</span> <span class="token function">bash</span></span></code></pre>
<p>Then, add these 3 lines to your .zshrc (or .bashrc):</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line"><span class="token builtin class-name">export</span> <span class="token assign-left variable">NVM_DIR</span><span class="token operator">=</span><span class="token string">"<span class="token environment constant">$HOME</span>/.nvm"</span></span><br /><span class="highlight-line"><span class="token punctuation">[</span> <span class="token parameter variable">-s</span> <span class="token string">"<span class="token variable">$NVM_DIR</span>/nvm.sh"</span> <span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token punctuation">\</span>. <span class="token string">"<span class="token variable">$NVM_DIR</span>/nvm.sh"</span></span><br /><span class="highlight-line"><span class="token punctuation">[</span> <span class="token parameter variable">-s</span> <span class="token string">"<span class="token variable">$NVM_DIR</span>/bash_completion"</span> <span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token punctuation">\</span>. <span class="token string">"<span class="token variable">$NVM_DIR</span>/bash_completion"</span></span></code></pre>
<p>Then, restart your terminal or type <code>source .zshrc</code> or <code>source .bashrc</code> to have it load the updated run commands.</p>
<p>Check that it’s working with <code>nvm -v</code>, then you can install Node with <code>nvm install node</code> for the latest release of Node (and NPM).</p>
<p>Special thanks to <a href="https://twitter.com/y3l2n">Yulan Lin</a> for all her help with setting up my Pixelbook!</p>
Making Google Fonts Faster in 20222019-02-06T00:00:00Zhttps://sia.codes/posts/making-google-fonts-faster/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/typewriter_keys_qgtruq.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/typewriter_keys_qgtruq.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/typewriter_keys_qgtruq.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/typewriter_keys_qgtruq.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/typewriter_keys_qgtruq.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Close-up of typewriter keys" importance="high" width="4000" height="1835" />
<figcaption>Photo by <a href="https://unsplash.com/photos/tFdt_ztePy4?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Bob Newman</a> on <a href="https://unsplash.com/collections/3603769/font?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>In this article, I will show you how to:</p>
<ol>
<li>Choose the best way to import your Google Fonts</li>
<li>Skip over some of the latency time for downloading fonts</li>
<li>Fix flash-of-invisible text (FOIT)</li>
<li>Self-host your fonts for faster speed and greater control</li>
</ol>
<h2 id="anatomy-of-a-google-fonts-request">Anatomy of a Google Fonts request <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#anatomy-of-a-google-fonts-request">#</a></h2>
<p>Let’s take a step back and look at what is happening when you request from Google Fonts using a standard <code><link></code> copied from their website:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/css?family=Muli:400<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span></span></code></pre>
<p>Did you notice that the link is for a stylesheet and not a font file? If we load the link’s <a href="https://fonts.googleapis.com/css?family=Muli:300,400,700%7COswald:500">href</a> into our browser, we see that Google Fonts loads a stylesheet of <code>@font-face</code> declarations for all the font styles that we requested in every character set that is available. Not all of these are used by default, thankfully.</p>
<p>Then, each <code>@font-face</code> declaration tells the browser to use a local version of the font, if available, before attempting to download the file from <code>fonts.gstatic.com</code>:</p>
<pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token comment">/* latin */</span></span><br /><span class="highlight-line"><span class="token atrule"><span class="token rule">@font-face</span></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">"Open Sans"</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">font-style</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">font-weight</span><span class="token punctuation">:</span> 400<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">src</span><span class="token punctuation">:</span> <span class="token function">local</span><span class="token punctuation">(</span><span class="token string">"Open Sans Regular"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">local</span><span class="token punctuation">(</span><span class="token string">"OpenSans-Regular"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0bf8pkAg.woff2<span class="token punctuation">)</span></span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">"woff2"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">unicode-range</span><span class="token punctuation">:</span> U+0000-00FF<span class="token punctuation">,</span> U+0131<span class="token punctuation">,</span> U+0152-0153<span class="token punctuation">,</span> U+02BB-02BC<span class="token punctuation">,</span> U+02C6<span class="token punctuation">,</span> U+02DA<span class="token punctuation">,</span> U+02DC<span class="token punctuation">,</span> U+2000-206F<span class="token punctuation">,</span> U+2074<span class="token punctuation">,</span> U+20AC<span class="token punctuation">,</span> U+2122<span class="token punctuation">,</span> U+2191<span class="token punctuation">,</span> U+2193<span class="token punctuation">,</span> U+2212<span class="token punctuation">,</span> U+2215<span class="token punctuation">,</span> U+FEFF<span class="token punctuation">,</span> U+FFFD<span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Understanding this architecture will help us understand why certain strategies work better for making our site faster.</p>
<h2 id="should-i-use-%3Clink%3E-or-%40import%3F">Should I use <code><link></code> or <code>@import</code>? <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#should-i-use-%3Clink%3E-or-%40import%3F">#</a></h2>
<p>Sometimes it's easier for us to get our custom fonts into our projects by importing them in the CSS:</p>
<pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token atrule"><span class="token rule">@import</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"https://fonts.googleapis.com/css?family=Open+Sans:400,700"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span></span></span></code></pre>
<p>Unfortunately, this makes our site load slower because we've increased the <a href="https://web.dev/critical-request-chains/">critical request depth</a> for no benefit. In the network waterfall below, we can see that each request is chained - the HTML is loaded on line 1, which triggers a call to style.css. Only after style.css is loaded and the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model">CSSOM</a> is created will the CSS from Google fonts then be triggered for download. And as we learned in the previous section, that file must also be downloaded and read before the fonts themselves will be downloaded (the final 2 rows):</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/webfonts_css_iygufk.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/webfonts_css_iygufk.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/webfonts_css_iygufk.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/webfonts_css_iygufk.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/webfonts_css_iygufk.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Webpagetest network waterfall showing wasted time due to using @import to load our Google font from the CSS" loading="lazy" width="1876" height="364" />
<figcaption>Loading Google fonts via @import in CSS increases the critical request depth and slows page load</figcaption>
</figure>
<p>By moving our font request to the <code><head></code> of our HTML instead, we can make our load faster because we've reduced the number of links in the chain for getting our font files:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-html_trnepj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-html_trnepj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-html_trnepj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-html_trnepj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-html_trnepj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Webpagetest network waterfall showing faster load by moving our Google font load to the HTML via a link tag in the head" loading="lazy" width="1876" height="364" />
<figcaption>Loading Google fonts in the HTML reduces the critical request depth and speeds up page load</figcaption>
</figure>
<aside>Always import your fonts from HTML, not CSS.</aside>
<h2 id="warm-up-that-connection-faster-with-preconnect">Warm up that connection faster with preconnect <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#warm-up-that-connection-faster-with-preconnect">#</a></h2>
<p>Look closely at that last waterfall, and you might spy another inefficiency. Go ahead and try to find it before you keep reading...</p>
<p>We have a minimum of 2 separate requests to 2 different hosts — first for the stylesheet at <code>fonts.googleapis.com</code>, and then to a unique URL for each font hosted at <code>fonts.gstatic.com</code>.</p>
<p>You may be asking yourself, "Why can’t I just use the direct link to the font?" Google Fonts are updated often so you might find yourself trying to load a font from a link that no longer exists pretty quickly. 🤦🏾</p>
<p>We can make one quick performance improvement by warming up the DNS lookup, TCP handshake, and TLS negotiation to the <code>fonts.gstatic.com</code> domain with <a href="https://www.igvita.com/2015/08/17/eliminating-roundtrips-with-preconnect/">preconnect</a>:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preconnect<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.gstatic.com/<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preconnect<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.gstatic.com/<span class="token punctuation">"</span></span> <span class="token attr-name">crossorigin</span> <span class="token punctuation">/></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/css?family=Muli:400<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span></span></code></pre>
<aside>Note that the <code>crossorigin</code> attribute is needed for assets that are loaded in anonymous mode (like fonts). Otherwise, the preconnect will only perform the DNS lookup portion of the connection.</aside>
<p>Why? If you don’t warm up the connection, the browser will wait until it sees the CSS call font files before it begins DNS/TCP/TLS:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-no-preconnect_fseenl.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-no-preconnect_fseenl.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-no-preconnect_fseenl.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-no-preconnect_fseenl.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-no-preconnect_fseenl.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="WebPageTest waterfall showing wasted time connecting to fonts.gstatic.com (DNS/TCP/TLS)" loading="lazy" width="1360" height="242" />
<figcaption>Loading Google Fonts without preconnect</figcaption>
</figure>
<p>This is wasted time because we KNOW that we will definitely need to request resources from <code>fonts.gstatic.com</code>. By adding the preconnect, we can perform DNS/TCP/TLS before the socket is needed, thereby moving forward that branch of the waterfall:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-preconnect_qleijg.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-preconnect_qleijg.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-preconnect_qleijg.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-preconnect_qleijg.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-preconnect_qleijg.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="WebPageTest waterfall showing connection happening earlier and not blocking Google font download" width="1360" height="241" />
<figcaption>Loading Google Fonts with preconnect to fonts.gstatic.com</figcaption>
</figure>
<p>What's really cool is that I noticed that Google Fonts recently added the preconnect line in the HTML snippet they create for you. Now you no longer need to remember to add it when grabbing new fonts. To update legacy projects, just copy and paste this line before the <code><link></code> calling your font in your HTML:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preconnect<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.gstatic.com<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></span></code></pre>
<figure>
<img src="https://sia.codes/img/fonts/preconnect-snippet.png" width="620" height="796" class="portrait-image" loading="lazy" style="max-width:620px" alt="Screenshot of Google Font's embed link generator" />
<figcaption>Google Fonts now supplies the preconnect statement in the HTML snippet automatically</figcaption>
</figure>
<aside>Preconnect to fonts.gstatic.com when using Google Fonts hosted fonts.</aside>
<h2 id="flash-of-invisible-text-and-the-font-display-property">Flash of Invisible text and the font-display property <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#flash-of-invisible-text-and-the-font-display-property">#</a></h2>
<p>We used to have no control over flash-of-invisible-text (FOIT) and flash-of-unstyled-text (FOUT) while fonts are loading:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/foit-emr_t0jvkk.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/foit-emr_t0jvkk.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/foit-emr_t0jvkk.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/foit-emr_t0jvkk.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/foit-emr_t0jvkk.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Dev Tools network tab, screenshot partially through load showing invisible text" loading="lazy" width="1360" height="748" />
<figcaption>FOIT in action — note the missing navbar text in the filmstrip screenshot (throttled to slow 3G)</figcaption>
</figure>
<p>Setting the <a href="https://font-display.glitch.me/"><code>font-display</code></a> property in the <code>@font-face</code> declaration in our CSS gives us that control. Different people have different opinions on FOIT (flash of invisible text) and FOUT (flash of unstyled text). For the most part, we prefer to show text as fast as possible even if that means a pesky transition to our preferred font once it loads. For strongly branded content, you may want to keep a FOIT over showing off-brand fonts.</p>
<p>If you’re okay with FOUT, or flash of unstyled text, then we can fix FOIT by adding <code>font-display: swap;</code> to your <code>@font-face</code> declarations. <a href="https://meowni.ca/">Monica Dinculescu</a> created a <a href="https://font-display.glitch.me/">fun Glitch playground</a> to demonstrate all of the <code>font-display</code> options. Last time I checked, the link was broken, but this handy diagram from her post helps us understand those options:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/font-display-options_vadanf" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/font-display-options_vadanf 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/font-display-options_vadanf 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/font-display-options_vadanf 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/font-display-options_vadanf 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="font-display options of block, swap, fallback, and optional visualized on a timeline of invisible, fallback, and webfont" loading="lazy" width="2672" height="1502" />
<figcaption>font-display options, image by Monica Dinculescu</figcaption>
</figure>
<p>We don't have control over the <code>@font-face</code> declarations in the Google Fonts stylesheet, but luckily they added an <a href="https://developers.google.com/fonts/docs/css2#use_font-display">API for modifying <code>font-display</code></a>. It's now included in the default snippet:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- Default HTML embed snippet from Google Fonts --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preconnect<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.gstatic.com<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><del class="highlight-line highlight-line-remove"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/css2?family=Redressed<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></del><br /><ins class="highlight-line highlight-line-add"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/css2?family=Redressed&display=swap<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></ins></code></pre>
<p>If you want to change the font display on a legacy project, add <code>&display=swap</code> to the tail of your link's href. Be sure to check out other useful <a href="https://font-display.glitch.me/">font-display options</a> like <code>optional</code> and <code>fallback</code>.</p>
<h2 id="self-host-your-web-fonts-for-full-control">Self-host your web fonts for full control <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#self-host-your-web-fonts-for-full-control">#</a></h2>
<p>Google Fonts is hosted on a pretty fast and reliable content delivery network (CDN), so why might we consider hosting on our own CDN?</p>
<p>Remember how we have a minimum of 2 separate requests to 2 different hosts? This makes it impossible to take advantage of <a href="https://developers.google.com/web/fundamentals/performance/http2/#request_and_response_multiplexing">HTTP/2 multiplexing</a> or <a href="https://twitter.com/addyosmani/status/743571393174872064?lang=en">resource hints</a>.</p>
<p>Second, while rare, if Google Fonts is down, we won’t get our fonts. If our own CDN is down, then at least we are consistently delivering nothing to our users, right? 🤷🏻️</p>
<p>Note that self-hosting your fonts will not necessarily lead to faster load times unless your assets are hosted on a CDN. Many providers make this easier like Netlify, which is where this site is hosted. Before moving to Netlify, I used AWS S3 and Cloudfront which requires more set up.</p>
<p>To have full control over our font files, loading, and CSS properties, we can self-host our Google Fonts. Luckily, <a href="http://mranftl.com/">Mario Ranftl</a> created <a href="https://google-webfonts-helper.herokuapp.com/fonts">google-webfonts-helper</a> which helps us do exactly that! It is an amazing tool for giving us font files and font-face declarations based on the fonts, charsets, styles, and browser support you select.</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/making-google-fonts-faster/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h3 id="use-google-webfonts-helper-to-download-our-fonts-and-provide-basic-css-font-face-declarations">Use google-webfonts-helper to download our fonts and provide basic CSS font-face declarations <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#use-google-webfonts-helper-to-download-our-fonts-and-provide-basic-css-font-face-declarations">#</a></h3>
<p>First, select the Google font you need from the left sidebar. Type in the search box for a filtered list (red arrow), then click on your font (blue arrow):</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-step-1_qzbp53.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-step-1_qzbp53.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-step-1_qzbp53.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-step-1_qzbp53.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-step-1_qzbp53.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="google-webfonts-helper search for font in top left and select" loading="lazy" width="1360" height="666" />
<figcaption>Step 1: Select a font.</figcaption>
</figure>
<p>Next, select your character sets and styles. Remember that more styles mean more for the client to download:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-select-sets_f4t6aq.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-select-sets_f4t6aq.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-select-sets_f4t6aq.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-select-sets_f4t6aq.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-select-sets_f4t6aq.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="google-webfonts-helper select character sets and styles from the main section" loading="lazy" width="1360" height="668" />
<figcaption>Select your character sets and styles (weight and style).</figcaption>
</figure>
<aside>A sometimes overlooked but easy way to make your sites load and render faster is to user fewer fonts. This applies to both different typefaces and styles (weight and italicized, etc.).</aside>
<p>Different fonts have different levels of character support and style options. For example, Open Sans supports many more charsets than Muli:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-open-sans_il3mdh.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-open-sans_il3mdh.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-open-sans_il3mdh.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-open-sans_il3mdh.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-open-sans_il3mdh.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Open Sans charsets include cyrillic, cyrillic-ext, greek, greek-ext, latin, latin-ext, and vietnamese" loading="lazy" width="1360" height="474" />
<figcaption>Open Sans supports many more character sets including Cyrillic, Greek, Vietnamese, and extended sets.</figcaption>
</figure>
<p>Your final choice is which browsers you want to support. “Modern Browsers” will give you WOFF and WOFF2 formats while “Best Support” will also give you TTF, EOT, and SVG. For our use case, we chose to only host WOFF (<a href="https://caniuse.com/woff">caniuse</a>) and WOFF2 (<a href="https://caniuse.com/woff2">caniuse</a>) while selecting system fonts as fallbacks for browsers older than IE9. Work with your design team to decide the best option for you.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-support_q32mh2.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-support_q32mh2.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-support_q32mh2.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-support_q32mh2.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-support_q32mh2.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="From Section 3, after selecting the file format option, you can copy the auto-generated CSS" loading="lazy" width="1360" height="669" />
<figcaption>Select “Best Support” for all file formats or “Modern Browsers” for only WOFF and WOFF2.</figcaption>
</figure>
<p>After selecting a browser support option, copy the provided CSS into your stylesheet near the beginning of your stylesheets before you call any of those font families. We choose to put this at the top of our variables partial when using SCSS. You can customize the font file location — the default assumes <code>../fonts/</code>. Don't forget to set your <code>font-display</code> property manually in the CSS to control FOIT.</p>
<p>Finally, download your files. Unzip them, and place them in your project in the appropriate location.</p>
<h2 id="loading-optimization%3A-preload-and-other-options">Loading optimization: preload and other options <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#loading-optimization%3A-preload-and-other-options">#</a></h2>
<p>So far, we have only moved where we are hosting files from Google’s servers to ours. This is nice, but we might be able to do more. In this section, I'll outline two options for further loading optimization - preload and inlining CSS.</p>
<h3 id="how-to-use-preload-to-load-fonts-faster">How to use preload to load fonts faster <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#how-to-use-preload-to-load-fonts-faster">#</a></h3>
<p>We can make our font files start download right away, before the browser knows whether it will need the font or not. By default, the browser only downloads a font after the HTML and CSS are parsed and the CSSOM is created.</p>
<p>The browser won't load font files that aren't needed. If warning bells are going off in your head, then you're right to worry. <strong>We only want to hijack this process if we know for sure that a font will be used on that page</strong>.</p>
<p>Once we know we definitely need a particular font on a page, we can preload it with the <a href="https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/"><code>preload</code></a> resource hint:</p>
<blockquote>
<p>Preload is a declarative fetch, allowing you to force the browser to make a request for a resource without blocking the document’s onload event.</p>
<p class="blockquote-source">—<a href="https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf">from “Preload, Prefetch And Priorities in Chrome”</a> by Addy Osmani</p>
</blockquote>
<aside><strong>Warning</strong>: Before we go any further, make sure you understand that `preload` will load a resource whether you use it or not. Only preload resources that are needed on a particular page, and limit how many resources you preload.</aside>
<p>How do we choose which file type to preload? Resource hints are not available in every browser, but all the <a href="https://caniuse.com/#search=preload">browsers that support preload</a> also <a href="https://caniuse.com/#search=woff2">support WOFF2</a> so we can safely choose only WOFF2.</p>
<p>In your HTML file, add resource hints for the WOFF2 font files you need for the current page:</p>
<pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span><br /><span class="highlight-line"> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preload<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">as</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>font<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>font/woff2<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./fonts/muli-v12-latin-regular.woff2<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">crossorigin</span></span><br /> <span class="token punctuation">/></span></span></code></pre>
<p>Let’s break down our preload <code><link></code> element:</p>
<ul>
<li><code>rel="preload"</code> tells the browser to declaratively fetch the resource but not “execute” it (our CSS will queue usage).</li>
<li><code>as="font"</code> tells the browser what it will be downloading so that it can set an appropriate priority. Without it, the browser would set a default low priority.</li>
<li><code>type="font/woff2</code> tells the browser the file type so that it only downloads the resource if it supports that file type.</li>
<li><code>crossorigin</code> is required because fonts are fetched using anonymous mode CORS.</li>
</ul>
<p>So how did we do? Let’s take a look at the performance before and after. Using <a href="http://webpagetest.org/">webpagetest.org</a> in easy mode (Moto G4, Chrome, slow 3G), our speed index was 4.147s using only preconnect, and 3.388s using self-hosting plus preload. The waterfalls for each show how we are saving time by playing with latency:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-self_aot28d" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-self_aot28d 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-self_aot28d 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-self_aot28d 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-self_aot28d 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="WebPageTest waterfall showing blocked font download" loading="lazy" width="1360" height="319" />
<figcaption>Loading from Google with preconnect to fonts.gstatic.com</figcaption>
</figure>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/fonts-preload_jmjnng" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/fonts-preload_jmjnng 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/fonts-preload_jmjnng 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/fonts-preload_jmjnng 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/fonts-preload_jmjnng 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="WebPageTest waterfall showing css and fonts being downloaded at the same time" loading="lazy" width="1360" height="288" />
<figcaption>Self-hosting fonts and using preload</figcaption>
</figure>
<h3 id="oh-no%2C-preload-made-my-page's-initial-render-slower!">Oh no, preload made my page's initial render slower! <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#oh-no%2C-preload-made-my-page's-initial-render-slower!">#</a></h3>
<p>Yes, this can happen. Unfortunately, the <code>preload</code> hint can throw a wrench into prioritization schemes for loading files to the browser. Preloaded files are currently loaded before other, more important files needed for initial render. On the plus side, sometimes it loads the font fast enough that the page doesn't need to render the fallback font first and then re-render and shift when the desired font comes in.</p>
<p>Your best strategy is to minimize how many resources you preload and TEST, TEST, TEST with <a href="https://webpagetest.org/">webpagetest.org</a>, which is similar to the browser's dev tools network tab. You can configure a test to do 9 runs at a time to get a better idea of the median performance, then compare before and after preloading.</p>
<p>Generally speaking, keep your preloads under 4-5, and preferably even lower. You could choose to only preload your most used styles and character sets (e.g., 400 weight and Latin for most websites written in English), and then let the browser handle the logic for the rest by having <a href="https://sia.codes/posts/making-google-fonts-faster/#subfont">subsets</a> with no preload. The <code>unicode-range</code> property in the font face declaration tells the browser which characters are included in a particular file or subset:</p>
<pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token comment">/* this unicode range is only for latin characters: */</span></span><br /><span class="highlight-line"><span class="token atrule"><span class="token rule">@font-face</span></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">"Open Sans"</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">font-style</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">font-weight</span><span class="token punctuation">:</span> 400<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">src</span><span class="token punctuation">:</span> <span class="token function">local</span><span class="token punctuation">(</span><span class="token string">"Open Sans Regular"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">local</span><span class="token punctuation">(</span><span class="token string">"OpenSans-Regular"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span>https://fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0bf8pkAg.woff2<span class="token punctuation">)</span></span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">"woff2"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">unicode-range</span><span class="token punctuation">:</span> U+0000-00FF<span class="token punctuation">,</span> U+0131<span class="token punctuation">,</span> U+0152-0153<span class="token punctuation">,</span> U+02BB-02BC<span class="token punctuation">,</span> U+02C6<span class="token punctuation">,</span> U+02DA<span class="token punctuation">,</span> U+02DC<span class="token punctuation">,</span> U+2000-206F<span class="token punctuation">,</span> U+2074<span class="token punctuation">,</span> U+20AC<span class="token punctuation">,</span> U+2122<span class="token punctuation">,</span> U+2191<span class="token punctuation">,</span> U+2193<span class="token punctuation">,</span> U+2212<span class="token punctuation">,</span> U+2215<span class="token punctuation">,</span> U+FEFF<span class="token punctuation">,</span> U+FFFD<span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<h3 id="what-are-my-options-if-i-don't-preload%3F">What are my options if I don't preload? <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#what-are-my-options-if-i-don't-preload%3F">#</a></h3>
<p>The core problem we have is that the browser does not discover that it needs a font until later, causing loading to be delayed until that discovery is made. So we reach for <code>preload</code> when another option is to make that discovery occur sooner. Another problem with <code>preload</code> is that it requires us to remember to remove a preload when a font is no longer used.</p>
<p>One way we can move up discovery is to inline our critical CSS into the <code><head></code> of our HTML. Then our font face declarations and font styles are available as soon as the html is downloaded which also contains the characters used, so the browser can make a better decision about whether to load the font file or not.</p>
<p>I do not recommend doing this manually. Check out <a href="https://web.dev/extract-critical-css/">Extract critical CSS</a> by Milica Mihajlija for an overview of the technique and different tools available.</p>
<p>Once again, this may not be the best solution for your website. The only way to know for sure is to test before and after.</p>
<h2 id="use-subfont-to-automate-some-steps">Use subfont to automate some steps <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#use-subfont-to-automate-some-steps">#</a></h2>
<p>So what if you don’t want to go through all of these steps? The <a href="https://github.com/Munter/subfont">subfont</a> npm package will do many of these steps in addition to dynamically subsetting your fonts at build. It takes some more set-up time, but it’s definitely worth a try.</p>
<p>Are you a fan of <a href="https://www.gatsbyjs.org/">Gatsby</a>? There’s even a <a href="https://www.gatsbyjs.org/packages/gatsby-plugin-subfont/">subfont plugin</a> for it.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">🚀 Day 3 of <a href="https://twitter.com/hashtag/devAdvent?src=hash&ref_src=twsrc%5Etfw">#devAdvent</a>: SubFont, by <a href="https://twitter.com/_munter_?ref_src=twsrc%5Etfw">@_munter_</a>!<br /><br />There are best practices for font loading performance that can shave second of load time. With Subfont, Peter automated the whole process. I used to do a lot of this by hand, now it's quick as an npm i!<a href="https://t.co/yukja6AqsX">https://t.co/yukja6AqsX</a> <a href="https://t.co/hgjLWa6cn9">pic.twitter.com/hgjLWa6cn9</a></p>— Sarah Drasner (@sarah_edo) <a href="https://twitter.com/sarah_edo/status/1069628705163681792?ref_src=twsrc%5Etfw">December 3, 2018</a></blockquote> <script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<h2 id="additional-considerations">Additional Considerations <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#additional-considerations">#</a></h2>
<h3 id="host-static-assets-on-a-cdn">Host static assets on a CDN <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#host-static-assets-on-a-cdn">#</a></h3>
<p>One thing Google Fonts does offer is a fast and reliable content delivery network (CDN). You should also host your static assets on a CDN for faster delivery to users in different regions. We use AWS S3 plus Cloudfront, the CDN service offered by Amazon, and Netlify which uses AWS and Google Cloud Platform behind the scenes in the same way, but many options exist.</p>
<h3 id="size-and-popular-fonts">Size and Popular Fonts <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#size-and-popular-fonts">#</a></h3>
<p>In some of my tests for our company website, I noticed smaller font file sizes for otherwise equal character sets for some fonts hosted by Google. My theory is this is due to Google’s variants for optimization:</p>
<blockquote>
<p>Google Fonts maintains 30+ optimized variants for each font and automatically detects and delivers the optimal variant for each platform and browser.</p>
<p class="blockquote-source">—from <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization#reducing_font_size_with_compression">Web Font Optimization</a> by Ilya Grigorik</p>
</blockquote>
<p>We used to say that very popular fonts like Open Sans and Roboto are likely to exist in your users’ cache. Sadly, shared cache is gone on all major browsers (and had been gone for a while in Safari) due to security. It was debatable how much benefit we actually got from it in the first place.</p>
<p>So, before you commit to a path of self-hosting, compare the tradeoffs of byte sizes and speed/control.</p>
<p>Want to see all the sample code and performance results? <a href="https://github.com/siakaramalegos/google-fonts-self-hosting">Here is the repo</a>.</p>
<h2 id="resources-you-should-checkout">Resources you should checkout <a class="direct-link" href="https://sia.codes/posts/making-google-fonts-faster/#resources-you-should-checkout">#</a></h2>
<ul>
<li><a href="https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf">Preload, Prefetch And Priorities in Chrome</a> by Addi Osmani</li>
<li><a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization">Web Font Optimization</a> by Ilya Grigorik</li>
<li><a href="https://font-display.glitch.me/">Font-display</a> by Monica Dinculescu</li>
</ul>
Justifying performance improvements using Google Analytics2019-06-13T00:00:00Zhttps://sia.codes/posts/justifying_performance_with_analytics/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/show-money_uolhum.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/show-money_uolhum.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/show-money_uolhum.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/show-money_uolhum.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/show-money_uolhum.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Man's hand holding out a fist full of dollars toward the viewer" importance="high" width="1360" height="765" />
<figcaption>Photo by <a href="https://unsplash.com/photos/MNXaW_ABlZY?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">lucas Favre</a> on <a href="https://unsplash.com/collections/1815009/checkout?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>Many of us who love the web and want to make it fast feel like we’re shouting out into the ether that <a href="https://developers.google.com/web/fundamentals/performance/why-performance-matters/">performance matters</a>, but I’m not sure anyone is listening. We write articles, give talks, organize whole conferences, and we even have a hashtag (#PerfMatters)! But, many companies still don’t listen. It’s not their fault necessarily.</p>
<blockquote>
<p>The business doesn’t care about having a fast site. It cares about making money.</p>
<p class="blockquote-source">—<a href="https://www.youtube.com/watch?v=mLjxXPHuIJo&list=PLNYkxOF6rcIATmAmz7HcCzongGvQEtx8i&index=12&t=0s">Elizabeth Sweeny and Paul Irish talking about speed at Google I/O 2019</a></p>
</blockquote>
<p>Businesses see pushing features as the primary activity that will increase revenue. They find it hard to picture themselves in all the <a href="https://wpostats.com/">stats about companies who have seen bottom-line impacts after improving performance</a>.</p>
<p><strong>We need to reframe the argument in terms of them making money.</strong> Not some other company, them. They don’t want to gamble based on some other company’s experience.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/casino_oiaqow.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/casino_oiaqow.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/casino_oiaqow.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/casino_oiaqow.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/casino_oiaqow.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Slot machines at a casino" width="1360" height="892" />
<figcaption>Photo by <a href="https://unsplash.com/photos/7H7KVCihBvI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Carl Raw</a> on <a href="https://unsplash.com/collections/1815009/checkout?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>This is where Google Analytics (GA) comes in handy. Most businesses already use it, and if they don’t, it’s a quick thing to add. To get the most out of this exercise, you will want to have revenue tracking in your analytics account as well (advanced e-commerce). If you do not have that set up yet, you can still track bounce rates and other metrics by page load speed.</p>
<p>In the rest of this post, I’ll show you how to gather and analyze the data related to load performance so that you can use it to justify work to improve performance.</p>
<h2 id="step-0%3A-clean-up-your-data">Step 0: Clean up your data <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-0%3A-clean-up-your-data">#</a></h2>
<p>Beware the bots. We want our data to be relevant, and unfortunately, a lot of bots out there access our websites and web applications. First, check to see if you already have bot filtering enabled. Go to <em>Admin > View Settings</em>, and scroll down to find the <em>Bot Filtering</em> setting:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/bot-filtering_cnunot.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/bot-filtering_cnunot.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/bot-filtering_cnunot.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/bot-filtering_cnunot.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/bot-filtering_cnunot.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Screenshot of Admin > View Settings and Bot Filtering checkbox" width="1360" height="827" />
<figcaption>Check the box for Exclude all hits from known bots and spiders.</figcaption>
</figure>
<p>Before you save a filter, preview the changes to make sure you’ll get the desired effects. You can create a test view first.</p>
<p>We already had this setting checked, but by chance, I found that we had nearly 15% of our users coming from bots using AWS data centers:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/bots_iejouk.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/bots_iejouk.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/bots_iejouk.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/bots_iejouk.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/bots_iejouk.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="1168" />
<figcaption>The ISP “amazon data services nova” accounted for nearly 15% of our users.</figcaption>
</figure>
<p>This isn’t good. Go to <em>Technology > Network</em> and see if some variation of “amazon data services” shows up in your Service Provider list. Luckily other people have written about this, so the steps to filter out these kinds of bots are already outlined in <a href="https://www.glowmetrics.com/blog/analytics-bot-spam-amazon/">this article by GlowMetrics</a>.</p>
<h2 id="step-1%3A-check-your-page-speed-sample-rate">Step 1: Check your page speed sample rate <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-1%3A-check-your-page-speed-sample-rate">#</a></h2>
<p>By default, only 1% of users will be tracked for speed metrics in Google Analytics. If your business gets a lot of traffic, this sample rate might be fine. If not, you might want to bump it up at least temporarily to both establish a baseline before changes and see the results after.</p>
<p>First, how much data are you getting? Go to <em>Behavior > Site Speed > Overview</em>. You should see a statement like “XX of pageviews sent page load sample”. For us, this wasn’t a high enough number for that time period to effectively bucket page loads later. You can see your page timings buckets by going to <em>Behavior > Site Speed > Page Timings</em>. By default, it puts you in the <em>Explorer</em> tab. Switch over to the <em>Distribution</em> tab, and you will see page load buckets, and how big the sample is, and the distribution for each:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/buckets_foldoi.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/buckets_foldoi.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/buckets_foldoi.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/buckets_foldoi.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/buckets_foldoi.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="501" />
<figcaption>Page Load Time Buckets view in Google Analytics</figcaption>
</figure>
<p>These sample sizes are fine for seeing a simple distribution of page load times, but once we start segmenting those buckets into bounce rates, made a purchase, etc., the sample sizes quickly become too small.</p>
<p>If you determine you need to increase your sample rate, use the <code>siteSpeedSampleRate</code> field when creating your tag (<a href="https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate">docs</a>):</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token function">ga</span><span class="token punctuation">(</span><span class="token string">'create'</span><span class="token punctuation">,</span> <span class="token string">'UA-XXXX-Y'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token string-property property">'siteSpeedSampleRate'</span><span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<h2 id="step-2%3A-wait">Step 2: Wait <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-2%3A-wait">#</a></h2>
<p>Gather baseline data. A month will give you a more solid baseline not subject to daily variations which might happen in a shorter report of a week or less.</p>
<h2 id="step-3%3A-set-up-your-report">Step 3: Set up your report <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-3%3A-set-up-your-report">#</a></h2>
<p>Now that you have some data to work with, let’s dive in and chop it up. Once logged in to your Google Analytics dashboard, go to the <em>Behavior > Site Speed > Page Timings</em> section.</p>
<ol>
<li>In the top right, change the time period to your desired time period. 10,000–20,000 total data points would be a good starting point to make sure the buckets we analyze don’t get too small.</li>
<li>By default, you will be in the <em>Explorer</em> tab. Switch to the <em>Distribution</em> tab to see your page timing buckets:</li>
</ol>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/distribution_cnd3mz.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/distribution_cnd3mz.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/distribution_cnd3mz.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/distribution_cnd3mz.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/distribution_cnd3mz.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="602" />
<figcaption>Page Timings, Distribution tab</figcaption>
</figure>
<p>Already, we can see some great data like our average page load time and then buckets of how many users experienced page load times of 0–1 sec, 1–3 sec, 3–7 sec, and so on. By expanding each of these buckets, you can see even finer grained data for each bucket. These smaller buckets vary by size from 0.2 seconds to 2 seconds to 15 seconds as you go into the higher load time brackets.</p>
<p>This is great, but what we’d really like to know is the bounce rate and the rate of users making a purchase for each bucket (or any key performance metric for the company). We can do this with segmenting. Above the chart and tabs, click on the <em>+Add Segment button</em>. Add <em>Bounced Sessions</em> and <em>Made a Purchase</em> (if you have advanced e-commerce enabled) to the segments. If another metric is vital to the business, select that one.</p>
<p>We now have a page-speed-bucketed chart for all views, bounced sessions, and views that resulted in a purchase. The only problem is that the percentages aren’t very useful — they show the percent of total views, not the percent of views in that bucket:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/segments_qylgwi.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/segments_qylgwi.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/segments_qylgwi.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/segments_qylgwi.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/segments_qylgwi.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="681" />
<figcaption>Segmented chart showing percent of TOTAL, not of that bucket</figcaption>
</figure>
<h2 id="step-4%3A-make-your-argument">Step 4: Make your argument <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-4%3A-make-your-argument">#</a></h2>
<p>To make our argument, we’re going to need to calculate the bounce and purchase rates within each bucket. For example, in the above screenshot, to calculate the purchase rate for users with a 1–3 second load time, divide 1,987 by 9,205 to give 21.6%. Repeat this for all buckets and for bounce rate.</p>
<p>You can export the top-level data and do this calculation in a spreadsheet. Unfortunately, the export does not include the finer-grained buckets. You would need to manually copy that data over for analysis if desired.</p>
<p>Now we can generate charts to share with the decision makers that show how both purchase rate declines and bounce rate increases as page load time increases for a user:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/purchased_qlwhzo.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/purchased_qlwhzo.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/purchased_qlwhzo.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/purchased_qlwhzo.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/purchased_qlwhzo.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="681" />
<figcaption>Made a purchase vs page load time showing decreased purchase rate with increased page load times</figcaption>
</figure>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/bounce_uwfrgx.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/bounce_uwfrgx.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/bounce_uwfrgx.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/bounce_uwfrgx.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/bounce_uwfrgx.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="" width="1360" height="681" />
<figcaption>Bounce rate vs page load time showing increased bounces with increased page load times</figcaption>
</figure>
<p>Want to go the extra mile? Calculate theoretically how much more money they might earn if you improve site speed by, say, 1 second. Determine the average revenue per purchase then move each bucket over by 1 second and calculate the difference in total revenue between the two scenarios.</p>
<h2 id="step-5%3A-improve-performance">Step 5: Improve performance <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-5%3A-improve-performance">#</a></h2>
<p>You’ve convinced the company to let you make performance improvements. Congratulations! Now you have to do the real work. Don’t know how to get started? <a href="https://web.dev/fast">web.dev/fast</a> is a good starting point.</p>
<h2 id="step-6%3A-make-it-rain">Step 6: Make it rain <a class="direct-link" href="https://sia.codes/posts/justifying_performance_with_analytics/#step-6%3A-make-it-rain">#</a></h2>
<figure class="portrait-image">
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/rain_tgtrfa.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/rain_tgtrfa.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/rain_tgtrfa.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/rain_tgtrfa.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/rain_tgtrfa.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Woman in fur coat sitting in a posh leather chair with money raining down on her" width="500" height="750" />
<figcaption>Photo by <a href="https://unsplash.com/photos/mMD6mossbVk?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Andrew Worley</a> on <a href="https://unsplash.com/collections/1815009/checkout?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>Once you’ve deployed your performance improvements, don’t forget to compare the before and after data for your key business metrics. By preparing analyses like these for our companies, we can better establish ourselves as trusted business partners… and do less work next time we need to convince them to invest in performance, whether that’s page load time, long-running JavaScript, paint, user experience, perceived performance or anything else related to performance.</p>
<p>Do you have a story about convincing your company to invest in performance? I’d love to hear about it!</p>
<p>Thanks to <a href="https://twitter.com/JoubranJad">Jad Joubran</a> for reviewing this post!</p>
An In-Depth Tutorial of Webmentions + Eleventy2019-11-22T00:00:00Zhttps://sia.codes/posts/webmentions-eleventy-in-depth/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/hands-laptop_rdfolj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/hands-laptop_rdfolj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/hands-laptop_rdfolj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/hands-laptop_rdfolj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/hands-laptop_rdfolj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="hands on a laptop keyboard" importance="high" width="1360" height="808" />
<figcaption>Photo by <a href="https://unsplash.com/@neonbrand?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">NeONBRAND</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>I am a huge fan of the static site generator <a href="https://www.11ty.io/">Eleventy</a> so far, and I was super excited to try out <a href="https://indieweb.org/Webmention">Webmentions</a> with them.</p>
<blockquote>
<p>Webmention is a web standard for mentions and conversations across the web, a powerful building block that is used for a growing federated network of comments, likes, reposts, and other rich interactions across the decentralized social web.</p>
<p class="blockquote-source">—from <a href="https://indieweb.org/Webmention">IndieWeb.org</a></p>
</blockquote>
<p>They are a cool tool for enabling social interactions when you host your own content. Max Böck wrote an excellent post, <a href="https://mxb.dev/blog/using-webmentions-on-static-sites/">Static Indieweb pt2: Using Webmentions</a>, which walks through his implementation. He also created an Eleventy starter, <a href="https://github.com/maxboeck/eleventy-webmentions">eleventy-webmentions</a>, which is a basic starter template with webmentions support.</p>
<p>So why am I writing this post? Sadly, I started with the <a href="https://github.com/11ty/eleventy-base-blog">eleventy-base-blog</a>, and didn't notice the <a href="https://github.com/maxboeck/eleventy-webmentions">eleventy-webmentions</a> starter until after I had already built my site. I also struggled to fully build out the functionality, partly because I'm still an Eleventy n00b. So I wanted to share the detailed steps I used in the hopes that it will help more of you join the Indie Web.</p>
<aside><strong>Prefer to learn by video?</strong> I gave <a href="https://sia.codes/posts/webmentions-eleventy-talk/">talks</a> at Jamstack Toronto and Magnolia JS about the concepts behind Webmentions and adding them to an Eleventy project. Check out <a href="https://sia.codes/posts/webmentions-eleventy-talk/">the recording</a>.</aside>
<p>The perspective of this post is adding webmentions to an Eleventy site after the fact. The files, folders, and config architecture match the <code>eleventy-base-blog</code>, but you can likely use this as a starting point for any Eleventy site. Make sure you watch out for spots where your analogous architecture may be different.</p>
<p>This post will cover how to:</p>
<ol>
<li>Set up webmentions for your website</li>
<li>Securely store your webmention token in Netlify and inject it during development</li>
<li>Fetch webmentions at build time</li>
<li>Save webmentions in a cached file that persists between Netlify builds</li>
<li>Render webmentions in Eleventy using Nunjucks</li>
</ol>
<p>The code in this post is a mash up of Max Böck's original post and <a href="https://github.com/maxboeck/mxb">personal site</a>, the <a href="https://github.com/maxboeck/eleventy-webmentions">eleventy-webmentions</a> starter, Zach Leatherman's <a href="https://github.com/zachleat/zachleat.com">personal site</a>, and the edits I made during my implementation. I am hugely grateful for their work, as I never would have gotten this far without it.</p>
<h2 id="the-serverless-deployment-architecture">The serverless deployment architecture <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#the-serverless-deployment-architecture">#</a></h2>
<p>Before we get started, let's outline the setup. My setup uses Eleventy paired with Github and Netlify.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/sia_karamalegos_netlify_devops1_zpdojl.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/sia_karamalegos_netlify_devops1_zpdojl.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/sia_karamalegos_netlify_devops1_zpdojl.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/sia_karamalegos_netlify_devops1_zpdojl.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/sia_karamalegos_netlify_devops1_zpdojl.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Three entities: a laptop, Github, and Netlify" width="2048" height="1536" />
<figcaption>The three entities used in my "dev ops"</figcaption>
</figure>
<p>If you're familiar with most Netlify setups, when I push my code to Github, that triggers a build and deploy on Netlify because the content of the <code>main</code> branch has been updated. We're going to add a <a href="https://github.com/siakaramalegos/netlify-plugin-cache-folder">cache folder plugin</a> that will stay in our build between deploys. This is where we will save our webmentions so that we only need to import new ones on build.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/sia_karamalegos_netlify_devops2_zp9pji.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/sia_karamalegos_netlify_devops2_zp9pji.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/sia_karamalegos_netlify_devops2_zp9pji.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/sia_karamalegos_netlify_devops2_zp9pji.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/sia_karamalegos_netlify_devops2_zp9pji.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Arrow from laptop to Github for git push, then arrow from Github to Netlify to build and deploy" width="2048" height="1536" />
<figcaption>New commits trigger new deploys, and the _cache folder is preserved between Netlify builds</figcaption>
</figure>
<p>Another cool tool we will use is <a href="https://github.com/features/actions">Github actions</a> to fake a cron job to trigger deploys on a recurring schedule. This is so we can import new webmentions every X hours.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/sia_karamalegos_netlify_devops3_st2bd2.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/sia_karamalegos_netlify_devops3_st2bd2.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/sia_karamalegos_netlify_devops3_st2bd2.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/sia_karamalegos_netlify_devops3_st2bd2.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/sia_karamalegos_netlify_devops3_st2bd2.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="New arrow from Github to Netlify showing new builds every 4 hours" width="2048" height="1536" />
<figcaption>Github actions sends a POST request to Netlify to trigger builds every 4 hours</figcaption>
</figure>
<p>Finally, we need to save a secret API token for pulling our webmentions. We want to save that securely in our <a href="https://docs.netlify.com/configure-builds/environment-variables/">Netlify environment variables</a>. Then, we can use <a href="https://docs.netlify.com/cli/get-started/"><code>netlify-cli</code></a> to use that secret when running in our local environment.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/sia_karamalegos_netlify_devops4_ljulpj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/sia_karamalegos_netlify_devops4_ljulpj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/sia_karamalegos_netlify_devops4_ljulpj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/sia_karamalegos_netlify_devops4_ljulpj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/sia_karamalegos_netlify_devops4_ljulpj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Arrow from laptop to Github for git push, then arrow from Github to Netlify to build and deploy" width="2048" height="1536" />
<figcaption>New commits trigger new deploys, and the _cache folder is preserved between builds</figcaption>
</figure>
<h2 id="step-1%3A-sign-up-for-webmentions-and-store-the-api-key">Step 1: Sign up for webmentions and store the API key <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-1%3A-sign-up-for-webmentions-and-store-the-api-key">#</a></h2>
<p>First, we need to sign up with <a href="http://webmention.io/">webmention.io</a>, the third-party service that lets us use the power of webmentions on static sites.</p>
<ol>
<li>Set up IndieAuth so that webmention will know that you control your domain. Follow the setup instructions <a href="https://indieauth.com/setup">on their site</a>.</li>
<li>Go to <a href="https://webmention.io/">webmention.io/</a>.</li>
<li>Enter your website's URL in the "Web Sign-In" input, and click "Sign in".</li>
</ol>
<p>If your sign in was successful, you should be directed to the webmentions dashboard where you will be given two <code><link></code> tags. You should put these in the <code><head></code> of your website:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/layouts/base.njk --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://webmention.io/sia.codes/webmention<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pingback<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://webmention.io/sia.codes/xmlrpc<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></span></code></pre>
<p>You'll also be given an API key. We want to safely store that <code>WEBMENTION_IO_TOKEN</code> in our <a href="https://docs.netlify.com/configure-builds/environment-variables/">Netlify environment variables</a>.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/env_netlify_k4m8k8.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/env_netlify_k4m8k8.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/env_netlify_k4m8k8.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/env_netlify_k4m8k8.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/env_netlify_k4m8k8.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Screenshot of Netlify site settings showing the WEBMENTION_IO_TOKEN variable" width="2554" height="794" />
<figcaption>In your Netlify dashboard, go to <strong>Site Settings > Build & Deploy > Environment</strong> to add WEBMENTION_IO_TOKEN and its value</figcaption>
</figure>
<p>To use the environment variable locally while developing on your machine, install the <a href="https://docs.netlify.com/cli/get-started/"><code>netlify-cli</code></a>:</p>
<pre><code>npm install netlify-cli -g
</code></pre>
<p>And change your development server script to use <code>netlify dev</code>. It will use your build script save in Netlify and hydrate all environment variables. If your previous script set input and output directories in the command itself, you will need to <a href="https://www.11ty.dev/docs/config/">move those settings</a> to your <strong>.eleventy.js</strong> config instead.</p>
<aside>The original version of this article recommended using <a href="https://www.npmjs.com/package/dotenv">dotenv</a> for managing env variables. I've found that using <a href="https://docs.netlify.com/cli/get-started/">Netlify CLI</a> is easier for me and requires no extra scripts. If you prefer the dotenv method, see this <a href="https://gist.github.com/siakaramalegos/ad2ef00cf9eb53cdeb651cd9f751b89c">gist</a>.</aside>
<p>You probably want some content in your webmentions. If you use Twitter, <a href="https://brid.gy/">Bridgy</a> is a great way to bring in mentions from Twitter. First make sure your website is listed in your profile, then link it.</p>
<h2 id="how-it's-all-going-to-work">How it's all going to work <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#how-it's-all-going-to-work">#</a></h2>
<p>When we run a build with <code>NODE_ENV=production</code>, we are going to fetch new webmentions from the last time we fetched. These will be saved in <code>_cache/webmentions.json</code>. These mentions come from the <a href="https://github.com/aaronpk/webmention.io#api">webmention.io API</a>.</p>
<p>When we do any build, for each page:</p>
<ul>
<li>From the webmentions cache in <code>_cache/webmentions.json</code>, only keep webmentions that match the URL of the page (for me, this is each blog post).</li>
<li>Use a <code>webmentionsByType</code> function to filter for one type (e.g., likes or replies)</li>
<li>Use a <code>size</code> function to calculate the count of those mentions by type</li>
<li>Render the count with mention type as a heading (e.g., "7 Replies")</li>
<li>Render a list of the mentions of that type (e.g., linked Twitter profile pictures representing each like)</li>
</ul>
<h2 id="step-2%3A-set-up%2C-dependencies%2C-and-the-netlify-cache">Step 2: Set up, dependencies, and the Netlify cache <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-2%3A-set-up%2C-dependencies%2C-and-the-netlify-cache">#</a></h2>
<p>First, we need to set up our domain as another property in our <code>_data/metadata.json</code>. Let's also add our root URL for use later:</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// _data/metadata.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">//...other metadata</span></span><br /><span class="highlight-line"> <span class="token property">"domain"</span><span class="token operator">:</span> <span class="token string">"sia.codes"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"https://sia.codes"</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Next, we'll add a few more dependencies:</p>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">npm</span> <span class="token function">install</span> <span class="token parameter variable">-D</span> lodash node-fetch netlify-plugin-cache-folder</span></code></pre>
<p>We will only request new webmentions in production builds. Thus, we need to update our <code>build</code> script to set the <code>NODE_ENV</code> in our <code>package.json</code>. This is the script that should be set as your Build Command in Netlify. To build locally, we'll need that environment variable locally. So add another script called <code>build:local</code>. For this to work, you main need to push these updates to Github so that Netlify has the new scripts:</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// package.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// ... more config</span></span><br /><span class="highlight-line"> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"NODE_ENV=production npx @11ty/eleventy"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"build:local"</span><span class="token operator">:</span> <span class="token string">"NODE_ENV=production netlify build"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"netlify dev"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token comment">// more scripts...</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>To finish setting up our <a href="https://github.com/siakaramalegos/netlify-plugin-cache-folder">cache folder plugin</a>, we also need to create a <strong>netlify.toml</strong> file in the root of our project with the following contents:</p>
<pre class="language-toml"><code class="language-toml"><span class="highlight-line"><span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm run build"</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">plugins</span><span class="token punctuation">]</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token key property">package</span> <span class="token punctuation">=</span> <span class="token string">"netlify-plugin-cache-folder"</span></span></code></pre>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/webmentions-eleventy-in-depth/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h2 id="step-3%3A-fetch-webmentions-during-the-eleventy-build">Step 3: Fetch webmentions during the Eleventy build <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-3%3A-fetch-webmentions-during-the-eleventy-build">#</a></h2>
<p>Now we can focus on the fetch code. Okay, okay, I know this next file is beaucoup long, but I thought it was more difficult to understand out of context. Here are the general steps happening in the code:</p>
<ol>
<li>Read any mentions from the file cache at <code>_cache/webmentions.json</code>.</li>
<li>If our environment is "production", fetch new webmentions since the last time we fetched. Merge them with the cached ones and save to the cache file. Return the merged set of mentions.</li>
<li>If our environment is not "production", return the cached mentions from the file</li>
</ol>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _data/webmentions.js</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> fetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'node-fetch'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> unionBy <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'lodash/unionBy'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> domain <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./metadata.json'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>domain</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Load .env variables with dotenv</span></span><br /><span class="highlight-line"><span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Define Cache Location and API Endpoint</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">CACHE_FILE_PATH</span> <span class="token operator">=</span> <span class="token string">'_cache/webmentions.json'</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">API</span> <span class="token operator">=</span> <span class="token string">'https://webmention.io/api'</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">TOKEN</span> <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">WEBMENTION_IO_TOKEN</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchWebmentions</span><span class="token punctuation">(</span><span class="token parameter">since<span class="token punctuation">,</span> perPage <span class="token operator">=</span> <span class="token number">10000</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// If we dont have a domain name or token, abort</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>domain <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">TOKEN</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">'>>> unable to fetch webmentions: missing domain or token'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token boolean">false</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">let</span> url <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">API</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/mentions.jf2?domain=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>domain<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&token=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&per-page=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>perPage<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>since<span class="token punctuation">)</span> url <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&since=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>since<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span> <span class="token comment">// only fetch new mentions</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> feed <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">>>> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>feed<span class="token punctuation">.</span>children<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> new webmentions fetched from </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">API</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> feed</span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token keyword">null</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Merge fresh webmentions with cached entries, unique per id</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">mergeWebmentions</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token function">unionBy</span><span class="token punctuation">(</span>a<span class="token punctuation">.</span>children<span class="token punctuation">,</span> b<span class="token punctuation">.</span>children<span class="token punctuation">,</span> <span class="token string">'wm-id'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// save combined webmentions in cache file</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">writeToCache</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> dir <span class="token operator">=</span> <span class="token string">'_cache'</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> fileContent <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token comment">// create cache folder if it doesnt exist already</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>dir<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> fs<span class="token punctuation">.</span><span class="token function">mkdirSync</span><span class="token punctuation">(</span>dir<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token comment">// write data to cache json file</span></span><br /><span class="highlight-line"> fs<span class="token punctuation">.</span><span class="token function">writeFile</span><span class="token punctuation">(</span><span class="token constant">CACHE_FILE_PATH</span><span class="token punctuation">,</span> fileContent<span class="token punctuation">,</span> <span class="token parameter">err</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token keyword">throw</span> err</span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">>>> webmentions cached to </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CACHE_FILE_PATH</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// get cache contents from json file</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">readFromCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span><span class="token constant">CACHE_FILE_PATH</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> cacheFile <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token constant">CACHE_FILE_PATH</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>cacheFile<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// no cache found.</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">lastFetched</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'>>> Reading webmentions from cache...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">const</span> cache <span class="token operator">=</span> <span class="token function">readFromCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>cache<span class="token punctuation">.</span>children<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">>>> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cache<span class="token punctuation">.</span>children<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> webmentions loaded from cache</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Only fetch new mentions in production</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">===</span> <span class="token string">'production'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'>>> Checking for new webmentions...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> feed <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchWebmentions</span><span class="token punctuation">(</span>cache<span class="token punctuation">.</span>lastFetched<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>feed<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> webmentions <span class="token operator">=</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">lastFetched</span><span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token function">mergeWebmentions</span><span class="token punctuation">(</span>cache<span class="token punctuation">,</span> feed<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token function">writeToCache</span><span class="token punctuation">(</span>webmentions<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> webmentions</span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">return</span> cache</span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<h2 id="step-4%3A-add-handy-filters-(functions)-for-our-templates">Step 4: Add handy filters (functions) for our templates <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-4%3A-add-handy-filters-(functions)-for-our-templates">#</a></h2>
<p>Now that we've populated our webmentions cache, we need to use it. First we have to generate the functions, or "filters" that Eleventy will use to build our templates.</p>
<p>First, I like keeping some filters separated from the main Eleventy config so that it doesn't get too bogged down. The separate filters file will define each of our filters in an object. The keys are the filter names and the values are the filter functions. In <code>_11ty/filters.js</code>, add each of our new filter functions:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _11ty/filters.js</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token punctuation">{</span> DateTime <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"luxon"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Already in eleventy-base-blog</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token function-variable function">getWebmentionsForUrl</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">webmentions<span class="token punctuation">,</span> url</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> webmentions<span class="token punctuation">.</span>children<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">entry</span> <span class="token operator">=></span> entry<span class="token punctuation">[</span><span class="token string">'wm-target'</span><span class="token punctuation">]</span> <span class="token operator">===</span> url<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token function-variable function">size</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">mentions</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token operator">!</span>mentions <span class="token operator">?</span> <span class="token number">0</span> <span class="token operator">:</span> mentions<span class="token punctuation">.</span>length</span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token function-variable function">webmentionsByType</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">mentions<span class="token punctuation">,</span> mentionType</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> mentions<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">entry</span> <span class="token operator">=></span> <span class="token operator">!</span><span class="token operator">!</span>entry<span class="token punctuation">[</span>mentionType<span class="token punctuation">]</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">readableDateFromISO</span><span class="token operator">:</span> <span class="token punctuation">(</span>dateStr<span class="token punctuation">,</span> formatStr <span class="token operator">=</span> <span class="token string">"dd LLL yyyy 'at' hh:mma"</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> DateTime<span class="token punctuation">.</span><span class="token function">fromISO</span><span class="token punctuation">(</span>dateStr<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFormat</span><span class="token punctuation">(</span>formatStr<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Now to use these new filters, in our <code>.eleventy.js</code>, we need to loop through the keys of that filters object to add each filter to our Eleventy config:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// .eleventy.js</span></span><br /><span class="highlight-line"><span class="token comment">// ...Other imports</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> filters <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./_11ty/filters'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Filters</span></span><br /><span class="highlight-line"> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>filters<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">filterName</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span>filterName<span class="token punctuation">,</span> filters<span class="token punctuation">[</span>filterName<span class="token punctuation">]</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Other configs...</span></span></code></pre>
<p>I do not have a sanitize HTML filter because I noticed the content data has a <code>text</code> field that is already sanitized. Maybe this is new or not true for all webmentions. I'll update this post if I add it in.</p>
<h2 id="step-5%3A-render-the-webmentions-in-eleventy-using-nunjucks">Step 5: Render the webmentions in Eleventy using Nunjucks <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-5%3A-render-the-webmentions-in-eleventy-using-nunjucks">#</a></h2>
<p>Now we're ready to put it all together and render our webmentions. I put them at the bottom of each blog post, so in my <code>_includes/layouts/post.njk</code>, I add a new section for the webmentions. Here, we are setting a variable called <code>webmentionUrl</code> to the page's full URL, and passing it into the partial for the <code>webmentions.njk</code> template:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/layouts/post.njk --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>section</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Webmentions<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% set webmentionUrl %}{{ page.url | url | absoluteUrl(site.url) }}{% endset %}</span><br /><span class="highlight-line"> {% include 'webmentions.njk' %}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>section</span><span class="token punctuation">></span></span></span></code></pre>
<p>Now we can write the webmentions template. In this example, I will show links, retweets, and replies. First, I set all of the variables I will need for rendering in a bit:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/webmentions.njk --></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- Filter the cached mentions to only include ones matching the post's url --></span></span><br /><span class="highlight-line"> {% set mentions = webmentions | getWebmentionsForUrl(metadata.url + webmentionUrl) %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Set reposts as mentions that are `repost-of` --></span></span><br /><span class="highlight-line"> {% set reposts = mentions | webmentionsByType('repost-of') %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Count the total reposts --></span></span><br /><span class="highlight-line"> {% set repostsSize = reposts | size %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Set likes as mentions that are `like-of` --></span></span><br /><span class="highlight-line"> {% set likes = mentions | webmentionsByType('like-of') %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Count the total likes --></span></span><br /><span class="highlight-line"> {% set likesSize = likes | size %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Set replies as mentions that are `in-reply-to` --></span></span><br /><span class="highlight-line"> {% set replies = mentions | webmentionsByType('in-reply-to') %}</span><br /><span class="highlight-line"> <span class="token comment"><!-- Count the total replies --></span></span><br /><span class="highlight-line"> {% set repliesSize = replies | size %}</span></code></pre>
<p>With our variables set, we can now use that data for rendering. Here I'll walk through only "replies", but feel free to see a snapshot of how I handled the remaining sets in <a href="https://gist.github.com/siakaramalegos/b1f7ded21f9ecddaee91e3f6d88e2e48">this gist</a>.</p>
<p>Since replies are more complex than just rendering a photo and link, I call another template to render the individual webmention. Here we render the count of replies and conditionally plural-ify the word "Reply". Then we loop through the reply webmentions to render them with a new nunjucks partial:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/webmentions.njk --></span></span><br /><span class="highlight-line"><span class="token comment"><!-- ...setting variables and other markup --></span></span><br /><span class="highlight-line">{% if repliesSize > 0 %}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention-replies<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h3</span><span class="token punctuation">></span></span>{{ repliesSize }} {% if repliesSize == "1" %}Reply{% else %}Replies{% endif %}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> {% for webmention in replies %}</span><br /><span class="highlight-line"> {% include 'webmention.njk' %}</span><br /><span class="highlight-line"> {% endfor %}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line">{% endif %}</span></code></pre>
<p>Finally, we can render our replies using that new partial for a single reply webmention. Here, if the author has a photo, we show it, otherwise we show an avatar. We also conditionally show their name if it exists, otherwise we show "Anonymous". We use our <code>readableDateFromISO</code> filter to show a human-friendly published date, and finally render the text of the webmention:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/webmention.njk --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention-{{ webmention['wm-id'] }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention__meta<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% if webmention.author %}</span><br /><span class="highlight-line"> {% if webmention.author.photo %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ webmention.author.photo }}<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ webmention.author.name }}<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>48<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>48<span class="token punctuation">"</span></span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% else %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ '/img/avatar.svg' | url }}<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>48<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>48<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% endif %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>h-card u-url<span class="token punctuation">"</span></span> <span class="token attr-name">{%</span> <span class="token attr-name">if</span> <span class="token attr-name">webmention.url</span> <span class="token attr-name">%}href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ webmention.url }}<span class="token punctuation">"</span></span> <span class="token attr-name">{%</span> <span class="token attr-name">endif</span> <span class="token attr-name">%}</span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>noopener noreferrer<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>p-name<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ webmention.author.name }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% else %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Anonymous<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% endif %}</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> {% if webmention.published %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>time</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>postlist-date<span class="token punctuation">"</span></span> <span class="token attr-name">datetime</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ webmention.published }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {{ webmention.published | readableDateFromISO }}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>time</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% endif %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {{ webmention.content.text }}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>article</span><span class="token punctuation">></span></span></span></code></pre>
<p>Time to bravely jumping into the black hole...</p>
<h2 id="step-6%3A-run-it!">Step 6: Run it! <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#step-6%3A-run-it!">#</a></h2>
<p>Does it work?!?! We can finally test it out. First run <code>npm run build:local</code> to generate an initial list of webmentions that is saved to the <code>_cache/webmentions.json</code> file. Then run your local development server and see if the rendering worked! Of course, you'll need to have at least one webmention associated with a page to see anything. 😁</p>
<p>You can see the result of my own implementation below. Good luck! Let me know how it turns out or if you find in bugs or errors in this post!</p>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#conclusion">#</a></h2>
<p>Webmentions let us own our content on our own domains while still engaging socially with other people through likes, replies, and other actions. How have you used webmentions on your site? <a href="https://twitter.com/TheGreenGreek">Tweet</a> at me to let me know!</p>
<p>Continue your journey by using Microformats. Keith Grant has a great write-up in his article <a href="https://keithjgrant.com/posts/2019/02/adding-webmention-support-to-a-static-site/">Adding Webmention Support to a Static Site</a>. Check out the "Enhancing with Microformats" section for an explanation and examples.</p>
<h2 id="additional-resources">Additional resources <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-in-depth/#additional-resources">#</a></h2>
<ul>
<li><a href="https://antonio.laguna.es/posts/usando-webmentions-en-eleventy/">Usando Webmentions en Eleventy</a> by Antonio Laguna en español</li>
<li>You can find the full code for my site on <a href="https://github.com/siakaramalegos/sia.codes-eleventy">Github</a>. It will evolve in the future, I'm sure, so you can focus on <a href="https://github.com/siakaramalegos/sia.codes-eleventy/commit/d7318565917b1342b38d6b3bff4e3e548276afca">this commit</a> which has the bulk of my changes for adding webmentions.</li>
<li>How I set up a "cron" job through Github actions to periodically rebuild my site on Netlify (to grab and post new webmentions) is covered in <a href="https://www.voorhoede.nl/en/blog/scheduling-netlify-deploys-with-github-actions/">Scheduling Netlify deploys with GitHub Actions</a>.</li>
</ul>
Show conditional Twitter intents with Eleventy2019-11-26T00:00:00Zhttps://sia.codes/posts/conditional-twitter-intents-with-eleventy/<p><img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/twitter-phone_b3q80y.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/twitter-phone_b3q80y.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/twitter-phone_b3q80y.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/twitter-phone_b3q80y.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/twitter-phone_b3q80y.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Mobile phone laying on table with blue screen and Twitter logo showing" importance="high" width="1360" height="907" /></p>
<p>In today's episode of Making Sia's Blog Better™️, I wanted to better encourage readers to engage in the conversation about my blog posts. I used <a href="https://www.11ty.io/">Eleventy</a> to build my blog and recently added <a href="https://indieweb.org/Webmention">Webmentions</a>, which are a really cool way of making self-hosted blogs a bit more engaging without implementing comments. You can check them out at the bottom of this page.</p>
<p>Currently, most of the interaction associated with my posts are through Twitter or syndicated copies on <a href="https://dev.to/thegreengreek">Dev.to</a> with a canonical link. Unfortunately, we don't have a way to integrate <a href="http://dev.to/">Dev.to</a> with webmentions yet, so my primary focus was increasing engagement through Twitter that will actually help populate the article's webmentions.</p>
<h2 id="twitter-web-intents">Twitter web intents <a class="direct-link" href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#twitter-web-intents">#</a></h2>
<p><a href="https://developer.twitter.com/en/docs/twitter-for-websites/web-intents/overview">Web intents</a> are the simplest way to add Twitter interactions on a website. You can build a link to compose, retweet, like, find, and more without the annoyance of added JavaScript or user tracking. Twitter wants you to use JavaScript, but the links work without it as well:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitter.com/intent/tweet?in_reply_to=463440424141459456<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Reply<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitter.com/intent/retweet?tweet_id=463440424141459456<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Retweet<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitter.com/intent/like?tweet_id=463440424141459456<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Like<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></span></code></pre>
<h2 id="webmentions">Webmentions <a class="direct-link" href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#webmentions">#</a></h2>
<p>If you aren't already familiar with webmentions, the connection between them and Twitter through <a href="https://brid.gy/">Bridgy</a> only scans your own Twitter account. Readers might not be familiar with this, so if they want a comment to actually show up on the blog, they need to reply to one of my tweets that contains a link to the post. I'll call this a "target tweet"</p>
<p>But what if I don't always have a tweet for that post? 🤔</p>
<h2 id="conditional-share-component">Conditional share component <a class="direct-link" href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#conditional-share-component">#</a></h2>
<p>Luckily with Eleventy and Nunjucks (or whatever templating format you use), we can set up a Twitter sharing box that uses different intents based on whether a target tweet has been specified.</p>
<p>Let's define what the box options should be...</p>
<p>If a target Tweet doesn't exist, then we want a box that looks like this where "share it" is for a fresh share and "find the conversation" searches Twitter for tweets with the post's URL:</p>
<div class="share-well" style="margin-bottom:40px">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"><path fill="#55acee" d="M23.44 4.83c-.8.37-1.5.38-2.22.02.93-.56.98-.96 1.32-2.02-.88.52-1.86.9-2.9 1.1-.82-.88-2-1.43-3.3-1.43-2.5 0-4.55 2.04-4.55 4.54 0 .36.03.7.1 1.04-3.77-.2-7.12-2-9.36-4.75-.4.67-.6 1.45-.6 2.3 0 1.56.8 2.95 2 3.77-.74-.03-1.44-.23-2.05-.57v.06c0 2.2 1.56 4.03 3.64 4.44-.67.2-1.37.2-2.06.08.58 1.8 2.26 3.12 4.25 3.16C5.78 18.1 3.37 18.74 1 18.46c2 1.3 4.4 2.04 6.97 2.04 8.35 0 12.92-6.92 12.92-12.93 0-.2 0-.4-.02-.6.9-.63 1.96-1.22 2.56-2.14z"></path></svg>
<p>If you liked this article and think others should read it, please <a href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#" target="_blank" rel="noopener">share it</a>. Or, <a href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#">find the conversation</a> on Twitter.</p>
</div>
<p>If a target Tweet does exist, then we want a box that looks like this where "Join the conversation" replies to the target tweet, and "retweet", well, retweets the target tweet:</p>
<div class="share-well" style="margin-bottom:40px">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"><path fill="#55acee" d="M23.44 4.83c-.8.37-1.5.38-2.22.02.93-.56.98-.96 1.32-2.02-.88.52-1.86.9-2.9 1.1-.82-.88-2-1.43-3.3-1.43-2.5 0-4.55 2.04-4.55 4.54 0 .36.03.7.1 1.04-3.77-.2-7.12-2-9.36-4.75-.4.67-.6 1.45-.6 2.3 0 1.56.8 2.95 2 3.77-.74-.03-1.44-.23-2.05-.57v.06c0 2.2 1.56 4.03 3.64 4.44-.67.2-1.37.2-2.06.08.58 1.8 2.26 3.12 4.25 3.16C5.78 18.1 3.37 18.74 1 18.46c2 1.3 4.4 2.04 6.97 2.04 8.35 0 12.92-6.92 12.92-12.93 0-.2 0-.4-.02-.6.9-.63 1.96-1.22 2.56-2.14z"></path></svg>
<p><a href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#">Join the conversation</a> on Twitter. Or, if you liked this article and think others should read it, please <a href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#" target="_blank" rel="noopener">retweet it</a>.</p>
</div>
<h2 id="code">Code <a class="direct-link" href="https://sia.codes/posts/conditional-twitter-intents-with-eleventy/#code">#</a></h2>
<p>First, if a post has a target tweet, add the tweet's ID to the post's front matter:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="highlight-line"><span class="token key atrule">title</span><span class="token punctuation">:</span> My adventures in turning a Pixelbook into a developer machine</span><br /><span class="highlight-line"><span class="token key atrule">description</span><span class="token punctuation">:</span> If you use Google Fonts<span class="token punctuation">,</span> a few additional steps can lead to much faster load times.</span><br /><span class="highlight-line"><span class="token key atrule">date</span><span class="token punctuation">:</span> <span class="token datetime number">2018-12-07</span></span><br /><span class="highlight-line"><span class="token key atrule">tags</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'Tools'</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"><span class="token key atrule">layout</span><span class="token punctuation">:</span> layouts/post.njk</span><br /><span class="highlight-line"><span class="token key atrule">tweetId</span><span class="token punctuation">:</span> <span class="token string">'1197670409543540738'</span></span></code></pre>
<p>Next, we need a few helper filters (functions) to generate the more complex web intent URLs. I choose to define these in a separate filter file and then load them into my <code>.eleventy.js</code> - read more about how in my <a href="https://sia.codes/posts/webmentions-eleventy-in-depth/#filters-for-build">post about implementing webmentions</a>.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _11ty/filters.js</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> rootUrl <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'../_data/metadata.json'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>url</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token function-variable function">generateShareLink</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">url<span class="token punctuation">,</span> text</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> shareText <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>text<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> by @TheGreenGreek</span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> postUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rootUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://twitter.com/intent/tweet/?text=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURI</span><span class="token punctuation">(</span>shareText<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;url=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURI</span><span class="token punctuation">(</span>postUrl<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token function-variable function">generateFindLink</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> postUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>rootUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://twitter.com/search?f=tweets&src=typd&q=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURI</span><span class="token punctuation">(</span>postUrl<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span></code></pre>
<p>Finally, in our post template, we can write a <a href="https://mozilla.github.io/nunjucks/templating.html#if">Nunjucks conditional</a> that generates different content based on whether a target tweet exists:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/layouts/post.njk --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>share-well<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>svg</span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 24 24<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>24<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>path</span> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#55acee<span class="token punctuation">"</span></span> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M23.44 4.83c-.8.37-1.5.38-2.22.02.93-.56.98-.96 1.32-2.02-.88.52-1.86.9-2.9 1.1-.82-.88-2-1.43-3.3-1.43-2.5 0-4.55 2.04-4.55 4.54 0 .36.03.7.1 1.04-3.77-.2-7.12-2-9.36-4.75-.4.67-.6 1.45-.6 2.3 0 1.56.8 2.95 2 3.77-.74-.03-1.44-.23-2.05-.57v.06c0 2.2 1.56 4.03 3.64 4.44-.67.2-1.37.2-2.06.08.58 1.8 2.26 3.12 4.25 3.16C5.78 18.1 3.37 18.74 1 18.46c2 1.3 4.4 2.04 6.97 2.04 8.35 0 12.92-6.92 12.92-12.93 0-.2 0-.4-.02-.6.9-.63 1.96-1.22 2.56-2.14z<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>svg</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% if tweetId %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitter.com/intent/tweet?in_reply_to={{ tweetId }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> Join the conversation<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span> on Twitter. Or, if you liked this article</span><br /><span class="highlight-line"> and think others should read it, please</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitter.com/intent/retweet?tweet_id={{ tweetId }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> retweet it<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>.</span><br /><span class="highlight-line"> {% else %}</span><br /><span class="highlight-line"> If you liked this article and think others should read it, please</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ page.url | generateShareLink(description) }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>share it<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>.</span><br /><span class="highlight-line"> Or, <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ page.url | generateDiscussionLink }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> find the conversation<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span> on Twitter.</span><br /><span class="highlight-line"> {% endif %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span></code></pre>
<p>Voilà! Conditional Twitter intent links that fit the situation best. What kind of engagement strategies do you use on your website? Please share in the comments or on Twitter! 😉</p>
<p><small>Cover photo by <a href="https://unsplash.com/@stereophototyp?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Sara Kurfeß</a> on <a href="https://unsplash.com/s/photos/twitter?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></p>
How to build a website in 20212020-02-11T00:00:00Zhttps://sia.codes/posts/how-to-build-a-website/<p><img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/disappointed-child_ooyndg.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/disappointed-child_ooyndg.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/disappointed-child_ooyndg.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/disappointed-child_ooyndg.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/disappointed-child_ooyndg.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Child grabbing their cheeks with a look of disappointment" importance="high" width="2400" height="1600" /></p>
<p><strong>Step 1</strong>: Buy a domain. You might have a problem if you already have 20 domains waiting to be used, but that's okay. Feel free to buy more domains. You never know when you might need that perfect domain name. P.S. <a href="https://vercel.com/">Vercel</a> has a great <a href="https://vercel.com/domains?limit=24">domain search tool</a> with hacks. Now I've just enabled your habit. You're welcome.</p>
<p><strong>Step 2a</strong>: Engage in Twitter debates about how JavaScript is ruining the web and which static site generators are best. Do this for 4 months.</p>
<p><strong>Step 3</strong>: Take an <a href="https://frontendmasters.com/courses/design-for-developers/">entire course to get better at design</a>. Spend 5 hours generating an <a href="https://codepen.io/nolasia/pen/PoomRqN">HSLa exploration CodePen</a> to visualize something that already exists. Bemoan color systems and values. Read a shout-out tweet about best conference talks that happen to be about <a href="https://www.youtube.com/watch?v=UbTZ9qSqimo&feature=youtu.be">React and color</a>. Find <a href="https://colorlab.dev/">a tool that does it way better than you</a>.</p>
<p><strong>Step 2b</strong>: Eventually some rando will tell you about <a href="https://www.11ty.dev/">Eleventy</a>, and you're like "sounds brilliant". Repeat step 2a.</p>
<p><strong>Step 4</strong>: Clone that <a href="https://github.com/11ty/eleventy-base-blog">starter repo</a> and fire it up. Waste 17 hours trying to figure out how to implement some esoteric design feature no one will notice. "Fuck, it's midnight on a Friday, why am I still doing this?" On Monday, "add a circle".</p>
<p><strong>Step 5</strong>: Let's do dark mode.</p>
<p><strong>Step 6</strong>: (Six hours later) Let's not do dark mode.</p>
<p><strong>Step 7</strong>: Look at other people's sites in envy.</p>
<p><strong>Step 8</strong>: Write the actual content.</p>
<p><strong>Step 9</strong>: Join the <a href="https://www.youtube.com/watch?v=ZDjIFGfrQKw">Learn with Jason livestream on variable fonts</a>. "Oh that's cool, must add."</p>
<p><strong>Step 10</strong>: Edit the color scheme 20 more times. Play with the design and variable fonts. Open Firefox because the font dev tools are 🔥. Go blind because Firefox oversaturated the colors. Recover from blindness, and edit the colors again.</p>
<p><strong>Step 11</strong>: Sees how CSS Tricks implemented dark mode and admire how well they did it. Silently close the tab.</p>
<p><strong>Step 12</strong>: Launch! Ugh, non-responsive iframes and bad break points. Run <a href="https://www.deque.com/axe/">Axe</a>. Feel like a terrible person. Fix issues (and edit colors again).</p>
<p><strong>Step 13</strong>: 5 months later, write a blog post.</p>
<p><em>Note to self: next time, just hire a designer.</em></p>
<p><small>Blog gallery photo by <a href="https://unsplash.com/@henrikkedue?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Henrikke Due</a> on Unsplash</small></p>
Responsive, Performant Images for the Web2020-04-17T00:00:00Zhttps://sia.codes/posts/responsive-images-perf-matters-video/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/images_slide_rmqvoj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/images_slide_rmqvoj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/images_slide_rmqvoj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/images_slide_rmqvoj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/images_slide_rmqvoj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Title slide from talk" importance="high" width="2432" height="1211" />
</figure>
<p>Want to learn how to make your images more responsive and more performant? Haven't gotten a chance to read that book/article/doc on web performance for images yet?</p>
<p>I spoke at my first virtual conference, PerfMatters Conf, and luckily they recorded it. This talk is a great intro into all things images for the web. Enjoy!</p>
<aside>Images account for 50% of the bytes downloaded to load a website. How can you make sure that your users only download the smallest image necessary while preserving image quality?</aside>
<p>In this talk, I focus on the underlying concepts in HTML and CSS for serving responsive images, which you can take with you no matter which tool you use. Which file formats suit which image types best? How can you use art direction in images to show the best image for a viewport layout?</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="XecoxR1ckbc" style="background-image: url('https://i.ytimg.com/vi/XecoxR1ckbc/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Responsive Images for the Web</span>
</button>
</lite-youtube>
Vintage Bundles talk at Magnolia JS2020-04-21T00:00:00Zhttps://sia.codes/posts/vintage-bundles-magnolia/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/vintage-bundles_anqbiu.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/vintage-bundles_anqbiu.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/vintage-bundles_anqbiu.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/vintage-bundles_anqbiu.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/vintage-bundles_anqbiu.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Title slide from talk" importance="high" width="2400" height="1600" />
</figure>
<p>More developers are starting to understand that web performance matters. From higher mobile search rankings to bottom-line revenue impacts, performance can make or break your web app. However, fixing performance can feel like a quagmire of expert-level nuanced understanding on so many topics. What would you think if I told you you could cut your JavaScript bundle size up to 50% by doing one thing only? Nearly 90% of worldwide web traffic runs on modern browsers, but we're transpiling all of our JavaScript down to ES5. That transpilation has a cost.</p>
<p>In this talk, we'll learn about differential serving, or serving modern bundles to modern browsers and legacy, transpiled bundles to older browsers. We'll talk about strategies, what to watch out for, and how to implement it using webpack. This talk is framework agnostic, and it's best if you have at least a basic understand of JavaScript.</p>
<p><a href="https://magnoliajs.com/">Magnolia JS</a> was my second virtual conference, and I had a blast participating. This video is the full day 1 video, but the embed should play only my talk by default. Check out the other talks that day too! Enjoy!</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="Qkc8p4D6JM0" style="background-image: url('https://i.ytimg.com/vi/Qkc8p4D6JM0/hqdefault.jpg');" params="start=10260&end=11280">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Vintage Bundles by Sia Karamalegos [ Magnolia JS ]</span>
</button>
</lite-youtube>
Architecting data in Eleventy2020-05-15T00:00:00Zhttps://sia.codes/posts/architecting-data-in-eleventy/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/possum_efpwzp.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/possum_efpwzp.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/possum_efpwzp.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/possum_efpwzp.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/possum_efpwzp.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Possum overlayed on a keyboard" importance="high" width="612" height="408" />
<figcaption>This is what happens when a fan of Eleventy is not a graphic designer.</figcaption>
</figure>
<p><a href="https://www.11ty.dev/">Eleventy</a> is a static site generator that makes building static, performant websites a breeze. It uses JavaScript to build pages at build time, but does not require any JavaScript in the client to render them.</p>
<p>Eleventy's magic comes with powerful tools for data, but the data model can be a lot to conceptualize when you're new to Eleventy. In this post, I'll explain the hierarchy of the data that we can work with and how to access it. I'll use real-world examples for learners like me who understand concepts better when they see them applied in practice.</p>
<p>Disclaimer: opinions ahead! I'm going to focus more on the concepts that will help you in decision making. Links are provided if you want to dive into the details of any one concept. I hope to make a second post in this series that talks about manipulating data, so stay tuned!</p>
<p>The examples here will use HTML, Markdown, JavaScript, JSON, and <a href="https://mozilla.github.io/nunjucks/templating.html">Nunjucks</a>. For reference, I'm using <a href="https://github.com/11ty/eleventy/releases/tag/v0.11.0">Eleventy version 0.11.0</a> as it has a few cool new tools.</p>
<h2 id="getting-started">Getting started <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#getting-started">#</a></h2>
<p>The <a href="https://www.11ty.dev/docs/">Eleventy docs</a> are a key place to start understanding the different features. We're going to take these a few steps further to give you an over-arching understanding of how it all works together.</p>
<p>To follow along, you can find the code in my <a href="https://github.com/siakaramalegos/eleventy-data-tutorial">eleventy-data-tutorial</a> repo. The <code>main</code> branch contains a bare-bones starting Eleventy app with an index.html and a single layout.</p>
<h2 id="how-do-i-see-my-data%3F%3F">How do I see my data?? <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#how-do-i-see-my-data%3F%3F">#</a></h2>
<p>As someone used to building apps with front-end frameworks or client-side JavaScript, I felt like a deer in the headlights when I first wanted to "see" my data. Eleventy is using JavaScript to build full HTML pages in Node, not render them in a browser. This means we don't have access to browser dev tools like the debugger or the browser console.</p>
<p>We do have access to the terminal/command line console and the rendered pages. New in version 0.11.0, we have access to a <a href="https://www.11ty.dev/docs/filters/log/"><code>log</code></a> "universal filter" which performs a <code>console.log()</code> accessible in our terminal (remember, we're in Node land!). <a href="https://mozilla.github.io/nunjucks/templating.html#filters">Filters</a> are functions, and we write them in our templates by first listing the first parameter, then the filter name. If the filter accepts more than one parameter, we add them in parentheses:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/layout.njk --></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- console.log the page data --></span></span><br /><span class="highlight-line">{{ page | log }}</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- run myCustomFilter on 2 params, the title data and anotherParam --></span></span><br /><span class="highlight-line">{{ title | myCustomFilter(anotherParam) }}</span></code></pre>
<p>I make heavy use of the <code>log</code> filter to debug my builds (since most of my bugs are from not handling the data correctly), and it's great to have this built in now. Another option is to output the data to the rendered page, but that doesn't work with complex objects.</p>
<p>Note that you can also run Eleventy in <a href="https://www.11ty.dev/docs/debugging/">debugging mode</a> for other information. I'm still learning how to best use this tool.</p>
<h2 id="page-data">Page data <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#page-data">#</a></h2>
<p>Every page has a <a href="https://www.11ty.dev/docs/data-eleventy-supplied/#page-variable-contents"><code>page</code></a> object available in the template which includes data like input and output file paths, the file slug, and URL. See it in your command line by logging it:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- _includes/layout.njk --></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- console.log the page data --></span></span><br /><span class="highlight-line">{{ page | log }}</span></code></pre>
<p>And your output will look something like this:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T19<span class="token operator">:</span><span class="token number">31</span><span class="token operator">:</span><span class="token number">02</span><span class="token punctuation">.</span>218Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/index'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/index.html'</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Note that the file slug is an empty string for the index file. If I add a new folder called <code>/posts</code> with a file called <code>my-first-post.md</code>, I get this page data:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">20</span><span class="token punctuation">.</span>649Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/posts/my-first-post.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-first-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/posts/my-first-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/posts/my-first-post/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/posts/my-first-post/index.html'</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>By default, Eleventy builds pages based on your file and directory structure. In the <a href="https://github.com/siakaramalegos/eleventy-data-tutorial/tree/1-page-data"><code>1-page-data</code> branch</a> of the repo, you can see the pages logged to the console if you run <code>npm start</code>.</p>
<p>Before we move on to custom data, note that Eleventy also provides <code>pagination</code> data to a page. Pagination is a very specific use case, so I won't cover it here. Read more about <a href="https://www.11ty.dev/docs/pagination/">pagination in the docs</a>.</p>
<h2 id="collection-data">Collection data <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#collection-data">#</a></h2>
<p>With <a href="https://www.11ty.dev/docs/collections/">collections</a>, we are upping the magicalness of Eleventy. Collections are groups of pages that are grouped by <a href="https://www.11ty.dev/docs/collections/#tag-syntax">tags</a>*. To conceptualize this, think of a traditional blog with posts on multiple topics. One post might be tagged <code>JavaScript</code> while another might be tagged both <code>JavaScript</code> and <code>HTML</code>. If you like relational databases, think of tags and pages as having a many-to-many relationship.</p>
<p>Collections are useful for rendering lists of pages that include the ability to navigate to those pages. For example, an index page for your blog posts or a list of pages with the same content tag.</p>
<p>Collections are JavaScript objects, and each key is the tag name. The value for each key is an array of pages. Tags are set using the data hierarchy which I'll get to in a bit, and this is what the <code>collections</code> object looks like if we <code>log</code> it from our home page:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// By default, the `all` key is created and includes all pages.</span></span><br /><span class="highlight-line"> <span class="token literal-property property">all</span><span class="token operator">:</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">template</span><span class="token operator">:</span> <span class="token punctuation">[</span>Template<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/index'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T19<span class="token operator">:</span><span class="token number">31</span><span class="token operator">:</span><span class="token number">02</span><span class="token punctuation">.</span>218Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">templateContent</span><span class="token operator">:</span> <span class="token punctuation">[</span>Getter<span class="token operator">/</span>Setter<span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token comment">// ...rest of all pages</span></span><br /><span class="highlight-line"> <span class="token comment">// Pages tagged as "posts"</span></span><br /><span class="highlight-line"> <span class="token literal-property property">posts</span><span class="token operator">:</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">template</span><span class="token operator">:</span> <span class="token punctuation">[</span>Template<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/posts/my-first-post.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-first-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/posts/my-first-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">12</span><span class="token operator">:</span><span class="token number">20</span><span class="token punctuation">.</span>649Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/posts/my-first-post/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/posts/my-first-post/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">templateContent</span><span class="token operator">:</span> <span class="token punctuation">[</span>Getter<span class="token operator">/</span>Setter<span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token comment">// ...rest of posts</span></span><br /><span class="highlight-line"> <span class="token literal-property property">podcasts</span><span class="token operator">:</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">template</span><span class="token operator">:</span> <span class="token punctuation">[</span>Template<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/podcasts/my-first-podcast.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-first-podcast'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/podcasts/my-first-podcast'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">23</span><span class="token operator">:</span><span class="token number">43</span><span class="token punctuation">.</span>665Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/podcasts/my-first-podcast/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/podcasts/my-first-podcast/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">templateContent</span><span class="token operator">:</span> <span class="token punctuation">[</span>Getter<span class="token operator">/</span>Setter<span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">JavaScript</span><span class="token operator">:</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">template</span><span class="token operator">:</span> <span class="token punctuation">[</span>Template<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/podcasts/my-first-podcast.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-first-podcast'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/podcasts/my-first-podcast'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">23</span><span class="token operator">:</span><span class="token number">43</span><span class="token punctuation">.</span>665Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/podcasts/my-first-podcast/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/podcasts/my-first-podcast/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">templateContent</span><span class="token operator">:</span> <span class="token punctuation">[</span>Getter<span class="token operator">/</span>Setter<span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">template</span><span class="token operator">:</span> <span class="token punctuation">[</span>Template<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/posts/my-second-post.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-second-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/posts/my-second-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">24</span><span class="token operator">:</span><span class="token number">27</span><span class="token punctuation">.</span>709Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/posts/my-second-post/index.html'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/posts/my-second-post/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">templateContent</span><span class="token operator">:</span> <span class="token punctuation">[</span>Getter<span class="token operator">/</span>Setter<span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">]</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Note that:</p>
<ul>
<li>The collections object by default includes an <code>all</code> key which includes all pages.</li>
<li>I have tagged by both content type (posts vs podcasts) which matches my routing, and by topic (JavaScript).</li>
</ul>
<p>You are not limited by how you want to use tags and collections.</p>
<p><strong>The benefit collections give you is grouping pages by a string key which gives you access to all group members' urls and other data.</strong></p>
<p>A new feature in version 0.11.0 is a universal filter for giving you <a href="https://www.11ty.dev/docs/filters/collection-items/">previous and next items in a collection</a>. By default, these are <a href="https://www.11ty.dev/docs/collections/#sorting">sorted</a> by file creation date which can be overridden.</p>
<p>In the <a href="https://github.com/siakaramalegos/eleventy-data-tutorial/tree/2-collections"><code>2-collections</code> branch</a> of the repo, I created index pages for both the podcasts and posts, and added those index pages to the site's navbar, all using collections.</p>
<h3 id="*-custom-collections">* Custom collections <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#*-custom-collections">#</a></h3>
<p>Tags are the most common way of creating collections, but you can actually create <a href="https://www.11ty.dev/docs/collections/#advanced-custom-filtering-and-sorting">custom collections</a> using JavaScript in your Eleventy config. <a href="https://twitter.com/philhawksworth">Phil Hawksworth</a> uses this feature in <a href="https://www.hawksworx.com/">his blog</a> to create a collection of the tags themselves as well as create a collection of all items in the blog folder, among other things:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// .eleventy.js</span></span><br /><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Assemble some collections</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"tagList"</span><span class="token punctuation">,</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./src/site/_filters/getTagList.js"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"posts"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> collection<span class="token punctuation">.</span><span class="token function">getFilteredByGlob</span><span class="token punctuation">(</span><span class="token string">"src/site/blog/*.md"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">reverse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"cards"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> collection<span class="token punctuation">.</span><span class="token function">getAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token string">"card"</span> <span class="token keyword">in</span> item<span class="token punctuation">.</span>data<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
<p>See Phil's <a href="https://github.com/philhawksworth/hawksworx.com/blob/fe1cfc2dfecea0b141b035f019cb315aaaeb02ef/.eleventy.js#L22-L31">source code</a>.</p>
<h2 id="template-data">Template data <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#template-data">#</a></h2>
<p>So far, we've only been using the data supplied by Eleventy with only a few custom data elements that I snuck in while you weren't looking. 👀 Let's take a look at those now.</p>
<p>In <code>/src/posts/my-first-post.md</code>, I use <a href="https://learnxinyminutes.com/docs/yaml/">YAML</a> front matter to set a few data attributes for my page - the <code>title</code>, which <code>layout</code> to use, and which <code>tags</code> should be applied to add this page to those collections:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="highlight-line"><span class="token comment"># /src/posts/my-first-post.md</span></span><br /><span class="highlight-line"><span class="token punctuation">---</span></span><br /><span class="highlight-line"><span class="token key atrule">title</span><span class="token punctuation">:</span> My first blog post</span><br /><span class="highlight-line"><span class="token key atrule">layout</span><span class="token punctuation">:</span> post.njk</span><br /><span class="highlight-line"><span class="token key atrule">tags</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'posts'</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"><span class="token punctuation">---</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">Bootcamp .NET npm branch Agile grep native senior. Database webpack</span><br /><span class="highlight-line">pairing build tool pull request imagemagick. AWS injection OOP</span><br /><span class="highlight-line">stack Dijkstra looks good to me Firefox bike<span class="token punctuation">-</span>shedding scrum master.</span></code></pre>
<p>We learned about <code>tags</code> already; <code>layout</code> is a similar special template data key in Eleventy which tells it which <a href="https://www.11ty.dev/docs/layouts/">layout file</a> to use for your page (found in a <code>/_includes/</code> folder). Other <a href="https://www.11ty.dev/docs/data-configuration/">special template data keys for templates</a> include <code>permalink</code>, <code>date</code>, and more.</p>
<h2 id="custom-data-and-the-data-hierarchy">Custom data and the data hierarchy <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#custom-data-and-the-data-hierarchy">#</a></h2>
<p>Finally, we come to custom data. In the example above, I set a <code>title</code> attribute in my front matter. This is not data automatically supplied nor used by Eleventy. It is completely custom. In this case, I use it to populate both my webpage's <code><title></code> element and the primary heading, or <code><h1></code>. Custom data you set in this manner is available directly in a template using the name you gave it:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- /src/_includes/post.njk --></span></span><br /><span class="highlight-line">---</span><br /><span class="highlight-line">layout: layout.njk</span><br /><span class="highlight-line">---</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line">{{ content | safe }}</span></code></pre>
<p>Eleventy uses a <a href="https://www.11ty.dev/docs/data/#sources-of-data">data hierarchy</a> so that you can set defaults or inheritance and then override them:</p>
<ol>
<li>Computed Data</li>
<li>Front Matter Data in a Template</li>
<li>Front Matter Data in Layouts</li>
<li>Template Data Files</li>
<li>Directory Data Files (and ascending Parent Directories)</li>
<li>Global Data Files</li>
</ol>
<p>In my example, we're using #2 in the hierarchy... and also #3 - you have to go to my highest-level layout to find it:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- /src/_includes/layout.njk --></span></span><br /><span class="highlight-line">---</span><br /><span class="highlight-line">title: My default layout title</span><br /><span class="highlight-line">---</span><br /><span class="highlight-line"><span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- ...rest of html --></span></span></code></pre>
<p>The <code>title</code> set in <code>my-first-post.md</code> overrides the <code>title</code> set in the layout. If a <code>title</code> attribute is missing, then the default one set in <code>layout.njk</code> is used. Wicked smart!</p>
<p>Now that we know about this data hierarchy, we can clean up some of our front matter by using a directory data file. Here's where we get a little muddy in our explanation since you can use the data hierarchy for template data too, not just custom data. In my <code>/posts/</code> folder, I can create a file with the same name as the folder and with either a <code>.json</code>, <code>.11tydata.json</code> or <code>.11tydata.js</code> extension that applies that data to all the files (i.e., templates/pages) in that folder.</p>
<p>We can use this to set the <code>layout</code> file and the <code>posts</code> tag to all the files in the <code>/posts/</code> folder, then remove those from the individual post files' front matter:</p>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token comment">// /src/posts/posts.json</span></span><br /><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"layout"</span><span class="token operator">:</span> <span class="token string">"post.njk"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token property">"tags"</span><span class="token operator">:</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token string">"posts"</span></span><br /><span class="highlight-line"> <span class="token punctuation">]</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<pre class="language-yaml"><code class="language-yaml"><span class="highlight-line"><span class="token comment"># /src/posts/my-first-post.md</span></span><br /><span class="highlight-line"><span class="token punctuation">---</span></span><br /><span class="highlight-line"><span class="token key atrule">title</span><span class="token punctuation">:</span> My first blog post</span><br /><span class="highlight-line"><span class="token punctuation">---</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">Bootcamp .NET npm branch Agile grep native senior. Database webpack</span><br /><span class="highlight-line">pairing build tool pull request imagemagick. AWS injection OOP</span><br /><span class="highlight-line">stack Dijkstra looks good to me Firefox bike<span class="token punctuation">-</span>shedding scrum master.</span></code></pre>
<p>Great, we're DRYing up the files! There's only one problem - the merge messed up our content tags. Our second blog post added a <code>JavaScript</code> content tag. That overrode the <code>posts</code> tag. Luckily, we can use <a href="https://www.11ty.dev/docs/data-deep-merge/">data deep merge</a> to instead merge data that is an object or array:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// .eleventy.js</span></span><br /><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">setDataDeepMerge</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
<p>Now our posts index page, <code>/src/posts/index.njk</code>, is showing up in our posts collection list because it's inheriting the tag from the directory. We can fix this by renaming it <code>posts.njk</code> and moving it up to the <code>/src/</code> directory. This move preserves the original routing due to the magic of Eleventy's directory- and file-based build method.</p>
<p>You can find the code for this section in the <a href="https://github.com/siakaramalegos/eleventy-data-tutorial/tree/3-data-hierarchy"><code>3-data-hierarchy</code> branch</a>. This was just one example of using the data hierarchy - you should definitely check out the <a href="https://www.11ty.dev/docs/data/#sources-of-data">data hierarchy docs</a> to learn about the other options too. I could spend loads of time explaining the hierarchy, but that would make it seem like the most important concept in all of Eleventy. Just know that it gives you the ability to inherit or scope data as you please. So if you need more precision on managing inheritance or scope, dive down on that concept more.</p>
<h3 id="what-custom-data-is-even-available-in-a-view%3F">What custom data is even available in a view? <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#what-custom-data-is-even-available-in-a-view%3F">#</a></h3>
<p>You're trying to build a page, but you can't figure out "where" your new variable that you thought you set. I haven't found a way to log everything available in a page - something akin to <code>self</code> or <code>this</code>. I have found a way to hack this with collections. For each item in a collection, you can <code>log</code> the <code>item.data</code> which will show the special Eleventy data attributes as well as your own custom ones:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">pkg</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// package.json data</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">layout</span><span class="token operator">:</span> <span class="token string">'post.njk'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">tags</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'posts'</span><span class="token punctuation">,</span> <span class="token string">'JavaScript'</span> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token comment">// Look! It's our custom title attribute:</span></span><br /><span class="highlight-line"> <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token string">'My second blog post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">page</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">date</span><span class="token operator">:</span> <span class="token number">2020</span><span class="token operator">-</span><span class="token number">05</span><span class="token operator">-</span>13T20<span class="token operator">:</span><span class="token number">24</span><span class="token operator">:</span><span class="token number">27</span><span class="token punctuation">.</span>709Z<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">inputPath</span><span class="token operator">:</span> <span class="token string">'./src/posts/my-second-post.md'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">fileSlug</span><span class="token operator">:</span> <span class="token string">'my-second-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">filePathStem</span><span class="token operator">:</span> <span class="token string">'/posts/my-second-post'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">'/posts/my-second-post/'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">outputPath</span><span class="token operator">:</span> <span class="token string">'_site/posts/my-second-post/index.html'</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">collections</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">all</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">nav</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">podcasts</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">JavaScript</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span> <span class="token punctuation">]</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">posts</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>Object<span class="token punctuation">]</span> <span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>If you know of a way to do this more easily, please share, and I'll update this post!</p>
<h2 id="custom-data-with-a-capital-d">Custom Data with a capital D <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#custom-data-with-a-capital-d">#</a></h2>
<p>The data hierarchy and examples I gave above are great for providing smart defaults, inheritance, and merging basic page data. But what about what I like to call "Data with a capital D"? Do you need to render something that is dependent on a large data object or array? Do you need to fetch data from another URL before statically rendering it? Do you need to manipulate some data to make it easier to use?</p>
<p>The data hierarchy technically handles that too, but we usually use either global data files, or maybe directory- or file-specific data files. Three examples I have implemented in Eleventy include:</p>
<ul>
<li>Showing my upcoming and past speaking events on <a href="https://sia.codes/speaking/">sia.codes/speaking</a> based on global data files <code>talks.js</code> and <code>events.js</code> (events can have many talks and talks can be repeated at different events).</li>
<li>Fetching webmentions for all my blog posts on sia.codes to show them at the bottom of an article with re-builds triggered every 4 hours to pull in new ones (<a href="https://sia.codes/posts/webmentions-eleventy-in-depth/">example article with webmentions</a> at the bottom).</li>
<li>Organizing courses, modules, and lessons in a new Jamstack course management system. (I hope to release an open source version soon!)</li>
</ul>
<p>I'll focus on the <a href="https://www.11ty.dev/docs/data-global/">global data file</a> method here. Data in files located in a <code>/_data/</code> directory is globally accessible in all pages using the filename. Your files can either be JSON, or you can use <code>module.exports</code> from a JavaScript file (actually, it can handle <a href="https://www.11ty.dev/docs/data-custom/">more data types</a> if you don't like JavaScript 😅). In our repo, <a href="https://github.com/siakaramalegos/eleventy-data-tutorial/tree/4-big-d-data">branch <code>4-big-d-data</code></a>, I created a dogs data file:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// /src/_data/dogs.js</span></span><br /><span class="highlight-line">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">[</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Harry'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">breed</span><span class="token operator">:</span> <span class="token string">'Jack Russell terrier'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">favoritePasttime</span><span class="token operator">:</span> <span class="token string">'finding hidey holes'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">stinkLevel</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'Priscilla'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">breed</span><span class="token operator">:</span> <span class="token string">'Australian shepherd'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">favoritePasttime</span><span class="token operator">:</span> <span class="token string">'starting farty parties'</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">stinkLevel</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">]</span></span></code></pre>
<p>If I then log <code>dogs</code> from any of my template/page files, I can see that exact data in my terminal. In this case, it is an array, so I can loop over it to render my dog info:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- /src/dogs.njk --></span></span><br /><span class="highlight-line">---</span><br /><span class="highlight-line">layout: layout.njk</span><br /><span class="highlight-line">title: Pup party</span><br /><span class="highlight-line">tags: ['nav']</span><br /><span class="highlight-line">---</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>My doggos<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>Much floof. Much fart.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% for dog in dogs %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {{ dog.name }} is a/an {{ dog.breed }} and likes {{ dog.favoritePasttime }}.</span><br /><span class="highlight-line"> {{ dog.name }}'s stink level from 1-5 is a {{ dog.stinkLevel }}.</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> {% endfor %}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- TODO: delete random logs --></span></span><br /><span class="highlight-line">{{ dogs | log }}</span></code></pre>
<p>If you needed to fetch data, you could use a JavaScript file and <a href="https://www.11ty.dev/docs/data-js/">return an async function</a> for your <code>module.exports</code>. It's a bit complex, but my <a href="https://github.com/siakaramalegos/sia.codes-eleventy/blob/master/_data/webmentions.js">webmentions code</a> is an example of this. If you're interested in the details, I wrote up a <a href="https://sia.codes/posts/webmentions-eleventy-in-depth/">full tutorial</a> on adding webmentions to an Eleventy site.</p>
<p>If you want to manipulate data before using it, you could "just use JavaScript". For example, in my online course project, I import my course>module>lesson hierarchy data from <code>/_data/courses.js</code> into another <code>/_data/lessonPrevNext.js</code> file to manually set a previous and next lesson since the sort order is a bit more nuanced. I wanted one source of truth, but needed something easier to work with in my views. <a href="https://www.11ty.dev/docs/data-computed/">Computed data</a> is another new feature in 0.11.0 that you can use as well.</p>
<h2 id="summary">Summary <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#summary">#</a></h2>
<p>Eleventy is a powerful static site generator with a lot of flexibilty in how to handle data. It's so flexible that sometimes your options for architecting data can be overwhelming. The primary ways I use data in developing Eleventy apps are:</p>
<ul>
<li><strong>page data</strong> - includes attributes like url and file slug</li>
<li><strong>collections</strong> - groups of pages/templates often to generate a list of links</li>
<li><strong>template data using the data hierarchy</strong> - special template data like layout, permalinks, tags, and dates as well as custom "small" data like titles and whether a page should be included in a nav bar</li>
<li><strong>global "big" data (though scope can be narrowed)</strong> - larger, more complex data that is easier to manage in a separate file and can also be fetched asynchronously (also technically still uses the data hieararchy)</li>
</ul>
<p>To see your data, use the <a href="https://www.11ty.dev/docs/filters/log/"><code>log</code></a> universal filter.</p>
<p>Have you used data in a unique way in your Eleventy sites? If so, I'd love to see your examples!</p>
<h2 id="thanks">Thanks <a class="direct-link" href="https://sia.codes/posts/architecting-data-in-eleventy/#thanks">#</a></h2>
<p>Special thanks to <a href="https://twitter.com/SpeakToChris">Chris Guzman</a>, <a href="https://twitter.com/aaronpeters">Aaron Peters</a>, <a href="https://twitter.com/davidrhoden">David Rhoden</a>, and <a href="https://twitter.com/philhawksworth">Phil Hawksworth</a> for giving me their time and feedback!</p>
<p>I apologize for the cover image abomination. It is a mash up of two great images on Unsplash by <a href="https://unsplash.com/photos/Mmz_ncRz4ZQ">Mikell Darling</a> and <a href="https://unsplash.com/photos/TZj-urJKRao">Yingchih</a>.</p>
Webmentions + Eleventy Talk2020-11-10T00:00:00Zhttps://sia.codes/posts/webmentions-eleventy-talk/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/webmentions-eleventy_kxnyud.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/webmentions-eleventy_kxnyud.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/webmentions-eleventy_kxnyud.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/webmentions-eleventy_kxnyud.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/webmentions-eleventy_kxnyud.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="two heart-shaped balloons" importance="high" width="3360" height="1972" />
<figcaption>Photo background by <a href="https://unsplash.com/@akshar_dave?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Akshar Dave</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>Webmentions are an exciting standard which help enable the IndieWeb. We can own our own content, hosted on our own domains, without sacrificing social connection and replies with other people!</p>
<p>In this talk, I discuss what webmentions are and how to implement them. In the longer version for <a href="https://www.magnoliajs.com/">Magnolia.js</a>, I also give a quick demo of Eleventy. The code is based on an Eleventy site, but the concepts should be applicable to any site.</p>
<h3 id="jamstack-toronto-(shorter)">Jamstack Toronto (shorter) <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-talk/#jamstack-toronto-(shorter)">#</a></h3>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="zjHb4xtnTvU" style="background-image: url('https://i.ytimg.com/vi/zjHb4xtnTvU/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Webmentions + Eleventy by Sia Karamalegos [ Jamstack Toronto ]</span>
</button>
</lite-youtube>
<h3 id="magnolia-js-(includes-short-eleventy-tutorial)">Magnolia JS (includes short Eleventy tutorial) <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-talk/#magnolia-js-(includes-short-eleventy-tutorial)">#</a></h3>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="-R07ATpOh4M" style="background-image: url('https://i.ytimg.com/vi/-R07ATpOh4M/hqdefault.jpg');" params="start=98">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Join the Indie Web with Eleventy and Webmentions - Sia Karamalegos (MagnoliaJS 2021)</span>
</button>
</lite-youtube>
<h2 id="slides">Slides <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-talk/#slides">#</a></h2>
<p>The latest slides can be accessed at <a href="https://projects.sia.codes/webmentions-11ty-25min/#/">projects.sia.codes/webmentions-11ty-25min/</a>. To advance the slides, use <code>n</code> for next and <code>p</code> for previous. The right arrow jumps to the next section (and left for previous section). Up and down to advance through slides within a section.</p>
<h2 id="resources">Resources <a class="direct-link" href="https://sia.codes/posts/webmentions-eleventy-talk/#resources">#</a></h2>
<p>If you're new to Eleventy, you can start with the <a href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/">mini-tutorial</a> to get a flavor of it.</p>
<p>Follow my <a href="https://sia.codes/posts/webmentions-eleventy-in-depth/">In-Depth Tutorial of Webmentions + Eleventy</a> for a step-by-step guide to get started. It includes links to many great resources and prior art.</p>
<p>The <a href="https://projects.sia.codes/webmentions-11ty-25min/#/">slides</a> are built as a live website, so all resource links mentioned are directly clickable.</p>
<p>Are you here for possums and image manipulation? Links to my source images are on the last slide. You can duotone your own varmint photos using <a href="https://duotone.shapefactory.co/">Duotone by ShapeFactory</a>.</p>
Optimize Images in Eleventy Using Cloudinary2020-12-11T00:00:00Zhttps://sia.codes/posts/eleventy-and-cloudinary-images/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/A_possum_and_a_movie_camera_1943_f4yflt.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/A_possum_and_a_movie_camera_1943_f4yflt.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/A_possum_and_a_movie_camera_1943_f4yflt.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/A_possum_and_a_movie_camera_1943_f4yflt.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/A_possum_and_a_movie_camera_1943_f4yflt.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="An Australian possum climbing a movie camera" importance="high" width="2953" height="2178" />
<figcaption>Source: <a href="https://commons.wikimedia.org/wiki/File:A_possum_and_a_movie_camera_1943.jpg">Wikimedia Commons</a> </figcaption>
</figure>
<p>Responsive images can be challenging to set up, from writing markup to generating the images. We can make this job much easier by using Cloudinary and Eleventy. <a href="https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/oq6yrskcixnvxvj1ofc0">Cloudinary</a> can host and transform our images, making generation of many file formats and sizes a matter of adding a param to a URL. <a href="https://www.11ty.dev/">Eleventy</a> is a hot JavaScript-based static site generator. It requires no client-side JavaScript, making it performant by default.</p>
<p>Originally, I did not set up Cloudinary on my Eleventy blog because I had a handful of images. I would create srcsets and formats <a href="https://github.com/siakaramalegos/images-on-the-command-line">using ImageMagick and cwebp</a>. But then, I got excited about using <a href="https://developers.google.com/search/docs/guides/search-gallery">structured data</a> for SEO. The image generation job got a LOT more complicated with more sizes and cropping.</p>
<p>In this post, first I'll go over how to serve responsive, performant images. Then, I'll show you how I implemented Cloudinary image hosting in Eleventy using <a href="https://www.11ty.dev/docs/shortcodes/">Eleventy shortcodes</a>.</p>
<h2 id="what's-in-an-%3Cimg%3E-tag%3F">What's in an <code><img></code> tag? <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#what's-in-an-%3Cimg%3E-tag%3F">#</a></h2>
<p>Let's take a look at a "fully-loaded" image tag in HTML:</p>
<pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pug_life.jpg<span class="token punctuation">"</span></span><br /><span class="highlight-line"> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pug_life_600.jpg 600w, pug_life_300.jpg 300w,<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(min-width: 760px) 600px, 300px<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Pug wearing a striped shirt<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>600<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>400<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span></code></pre>
<p>Why did I include all those attributes? Let's take a look at each...</p>
<ul>
<li><strong><code>src</code></strong> - the image to display (required!)</li>
<li><strong><code>srcset</code></strong> - for modern browsers, a set of candidate images and their widths in pixels</li>
<li><strong><code>sizes</code></strong> - for modern browsers, how wide the image will be displayed at various screen widths</li>
<li><strong><code>alt</code></strong> - description of the image</li>
<li><strong><code>width</code></strong> - the image width</li>
<li><strong><code>height</code></strong> - the image height</li>
<li><strong><code>loading</code></strong> - whether to lazy-load images and iframes (check for support with <a href="https://caniuse.com/loading-lazy-attr">caniuse</a>)</li>
</ul>
<h2 id="use-srcset-and-sizes-so-the-browser-can-pick-the-best-image">Use <code>srcset</code> and <code>sizes</code> so the browser can pick the best image <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#use-srcset-and-sizes-so-the-browser-can-pick-the-best-image">#</a></h2>
<p><code>srcset</code> and <code>sizes</code> give modern browsers a set of images and instructions for how wide they will be displayed. The browser will make the best decision on which image to load based on the user's screen width and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio">device pixel ratio (DPR)</a>. For example, those nice Retina screens (DPR of 2) need images twice as wide as the slot we're putting them in if we want them to look good. Stated another way, if your CSS says to display an image at 100px wide, we need to supply an image that is 200px wide.</p>
<p>The <code>sizes</code> attribute can be tricky to write by hand. My favorite way (a.k.a, the lazy way) is to first give the image a <code>srcset</code>, then run the page through <a href="https://ausi.github.io/respimagelint/">RespImageLint</a>. RespImageLint is a nifty bookmarklet that analyzes the images on a webpage. It will tell you how far off your images are in their size, and will also give suggestions for the <code>sizes</code> attribute:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/respimagelint_bnumrs.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/respimagelint_bnumrs.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/respimagelint_bnumrs.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/respimagelint_bnumrs.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/respimagelint_bnumrs.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Feedback, code, and image preview in a RespImageLint result" loading="lazy" width="2408" height="1360" />
<figcaption>RespImageLint will suggest a sizes attribute if you provide a srcset</figcaption>
</figure>
<h2 id="how-to-prevent-layout-shift">How to prevent layout shift <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#how-to-prevent-layout-shift">#</a></h2>
<p>To prevent layout shift once the image loads, we need to provide the browser with an aspect ratio. Currently, the way to do that is to set a height and width on the image in HTML. Use the original image's dimensions since the actual size doesn't matter. The aspect ratio is what is important. Your CSS will control the actual height and width.</p>
<p>To prevent weird stretching, set an auto height in your CSS:</p>
<pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">img</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">max-width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token property">height</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Jen Simmons recorded a great <a href="https://www.youtube.com/watch?v=4-d_SoCHeWE&feature=youtu.be">short video</a> on this trick.</p>
<h2 id="lazy-load-offscreen-images-for-performance">Lazy load offscreen images for performance <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#lazy-load-offscreen-images-for-performance">#</a></h2>
<p>We now have <a href="https://caniuse.com/loading-lazy-attr">partial support</a> for lazy loading images and iframes! If you set the <code>loading</code> attribute to <code>lazy</code>, the browser will use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">IntersectionObserver</a> to detect if a user scrolls near the image or iframe and only load it at that time.</p>
<p>At the time of writing, 78% of my blog's visitors use browsers that support native lazy loading. Thus, I'm implementing it now. <a href="https://sia.codes/posts/google-analytics-caniuse-magic/">Import your Google Analytics data into caniuse</a> to see how many of your visitors have support for any given web feature.</p>
<p>Note that you should not lazy-load images that are in the viewport on initial load ("above the fold"). This can lower your <a href="https://web.dev/vitals/">performance scores</a>.</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/eleventy-and-cloudinary-images/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h2 id="use-eleventy-shortcodes-and-filters-for-easy-reuse">Use Eleventy shortcodes and filters for easy reuse <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#use-eleventy-shortcodes-and-filters-for-easy-reuse">#</a></h2>
<p>What are <a href="https://www.11ty.dev/docs/shortcodes/">Eleventy shortcodes</a>? Shortcodes are like <a href="https://www.11ty.dev/docs/filters/">filters</a> in that they allow us to reuse code. Nunjucks, Liquid, and Handlebars templates all support both shortcodes and filters. For simplicity, the rest of this post will use Nunjucks.</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- FILTER using Nunjucks --></span></span><br /><span class="highlight-line"><span class="token comment"><!-- The filter, or function, is makeUppercase, and the first and only parameter is name. --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>{{ name | makeUppercase }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- SHORTCODE using Nunjucks --></span></span><br /><span class="highlight-line"><span class="token comment"><!-- The shortcode is user, and the parameters are firstName and lastName. --></span></span><br /><span class="highlight-line">{% user firstName, lastName %}</span></code></pre>
<p>For this use case, we could use either. I chose shortcodes since most of the other solutions use them, and I wanted to try them out for the first time.</p>
<h2 id="show-me-the-code!">Show me the code! <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#show-me-the-code!">#</a></h2>
<p>Now that you know how I make images responsive, I can explain the rationale behind my solution.</p>
<p>The existing solutions used shortcodes that provided the full image tag based on the filename, alt, and a few other attributes. I wanted the ability to also provide all the attributes previously mentioned (loading, sizes, etc.) plus others like <code>class</code>.</p>
<p>The shortcode became unwieldy with this many parameters. Then I realized that the HTML itself was only marginally longer. Why not use HTML? The onerous part of writing the markup is setting the image urls and generating the srcsets.</p>
<blockquote>
<p>Why not use HTML?</p>
</blockquote>
<p>Hence, I created shortcodes that do only that - generate the <code>src</code> and <code>srcset</code>. Everything else can be set as needed in the HTML:</p>
<pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{% src 'possum_film_director.jpg' %}<span class="token punctuation">"</span></span><br /><span class="highlight-line"> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{% srcset 'possum_film_director.jpg' %}<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{% defaultSizes %}<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Possum directing a movie<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2953<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2178<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>super-great-style-class<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span></code></pre>
<p>I don't need a <code><picture></code> tag. Cloudinary can serve the best image format based on the user's browser through the <a href="https://cloudinary.com/documentation/image_transformations#automatic_format_selection_f_auto">f_auto transformation</a>. This is one of the primary reasons I use Cloudinary. It makes my markup so much simpler.</p>
<aside>If you found this article helpful, you can <a href="https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/oq6yrskcixnvxvj1ofc0">sign up for a free Cloudinary account</a> with this link, and I'll get a few extra Cloudinary credits per month.</aside>
<h3 id="add-shortcodes-for-srcset-and-src-attributes">Add shortcodes for <code>srcset</code> and <code>src</code> attributes <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#add-shortcodes-for-srcset-and-src-attributes">#</a></h3>
<p>I gave the shortcodes smart default widths based on the styles for my site, but I allow an optional parameter to set them when I invoke the shortcode. The first step is to set our constants:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _11ty/shortcodes.js</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Set constants for the Cloudinary URL and fallback widths for images when not supplied by the shorcode params</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">CLOUDNAME</span> <span class="token operator">=</span> <span class="token string">"[your Cloudinary cloud name]"</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">FOLDER</span> <span class="token operator">=</span> <span class="token string">"[optional asset folder in Cloudinary]"</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">BASE_URL</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://res.cloudinary.com/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CLOUDNAME</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/image/upload/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">FALLBACK_WIDTHS</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token number">300</span><span class="token punctuation">,</span> <span class="token number">600</span><span class="token punctuation">,</span> <span class="token number">680</span><span class="token punctuation">,</span> <span class="token number">1360</span> <span class="token punctuation">]</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token constant">FALLBACK_WIDTH</span> <span class="token operator">=</span> <span class="token number">680</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// ...</span></span></code></pre>
<p>Then, we can define the shortcodes to create a <code>src</code> and reuse that function to create a <code>srcset</code>. These use the given widths or our fallback widths from the constants previously set:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _11ty/shortcodes.js</span></span><br /><span class="highlight-line"><span class="token comment">// ...</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Generate srcset attribute using the fallback widths or a supplied array of widths</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">getSrcset</span><span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> widths</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> widthSet <span class="token operator">=</span> widths <span class="token operator">?</span> widths <span class="token operator">:</span> <span class="token constant">FALLBACK_WIDTHS</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> widthSet<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">width</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getSrc</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> width<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>width<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">w</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">", "</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Generate the src attribute using the fallback width or a width supplied</span></span><br /><span class="highlight-line"><span class="token comment">// by the shortcode params</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">getSrc</span><span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> width</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">BASE_URL</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">q_auto,f_auto,w_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>width <span class="token operator">?</span> width <span class="token operator">:</span> <span class="token constant">FALLBACK_WIDTH</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">FOLDER</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>file<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// ...</span></span></code></pre>
<p>The final step in our shortcodes file is to export the two shortcodes to access them in our Eleventy config:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// _11ty/shortcodes.js</span></span><br /><span class="highlight-line"><span class="token comment">// ...</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Export the two shortcodes to be able to access them in our Eleventy config</span></span><br /><span class="highlight-line">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token function-variable function">srcset</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> widths</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">getSrcset</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> widths<span class="token punctuation">)</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token function-variable function">src</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> width</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">getSrc</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> width<span class="token punctuation">)</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Now we can add the shortcodes to our Eleventy config:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// .eleventy.js</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> <span class="token punctuation">{</span> srcset<span class="token punctuation">,</span> src <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./_11ty/shortcodes"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">eleventyConfig<span class="token punctuation">.</span><span class="token function">addShortcode</span><span class="token punctuation">(</span><span class="token string">'src'</span><span class="token punctuation">,</span> src<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line">eleventyConfig<span class="token punctuation">.</span><span class="token function">addShortcode</span><span class="token punctuation">(</span><span class="token string">'srcset'</span><span class="token punctuation">,</span> srcset<span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
<p>Voilà!</p>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/eleventy-and-cloudinary-images/#conclusion">#</a></h2>
<p>Eleventy shortcodes help us make the onerous process of generating source sets for our images easier. We can use the flexibility of HTML to customize all the other behavior we want (e.g., lazy loading, classes, etc.).</p>
<p>Check out the full demo on <a href="https://codesandbox.io/s/eleventy-image-shortcodes-with-cloudinary-ycq4r?from-embed=&file=/src/_11ty/shortcodes.js">CodeSandbox</a>.</p>
<p>How do you use Eleventy with Cloudinary? I haven't turned this into a plugin yet. Should I? <a href="https://twitter.com/TheGreenGreek">Ping me on Twitter</a> with your thoughts!</p>
<p>More resources:</p>
<ul>
<li><a href="https://www.11ty.dev/docs/shortcodes/">Eleventy shortcodes</a></li>
<li><a href="https://sia.codes/posts/responsive-images-perf-matters-video/">Responsive, Performant Images for the Web</a></li>
<li><a href="https://www.11ty.dev/">Eleventy</a></li>
</ul>
<p>Cover image from <a href="https://commons.wikimedia.org/wiki/File:A_possum_and_a_movie_camera_1943.jpg">Wikimedia Commons</a></p>
Faster YouTube embeds in Eleventy2021-04-07T00:00:00Zhttps://sia.codes/posts/lite-youtube-embed-eleventy/<p>Video is great, but the default YouTube embed share is both bloated and not privacy-minded. I finally switched my YouTube embeds to <a href="https://github.com/paulirish/lite-youtube-embed">lite-youtube-embed</a> by <a href="https://twitter.com/paul_irish">Paul Irish</a>, and it's fantastic. It loads "faster than a sneeze" and uses the no-cookie version by default.</p>
<p>This post will explain how to set up a reusable lite-youtube-embed "component" using Nunjucks partials in an Eleventy project.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/before-lite-yt_ledsqn.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/before-lite-yt_ledsqn.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/before-lite-yt_ledsqn.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/before-lite-yt_ledsqn.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/before-lite-yt_ledsqn.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Screenshot of performance scores in Lighthouse (71)" width="1846" height="812" />
<figcaption>Lighthouse scores before implementing lite-youtube-embed</figcaption>
</figure>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/after-lite-yt_boupli.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/after-lite-yt_boupli.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/after-lite-yt_boupli.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/after-lite-yt_boupli.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/after-lite-yt_boupli.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Screenshot of performance scores in Lighthouse (97)" loading="lazy" width="1846" height="812" />
<figcaption>Lighthouse scores after implementing lite-youtube-embed</figcaption>
</figure>
<h2 id="step-1%3A-get-the-source-code">Step 1: Get the source code <a class="direct-link" href="https://sia.codes/posts/lite-youtube-embed-eleventy/#step-1%3A-get-the-source-code">#</a></h2>
<p>This step will vary depending on how you currently load JavaScript in your Eleventy application. I try to keep my blog as minimal as possible, so I have no significant client-side JS. Thus, I chose to download the JavaScript file instead of adding the npm package and a bundle step. If you already have a bundler set up, then add the npm package instead.</p>
<p>These steps describe how to add it to an Eleventy project with no JavaScript files yet:</p>
<ol>
<li>Add the source code <a href="https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.js">JavaScript file</a> to a /javascript/ folder in your project.</li>
<li>Update the Eleventy config to pass the /javascript/ folder through to our build:</li>
</ol>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// .eleventy.js</span></span><br /><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Copy all files in the JavaScript folder to our output directory.</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token string">"src/javascript"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// more config stuff...</span></span></code></pre>
<aside>I choose to organize my source code in an <strong>/src/</strong> folder (my input directory). If your code is in the root of your project, then ignore mentions of <strong>/src/</strong> in this post.</aside>
<p>Additionally, we need to grab the <a href="https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.css">CSS from the source code</a>. For simplicity, I copied it into my existing CSS file since the content wasn't that long. If your video looks weird after implementing, your project's existing styles are probably impacting it. For example, I had some iframe styles that caused the video to shift down inside its container.</p>
<h2 id="youtube-partial">YouTube partial <a class="direct-link" href="https://sia.codes/posts/lite-youtube-embed-eleventy/#youtube-partial">#</a></h2>
<p>Now, we need to create a new template for a YouTube video. I'm using Nunjucks to create a video "component". In the <strong>/_includes/</strong> folder, create a youtube.njk file with the following content:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- src/_includes/youtube.njk --></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- Load the JS only when the component is needed --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/javascript/lite-yt-embed.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- Web component for lite-youtube-embed --></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>lite-youtube</span> <span class="token attr-name">videoid</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ videoId }}<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">background-image</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">'https://i.ytimg.com/vi/{{ videoId }}/hqdefault.jpg'</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">{%</span> <span class="token attr-name">if</span> <span class="token attr-name">params</span> <span class="token attr-name">%}</span> <span class="token attr-name">params</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ params }}<span class="token punctuation">"</span></span><span class="token attr-name">{%</span> <span class="token attr-name">endif</span> <span class="token attr-name">%}</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lty-playbtn<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lyt-visually-hidden<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Play Video: {{ videoTitle }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>lite-youtube</span><span class="token punctuation">></span></span></span></code></pre>
<p>The web component will use up to 3 parameters:</p>
<ul>
<li><code>videoId</code> - the id of the YouTube video which you can grab from its URL. For example, "XecoxR1ckbc" is the id of the video in this URL: <a href="https://www.youtube.com/embed/XecoxR1ckbc">https://www.youtube.com/embed/XecoxR1ckbc</a></li>
<li><code>videoTitle</code> - the video title</li>
<li><code>params</code> - optional params like <code>start</code> and <code>end</code> times</li>
</ul>
<p>I chose to defer the YouTube script loading for progressive enhancement. Our tiny bit of JavaScript from lite-youtube-embed will always load. But, the larger code from YouTube will only load if a user clicks on the video to play it. See the <a href="https://github.com/paulirish/lite-youtube-embed/blob/master/readme.md">readme</a> if you'd like to always load the YouTube scripts regardless.</p>
<h2 id="using-the-new-youtube-partial">Using the new YouTube partial <a class="direct-link" href="https://sia.codes/posts/lite-youtube-embed-eleventy/#using-the-new-youtube-partial">#</a></h2>
<p>Now that our set up is complete, we can use our new "component" in any file. First, we set the variables that pass through to the component, then we include the partial. This example also illustrates how to add a start and end time to the optional params:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- In any other file (e.g., a blog post): --></span></span><br /><span class="highlight-line">{% set videoTitle = "Vintage Bundles by Sia Karamalegos [ Magnolia JS ]" %}</span><br /><span class="highlight-line">{% set videoId = "Qkc8p4D6JM0" %}</span><br /><span class="highlight-line">{% set params = "start=10260&end=11280" %}</span><br /><span class="highlight-line">{% include 'youtube.njk' %}</span></code></pre>
<p>You can see it in action with this video about webmentions! The live video:</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="zjHb4xtnTvU" style="background-image: url('https://i.ytimg.com/vi/zjHb4xtnTvU/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Webmentions + Eleventy by Sia Karamalegos [ Jamstack Toronto ]</span>
</button>
</lite-youtube>
<p>What my source code looks like:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line">+{% set videoTitle = "Webmentions + Eleventy by Sia Karamalegos [ Jamstack Toronto ]" %}</span><br /><span class="highlight-line">+{% set videoId = "zjHb4xtnTvU" %}</span><br /><span class="highlight-line">+{% include 'youtube.njk' %}</span></code></pre>
<p>What the compiled HTML looks like (Eleventy output):</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>videoWrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>iframe</span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Webmentions + Eleventy by Sia Karamalegos [ Jamstack Toronto ]<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>560<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>315<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://www.youtube.com/embed/zjHb4xtnTvU<span class="token punctuation">"</span></span> <span class="token attr-name">frameborder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">allow</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture<span class="token punctuation">"</span></span> <span class="token attr-name">allowfullscreen</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>iframe</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></span></code></pre>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/lite-youtube-embed-eleventy/#conclusion">#</a></h2>
<p>Switching to <a href="https://github.com/paulirish/lite-youtube-embed">lite-youtube-embed</a> can greatly improve our load performance. It also better protects the privacy of our users. We can use HTML templates in Eleventy to create a YouTube video component, making reuse a breeze.</p>
Adding Prettier to a Project2021-04-19T00:00:00Zhttps://sia.codes/posts/how-to-add-prettier-to-a-project/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/laura-chouette-yxcCgzSB_iI-unsplash_myrt2f.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/laura-chouette-yxcCgzSB_iI-unsplash_myrt2f.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/laura-chouette-yxcCgzSB_iI-unsplash_myrt2f.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/laura-chouette-yxcCgzSB_iI-unsplash_myrt2f.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/laura-chouette-yxcCgzSB_iI-unsplash_myrt2f.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Woman's hand holding a brush up to an eyemakeup palette in the shape of a laptop" importance="high" width="3439" height="2578" />
<figcaption>Take time to make your code prettier. Photo by <a href="https://unsplash.com/@laurachouette?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Laura Chouette</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>While working at a smaller dev shop, our team hit the point at which the inconsistent code formats between and within projects was becoming a pain. Our needs included:</p>
<ol>
<li>A consistent linter/formatter for all projects in a particular language</li>
<li>An autoformatter so developers didn't spend time "fixing" linter errors</li>
<li>A tool readily available in tools like VS Code which could apply changes on save</li>
</ol>
<p>We decided to go with <a href="https://prettier.io/">Prettier</a>. We also added a pre-commit hook to ensure that all code changes complied with the new authoritarianism.</p>
<p>I initially published this as a <a href="https://gist.github.com/siakaramalegos/4a5cdab1f44ffb217a48d5260043f8ae">gist</a> to help when setting up new projects at that company. Today, it was useful for a client I was working with, so I'm sharing it now in an article in case the same use case fits for you, and you'd like a handy reference.</p>
<h2 id="the-steps">The Steps <a class="direct-link" href="https://sia.codes/posts/how-to-add-prettier-to-a-project/#the-steps">#</a></h2>
<p>Most of these steps can be found in the <a href="https://prettier.io/docs/en/install.html">docs</a> and through other links in the docs.</p>
<aside>A key step here is to run Prettier on all the files in a separate commit. You don't want to pollute all your future pull request diffs with formatting changes.</aside>
<ol>
<li>Install prettier:</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">npm</span> <span class="token function">install</span> --save-dev --save-exact prettier</span></code></pre>
<ol start="2">
<li>Create an empty config file to let tools know you're using Prettier:</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token builtin class-name">echo</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token operator">></span> .prettierrc.json</span></code></pre>
<ol start="3">
<li>Create a <code>.prettierignore</code> file to let tools know which files NOT to format. <code>node_modules</code> are ignored by default. Some suggestions:</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">build</span><br /><span class="highlight-line">coverage</span><br /><span class="highlight-line">.package-lock.json</span><br /><span class="highlight-line">*.min.*</span></code></pre>
<ol start="4">
<li>Manually run Prettier to re-format all the files in the project:</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ npx prettier <span class="token parameter variable">--write</span> <span class="token builtin class-name">.</span></span></code></pre>
<ol start="5">
<li>Set up your code editor to auto-format on save for ease of use. See <a href="https://prettier.io/docs/en/editors.html">instructions</a> for various editors.</li>
<li>Set up commit hooks with <a href="https://github.com/azz/pretty-quick">pretty-quick</a> and <a href="https://github.com/typicode/husky">husky</a>. First, install them as dev dependencies:</li>
</ol>
<pre class="language-bash"><code class="language-bash"><span class="highlight-line">$ <span class="token function">npm</span> i --save-dev pretty-quick husky</span></code></pre>
<ol start="7">
<li>Finally, add the pre-commit instructions to your <code>package.json</code> file:</li>
</ol>
<pre class="language-json"><code class="language-json"><span class="highlight-line"><span class="token property">"husky"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"hooks"</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">"pre-commit"</span><span class="token operator">:</span> <span class="token string">"pretty-quick --staged"</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">,</span></span></code></pre>
<p>Now when you commit your changes, files in the commit will automatically be formatted!</p>
Itsiest, Bitsiest Eleventy Tutorial2021-05-24T00:00:00Zhttps://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/mic-possum_owln9h.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/mic-possum_owln9h.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/mic-possum_owln9h.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/mic-possum_owln9h.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/mic-possum_owln9h.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="News reporter holding a microphone up to a possum with its mouth open as if speaking" importance="high" width="1830" height="986" />
<figcaption>Photo from <a href="https://twitter.com/PossumEveryHour/status/1396677350759227392">@PossumEveryHour</a></figcaption>
</figure>
<p>I like to talk and write about Eleventy a LOT. I always run into this problem of having to introduce Eleventy to people not familiar with it in a short way. So, I wrote up this miniature demo to give people a flavor of Eleventy without overwhelming them with all the details. If you like it as much as I do, maybe it will inspire you to learn more!</p>
<aside><strong>Do you prefer learning by video?</strong> I included a walkthrough of this demo in my <a href="https://sia.codes/posts/webmentions-eleventy-talk/#magnolia-js-(includes-short-eleventy-tutorial)">talk on Webmentions + Eleventy at Magnolia JS</a>.</aside>
<p>The code for this repo can be found on <a href="https://github.com/siakaramalegos/eleventy-demo">Github</a>. This article is meant for people new to Eleventy and will show you how to:</p>
<ol>
<li>Start up the most minimal Eleventy project with one page (the <code>main</code> branch)</li>
<li>Add a layout and styles (the <code>2-layout-styles</code> branch)</li>
<li>Add a blog and a list of all blog posts (the <code>3-blog</code> branch)</li>
</ol>
<p>To get started, clone the <a href="https://github.com/siakaramalegos/eleventy-demo">repo</a>, cd into it, and run <code>npm install</code>.</p>
<h2 id="taking-a-step-back">Taking a step back <a class="direct-link" href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/#taking-a-step-back">#</a></h2>
<p>The steps to get it to this point ("step 1") were:</p>
<ol>
<li>Make a new directory</li>
<li>cd into it</li>
<li><code>npm init -y</code></li>
<li>Install Eleventy with <code>npm install @11ty/eleventy</code></li>
<li>Edit the package.json to add a <code>start</code> script of <code>npx @11ty/eleventy --serve</code> and a build script of <code>npx @11ty/eleventy</code>.</li>
<li>Create <a href="http://index.md/">index.md</a></li>
<li>Run the start script. Eleventy processes <a href="http://index.md/">index.md</a> into the default output folder <code>/_site/</code> with the filename <code>index.html</code>.</li>
</ol>
<h2 id="step-2%3A-add-a-layout-and-styles">Step 2: Add a layout and styles <a class="direct-link" href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/#step-2%3A-add-a-layout-and-styles">#</a></h2>
<p>Checkout branch <code>2-layout-styles</code> to see this next step. In this step, I move our source code to a <code>/src/</code> folder, add a layout file, and add a CSS styles file.</p>
<p>To build it on your own:</p>
<p><strong>First, we move our source code to <code>/src/</code>:</strong></p>
<ol>
<li>Create <code>/src/</code> and move <code>index.md</code> into it.</li>
<li>Create a <code>.eleventy.js</code> file in the root of the project with the following content:</li>
</ol>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Set custom directories for input, output, includes, and data</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">dir</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">input</span><span class="token operator">:</span> <span class="token string">"src"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">includes</span><span class="token operator">:</span> <span class="token string">"_includes"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token string">"_data"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token string">"_site"</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
<p>Most of those are defaults - change their name in this file if you'd like to use a different name. You'll need to restart your dev server for any changes in this file to take effect.</p>
<p><strong>Next, add a layout:</strong></p>
<ol>
<li>Create <code>/src/_includes/layout.njk</code>. This is a <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> template, but you can use <a href="https://www.11ty.dev/docs/">many others</a>. The stuff in curly brackets are things that we will fill in at build time:</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>X-UA-Compatible<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>IE=edge<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- Grab title from the page data and dump it here --></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- Grab the content from the page data, dump it here, and mark it as safe --></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- Safe docs: https://mozilla.github.io/nunjucks/templating.html#safe --></span></span><br /><span class="highlight-line"> {{ content | safe }}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></span></code></pre>
<ol start="2">
<li>Add <a href="https://www.11ty.dev/docs/data-frontmatter/">YAML frontmatter</a> to the top of our <code>/src/index.md</code> file to tell it which layout to use and to set the <code>title</code> data attribute:</li>
</ol>
<pre class="language-yaml"><code class="language-yaml"><span class="highlight-line"><span class="token punctuation">---</span></span><br /><span class="highlight-line"><span class="token key atrule">layout</span><span class="token punctuation">:</span> layout.njk</span><br /><span class="highlight-line"><span class="token key atrule">title</span><span class="token punctuation">:</span> The Best Eleventy Demo TM</span><br /><span class="highlight-line"><span class="token punctuation">---</span></span></code></pre>
<p><strong>Finally, add some CSS:</strong></p>
<ol>
<li>Create <code>/src/style.css</code>. Add some CSS to that file.</li>
<li>Add <code><link rel="stylesheet" href="/style.css"></code> to the head of <code>/src/_includes/layout.njk</code>.</li>
<li>Now we need to tell Eleventy to <a href="https://www.11ty.dev/docs/copy/#manual-passthrough-file-copy-(faster)">"pass through"</a> any CSS files. We do this in <code>.eleventy.js</code>:</li>
</ol>
<pre class="language-js"><code class="language-js"><span class="highlight-line">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Copy `src/style.css` to `_site/style.css`</span></span><br /><span class="highlight-line"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token string">"src/style.css"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// When a passthrough file is modified, rebuild the pages:</span></span><br /><span class="highlight-line"> <span class="token literal-property property">passthroughFileCopy</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">dir</span><span class="token operator">:</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">input</span><span class="token operator">:</span> <span class="token string">"src"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">includes</span><span class="token operator">:</span> <span class="token string">"_includes"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token string">"_data"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token string">"_site"</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
<h2 id="step-3%3A-add-a-blog">Step 3: Add a blog <a class="direct-link" href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/#step-3%3A-add-a-blog">#</a></h2>
<p>Checkout branch <code>3-blog</code> to see this next step. In this step, I create blog posts and an index of those posts.</p>
<ol>
<li>Create a <code>/src/blog/</code> folder.</li>
<li>Add our first post in that folder <code>welcome-to-my-blog.md</code>, remembering to set the layout and title:</li>
</ol>
<pre class="language-text"><code class="language-text"><span class="highlight-line">---</span><br /><span class="highlight-line">layout: layout.njk</span><br /><span class="highlight-line">title: Welcome to my blog</span><br /><span class="highlight-line">---</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"># Welcome</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">These are profound thoughts.</span></code></pre>
<p>We can now access it at <a href="http://localhost:8080/blog/welcome-to-my-blog/">http://localhost:8080/blog/welcome-to-my-blog/</a>, but it would be nice to get some links on our home page for all our posts. For that, we should make a <a href="https://www.11ty.dev/docs/collections/">collection</a> for our blog posts. We will do this using tags.</p>
<aside><strong>Tip</strong>: You can log data to your terminal using the <code>log</code> filter which is included in Eleventy for free! For example, <code>{{ collections | log }}</code> to see all your collections. Right now, we only have the <code>all</code> collection which contains all our pages (home and first blog post).</aside>
<ol>
<li>Add a <code>blog</code> tag to our blog post's frontmatter:</li>
</ol>
<pre class="language-text"><code class="language-text"><span class="highlight-line">---</span><br /><span class="highlight-line">layout: layout.njk</span><br /><span class="highlight-line">title: Welcome to my blog</span><br /><span class="highlight-line">tags: blog</span><br /><span class="highlight-line">---</span></code></pre>
<ol start="2">
<li>Change our <code>/src/index.md</code> file to use Nunjucks instead by changing <code>.md</code> to <code>.njk</code> and changing the current content to html:</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="highlight-line">---</span><br /><span class="highlight-line">layout: layout.njk</span><br /><span class="highlight-line">title: The Best Eleventy Demo TM</span><br /><span class="highlight-line">---</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Yo Eleventy<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>This site rocks.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span></span></code></pre>
<ol start="3">
<li>Render a list of blogs on our index/home page (<code>/src/index.njk</code>) usink a <a href="https://mozilla.github.io/nunjucks/templating.html#for">Nunjucks for loop</a>:</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>ul</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line">{% for post in collections.blog %}</span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ post.url }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ post.data.title }}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>li</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line">{% endfor %}</span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>ul</span><span class="token punctuation">></span></span></span></code></pre>
<ol start="4">
<li>Add another post and see it magically appear!</li>
<li>Add a "nav" to your home page so people can get back to it from the blog page. In <code>/src/_includes/layout.njk</code> inside the <code><body></code>:</li>
</ol>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>nav</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Home<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>nav</span><span class="token punctuation">></span></span></span></code></pre>
<p>This is when I'd probably make another layout for a blog post so that the title is automatically rendered in its <code><h1></code>, but then this baby demo would be longer. :)</p>
<h2 id="moving-forward">Moving Forward <a class="direct-link" href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/#moving-forward">#</a></h2>
<p>Once you've had a chance to play with collections and other forms of data in Eleventy, I recommend you check out my article <a href="https://sia.codes/posts/architecting-data-in-eleventy/">Architecting data in Eleventy</a> to learn more. It might be a bit much if this is your first time.</p>
<p>What else can Eleventy do? So much! Here's a list of some of my favorite features:</p>
<ul>
<li>Generating <a href="https://www.11ty.dev/docs/pages-from-data/">pages based on a data</a> set (JavaScript, JSON), like my individual <a href="https://games.sia.codes/terraforming-mars/">game pages</a> in my <a href="https://games.sia.codes/">boardgame shelf site</a>. <a href="https://github.com/siakaramalegos/games">Code</a></li>
<li>Creating <a href="https://www.11ty.dev/docs/layout-chaining/">layouts</a> within layouts and template <a href="https://mozilla.github.io/nunjucks/templating.html#include">partials</a> (like creating components)</li>
<li>Using <a href="https://www.11ty.dev/docs/filters/">filters</a> and <a href="https://www.11ty.dev/docs/shortcodes/">shortcodes</a> to make reusable functions and code</li>
</ul>
<!-- hi https://twitter.com/PossumEveryHour/status/1396722646054838278 -->
<!-- twiddle https://twitter.com/PossumEveryHour/status/1391030131821629443 -->
Going Intergalactic with Serverless Functions on Netlify2021-06-14T00:00:00Zhttps://sia.codes/posts/serverless-functions-netlify/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/mwcenter_spitzer_s002ha.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/mwcenter_spitzer_s002ha.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/mwcenter_spitzer_s002ha.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/mwcenter_spitzer_s002ha.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/mwcenter_spitzer_s002ha.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Nebula with red, purple, and navy colors dominant in the nebula clouds with stars clustered in the middle" importance="high" width="900" height="600" />
<figcaption><a href="https://apod.nasa.gov/apod/ap090614.html">"Stars at the Galactic Center"</a> APOD from June 14, 2009</figcaption>
</figure>
<p>This post is a companion to the talk I give on buidling serverless functions with Netlify (<a href="https://projects.sia.codes/serverless-netlify-talk/">slides</a>). I usually give it to groups newer to web development such as bootcampers.</p>
<p>My goal here is to give a step-by-step tutorial for those wanting to duplicate my demo, or anyone interested in an intro tutorial on the topic! This post will walk through:</p>
<ol>
<li>Setting up your first Netlify deploy through GitHub</li>
<li>Installing and using the Netlify-CLI</li>
<li>Setting up and securely using environment variable secrets</li>
<li>Writing a serverless function to securely request NASA's Astronomy Picture of the Day</li>
</ol>
<p>To get the most out of this tutorial, it's best if you're already familiar with HTML, JavaScript, npm, and the command line.</p>
<h2 id="what-are-we-building%3F">What are we building? <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#what-are-we-building%3F">#</a></h2>
<p>We are going to build the first feature of my <a href="https://astrobirthday.netlify.app/">AstroBirthday</a> site. Namely, when a user selects a date and clicks the button, we will fetch NASA's <a href="https://apod.nasa.gov/apod/astropix.html">Astronomy Picture of the Day</a> (APOD for short) for that date. This app will use the <a href="https://api.nasa.gov/">free API</a> from NASA (rate-limited). We will generate an API key, store it securely, make the API request, and send the data to our front end to render.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/PIA19363_1024_bhff9v.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/PIA19363_1024_bhff9v.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/PIA19363_1024_bhff9v.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/PIA19363_1024_bhff9v.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/PIA19363_1024_bhff9v.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="This close-up from the Mars Reconnaissance Orbiter's HiRISE camera shows weathered craters and windblown deposits in southern Acidalia Planitia. A striking shade of blue in standard HiRISE image colors, to the human eye the area would probably look grey or a little reddish." width="900" height="600" />
<figcaption><a href="https://apod.nasa.gov/apod/ap190622.html">"Ares 3 Landing Site: The Martian Revisited"</a> APOD from June 22, 2019</figcaption>
</figure>
<h2 id="what-is-serverless-architecture%3F">What is serverless architecture? <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#what-is-serverless-architecture%3F">#</a></h2>
<p>Before we dive into the how, let's learn the what. In serverless applications (from <a href="https://www.twilio.com/docs/glossary/what-is-serverless-architecture">Serverless Architecture</a>):</p>
<ul>
<li>Applications are broken up into individual functions</li>
<li>Hosted by a 3rd party service</li>
<li>Can be invoked and scaled individually</li>
<li>No need for server management by the developer</li>
</ul>
<p>That last point is a big one for me. I love developing web apps, but thinking about managing a server makes my head hurt.</p>
<h2 id="what-is-jamstack%3F">What is Jamstack? <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#what-is-jamstack%3F">#</a></h2>
<p>Jamstack is...</p>
<blockquote>
<p>A modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup</p>
<p class="blockquote-source">—Mathias Biilmann (CEO & Co-founder of Netlify), from <a href="https://jamstack.wtf/">jamstack.wtf/</a></p>
</blockquote>
<p>The following figure from the Netlify site shows us how all the pieces fit together. First, we have a standard front end built with HTML, CSS, JavaScript and other assets like images. A provider like Netlify can handle automatic builds and deploys among other CI/CD things. Finally, instead of a full-featured backend, we use API's like a set of services to get the features we want. Those APIs can be external like <a href="https://stripe.com/">Stripe</a> for payments, <a href="https://www.sanity.io/">Sanity.io</a> for CMS, and <a href="https://hasura.io/">Hasura Cloud</a> for databases.</p>
<figure>
<img src="https://sia.codes/img/posts/how-it-works.svg" alt="Screenshot of how Jamstack works" loading="lazy" width="928" height="477" />
<figcaption>From <a href="https://www.netlify.com/jamstack/">www.netlify.com/jamstack/</a></figcaption>
</figure>
<p>Our frontends can be vanilla HTML, CSS, and JavaScript, but we can also use many different frameworks which are capable of building static pages. Prebuilding pages at build time instead of at request time makes our site faster.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/static-generators_jwwows.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/static-generators_jwwows.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/static-generators_jwwows.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/static-generators_jwwows.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/static-generators_jwwows.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="11ty, Next.js, Jekyll, NuxtJS, Gatsby, Hugo" width="1884" height="1031" />
<figcaption>Choose your own flavor of static-site generator</figcaption>
</figure>
<aside>In this demo, I use <a href="https://www.11ty.dev/">Eleventy</a> since it's very close to vanilla HTML and requires no front-end JavaScript by default. I want the web to be faster for users, so I minimize my client-side JavaScript. If you're interested in a short tutorial on Eleventy to see if you like it, check out my <a href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/">Itsiest, Bitsiest Eleventy Tutorial</a>.</aside>
<h2 id="why-i-%E2%99%A5%EF%B8%8F-netlify">Why I ♥️ Netlify <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#why-i-%E2%99%A5%EF%B8%8F-netlify">#</a></h2>
<p>I first came to love Netlify because deploying static sites was so much easier than some of the other methods I was using. You'll get to see how in the next sections. No more manual builds and file uploading nor complex continual integration setups! My deployment steps consist of just one step now - <code>git push</code>.</p>
<p>Other features that got me to love Netlify even more are:</p>
<ul>
<li>Automatic deploy previews on pull requests</li>
<li>Automatic reverting to previous version if a build fails</li>
<li>Generous free tier for forms (no backends or widgets!)</li>
<li>Automatic HTTPS/SSL certificates</li>
<li>CDN/edge deployments (depending on DNS setup)</li>
<li>Automatic Brotli compression</li>
</ul>
<p>This is all before I started using it for serverless functions. Are you a fan of Netlify? Tell me your favorite features in my <a href="https://sia.codes/posts/serverless-functions-netlify/#webmentions">webmentions</a>!</p>
<h2 id="hello%2C-netlify">Hello, Netlify <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#hello%2C-netlify">#</a></h2>
<p>Enough of the chit-chat, let's get started. If you've already deployed to Netlify, you can skip this step. Otherwise, here is the most basic way to deploy a site to Netlify. You create a GitHub repo, connect to it inside Netlify, and voilà, it's live!</p>
<ol>
<li>Make a repo on GitHub with an <a href="https://gist.github.com/siakaramalegos/31197c13dd1110b34e715706232e392b">index.html</a> file containing <code><h1>Hello, Netlify</h1></code>.</li>
<li>Create a <a href="https://www.netlify.com/">Netlify</a> account. It's free!</li>
<li>In your dashboard, select "New site from Git"</li>
<li>Link your Netlify account to GitHub, find the repo you just created, and select it.</li>
<li>Notice the other questions it asks you like branch, build script, and build directory. We don't need them here, but you can customize all these things in more complex projects.</li>
<li>Click to deploy your first website. You'll get an auto-generated random URL like <code>https://elastic-puppy-7a396e.netlify.app/</code></li>
</ol>
<p>Now whenever you update the <code>main</code> branch (or whichever branch you pointed Netlify to) on GitHub, the site will automatically deploy. You can also use GitLab or BitBucket instead of GitHub.</p>
<p>Now we're going to put it on steroids to make it even more powerful.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/carina2_hst_1080_shnrcd.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/carina2_hst_1080_shnrcd.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/carina2_hst_1080_shnrcd.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/carina2_hst_1080_shnrcd.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/carina2_hst_1080_shnrcd.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="How do violent stars affect their surroundings? To help find out, astronomers created a 48-frame high-resolution, controlled-color panorama of the center of the Carina Nebula, one of the largest star forming regions on the night sky. The featured image, taken in 2007, was the most detailed image of the Carina Nebula yet taken. Cataloged as NGC 3372, the Carina Nebula is home to streams of hot gas, pools of cool gas, knots of dark globules, and pillars of dense dusty interstellar matter. The Keyhole Nebula, visible left of center, houses several of the most massive stars known. These large and violent stars likely formed in dark globules and continually reshape the nebula with their energetic light, outflowing stellar winds, and ultimately by ending their lives in supernova explosions. Visible to the unaided eye, the entire Carina Nebula spans over 450 light years and lies about 8,500 light-years away toward the constellation of Ship's Keel (Carina)." width="1080" height="523" />
<figcaption><a href="https://apod.nasa.gov/apod/ap190623.html">"Carina Nebula Panorama from Hubble"</a> APOD from June 23, 2019</figcaption>
</figure>
<h2 id="netlify-cli">Netlify CLI <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#netlify-cli">#</a></h2>
<p>Netlify CLI lets us more easily build serverless functions in a dev environment and then port them to production. We can also do other things like manage secrets between both environments.</p>
<p>To get started, install it on the command line and login:</p>
<pre class="language-text"><code class="language-text"><span class="highlight-line">$ npm install netlify-cli -g</span><br /><span class="highlight-line">$ netlify</span><br /><span class="highlight-line">$ netlify login</span></code></pre>
<p>Your install message will look something like this:</p>
<pre class="language-text"><code class="language-text"><span class="highlight-line">Success! Netlify CLI has been installed!</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">Your device is now configured to use Netlify CLI to deploy and manage</span><br /><span class="highlight-line">your Netlify sites.</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">Next steps:</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> netlify init Connect or create a Netlify site from current</span><br /><span class="highlight-line"> directory</span><br /><span class="highlight-line"> netlify deploy Deploy the latest changes to your Netlify site</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">For more information on the CLI run netlify help</span><br /><span class="highlight-line">Or visit the docs at https://cli.netlify.com</span></code></pre>
<p>I don't usually use <code>netlify deploy</code> since I usually commit my code and push to GitHub to trigger deploys instead. However, <code>netlify init</code> is a handy way to create a new Netlify site with a repo. If you type <code>netlify</code> and hit enter, you'll see all the command options. Here are a few I use:</p>
<pre class="language-text"><code class="language-text"><span class="highlight-line">COMMANDS</span><br /><span class="highlight-line"> dev Local dev server</span><br /><span class="highlight-line"> init Configure continuous deployment for a new or existing site</span><br /><span class="highlight-line"> login Login to your Netlify account</span><br /><span class="highlight-line"> open Open settings for the site linked to the current folder</span></code></pre>
<p>To learn more, check out <a href="https://docs.netlify.com/cli/get-started/">Get started with Netlify CLI</a>.</p>
<h2 id="hello%2C-netlify-cli!">Hello, Netlify CLI! <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#hello%2C-netlify-cli!">#</a></h2>
<p>Let's get started with Netlify CLI by adding it to our demo project! Fork, clone, and install dependencies. Then, run <code>netlify init</code>:</p>
<ol>
<li><strong>Fork</strong> <a href="https://github.com/siakaramalegos/serverless-netlify-demo">the demo</a> (don't clone!)</li>
<li>Clone <strong>your</strong> repo.</li>
<li><code>cd serverless-netlify-demo</code></li>
<li><code>npm install</code></li>
<li>Run <code>netlify init</code> and answer the questions. For this project, the build command is <code>npm run build</code> and the deploy directory is <code>_site</code>. Answer "yes" when it asks to generate a <strong>netlify.toml</strong> file.</li>
<li>Run <code>netlify open</code> to have a browser window opening to your site's page on the Netlify dashboard. You'll also be able to see the URL generated for your site. Note that adding a custom domain is free on Netlify.</li>
</ol>
<p>Now we can really get started!</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/PIA22486CuriositySelf2018dustStorm1024_dumijh.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/PIA22486CuriositySelf2018dustStorm1024_dumijh.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/PIA22486CuriositySelf2018dustStorm1024_dumijh.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/PIA22486CuriositySelf2018dustStorm1024_dumijh.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/PIA22486CuriositySelf2018dustStorm1024_dumijh.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Winds on Mars can't actually blow spacecraft over. But in the low gravity, martian winds can loft fine dust particles in planet-wide storms, like the dust storm now raging on the Red Planet. From the martian surface on sol 2082 (June 15), this self-portrait from the Curiosity rover shows the effects of the dust storm, reducing sunlight and visibility at the rover's location in Gale crater. " width="1024" height="785" />
<figcaption><a href="https://apod.nasa.gov/apod/ap180623.html">"Curiosity's Dusty Self"</a> APOD from June 23, 2018</figcaption>
</figure>
<h2 id="environment-variables%3A-keeping-secrets-secret-%F0%9F%95%B5%F0%9F%8F%BB%E2%80%8D%E2%99%80%EF%B8%8F">Environment Variables: Keeping secrets secret 🕵🏻♀️ <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#environment-variables%3A-keeping-secrets-secret-%F0%9F%95%B5%F0%9F%8F%BB%E2%80%8D%E2%99%80%EF%B8%8F">#</a></h2>
<p>Before we build our serverless function to request the Astronomy Picture of the Day, we need to get an API key and store it securely.</p>
<ol>
<li>Get an API key at <a href="https://api.nasa.gov/">api.nasa.gov/</a></li>
<li>Run <code>netlify open</code> to go to the Netlify UI</li>
<li>Save as <code>NASA_API_KEY</code> in <strong>Build & Deploy</strong> > <strong>Environment</strong></li>
<li>Run <code>netlify dev</code> to confirm it gets injected for local dev</li>
</ol>
<p>It's a good idea to update your <code>start</code> script in your <strong>package.json</strong> file to use <code>netlify dev</code>.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/env-netlify-dev_xvgdqo.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/env-netlify-dev_xvgdqo.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/env-netlify-dev_xvgdqo.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/env-netlify-dev_xvgdqo.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/env-netlify-dev_xvgdqo.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Netlify Dev: Injected build setting env var: NASA_API_KEY" width="1576" height="1048" />
<figcaption>Our environment variable is injected when we run Netlify Dev</figcaption>
</figure>
<aside><strong>Note:</strong> You can set some env vars in your netlify.toml, but don't put secret ones there as that file must be checked into git.</aside>
<h2 id="netlify-functions">Netlify Functions <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#netlify-functions">#</a></h2>
<p>Let's talk about Netlify's flavor of serverless functions which they simply call <a href="https://docs.netlify.com/functions/overview/">functions</a>:</p>
<ul>
<li>Uses AWS serverless Lambda under the hood</li>
<li>Version-controlled, built, and deployed with your site</li>
<li>Automatic service discovery (for deploy previews and rollbacks)</li>
<li>JavaScript, TypeScript, and Go supported</li>
</ul>
<p>At the time of writing, the free tier included:</p>
<ul>
<li>125K function endpoint requests per month</li>
<li>100 hours of function run time per month</li>
</ul>
<p>Make sure to check Netlify's current <a href="https://www.netlify.com/pricing/">pricing</a>.</p>
<h3 id="netlify.toml">netlify.toml <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#netlify.toml">#</a></h3>
<p>To get started, let's check our <strong>netlify.toml</strong> file generated when we ran <code>netlify init</code>. This file sets up the <a href="https://docs.netlify.com/configure-builds/file-based-configuration/">configuration</a> for our Netlify builds. I think the default folder for functions is <strong>netlify/functions</strong>, but I simplified mine to only <strong>functions</strong>:</p>
<pre class="language-toml"><code class="language-toml"><span class="highlight-line"><span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span></span><br /><span class="highlight-line"> <span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm run build"</span></span><br /><span class="highlight-line"> <span class="token key property">publish</span> <span class="token punctuation">=</span> <span class="token string">"_site"</span></span><br /><span class="highlight-line"> <span class="token key property">functions</span> <span class="token punctuation">=</span> <span class="token string">"functions"</span></span></code></pre>
<p>This tells Netlify where to look for our functions. It will sync with our settings and point to where the functions can be found in our project. Now we can:</p>
<ul>
<li>Write functions in <strong>/functions/hello.js</strong></li>
<li>Access functions via fetch calls to <strong>/.netlify/functions/hello</strong></li>
</ul>
<h3 id="functions%2Fhello.js">functions/hello.js <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#functions%2Fhello.js">#</a></h3>
<p>In the tradition of our people, let's write our first Hello World function. Create a new file in the <strong>/functions/</strong> folder callled <strong>hello.js</strong>:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// functions/hello.js</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Functions must export a handler with this syntax:</span></span><br /><span class="highlight-line">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Log our parameters so we can check them out later</span></span><br /><span class="highlight-line"> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token punctuation">{</span>event<span class="token punctuation">,</span> context<span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// At a minimum, you must return an object with an HTTP status code to prevent timeouts:</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"Hello World"</span><span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>We'll talk about what's in the event and context objects in a bit. To learn more about functions, read <a href="https://docs.netlify.com/functions/build-with-javascript/">Build serverless functions with JavaScript</a> in the docs.</p>
<h3 id="src%2Findex.html">src/index.html <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#src%2Findex.html">#</a></h3>
<p>Now that we have our function endpoint, we can use it in our front end. In <strong>src/index.html</strong>, add this script to the bottom of the file:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token comment"><!-- src/index.html --></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment"><!-- At bottom of file: --></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="highlight-line"> <span class="token comment">// Fetch our serverless function endpoint via GET</span></span><br /><span class="highlight-line"> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'/.netlify/functions/hello'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">json</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token punctuation">{</span>json<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span></span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>You may need to restart your server (<code>netlify dev</code>), then check the browser console for the value of <code>{json}</code>. If your code isn't working, compare it against the <a href="https://github.com/siakaramalegos/serverless-netlify-demo/tree/2-hello-functions/"><code>2-hello-functions</code> branch</a> of the demo.</p>
<h3 id="event-and-context">event and context <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#event-and-context">#</a></h3>
<p>What about those <code>console.log</code> statements in our <strong>hello.js</strong> function? Well, those are technically on the server side so you will not see them in the browser console. We can find them in our command line/terminal console.</p>
<p>The <code>event</code> parameter gives us important data from the request like the path, headers, any query string parameters, and the body:</p>
<pre class="language-js"><code class="language-js"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token string-property property">"path"</span><span class="token operator">:</span> <span class="token string">"Path parameter"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token string-property property">"httpMethod"</span><span class="token operator">:</span> <span class="token string">"Incoming request's method name"</span></span><br /><span class="highlight-line"> <span class="token string-property property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">/* Incoming request headers */</span> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token string-property property">"queryStringParameters"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">/* query string parameters */</span> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token string-property property">"body"</span><span class="token operator">:</span> <span class="token string">"A JSON string of the request payload."</span></span><br /><span class="highlight-line"> <span class="token string-property property">"isBase64Encoded"</span><span class="token operator">:</span> <span class="token comment">/* A boolean flag to indicate if the applicable request payload is Base64-encode */</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>The <code>context</code> parameter includes information about the context in which the serverless function was called, like certain Identity user information, for example.</p>
<aside>Once we push to Netlify, we can no longer find our server side logs in our terminal. We access them in the Netlify UI. Go to the <strong>Functions</strong> tab, then select the function you want to see the logs for.</aside>
<p>What can we return from our serverless functions? We've seen status code and body, but we can also send headers among other things:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token string-property property">"isBase64Encoded"</span><span class="token operator">:</span> <span class="token comment">/* true|false */</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token string-property property">"statusCode"</span><span class="token operator">:</span> httpStatusCode<span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token string-property property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">"headerName"</span><span class="token operator">:</span> <span class="token string">"headerValue"</span><span class="token punctuation">,</span> <span class="token operator">...</span> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token string-property property">"multiValueHeaders"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">"headerName"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"headerValue"</span><span class="token punctuation">,</span> <span class="token string">"headerValue2"</span><span class="token punctuation">,</span> <span class="token operator">...</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token operator">...</span> <span class="token punctuation">}</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token string-property property">"body"</span><span class="token operator">:</span> <span class="token string">"..."</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/moonshorty_apollo17_960_fqb6tl.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/moonshorty_apollo17_960_fqb6tl.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/moonshorty_apollo17_960_fqb6tl.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/moonshorty_apollo17_960_fqb6tl.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/moonshorty_apollo17_960_fqb6tl.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="In December of 1972, Apollo 17 astronauts Eugene Cernan and Harrison Schmitt spent about 75 hours on the Moon in the Taurus-Littrow valley, while colleague Ronald Evans orbited overhead. This sharp image was taken by Cernan as he and Schmitt roamed the valley floor. The image shows Schmitt on the left with the lunar rover at the edge of Shorty Crater, near the spot where geologist Schmitt discovered orange lunar soil. The Apollo 17 crew returned with 110 kilograms of rock and soil samples, more than was returned from any of the other lunar landing sites. Now forty years later, Cernan and Schmitt are still the last to walk on the Moon." width="960" height="637" />
<figcaption><a href="https://apod.nasa.gov/apod/ap120624.html">"Apollo 17 at Shorty Crater"</a> APOD from June 24, 2012</figcaption>
</figure>
<h2 id="bringing-it-all-together">Bringing it all together <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#bringing-it-all-together">#</a></h2>
<p>Finally, we can build our feature to fetch the Astronomy Picture of the Day.</p>
<h3 id="front-end-request">Front end request <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#front-end-request">#</a></h3>
<p>First, in <strong>src/index.html</strong>, add an async function to handle the form submission which will fetch from our serverless function endpoint. For now, we'll just log the response to the console. Don't forget to add the event listener.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// index.html, inside a <script> tag</span></span><br /><span class="highlight-line"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">handleSubmit</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token keyword">const</span> formData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FormData</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> date <span class="token operator">=</span> formData<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"date"</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Pass the date into the request body</span></span><br /><span class="highlight-line"> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'/.netlify/functions/apod'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span>date<span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Bad network responses do not automatically throw errors</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'Network response was not ok'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">json</span> <span class="token operator">=></span> <span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>json<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token parameter">error</span> <span class="token operator">=></span> <span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Event listener</span></span><br /><span class="highlight-line">document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"#birthday-form"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"submit"</span><span class="token punctuation">,</span> handleSubmit<span class="token punctuation">)</span></span></code></pre>
<aside>Learn more about <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">FormData</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful">Checking that fetch is successful</a>.</aside>
<h3 id="%22backend%22-serverless-function">"Backend" serverless function <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#%22backend%22-serverless-function">#</a></h3>
<p>Now let's build out the "backend" function. Node does not have fetch available in its api so we need to install it in our project:</p>
<pre class="language-text"><code class="language-text"><span class="highlight-line">$ npm install node-fetch --save</span></code></pre>
<p>Then, we can use fetch in our serverless function. First we'll grab the date from the event's body then use it plus our API key stored in Netlify to fetch the APOD for that date.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// functions/apod.js</span></span><br /><span class="highlight-line"><span class="token keyword">const</span> fetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"node-fetch"</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> <span class="token constant">BASE_URL</span> <span class="token operator">=</span> <span class="token string">"https://api.nasa.gov/planetary/apod"</span></span><br /><span class="highlight-line"> <span class="token comment">// Get the date from the event body</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> <span class="token punctuation">{</span>date<span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>body<span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Access the environment variables using process.env and request the APOD</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">BASE_URL</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?api_key=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NASA_API_KEY</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&date=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>date<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'Network response was not ok'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Return OK status and the data from the response</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token parameter">error</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// Return a network error and the text of the error</span></span><br /><span class="highlight-line"> <span class="token keyword">return</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">500</span><span class="token punctuation">,</span></span><br /><span class="highlight-line"> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span>error<span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre>
<p>Save everything, run <code>npm start</code>, select a date in the form, and hit submit. If everything works, you'll have the data logged to the browser console!</p>
<h3 id="rendering-the-results">Rendering the results <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#rendering-the-results">#</a></h3>
<p>Let's finish up this feature by rendering our data so users can see those pretty pictures. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"><code><template></code></a> tags are really handy in vanilla JS. They let us clone an HTML template and then simply fill in the missing attributes like image <code>src</code> rather than creating all the markup in JavaScript:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="highlight-line"><span class="token comment">// index.html, inside the <script></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Don't allow selecting the future</span></span><br /><span class="highlight-line">document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"#date-input"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>max <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toLocaleDateString</span><span class="token punctuation">(</span><span class="token string">'en-CA'</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">// The target is where we will "dump" our APOD component onto the page</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> target <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"#apods"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token comment">// Clear it out (in case this is not the first time one was requested)</span></span><br /><span class="highlight-line"> target<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token string">""</span></span><br /><span class="highlight-line"> <span class="token comment">// Grab the template for the APOD component</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> template <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".apod-template"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Clone the template and fill in the key data with our APOD response data</span></span><br /><span class="highlight-line"> <span class="token keyword">const</span> clone <span class="token operator">=</span> template<span class="token punctuation">.</span>content<span class="token punctuation">.</span><span class="token function">cloneNode</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"img"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>src <span class="token operator">=</span> data<span class="token punctuation">.</span>url</span><br /><span class="highlight-line"> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"h2"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerText <span class="token operator">=</span> data<span class="token punctuation">.</span>title</span><br /><span class="highlight-line"> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".explanation"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerText <span class="token operator">=</span> data<span class="token punctuation">.</span>explanation</span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Support the original photographers by showing copyrights if applicable</span></span><br /><span class="highlight-line"> <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>copyright<span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">".copyright"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerText <span class="token operator">=</span> data<span class="token punctuation">.</span>copyright</span><br /><span class="highlight-line"> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"small"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">"hidden"</span><span class="token punctuation">)</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">// Dump our APOD component into the target</span></span><br /><span class="highlight-line"> target<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>clone<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// ... rest of code including handleSubmit function</span></span></code></pre>
<p>Now, we can replace the <code>console.log(json)</code> of our data with a call to <code>render(json.data)</code> inside the <code>handleSubmit</code> function. Save, refresh, and test it out! If your code isn't working, compare it against the <a href="https://github.com/siakaramalegos/serverless-netlify-demo/tree/3-apod-function/"><code>3-apod-function</code> branch</a> of the demo.</p>
<h2 id="next-steps">Next Steps <a class="direct-link" href="https://sia.codes/posts/serverless-functions-netlify/#next-steps">#</a></h2>
<p>Hopefully this post gave you a better understanding of how serverless functions work, especially on Netlify.</p>
<p>If you'd like a more in-depth workshop that also covers authentication and CRUD operations using a cloud-based GraphQL database, I highly recommend <a href="https://twitter.com/jlengstorf">Jason Lengtstorf's</a> course <a href="https://frontendmasters.com/workshops/serverless-functions/">Introduction to Serverless Functions</a> on Frontend Masters. The <a href="https://www.learnwithjason.dev/">LearnWithJason</a> livestream also frequently covers serverless topics like <a href="https://www.learnwithjason.dev/sell-products-on-the-jamstack">Sell Products on the Jamstack</a>.</p>
How to Eliminate Render-Blocking Resources: a Deep Dive2021-07-13T00:00:00Zhttps://sia.codes/posts/render-blocking-resources/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/buffalo-blocking-road-tim-wilson_mpq4nt.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/buffalo-blocking-road-tim-wilson_mpq4nt.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/buffalo-blocking-road-tim-wilson_mpq4nt.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/buffalo-blocking-road-tim-wilson_mpq4nt.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/buffalo-blocking-road-tim-wilson_mpq4nt.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Large buffalo blocking the roadway" importance="high" width="4001" height="2671" />
<figcaption>Don't let render-blocking resources block you on the road to good performance. Photo by <a href="https://unsplash.com/@timwilson7?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Tim Wilson</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>
<p>You might be here because Lighthouse told you to "eliminate render-blocking resources". Render-blocking resources are a common hurdle to rendering your page faster. They impact your Web Vitals which now impact your SEO. Slow render times also frustrate your users and can cause them to abandon your page.</p>
<p>I worked with one client to reduce their render-blocking resources and improved their site loading speed. We went from 13% to 80% of page visits experiencing First Contentful Paint (FCP) in less than 1.8 seconds. And we're not done yet!</p>
<aside><strong>Why is 1.8 seconds significant for First Contentful Paint?</strong> <a href="https://httparchive.org/reports/loading-speed#fcp">HTTP Archive</a> collects data from the top ~7 million URLs. Lighthouse <a href="https://web.dev/performance-scoring/#metric-scores">uses this data</a> to determine the top performing sites. Then the "green" level is set at the point where increased performance improvements have diminishing returns.</aside>
<p>Understanding render-blocking resources will enable you to fix this common web performance issue. In this post, you will learn:</p>
<ul>
<li><a href="https://sia.codes/posts/render-blocking-resources/#what-is-the-critical-rendering-path%3F">What the critical rendering path is</a></li>
<li><a href="https://sia.codes/posts/render-blocking-resources/#what-are-render-blocking-resources%3F">What render-blocking resources are</a></li>
<li><a href="https://sia.codes/posts/render-blocking-resources/#why-is-it-so-important-to-eliminate-render-blocking-resources%3F">Why they are important to performance</a></li>
<li><a href="https://sia.codes/posts/render-blocking-resources/#how-do-i-test-my-website-for-render-blocking-resources%3F">How to test and measure your website</a></li>
<li><a href="https://sia.codes/posts/render-blocking-resources/#how-do-i-remove-render-blocking-resources%3F">How to fix this issue</a></li>
</ul>
<p>Before we continue, we need to take a step back and understand what the critical rendering path is.</p>
<h2 id="what-is-the-critical-rendering-path%3F">What is the critical rendering path? <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#what-is-the-critical-rendering-path%3F">#</a></h2>
<p>We write HTML, CSS, and JavaScript in files and then deliver those files to the browser. The browser converts those files into the page you see through the <strong>critical rendering path</strong>. The steps are:</p>
<ol>
<li>Download the <strong>HTML</strong></li>
<li>Read the HTML, and concurrently:
<ul>
<li>Construct the <strong>Document Object Model</strong> (DOM)</li>
<li>Notice a <code><link></code> tag for a stylesheet and download the <strong>CSS</strong></li>
</ul>
</li>
<li>Read the CSS and construct the <strong>CSS Object Model</strong> (CSSOM)</li>
<li>Combine the DOM and the CSSOM into a <strong>render tree</strong></li>
<li>Using the render tree, compute the <strong>layout</strong> (size and position of each element)</li>
<li><strong>Paint</strong>, or render, the pixels on the page</li>
</ol>
<figure id="critical-render-path">
<img src="https://sia.codes/img/critical_render_path_sia_karamalegos.svg" loading="lazy" alt="Steps of the critical render path visualized in a diagram" width="595" height="707" />
<figcaption>The critical render path (<a href="https://sia.codes/posts/render-blocking-resources/#critical-render-path">link</a>)</figcaption>
</figure>
<p>We want this process to go as fast as possible. Can you guess what makes it go slower?</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/render-blocking-resources/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h2 id="what-are-render-blocking-resources%3F">What are render-blocking resources? <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#what-are-render-blocking-resources%3F">#</a></h2>
<p>Render-blocking resources are files that "press pause" on the critical rendering path. They interrupt one or more of the steps.</p>
<p>HTML is technically render blocking because you need it to create the DOM. Without the HTML we would not even have a page to render.</p>
<p>However, HTML is not usually the cause of our problems...</p>
<p><strong>CSS is render blocking</strong>. The browser needs it before it can create the CSSOM, which blocks all later steps. As soon as the browser encounters a stylesheet <code><link></code> or <code><style></code> tag, it must download and parse the contents. Then it must create the CSSOM before the rest of the render can continue. You can see this represented at the triangle point in the <a href="https://sia.codes/posts/render-blocking-resources/#critical-render-path">diagram</a>. The render tree cannot continue until both the CSSOM and DOM are created.</p>
<p><strong>JavaScript CAN be render blocking</strong>. When the browser encounters a script meant to run synchronously, it will stop DOM creation until the script is finished running:</p>
<figure id="critical-render-path-js">
<img src="https://sia.codes/img/critical_render_path_JS_karamalegos.svg" loading="lazy" alt="HTML encounters a synchronous script in the head which stops the parser" width="794" height="858" />
<figcaption>Synchronous JavaScript (no async or defer) will block the HTML parser during both download and execution of the JavaScript (<a href="https://sia.codes/posts/render-blocking-resources/#critical-render-path-js">link</a>)</figcaption>
</figure>
<p>Additionally, if CSS appears before a script, the script will not be executed until the CSSOM is created. This is because JavaScript can also interact with the CSSOM, and we would not want a race condition between them.</p>
<figure id="critical-render-path-css-js">
<img src="https://sia.codes/img/critical_render_path_CSS_JS_karamalegos_2.svg" loading="lazy" alt="HTML encounters CSS first, then a synchronous script in the head which stops the parser" width="595" height="862" />
<figcaption>JavaScript execution is blocked until the CSSOM is created (<a href="https://sia.codes/posts/render-blocking-resources/#critical-render-path-css-js">link</a>)</figcaption>
</figure>
<p>CSS blocks script execution, and JavaScript blocks construction of the DOM! Sounds like a giant mess, right? Stay tuned to <a href="https://sia.codes/posts/render-blocking-resources/#how-do-i-remove-render-blocking-resources%3F">learn how we can clean it up</a>!</p>
<aside><strong>Note:</strong> Images and fonts are not render-blocking. They might be rendering slowly due to render-blocking resources or other performance issues. Read <a href="https://sia.codes/posts/making-google-fonts-faster/">Making Google Fonts Faster</a> for tips on fonts. Watch my introductory talk on <a href="https://sia.codes/posts/responsive-images-perf-matters-video/">Responsive Images</a> to learn more about optimizing images.</aside>
<h2 id="why-is-it-so-important-to-eliminate-render-blocking-resources%3F">Why is it so important to eliminate render-blocking resources? <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#why-is-it-so-important-to-eliminate-render-blocking-resources%3F">#</a></h2>
<p>Render-blocking resources can trigger a cascade of failures for web performance. First paint gets slowed down which causes Largest Contentful Paint (LCP) to be slower. LCP is one of the Core Web Vitals which are now <a href="https://developers.google.com/search/docs/advanced/experience/page-experience">used to calculate your search engine rankings</a>.</p>
<p>SEO is important for discovery of your website. Performance is critical for keeping a visitor on your page. Page abandonment increases significantly if the page doesn't load within 3 seconds. Many companies have seen significant increases in conversions after improving performance.</p>
<blockquote>
<p>Vodafone improved their LCP by 31%, resulting in an 8% increase in sales, a 15% increase in their lead to visit rate, and an 11% increase in their cart to visit rate.</p>
<p class="blockquote-source">—<a href="https://wpostats.com/2021/03/18/vodafone-lcp-sales.html">WPO stats</a></p>
</blockquote>
<p><a href="https://wpostats.com/">WPO stats</a> lists many cases where improving performance resulted in real business impacts.</p>
<h2 id="how-do-i-test-my-website-for-render-blocking-resources%3F">How do I test my website for render-blocking resources? <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#how-do-i-test-my-website-for-render-blocking-resources%3F">#</a></h2>
<p>If you failed this metric in Lighthouse, then you've already discovered one way to test for this. If you're new to Lighthouse, then check out the official <a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> page to get started.</p>
<p>We all have render-blocking resources on our sites (all the CSS!). The problem comes in when it significantly impacts our performance. When this occurs, Lighthouse flags it, and we should do something about it.</p>
<p>Lighthouse candidates for render-blocking resources include both scripts and styles:</p>
<ul>
<li><code><script></code> tags in the <code><head></code> which do not have at least one of the following attributes: <code>async</code>, <code>defer</code>, <code>module</code></li>
<li>Stylesheet <code><link></code> tags in the <code><head></code> without a <code>disabled</code> attribute or a media query which does not match (e.g., <code>print</code>)</li>
</ul>
<p>If you fail this metric, your Lighthouse results will look something like this:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/eliminate_render_blocking_resources_lighthouse_ujbxsj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/eliminate_render_blocking_resources_lighthouse_ujbxsj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/eliminate_render_blocking_resources_lighthouse_ujbxsj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/eliminate_render_blocking_resources_lighthouse_ujbxsj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/eliminate_render_blocking_resources_lighthouse_ujbxsj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Eliminate render-blocking resources: Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. The table shows these two resources listed: /css?family=…(fonts.googleapis.com) and /scripts/jquery-3.4.1.js(lh-perf-failures.glitch.me)" width="1774" height="626" />
<figcaption>Once you expand the metric name, you will see a list of resources that blocked render</figcaption>
</figure>
<p>Lighthouse lists the Google Fonts stylesheet followed by a JQuery script. Let's dig in some more. Let's inspect the <code><head></code> on that <a href="https://lh-perf-failures.glitch.me/">sample failure site</a>. It shows us 3 stylesheets followed by 3 scripts, then 2 more stylesheets:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>Lighthouse performance audit failures<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>utf-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>X-UA-Compatible<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>IE=edge<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/bootstrap-grid.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/bootstrap-reboot.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scripts/jquery-3.4.1.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scripts/bootstrap.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scripts/bootstrap.bundle.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/main.css<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>screen,print<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/bootstrap.css<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>screen,print<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span></span></code></pre>
<p>Lighthouse could have flagged any one of those 3 initial stylesheets. The root cause of this failure is:</p>
<ul>
<li>The first 3 stylesheets block the 3 synchronous scripts from running. The browser must first download the stylesheet and create the CSSOM.</li>
<li>The browser cannot construct the rest of the DOM until it downloads, parses, and executes the 3 scripts.</li>
</ul>
<p>We're digging a deep hole here.</p>
<p>Another way to test for render-blocking resources is to use <a href="https://webpagetest.org/easy">WebPageTest</a>. WebPageTest is the next step up in performance profiling. If you enter a URL, it will run a performance test on a real mobile device. Once the test finishes, click on the waterfall for the median run. Every render-blocking resource will have an orange circle with a white X next to its row:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/render_blocking_indicator_webpagetest_j5thod.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/render_blocking_indicator_webpagetest_j5thod.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/render_blocking_indicator_webpagetest_j5thod.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/render_blocking_indicator_webpagetest_j5thod.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/render_blocking_indicator_webpagetest_j5thod.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="WebPageTest waterfall with every render-blocking request row circled in red" width="1836" height="828" />
<figcaption>The WebPageTest waterfall will highligh render-blocking requests with an orange icon</figcaption>
</figure>
<p>Check out the official <a href="https://docs.webpagetest.org/getting-started/">WebPageTest docs</a> for more detailed information to get started.</p>
<h2 id="how-do-i-remove-render-blocking-resources%3F">How do I remove render-blocking resources? <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#how-do-i-remove-render-blocking-resources%3F">#</a></h2>
<p>It's time to fill in that hole and fix our website. Let's dive deep into both CSS and JavaScript. Our goal is not to eliminate all render-blocking resources but to lower their impact on performance. The Lighthouse metric is good for determining when you reach that point.</p>
<h3 id="deep-dive%3A-optimizing-css-for-the-critical-rendering-path">Deep-dive: optimizing CSS for the critical rendering path <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#deep-dive%3A-optimizing-css-for-the-critical-rendering-path">#</a></h3>
<p>For our CSS, we want to</p>
<ol>
<li><strong>Minimize</strong> the size of our styles</li>
<li>Deliver them to the client <strong>quickly</strong> and <strong>effectively</strong></li>
</ol>
<p>That <a href="https://lh-perf-failures.glitch.me/">sample failure site</a> is great. I need to buy whoever built it a beer. It's so juicy with performance problems. Let's take a step back and observe all the stylesheet tags. We can see that this website is using Bootstrap and a lot of Bootstrap dependencies as well as 12 Google fonts. It also has screen and print styles combined into single CSS files.</p>
<p>The first question I would ask is:</p>
<blockquote>
<p>Do I need all those dependencies?</p>
</blockquote>
<p>Don't skip that first question. The easiest way to minimize total size for any asset is to annihilate it. Raze it to the ground. For example, 12 Google fonts seems excessive. I try to keep my web fonts down to 3-4 style sets max.</p>
<aside>If you use <strong>Wordpress</strong>, be very careful how many and which plugins you use. Many of them were naively built without performance in mind. Look at alternatives with better performance.</aside>
<p>The next easiest way to minimize total CSS is to, well, minimize it. Minification is the process by which a build tool removes unused white space. Fewer characters = smaller size = faster download. If you're using a CSS package, make sure you use the minified version of it. The next step up is to use a build tool to automatically minimize all your CSS. Example tools include Gulp, Grunt, webpack, and Parcel. <a href="https://www.snowpack.dev/">Snowpack</a> and <a href="https://vitejs.dev/">Vite</a> are interesting newer tools.</p>
<p>Next, I would try to break up my CSS into smaller chunks. Ideally, we only want to deliver the CSS that we will actually use. You can see how much of your CSS (and JavaScript) is actually used with the <a href="https://developer.chrome.com/docs/devtools/coverage/">Coverage drawer</a> in Chrome Dev Tools.</p>
<ol>
<li>Open Dev Tools</li>
<li>Press Cmd + Shift + P to open the quick menu</li>
<li>Type "Coverage" and then select "Show Coverage"</li>
</ol>
<p>The drawer will open up for you, and you can click the reload button to start a new coverage analysis. Here is an example from the New York Times website:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/coverage_analysis_css_njif6w.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/coverage_analysis_css_njif6w.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/coverage_analysis_css_njif6w.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/coverage_analysis_css_njif6w.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/coverage_analysis_css_njif6w.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Chrome Dev Tools Coverage tab showing table of URLs, total bytes, and unused bytes with a bar chart representing used/unused bytes" width="2152" height="696" />
<figcaption>The Coverage tool gives us data for used versus unused bytes</figcaption>
</figure>
<p>If your total CSS bytes are small, then the unused percentage becomes less important. For example, my home page is around 57% unused. Because my total CSS is small, my Lighthouse scores are still around 97-100.</p>
<p>Note that it's only accurate for the styles (or scripts) used thus far. As you interact or resize the page, those numbers will go up.</p>
<p>Your goal is not necessarily 0% unused. However, if you see big red bars and high unused bytes, it's time to reduce the CSS needed for initial render. Remove dependencies, use code splitting, or inline critical CSS and defer the rest. For those last two options, you definitely want to use a build tool. Check out <a href="https://web.dev/extract-critical-css/">Extract critical CSS</a> for more details including popular tools.</p>
<p>If you have a lot of non-screen styles, consider extracting them to their own stylesheet. Then use a media query on the <code><link></code> tag. For example:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/main.css<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>screen<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span><br /><span class="highlight-line"> <span class="token comment"><!-- Only downloaded for print: --></span></span><br /><span class="highlight-line"> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>styles/main_print.css<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>print<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span></code></pre>
<p>Finally, do not use <code>@import</code> in your stylesheets to load more stylesheets. The browser won't discover it until later. It's best to load them with <code><link></code> tags in your HTML.</p>
<h3 id="deep-dive%3A-optimizing-javascript-for-the-critical-rendering-path">Deep-dive: optimizing JavaScript for the critical rendering path <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#deep-dive%3A-optimizing-javascript-for-the-critical-rendering-path">#</a></h3>
<p>As I mentioned earlier, JavaScript is parser-blocking. This means that it blocks DOM construction until it is finished executing. Like CSS, we want to:</p>
<ol>
<li><strong>Minimize</strong> the size of our scripts</li>
<li>Deliver them to the client <strong>quickly</strong> and <strong>effectively</strong></li>
</ol>
<p>The Coverage drawer also analyzes your scripts. You can filter the results between CSS and JavaScript. Again, the first question to ask yourself is:</p>
<blockquote>
<p>Do I need all those dependencies?</p>
</blockquote>
<p>JavaScript is our most expensive asset and prone to bloat. Trim out unused dependencies. Additionally, use <a href="https://developer.mozilla.org/en-US/docs/Glossary/Code_splitting">code-splitting</a>, <a href="https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking">tree-shaking</a>, and/or <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading">lazy loading</a> features as needed. Your build tool is your friend for all these strategies.</p>
<p>Let's talk about delivering our JavaScript effectively. The best diagram I've seen for understanding async vs defer vs module is from the <a href="https://html.spec.whatwg.org/multipage/scripting.html">HTML spec</a>:</p>
<p><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 820 200"><style><![CDATA[.svg-label,.svg-tag{dominant-baseline:central}.svg-tag{font-weight:700;font-family:monospace;font-size:13px}.svg-label{font-family:sans-serif;font-size:10px}.parser{stroke:#6eae00;fill:#6eae00}.fetch{stroke:#0035ff;fill:#0035ff}.execution{stroke:red;fill:red}.progress{stroke-width:2}.progress.parser:not(.first){marker-start:url(#parser-marker)}.progress.parser:not(.last){marker-end:url(#parser-marker)}.progress.fetch:not(.first){marker-start:url(#fetch-marker)}.progress.fetch:not(.last){marker-end:url(#fetch-marker)}.progress.execution:not(.first){marker-start:url(#execution-marker)}.progress.execution:not(.last){marker-end:url(#execution-marker)}marker>circle{stroke-width:0}.connector{stroke:#cecece;stroke-width:1}]]></style><path d="M0 33.5h820M0 66.5h820M0 99.5h820m-820 33h820M245.5 1v28m0 9v24m0 6v27m0 9v24m0 9v28" stroke="#6a9400" stroke-dasharray="1,1"></path><defs><marker id="parser-marker" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5"><circle cx="1.5" cy="1.5" r="1.5" class="parser"></circle></marker><marker id="fetch-marker" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5"><circle cx="1.5" cy="1.5" r="1.5" class="fetch"></circle></marker><marker id="execution-marker" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5"><circle cx="1.5" cy="1.5" r="1.5" class="execution"></circle></marker></defs><text x="12" y="16.75" class="svg-tag"><script></text><text y="9" class="svg-label" transform="translate(252)">Scripting:</text><text y="24" class="svg-label" transform="translate(252)">HTML Parser:</text><path class="connector" d="M509 9v15M656 9v15"></path><path class="parser progress first" d="M358 24h151"></path><path class="fetch progress" d="M509 9h97"></path><path class="execution progress" d="M606 9h50"></path><path class="parser progress last" d="M656 24h128"></path><text x="12" y="16.75" class="svg-tag" transform="translate(0 33)"><script defer></text><text y="9" class="svg-label" transform="translate(252 33)">Scripting:</text><text y="24" class="svg-label" transform="translate(252 33)">HTML Parser:</text><path class="connector" d="M736 42v15"></path><path class="parser progress first" d="M358 57h378"></path><path class="fetch progress" d="M509 42h97"></path><path class="execution progress last" d="M736 42h48"></path><g><text x="12" y="16.75" class="svg-tag" transform="translate(0 66)"><script async></text><text y="9" class="svg-label" transform="translate(252 66)">Scripting:</text><text y="24" class="svg-label" transform="translate(252 66)">HTML Parser:</text><path class="connector" d="M606 75v15m50-15v15"></path><path class="parser progress first" d="M358 90h248"></path><path class="fetch progress" d="M509 75h97"></path><path class="execution progress" d="M606 75h50"></path><path class="parser progress last" d="M656 90h128"></path></g><g><text x="12" y="16.75" class="svg-tag" transform="translate(0 99)"><script type="module"></text><text y="9" class="svg-label" transform="translate(252 99)">Scripting:</text><text y="24" class="svg-label" transform="translate(252 99)">HTML Parser:</text><path class="connector" d="M736 108v15"></path><path class="parser progress first" d="M358 123h378"></path><path class="fetch progress" d="M509 108h97m0 0h20m-20 0l20 7.5m0 0h20m0 0h20m-20 0l20-7.5"></path><path class="execution progress last" d="M736 108h48"></path></g><g><text x="12" y="16.75" class="svg-tag" transform="translate(0 132)"><script type="module" async></text><text y="9" class="svg-label" transform="translate(252 132)">Scripting:</text><text y="24" class="svg-label" transform="translate(252 132)">HTML Parser:</text><path class="connector" d="M666 141v15m50-15v15"></path><path class="parser progress first" d="M358 156h308"></path><path class="fetch progress" d="M509 141h97m0 0h20m-20 0l20 7.5m0 0h20m0 0h20m-20 0l20-7.5"></path><path class="execution progress" d="M666 141h50"></path><path class="parser progress last" d="M716 156h68"></path></g><g class="legend" transform="translate(357.5 172)"><circle cx="3" cy="3" r="3" class="parser"></circle><text x="9" y="3" class="svg-label">parser</text><circle cx="50" cy="3" r="3" class="fetch"></circle><text x="56" y="3" class="svg-label">fetch</text><circle cx="90" cy="3" r="3" class="execution"></circle><text x="96" y="3" class="svg-label">execution</text></g><text x="782" y="175" text-anchor="end" class="svg-label">runtime →</text></svg></p>
<ul>
<li><strong>no attributes</strong>: The HTML parser is blocked during script download and execution.</li>
<li><strong>defer</strong>: The HTML parser is not blocked. The browser downloads scripts as it identifies them. It only executes the scripts once it finishes creating the DOM.</li>
<li><strong>async</strong>: The HTML parser is blocked during script execution. The browser downloads scripts as it identifies them. After download, the scripts block the HTML parser until execution finishes.</li>
<li><strong>module</strong>: Behaves like defer but can manage ES6 module imports.</li>
</ul>
<p>Choose wisely. In most cases you will want either <code>defer</code> or <code>async</code> to optimize the critical rendering path. If you have an inline script which must run synchronously, test moving it above your styles in your HTML.</p>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/render-blocking-resources/#conclusion">#</a></h2>
<p>Render-blocking resources can kick off a cascade of performance issues. Those performance issues result in unhappy users who abandon your page faster.</p>
<p>Lighthouse and the Coverage tool can help you identify this issue and evaluate what your best options are. We learned to:</p>
<ul>
<li>reduce our CSS and JavaScript bytes,</li>
<li>lazy-load non-critical CSS and JavaScript, and</li>
<li>use the <code>defer</code>, <code>async</code>, or <code>module</code> attribute on our scripts.</li>
</ul>
<p>I'd love to hear what worked for you! <a href="https://mobile.twitter.com/TheGreenGreek">Tweet at me</a> with your replies.</p>
<p>Fixing web performance issues can be overwhelming. If you need more help, I'm available for performance consulting projects. <a href="https://sia.codes/contact">Contact me</a> today!</p>
<p>Special thanks to <a href="https://twitter.com/tunetheweb">Barry Pollard</a> and <a href="https://ricaud.me/">Anthony Ricaud</a> for their help with proofreading and editing!</p>
#30DaysOfWebPerf: The Pros and Cons of a Twitter series2021-07-19T00:00:00Zhttps://sia.codes/posts/30-days-web-perf/<p><img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/30DaysOfWebPerf_Sia_Karamalegos_tr9xh0.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/30DaysOfWebPerf_Sia_Karamalegos_tr9xh0.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/30DaysOfWebPerf_Sia_Karamalegos_tr9xh0.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/30DaysOfWebPerf_Sia_Karamalegos_tr9xh0.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/30DaysOfWebPerf_Sia_Karamalegos_tr9xh0.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="30 Days of Web Perf by Sia Karamalegos" importance="high" width="941" height="627" /></p>
<p>In November of 2019, I decided to try out a new way to deliver web performance tips to my Twitter followers. Learning web performance can be overwhelming, so I focused on small bits of information. Thus was born my #30DaysOfWebPerf series.</p>
<p>Twitter is great for small bits of info, right? Well, it was great in some ways and not so great in others. I'll tell you why in this post.</p>
<p>You can see the full series in this <a href="https://twitter.com/i/events/1205601342015918080">Twitter Moment</a>, and here's a preview of Day 1:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Day 1 of⚡️ <a href="https://twitter.com/hashtag/30DaysOfWebPerf?src=hash&ref_src=twsrc%5Etfw">#30DaysOfWebPerf</a> ⚡️<br /><br />For our 1st day in this series on web performance, we're tackling the 🐘 in the room. <br /><br />Lighthouse is an amazing tool that helps you identify specific speed problems on your website... [thread]<a href="https://t.co/VDwXemsBf7">https://t.co/VDwXemsBf7</a><a href="https://twitter.com/hashtag/Lighthouse?src=hash&ref_src=twsrc%5Etfw">#Lighthouse</a> <a href="https://twitter.com/ChromeDevTools?ref_src=twsrc%5Etfw">@ChromeDevTools</a> <a href="https://t.co/GZhWLoGEdx">pic.twitter.com/GZhWLoGEdx</a></p>— Sia Karamalegos (@TheGreenGreek) <a href="https://twitter.com/TheGreenGreek/status/1190282832972828673?ref_src=twsrc%5Etfw">November 1, 2019</a></blockquote><script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Each day, I would write a short thread on Twitter. Then I would re-publish that content on my website and <a href="http://dev.to/">Dev.to</a>.</p>
<h2 id="the-good%3A-impact">The Good: Impact <a class="direct-link" href="https://sia.codes/posts/30-days-web-perf/#the-good%3A-impact">#</a></h2>
<p>The benefits were twofold for me:</p>
<ul>
<li>Many folks replied saying they learned something new. They applied one of the tips to their site and saw loading performance improve. I love it when I make someone's life easier, so this was great to hear.</li>
<li>I got at least 400 new followers. I should have paid more attention to the numbers.</li>
</ul>
<h2 id="the-bad%3A-no-editing-and-bad-seo">The Bad: No editing and bad SEO <a class="direct-link" href="https://sia.codes/posts/30-days-web-perf/#the-bad%3A-no-editing-and-bad-seo">#</a></h2>
<p>The great thing about the web is that it's always changing. The bad thing about the web is that it's always changing. 😅 This makes it challenging to document anything as we always have a moving target. I like to edit my blog posts to keep them updated with new features, discoveries, and opinions.</p>
<p>Unfortunately, tweets are not editable. They are meant to be consumed around the time they are written. Later, they are best left in the dust.</p>
<p>I eventually stopped posting to my website as it flooded my blog post list. Those posts drowned out my higher-quality, more in-depth posts. Google Search values pages with higher on-page time. Dwelling can mean that people find it more engaging. Three tweets on a page don't take long to read. They are probably best kept on Twitter.</p>
<h2 id="the-ugly%3A-javascript-embeds">The Ugly: JavaScript embeds <a class="direct-link" href="https://sia.codes/posts/30-days-web-perf/#the-ugly%3A-javascript-embeds">#</a></h2>
<p>Twitter embeds are ugly. And I mean really ugly. My performance scores are at least 25% lower on any page that has a Twitter embed. I could try to find a tool like <a href="https://github.com/paulirish/lite-youtube-embed">lite-youtube-embed</a> or make one myself. Do let me know if you find one! Or I could recognize that tweets are not the right medium for long-term thought storage.</p>
<h2 id="conclusion%3A-the-future-of-serial-content-delivery">Conclusion: The future of serial content delivery <a class="direct-link" href="https://sia.codes/posts/30-days-web-perf/#conclusion%3A-the-future-of-serial-content-delivery">#</a></h2>
<p>This was a great way to get temporary buzz on a topic, help a few folks out in passing, and gain new followers. If I did it again, I would not bother trying to document them on my site. I'm a big believer in owning your own content, but not all content weathers time well.</p>
<p>Lately, I've been signing up for several 1-week email "challenges" or "courses". Maybe it's a new fad in 2021. The content in each email seems to be shorter than a typical blog post but longer than a tweet. And if you manage it as a campaign that someone can sign up for at any time, then you can edit it when it gets out of date.</p>
<p>What do you think? What are some good ways to deliver smaller chunks of serialized content? <a href="https://twitter.com/TheGreenGreek/status/1417479153037709312">Tweet at me</a> with your thoughts.</p>
What to know about AVIF on Cloudinary2021-07-21T00:00:00Zhttps://sia.codes/posts/avif-on-cloudinary/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/girl_holding_camera_t08zv8.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/girl_holding_camera_t08zv8.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/girl_holding_camera_t08zv8.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/girl_holding_camera_t08zv8.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/girl_holding_camera_t08zv8.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Possum overlayed on a keyboard" importance="high" width="3000" height="2072" />
<figcaption>Get ready for some A+ photography now that I have AVIF enabled. Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kelly Sikkema</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a> with added <a href="https://duotone.shapefactory.co/">duotone</a>.
</figcaption>
</figure>
<p>I just enabled AVIF on Cloudinary and... WOW. My image file sizes dropped by around 37-50% with equal quality.</p>
<p>Most of how to use AVIF on Cloudinary is covered by <a href="https://cloudinary.com/blog/how_to_adopt_avif_for_images_with_cloudinary">How to Adopt AVIF for Images With Cloudinary</a> by Eric Portis. In this post, I want to cover some gotchas and things to know.</p>
<h2 id="what-is-avif%3F">What is AVIF? <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#what-is-avif%3F">#</a></h2>
<p>Before we get started, let's get the back story.</p>
<blockquote>
<p>AVIF is a new image format derived from the keyframes of AV1 video.</p>
<p class="blockquote-source">— Jake Archibald from <a href="https://jakearchibald.com/2020/avif-has-landed/">AVIF has landed</a></p>
</blockquote>
<p>This is not a technical post about image codecs. If you want to nerd out on the technical stuff, I highly recommend <a href="https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4?gi=c175c45e28a1">AVIF for Next-Generation Image Coding</a> from the Netflix Technology Blog.</p>
<p>Instead, let's look at the difference between formats with equal quality (using <a href="https://cloudinary.com/documentation/image_optimization#automatic_quality_selection_q_auto"><code>q_auto</code></a>). Note that you will only see all these images on browsers that support both AVIF and WEBP...</p>
<div class="text-center">
<button class="button" onclick="openImg('AVIF')">AVIF</button>
<button class="button" onclick="openImg('WEBP')">WEBP</button>
<button class="button" onclick="openImg('JPEG')">JPEG</button>
</div>
<figure id="AVIF" class="imgFormat">
<figcaption><strong>AVIF</strong>: 31.1 kB (desktop, 2x screen)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<figure id="WEBP" class="imgFormat" style="display:none">
<figcaption><strong>WEBP</strong>: 80.2 kB (desktop, 2x screen)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_webp,w_680/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_webp,w_300/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_webp,w_600/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_webp,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_webp,w_1856/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<figure id="JPEG" class="imgFormat" style="display:none">
<figcaption><strong>JPEG</strong>: 112 kB (desktop, 2x screen)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,w_680/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,w_300/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,w_600/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,w_1856/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<script>
function openImg(imgFormatName) {
var i;
var x = document.getElementsByClassName("imgFormat");
for (i = 0; i < x.length; i++) {
x[i].style.display = "none";
}
document.getElementById(imgFormatName).style.display = "block";
}
</script>
<p>How low can we go? Now let's look at equal-ish file sizes with different quality (compression)...</p>
<div class="text-center">
<button class="button" onclick="openImg2('AVIF-2')">AVIF</button>
<button class="button" onclick="openImg2('WEBP-2')">WEBP</button>
<button class="button" onclick="openImg2('JPEG-2')">JPEG</button>
</div>
<figure id="AVIF-2" class="imgFormat2">
<figcaption><strong>AVIF</strong>: 12.1 kB (auto quality, 928px wide)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<figure id="WEBP-2" class="imgFormat2" style="display:none">
<figcaption><strong>WEBP</strong>: 13.3 kB (quality 55)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_55,f_webp,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<figure id="JPEG-2" class="imgFormat2" style="display:none">
<figcaption><strong>JPEG</strong>: 11.9 kB (quality 35)</figcaption>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_35,w_928/v1607719366/sia.codes/karsten-winegeart-6Ja5I4hRLyc-unsplash_c287i6.jpg" alt="Large buffalo blocking the roadway" width="4001" height="2671" loading="lazy" />
</figure>
<script>
function openImg2(imgFormat2Name) {
var i;
var x = document.getElementsByClassName("imgFormat2");
for (i = 0; i < x.length; i++) {
x[i].style.display = "none";
}
document.getElementById(imgFormat2Name).style.display = "block";
}
</script>
<aside><strong>Note:</strong> Nearly every post on AVIF shows these side-by-side comparisons of AVIF, WEBP, and JPEG. I did not want to feel left out. Plus, it was fun.</aside>
<h2 id="which-browsers-support-avif%3F">Which browsers support AVIF? <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#which-browsers-support-avif%3F">#</a></h2>
<p>At the time of writing, AVIF is only supported on Chrome, Opera, Samsung Internet, and a few others. You can also enable it in Firefox. Check <a href="https://caniuse.com/avif">caniuse for AVIF</a> for the latest numbers.</p>
<p>Currently, 61.27% of my blog's traffic supports AVIF (67.24% globally). Did you know that you can import your Google Analytics data into caniuse to see what percent of your traffic has support for a given feature? See <a href="https://sia.codes/posts/google-analytics-caniuse-magic/">Google Analytics + caniuse = <em>MAGIC</em></a>.</p>
<h2 id="how-do-you-use-avif-on-cloudinary%3F">How do you use AVIF on Cloudinary? <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#how-do-you-use-avif-on-cloudinary%3F">#</a></h2>
<p>You can use AVIF on Cloudinary one of two ways:</p>
<ul>
<li>Use the <a href="https://cloudinary.com/documentation/image_transformations#image_format_support"><code>f_avif</code></a> format transformation</li>
<li>Use the <a href="https://cloudinary.com/documentation/image_transformations#automatic_format_selection_f_auto"><code>f_auto</code></a> auto format transformation, but only after you <a href="https://support.cloudinary.com/hc/en-us/requests/new">sign up for the beta</a></li>
</ul>
<p>I am lazy. I am not ashamed of this. I love the <code>f_auto</code> transformation because it means that instead of markup like this:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>picture</span><span class="token punctuation">></span></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>source</span><br /><span class="highlight-line"> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[baseURL]/q_auto,f_avif,w_300/file.jpg 300w, [baseURL]/q_auto,f_avif,w_600/file.jpg 600w, [baseURL]/q_auto,f_avif,w_928/file.jpg 928w, [baseURL]/q_auto,f_avif,w_1856/file.jpg 1856w<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(min-width: 980px) 928px, calc(95.15vw + 15px)<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/avif<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>source</span><br /><span class="highlight-line"> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[baseURL]/q_auto,f_webp,w_300/file.jpg 300w, [baseURL]/q_auto,f_webp,w_600/file.jpg 600w, [baseURL]/q_auto,f_webp,w_928/file.jpg 928w,[baseURL]/q_auto,f_webp,w_1856/file.jpg 1856w<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(min-width: 980px) 928px, calc(95.15vw + 15px)<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span><br /><span class="highlight-line"> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[baseURL]/q_auto,w_680/file.jpg<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(min-width: 980px) 928px, calc(95.15vw + 15px)<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Large buffalo blocking the roadway<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>4001<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2671<span class="token punctuation">"</span></span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span><br /><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>picture</span><span class="token punctuation">></span></span></span></code></pre>
<p>I can write much shorter markup and let Cloudinary do the work:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span><br /><span class="highlight-line"> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[baseURL]/q_auto,f_auto,w_680/file.jpg<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[baseURL]/q_auto,f_auto,w_300/file.jpg 300w, [baseURL]/q_auto,f_auto,w_600/file.jpg 600w, [baseURL]/q_auto,f_auto,w_928/file.jpg 928w, [baseURL]/q_auto,f_auto,w_1856/file.jpg 1856w<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(min-width: 980px) 928px, calc(95.15vw + 15px)<span class="token punctuation">"</span></span></span><br /><span class="highlight-line"> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Large buffalo blocking the roadway<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>4001<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2671<span class="token punctuation">"</span></span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span></span><br /> <span class="token punctuation">></span></span></code></pre>
<p>With <code>f_auto</code>, Cloudinary will pick the best file format based on the user's browser.</p>
<aside>I'm so lazy that I don't even write this markup. I created shortcodes in Eleventy to generate my src and srcsets for me. Read about it in <a href="https://sia.codes/posts/eleventy-and-cloudinary-images/">Optimize Images in Eleventy Using Cloudinary</a>.</aside>
<h2 id="what-are-the-tradeoffs-of-using-avif-on-cloudinary%3F">What are the tradeoffs of using AVIF on Cloudinary? <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#what-are-the-tradeoffs-of-using-avif-on-cloudinary%3F">#</a></h2>
<p>Before you sprint to your code editor...</p>
<ul>
<li>AVIF transformations are slow</li>
<li>AVIF transformations use up your credits faster</li>
</ul>
<p>Let's talk about how to overcome those problems.</p>
<h3 id="reduce-delays-for-your-users">Reduce delays for your users <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#reduce-delays-for-your-users">#</a></h3>
<p>Using AVIF increases your transformations usage because AVIF is intensive to encode. This also means that it takes longer for the transformation to occur. It's noticeable with delays of a few seconds.</p>
<p>For my blog, I overcome this obstacle by viewing new posts at different screens sizes before publishing. Then Cloudinary caches my AVIF images so they are ready for the next request.</p>
<p>This is not reasonable for larger projects. Instead, you can use <a href="https://cloudinary.com/documentation/transformations_on_upload#eager_transformations">eager transformations</a> to transform on upload through the API.</p>
<h3 id="the-credit-usage-details">The credit usage details <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#the-credit-usage-details">#</a></h3>
<p>At the time of writing (July 2021), Cloudinary counts AVIF transformations as follows:</p>
<ul>
<li>Images 2MP and below count as 1 transformation</li>
<li>Each 1MP above 2MP will count as another 0.5 transformations</li>
</ul>
<p>This seems high cost on the surface but is a good deal. I understood this once I saw how long AVIF transformations take compared to other formats. AVIF transforms are 70x-100x more CPU intensive than that of JPEG!</p>
<p>If you want to sign up for the <code>f_auto</code> beta, you can <a href="https://support.cloudinary.com/hc/en-us/requests/new">contact Cloudinary</a>.</p>
<aside>If you found this article helpful, you can <a href="https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/oq6yrskcixnvxvj1ofc0">sign up for a free Cloudinary account</a> with this link, and I'll get a few extra Cloudinary credits per month.</aside>
<h2 id="results-and-next-steps">Results and next steps <a class="direct-link" href="https://sia.codes/posts/avif-on-cloudinary/#results-and-next-steps">#</a></h2>
<p>The buffalo picture (from my recent post on <a href="https://sia.codes/posts/render-blocking-resources/">render-blocking resources</a>) went from 198 kB to 97 kB. The results around my site vary based on the image, and the savings increase on larger (width) images.</p>
<p>Nowadays, we have devices with 2x, 3x, and higher <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio">DPR</a> (device pixel ratio) screens. Humans can't see much better than 2x. Also, we can't detect lower quality as well on higher DPR images.</p>
<p>So my next step is to manually DPR-cap to 2x sizes with higher compression (lower quality) on the 2x srcset. I've implemented this for one client and saw great results. Now it's time to apply it to my blog!</p>
<p>Jake Archibald wrote up how to do this in <a href="https://jakearchibald.com/2021/serving-sharp-images-to-high-density-screens/">Half the size of images by optimising for high density displays</a>.</p>
<p><a href="https://twitter.com/yoavweiss">Yoav Weiss</a> wants to run an origin trial for <a href="https://chromium-review.googlesource.com/c/chromium/src/+/2395619">adding a max DPR value to srcsets</a>. That will be exciting if it happens!</p>
<p>Have you started using AVIF images on your sites yet? What method are you using?</p>
<p>Don't miss a post. <a href="https://sia.codes/posts/avif-on-cloudinary/#inform">Sign up for my newsletter</a>!</p>
<p><small>Pug photo credit: <a href="https://unsplash.com/@karsten116?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Karsten Winegeart</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>.</small></p>
Explore JavaScript Dependencies With Lighthouse Treemap2021-08-02T00:00:00Zhttps://sia.codes/posts/lighthouse-treemap/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/raphael-schaller-D6uxeDSylxo-unsplash_hpbbnp.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/raphael-schaller-D6uxeDSylxo-unsplash_hpbbnp.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/raphael-schaller-D6uxeDSylxo-unsplash_hpbbnp.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/raphael-schaller-D6uxeDSylxo-unsplash_hpbbnp.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/raphael-schaller-D6uxeDSylxo-unsplash_hpbbnp.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="View of a roundabout from above surrounded by trees" importance="high" width="2000" height="1123" />
<figcaption>Find your way with Lighthouse Treemap. Photo by <a href="https://unsplash.com/@raphaelphotoch?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Raphael Schaller</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
</figcaption>
</figure>
<p>Lighthouse Treemap is a new tool that helps us evaluate the efficiency of the JavaScript on our websites. It shows us:</p>
<ul>
<li>The bytes of JavaScript by file</li>
<li>If sourcemaps are enabled, the bytes of JavaScript by module</li>
<li>Bytes of JavaScript used versus unused (execution) for page load</li>
</ul>
<p>JavaScript is often the biggest culprit when it comes to poor web performance. This tool can help us find our biggest dependencies and opportunities for improvement.</p>
<p>In this post, I'll cover:</p>
<ul>
<li><a href="https://sia.codes/posts/lighthouse-treemap/#what-is-lighthouse-treemap%3F">What is Lighthouse Treemap?</a></li>
<li><a href="https://sia.codes/posts/lighthouse-treemap/#existing-similar-tools">Existing similar tools</a></li>
<li><a href="https://sia.codes/posts/lighthouse-treemap/#the-gap-lighthouse-treemap-fills">The gap Lighthouse Treemap fills</a></li>
<li><a href="https://sia.codes/posts/lighthouse-treemap/#the-limitations-of-lighthouse-treemap">Limitations</a></li>
<li><a href="https://sia.codes/posts/lighthouse-treemap/#how-do-i-access-lighthouse-treemap%3F">How do I access Lighthouse Treemap?</a></li>
</ul>
<h2 id="what-is-lighthouse-treemap%3F">What is Lighthouse Treemap? <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#what-is-lighthouse-treemap%3F">#</a></h2>
<p>First, lets understand what a <strong>treemap</strong> is:</p>
<blockquote>
<p>Treemaps display hierarchical (tree-structured) data as a set of nested rectangles. Each branch of the tree is given a rectangle, which is then tiled with smaller rectangles representing sub-branches. A leaf node's rectangle has an area proportional to a specified dimension of the data.</p>
<p class="blockquote-source">- from <a href="https://en.wikipedia.org/wiki/Treemapping">Treemapping</a> on Wikipedia</p>
</blockquote>
<p>Below is an example Lighthouse Treemap from <a href="http://github.com/">github.com</a>. It contains 2 sections:</p>
<ul>
<li>The upper, larger part is the JavaScript treemap</li>
<li>The bottom part is the JavaScript code coverage table</li>
</ul>
<p>In the treemap section, the size of each rectangle represents the number of bytes for that script. The treemap also shows the actual number of bytes and percent of total bytes. White gaps in the grid separate each file. Rectangles inside the files represent the individual modules in those scripts:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/lighthouse-treemap-visual_opw8ua.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/lighthouse-treemap-visual_opw8ua.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/lighthouse-treemap-visual_opw8ua.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/lighthouse-treemap-visual_opw8ua.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/lighthouse-treemap-visual_opw8ua.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Grid of rectangles on top of report, and table with single stacked bar charts on the bottom for each JavaScript file" loading="lazy" width="3356" height="1718" />
<figcaption>The Lighthouse Treemap is two reports: the treemap itself and a code coverage report.
</figcaption>
</figure>
<p>To see module-level data, sourcemaps need to be enabled. This report from <a href="http://trello.com/">trello.com</a> shows what the Lighthouse Treemap looks like without sourcemaps:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/lighthouse-treemap-no-sourcemaps_rvxmqj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/lighthouse-treemap-no-sourcemaps_rvxmqj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/lighthouse-treemap-no-sourcemaps_rvxmqj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/lighthouse-treemap-no-sourcemaps_rvxmqj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/lighthouse-treemap-no-sourcemaps_rvxmqj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="The grid of rectangles on top only shows the outer/file rectangle but no module rectangles" loading="lazy" width="3360" height="1720" />
<figcaption>When sourcemaps are not found, only the file-level rectangles will show
</figcaption>
</figure>
<p>In my experience, I've found reports often are a mixture of the two levels of data. Most third-party scripts will not show modules, and many first-party ones will.</p>
<p>Clicking "Toggle Table" will hide the coverage report and make the treemap larger, revealing more rectangles. We can click on a rectangle to zoom in on it and see its dependencies better. To return, click on the title above the rectangle. Here's a short video demonstrating these actions (and more!):</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="oigOsQkhyVc" style="background-image: url('https://i.ytimg.com/vi/oigOsQkhyVc/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Lighthouse Treemap: Toggle table and dive deeper in the tree</span>
</button>
</lite-youtube>
<p>The Chrome team went one step farther by adding the code coverage report. Underneath the treemap, we can see how much of our JavaScript was actually used on page load, for each file. You can also click "Unused Bytes" in the top right corner of the treemap to see unused bytes on the treemap:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/lighthouse-treemap-unused-bytes_c9i8ee.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/lighthouse-treemap-unused-bytes_c9i8ee.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/lighthouse-treemap-unused-bytes_c9i8ee.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/lighthouse-treemap-unused-bytes_c9i8ee.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/lighthouse-treemap-unused-bytes_c9i8ee.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="The grid of rectangles on top only shows the outer/file rectangle but no module rectangles" loading="lazy" width="3358" height="1714" />
<figcaption>When sourcemaps are not found, only the file-level rectangles will show
</figcaption>
</figure>
<p>This is pretty slick as deep-diving on the treemap will show unused bytes for each module too.</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/lighthouse-treemap/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h2 id="existing-similar-tools">Existing similar tools <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#existing-similar-tools">#</a></h2>
<p>The Lighthouse Treemap functionality is similar to two existing toolsets. First, the Coverage tool in Chrome is almost the same tool as the bottom part of the treemap. Second, the treemap itself is very similar to bundle analyzers.</p>
<h3 id="chrome's-coverage-tool">Chrome's Coverage tool <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#chrome's-coverage-tool">#</a></h3>
<p>The <a href="https://developer.chrome.com/docs/devtools/coverage/">Coverage tool</a> already exists in Chrome Dev Tools. The original tool also lists CSS files and used vs unused CSS bytes. You can interact with the page to see the percent used bytes increase.</p>
<p>To access and use it:</p>
<ol>
<li>Open Dev Tools</li>
<li>Type <em>Cmd + Shift + p</em> to open the search tool</li>
<li>Type "coverage" and click on the Show Coverage drawer</li>
<li>You might need to drag the drawer higher to see it</li>
<li>Click the reload button to start a new report</li>
<li>Interact with the page and see how the JavaScript and CSS usage goes up</li>
</ol>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="WH3pCYZgEEA" style="background-image: url('https://i.ytimg.com/vi/WH3pCYZgEEA/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Coverage tool in Chrome Dev Tools</span>
</button>
</lite-youtube>
<h3 id="bundle-analyzers">Bundle analyzers <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#bundle-analyzers">#</a></h3>
<p>The treemap itself is very similar to bundle analyzers. <a href="https://github.com/webpack-contrib/webpack-bundle-analyzer">Webpack-bundle-analyzer</a> is one of my favorites. These tools help you understand the size of all the bundles (and modules) that your application code generates.</p>
<p>I deployed the output in this <a href="https://projects.sia.codes/webpack-bundle-analyzer-example/">webpack-bundle-analyzer example</a> so you can interact with it live. Or, you can watch this quick demonstration:</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="LlmI5M-TK2k" style="background-image: url('https://i.ytimg.com/vi/LlmI5M-TK2k/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: webpack-bundle-analyzer demo</span>
</button>
</lite-youtube>
<p>You might have noticed a key difference in what bundle analyzers show you versus Lighthouse Treemap...</p>
<h2 id="the-gap-lighthouse-treemap-fills">The gap Lighthouse Treemap fills <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#the-gap-lighthouse-treemap-fills">#</a></h2>
<p>Bundle analyzers are great, but they do not show which bundles are loaded and used on your site. The closest tool I've seen that shows this is the <a href="https://nextjs.org/">Next.js</a> build script output:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/next-js-bundle-size-output_dhke6c.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/next-js-bundle-size-output_dhke6c.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/next-js-bundle-size-output_dhke6c.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/next-js-bundle-size-output_dhke6c.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/next-js-bundle-size-output_dhke6c.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Terminal output listing pages and chunks as well as their sizes and how much is used for first load" loading="lazy" width="1802" height="1040" />
<figcaption>Next.js build output will list page/route and chunk sizes
</figcaption>
</figure>
<p>I love how Next.js differentiates the chunks and bytes used for the initial load. This output would be great to pass through a budget tool to prevent commits/PRs from exceeding a target amount of JavaScript.</p>
<p>It is a handy tool, but it doesn't give me much introspection into which modules are the heaviest. I need this knowledge so I can improve or replace large dependencies. Finally, this and other bundle analyzers don't show me any third-party scripts loaded from outside the application code.</p>
<h2 id="the-limitations-of-lighthouse-treemap">The Limitations of Lighthouse Treemap <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#the-limitations-of-lighthouse-treemap">#</a></h2>
<p>Lighthouse Treemap is not a Swiss army knife. You wouldn't want to prepare a gourmet dinner only using the tiny knife tool. Nor open a bottle of wine with the tiny corkscrew if you had a corkscrew dedicated to that job.</p>
<p>Lighthouse Treemap is great for evaluating the JavaScript chunks/files and modules needed for initial load. It visualizes relative sizes so you can quickly see the largest ones. And, you can apply a used/unused filter to find the most inefficient pieces of code.</p>
<p>Now we need to understand a few of its limitations:</p>
<ul>
<li>Sourcemaps are needed to show finer-grained details.</li>
<li>The Coverage report is only for initial load.</li>
<li>It doesn't show dependencies loaded later.</li>
</ul>
<p>While sourcemaps are needed to get the most benefit, even without them I can see which chunks are largest. I can also see how many are core features versus third-party tracking scripts. You can run the dedicated Coverage tool to explore usage as you interact with the page. Your goal isn't 0% unused. Finally, for scripts loaded later, use the Network tab and bundle analyzers.</p>
<h2 id="how-do-i-access-lighthouse-treemap%3F">How do I access Lighthouse Treemap? <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#how-do-i-access-lighthouse-treemap%3F">#</a></h2>
<p>Now that you know all about Lighthouse Treemap, how do you access it? It's now fully released in Lighthouse in Chrome! You can access it through many routes:</p>
<ol>
<li><a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> in Chrome Dev Tools</li>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a></li>
<li>Running Lighthouse, downloading the JSON, and uploading it <a href="https://googlechrome.github.io/lighthouse/treemap/?gzip=1">here</a></li>
<li>Through the <a href="https://github.com/GoogleChrome/lighthouse#using-the-node-cli">Lighthouse Node CLI</a></li>
</ol>
<p>Once you have a Lighthouse report (1, 2, or 4 from above), then you'll find the View Treemap button below the metrics:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/lighthouse-treemap-button_sdxkjh" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/lighthouse-treemap-button_sdxkjh 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/lighthouse-treemap-button_sdxkjh 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/lighthouse-treemap-button_sdxkjh 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/lighthouse-treemap-button_sdxkjh 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Dev Tools with a Lighthouse report open showing the button between the metrics and screenshots sections" loading="lazy" width="3358" height="1718" />
<figcaption>Click on "View Treemap" to open a new tab with the Lighthouse Treemap
</figcaption>
</figure>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/lighthouse-treemap/#conclusion">#</a></h2>
<p>Lighthouse Treemap is a welcome new tool in the web performance arsenal. It fills a much-needed gap in evaluating the JavaScript downloaded for initial page load. We can see which chunks are the largest, and which modules within those chunks are contributing most to our bundles. Finally, it visualizes how much JavaScript is unused so we can quickly find the worst offenders and take action.</p>
<p>Finally, Lighthouse Treemap helps me evaluate any website from the outside without having to add dependencies like bundle analyzers.</p>
<p>Have you tried out Lighthouse Treemap yet? Has it made measuring performance for your site easier? Let me know in the webmentions!</p>
<p>Don't miss a post. <a href="https://sia.codes/posts/lighthouse-treemap/#inform">Sign up for my newsletter</a>!</p>
An Informal Survey of Web Performance Tooling in 20212021-08-04T00:00:00Zhttps://sia.codes/posts/survey-web-performance-tooling/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/gustavo-campos-B87zMorEZRo-unsplash_ylwcwa.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/gustavo-campos-B87zMorEZRo-unsplash_ylwcwa.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/gustavo-campos-B87zMorEZRo-unsplash_ylwcwa.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/gustavo-campos-B87zMorEZRo-unsplash_ylwcwa.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/gustavo-campos-B87zMorEZRo-unsplash_ylwcwa.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Red Formula 1 race car in the pit with mechanics all around racing to change tires, etc. View from above." importance="high" width="1830" height="986" />
<figcaption>Start your testing engines with some new performance tooling. Photo by <a href="https://unsplash.com/@gustavocpo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">gustavo Campos</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash.</a>
</figcaption>
</figure>
<p>Sometimes it's hard to discover all the great web performance measurement tools out there. What are performance engineers using in 2021? What tools do they want to use?</p>
<p>I set out to answer these questions by creating a survey. I shared this survey with the web performance engineering community. It's not statistically perfect (27 responses), but I still found it insightful. I also learned a lot for next year.</p>
<p>This post will cover:</p>
<ul>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#survey-questions">Survey questions</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#top-web-performance-tools">Top web performance tools</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#top-performance-tools-people-want-to-use">Top tools people want to use</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#all-results">All results</a> (data and chart)
<ul>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#great-tools-that-were-left-out">Great tools that were left out</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#favorite-web-performance-tools">Favorite tools</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#least-favorite-tools">Least favorite tools</a></li>
</ul>
</li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#lessons-learned-and-conclusion">Lessons learned and conclusion</a></li>
</ul>
<h2 id="survey-questions">Survey questions <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#survey-questions">#</a></h2>
<p>The survey asked one question about a large list of tools, "Which tools do you know about and use?" Respondents chose one answer that described their experience with it:</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/web_perf_tools_survey_preview_byxg98.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/web_perf_tools_survey_preview_byxg98.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/web_perf_tools_survey_preview_byxg98.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/web_perf_tools_survey_preview_byxg98.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/web_perf_tools_survey_preview_byxg98.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Question: Which tools do you know about and use? Column headings: I use this tool regularly, I use this tool sometimes, I want to use this tool, I have no interest in this tool, I don't know this tool. Rows: WebPageTest, Lighthouse, Chrome Network & Performance Tabs, Chrome Coverage Tool, Lighthouse Treemap (Canary), Sitespeed.io, Boomerang, yellowlab.tools, Lighthouse Parade, pagespeed.compare, WebPageTest, Lighthouse, Chrome Network & Performance Tabs" loading="lazy" width="1832" height="1252" />
<figcaption>Respondents could choose only one answer for each tool
</figcaption>
</figure>
<p>Find the full list of tools in the <a href="https://sia.codes/posts/survey-web-performance-tooling/#all-results">results</a> section below.</p>
<p>These three open-ended questions were also asked at the end:</p>
<ul>
<li>Please list any great tools that we left out, and tell us why it's a great tool for measuring web performance! Names and links help.</li>
<li>What are your top 3 tools favorite tools right now and why?</li>
<li>What are your least favorite tools from that list and why?</li>
</ul>
<p>Now let's jump into the data!</p>
<h2 id="top-web-performance-tools">Top web performance tools <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#top-web-performance-tools">#</a></h2>
<p>Combining the data from the first two columns ("I use this tool regularly" and "I use this tool sometimes") yields a list of the most used tools:</p>
<ol>
<li><a href="https://webpagetest.org/">WebPageTest</a> and Chrome <a href="https://developer.chrome.com/docs/devtools/network/">Network</a> & <a href="https://developer.chrome.com/docs/devtools/speed/get-started/">Performance</a> Tabs (tied for 1st)</li>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a> and Running on a browser with JavaScript disabled (tied for 2nd)</li>
<li><a href="https://developer.chrome.com/docs/devtools/coverage/">Chrome Coverage tool</a></li>
<li><a href="https://www.ssllabs.com/ssltest/index.html">Qualys SSL Server Test</a></li>
<li><a href="https://webspeedtest.cloudinary.com/">Cloudinary Image Analysis Tool</a></li>
<li><a href="https://product.webpagetest.org/api">WebPageTest API</a> and <a href="https://wpt-compare.app/">WebPageTest comparison URL generator</a> (tied for 6th)</li>
</ol>
<p>We can see that WebPageTest and Chrome dominate the list of the top tools. Yet, Qualys' SSL server test and Cloudinary's image analysis tool fill important gaps.</p>
<p>I appreciate that one of the top tools is not a tool at all: turning JavaScript off in the browser settings and seeing what happens on page load.</p>
<aside><strong>Testing without JavaScript</strong>: Going into your browser settings to turn JavaScript on and off can be tiring. My solution is a <a href="https://support.google.com/chrome/answer/2364824?hl=en&co=GENIE.Platform%3DDesktop">separate Chrome profile</a> where JavaScript is permanently turned off. It's buddies with my no-extensions Chrome profile that I use for performance testing. Browser extensions to toggle JavaScript also exist.</aside>
<h3 id="top-regularly-used-performance-tools">Top regularly used performance tools <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#top-regularly-used-performance-tools">#</a></h3>
<p>Digging deeper into the most used tools, the ones performance engineers reach for the most often are:</p>
<ol>
<li>Chrome <a href="https://developer.chrome.com/docs/devtools/network/">Network</a> & <a href="https://developer.chrome.com/docs/devtools/speed/get-started/">Performance</a> Tabs</li>
<li><a href="https://webpagetest.org/">WebPageTest</a></li>
<li><a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a></li>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a></li>
<li>Running on a browser with JavaScript disabled</li>
<li><a href="https://www.sitespeed.io/">Sitespeed.io</a></li>
</ol>
<p>This category had clear leaders. Only these tools had 25% or more respondents say that they used them regularly.</p>
<h3 id="top-intermittently-used-performance-tools">Top intermittently used performance tools <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#top-intermittently-used-performance-tools">#</a></h3>
<ol>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a>, <a href="https://developer.chrome.com/docs/devtools/coverage/">Chrome Coverage tool</a>, and <a href="https://webspeedtest.cloudinary.com/">Cloudinary Image Analysis Tool</a> (tied for 1st)</li>
<li><a href="https://www.ssllabs.com/ssltest/index.html">Qualys SSL Server Test</a></li>
<li><a href="https://product.webpagetest.org/api">WebPageTest API</a></li>
<li><a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a></li>
<li>Running on a browser with JavaScript disabled, <a href="https://wpt-compare.app/">WebPageTest comparison URL generator</a>, <a href="https://github.com/webpack-contrib/webpack-bundle-analyzer">webpack-bundle-analyzer</a>, and <a href="https://yellowlab.tools/">Yellow Lab Tools</a> (tied for 5th)</li>
</ol>
<p>This category had a lot of ties and steadily declining distribution. This group represents those used sometimes by 33% or more respondents. Be sure to check <a href="https://sia.codes/posts/survey-web-performance-tooling/#all-results">all results</a> to see more tools.</p>
<h2 id="top-performance-tools-people-want-to-use">Top performance tools people want to use <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#top-performance-tools-people-want-to-use">#</a></h2>
<ol>
<li><a href="https://developer.akamai.com/tools/boomerang">Boomerang</a></li>
<li><a href="https://sia.codes/posts/survey-web-performance-tooling/#how-can-i-access-lightouse-treemap%3F">Lighthouse Treemap</a></li>
<li><a href="https://www.sitespeed.io/">Sitespeed.io</a></li>
<li><a href="https://product.webpagetest.org/api">WebPageTest API</a> and <a href="https://github.com/GoogleChrome/lighthouse-ci">Lighthouse CI</a> (tied for 4th)</li>
<li><a href="https://www.frustrationindex.com/">FRUSTRATIONindex</a></li>
<li><a href="https://pagespeed.compare/">PageSpeed Compare</a></li>
</ol>
<p>This is an interesting category. It's the set of people who do not already use this tool but would like to. This suggests to me that they see the potential value but something is stopping them. The hurdles could be:</p>
<ul>
<li><strong>Priority</strong> - Do I have the time to try it?</li>
<li><strong>Accessibility</strong> - Do I need to install something special? Does it cost money? Does it have an elaborate set up?</li>
<li><strong>Complexity</strong> - Do I know how to use it? Can I easily learn?</li>
</ul>
<p>I bet an opportunity exists for more documentation or learning resources for these tools. I wrote <a href="https://sia.codes/posts/lighthouse-treemap/">Explore JavaScript Dependencies With Lighthouse Treemap</a> because of this data!</p>
<h2 id="all-results">All results <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#all-results">#</a></h2>
<p>The stacked bar chart below shows the full results. Also, you can find the raw data in this <a href="https://docs.google.com/spreadsheets/d/141cq1kpfewFr0Nx_66XbocyW_zqNSf1oXcn5s5z4K2A/edit?usp=sharing">Google spreadsheet</a>.</p>
<figure id="all-results">
<img src="https://sia.codes/img/posts/performance_tools_survey_results.svg" loading="lazy" alt="Stacked bar chart of survey results - see spreadsheet for the data table" width="1345" height="1063" />
<figcaption>All responses for all tools</figcaption>
</figure>
<h3 id="great-tools-that-were-left-out">Great tools that were left out <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#great-tools-that-were-left-out">#</a></h3>
<p>One of the open-ended questions asked respondents which other tools they thought were great. I'll be sure to add these to the survey next year:</p>
<ul>
<li><a href="https://www.speedcurve.com/">SpeedCurve</a> (mentioned 3 times) - "combined both synthetic testing and real user monitoring to help get a more rounded view of user experience"</li>
<li><a href="https://calibreapp.com/">Calibre</a> (mentioned 3 times) - "Nice and intuitive; I've used it a lot at my work to track both my own estate, and the competition."</li>
<li><a href="https://requestmap.webperf.tools/">Request Map</a> (mentioned twice)</li>
<li><a href="https://whatdoesmysitecost.com/">What Does My Site Cost?</a> - "Good for helping sell perf improvements on global sites."</li>
<li><a href="https://github.com/sitespeedio/browsertime">Browsertime</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Tools/Performance">Firefox performance tab</a></li>
<li><a href="https://www.catchpoint.com/">Catchpoint</a></li>
<li>Core Web Vitals in Search Console - "Real user data is immensely valuable for the many clients we have that can't afford to stand up RUM analytics tools of their own."</li>
<li>CrUX API for origin RUM data- "We use the raw data from this extensively in our own custom dashboarding."</li>
<li><a href="https://developer.fastly.com/reference/api/metrics-stats/historical-stats/">Fastly Historical Stats API</a></li>
<li><a href="https://treo.sh/">Treo</a> - "nice performance tracking, including CWV from CrUX for free with <a href="https://treo.sh/sitespeed/">Treo Site Speed</a>"</li>
</ul>
<p>Real-user monitoring seems to be a theme here. Some are paid tools with nice tracking and dashboards. Others are iterations of how to access the Chrome UX Report data for a particular site (Core Web Vitals).</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/survey-web-performance-tooling/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h3 id="favorite-web-performance-tools">Favorite web performance tools <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#favorite-web-performance-tools">#</a></h3>
<p>The favorites data can be summarized with the following quote from the responses:</p>
<blockquote>
<p>WebPagetest, WebPagetest, WebPagetest. It's just an amazing tool, it does loads of stuff, and it's constantly improving.</p>
</blockquote>
<p>WebPageTest was mentioned by 70% of the respondents. That number jumps up to 86% if you only include people who answered the question. No other tool came close, but here are the top 5:</p>
<ol>
<li><a href="https://webpagetest.org/">WebPageTest</a></li>
<li>Chrome <a href="https://developer.chrome.com/docs/devtools/network/">Network</a> & <a href="https://developer.chrome.com/docs/devtools/speed/get-started/">Performance</a> Tabs (or "Dev Tools")</li>
<li><a href="https://www.sitespeed.io/">Sitespeed.io</a></li>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a></li>
<li><a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a></li>
</ol>
<h3 id="least-favorite-tools">Least favorite tools <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#least-favorite-tools">#</a></h3>
<p>For this question, most people were happy with all tools. However, some concerns are valid though it's not always about the tool itself. Here are a few quotes:</p>
<blockquote>
<p>PageSpeed Insights - This tool is both a blessing and a curse. It's a blessing because for developers it has a lot of useful information if you know how to read it. But we have a lot of non-developers who just look at the score and judge by that.</p>
</blockquote>
<p>Lighthouse is the engine behind the PageSpeed Insights scores and recommendations. I both love and semi-dislike Lighthouse for the same reason. Even developers can focus on the top score at the expense of everything else. Lighthouse is great for people newer to performance because so many learning resources are connected to each audit. But, we need to get past focusing on "Lighthouse 100".</p>
<blockquote>
<p>Lighthouse and WebPageTest: If you use Lighthouse you supports Chrome monopoly and the Googles surveillance capitalism. I really liked WebPageTest when it was Open Source. Now it's got a non Open Source license and backed by venture capitalists. Every issue you create, every move you make with that tool, you help those venture capitalists to make more money when they sell Catchpoint.</p>
</blockquote>
<p>Another comment lamented the US-focus of PageSpeed Insights. We need to be mindful of a website's target audience. If it's a non-US country, then testing from the US is never going to yield results in line with real user experiences.</p>
<p>A few mentioned the Chrome Coverage tool. Different user journeys could use different parts of the code. I always give a caveat with this tool because once you start interacting with the page, the JS and CSS usages increase.</p>
<h2 id="lessons-learned-and-conclusion">Lessons learned and conclusion <a class="direct-link" href="https://sia.codes/posts/survey-web-performance-tooling/#lessons-learned-and-conclusion">#</a></h2>
<p>I think this was a great starting point for a survey of this type. In the future, I'd like to:</p>
<ul>
<li>Include more RUM tools</li>
<li>Get suggestions for new tools to add before launching</li>
<li>Ask questions about respondents' roles/jobs</li>
<li>Get more of the performance community to respond to the survey</li>
</ul>
<p>Are these results in line with what you expected? Was anything surprising? Do you have suggestions for next year? Let me know on Twitter or in the webmentions!</p>
Build that blog already2021-08-20T00:00:00Zhttps://sia.codes/posts/build-that-blog-already/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/build_a_blog_workshop_bxvuo9.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/build_a_blog_workshop_bxvuo9.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/build_a_blog_workshop_bxvuo9.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/build_a_blog_workshop_bxvuo9.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/build_a_blog_workshop_bxvuo9.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Build a bear workshop logo with the bear overlayed with an RSS icon and the word bear overwritten with 'blog' by hand" importance="high" width="600" height="400" />
<figcaption>This lovely promo image is courtesy of <a href="https://twitter.com/eleven_ty/status/1427657012184223752">Zach Leatherman</a></figcaption>
</figure>
<p>This week I held a mini-workshop called <a href="https://gdg.community.dev/events/details/google-gdg-new-orleans-presents-lets-build-and-deploy-your-blog-already/">Let's build and deploy your blog already</a>. It was unexpectedly popular. I suspect this is because as humans, we all tend to procrastinate, and workshops are a great opportunity to finally get that to-do checked off.</p>
<p>I am a freelance developer and performance engineer. I now get more new clients from conversions on my blog than through word of mouth. It took a long time to get here, but you'll never arrive if you never start.</p>
<p>So what's holding you back from starting that tech blog? Don't know where to start? Feel like everything needs to be perfect first? In this post, I'll give you a few tips for making those brave first steps.</p>
<h2 id="so-many-tools-are-out-there%2C-what-if-i-pick-the-wrong-one%3F">So many tools are out there, what if I pick the wrong one? <a class="direct-link" href="https://sia.codes/posts/build-that-blog-already/#so-many-tools-are-out-there%2C-what-if-i-pick-the-wrong-one%3F">#</a></h2>
<p>Popular tools come and go like the wind. My advice is to pick the one that will be the easiest for you to consistently blog. Secondarily, if you like tinkering, consider how you like interacting and customizing that tool.</p>
<p>Don't worry about whether it's the hottest new framework. You won't get a job based on which framework you used to build your blog. You'll get a job because you're actually writing interesting content on your blog. Content is king, not the framework.</p>
<p>Some things to consider when selecting tools:</p>
<ul>
<li>Make sure it's in a language you like. Don't pick a Ruby-based tool if you prefer JavaScript.</li>
<li>Consider how easy it is to build and deploy.</li>
<li>Consider user experience, including performance. If you're loading a giant JavaScript bundle for your framework that means a longer load time for your readers.</li>
</ul>
<p>For me, <a href="https://www.11ty.dev/">Eleventy</a> paired with <a href="https://netlify.com/">Netlify</a> checks all the right boxes. It's JavaScript-based but sends no client-side JavaScript to the browser. I can write HTML, Markdown, Nunjucks, and many other templating languages. Build times are fast, and deploying with Netlify only requires a <code>git push</code> to GitHub. That's me though - find the tool that will work best for you.</p>
<aside>Want to get a flavor of Eleventy? Check out my post <a href="https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/">Itsiest, Bitsiest Eleventy Tutorial</a>. If you like it, check out some of the <a href="https://www.11ty.dev/docs/starter/">starter projects</a>. I forked the eleventy-base-blog on into my <a href="https://github.com/siakaramalegos/11ty-sia-blog">own starter</a>. With one click, you can <a href="https://app.netlify.com/start/deploy?repository=https://github.com/siakaramalegos/11ty-sia-blog">deploy to Netlify</a> or Vercel while generating a GitHub repo.</aside>
<h2 id="no-one-will-read-my-posts-unless-they're-on-%5Binsert-platform-here%5D%2C-right%3F">No one will read my posts unless they're on [insert platform here], right? <a class="direct-link" href="https://sia.codes/posts/build-that-blog-already/#no-one-will-read-my-posts-unless-they're-on-%5Binsert-platform-here%5D%2C-right%3F">#</a></h2>
<p>Yes and no. You might not have a lot of readers in the beginning. That's totally fine. The numbers will come in time. The good news is you don't have to pick hosting your own blog over posting on <a href="http://dev.to/">Dev.to</a> or other platforms. You can do both. This is called <strong>syndication</strong>.</p>
<p>Sometimes platforms die, and if you only have your content hosted there, you can lose it all.</p>
<p>To properly syndicate without an SEO penalty, you need to set the canonical URL both on your original post and on syndicated copies. If you inspect the <code><head></code> tag on any page on my blog, you'll see its corresponding canonical URL meta tag:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>canonical<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://sia.codes/posts/itsiest-bitsiest-eleventy-tutorial/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span></code></pre>
<p>In Eleventy, I can programmatically set this in my base layout like so:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>canonical<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://sia.codes{{ page.url }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></span></code></pre>
<p>Other tools should have similar capabilities.</p>
<p>You also need to set the canonical URL on other publishers when you copy your posts there. Each one is different, but I'll highlight <a href="https://dev.to/">Dev.to</a> here. Dev is very canonical-URL friendly.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/dev_canonical_link_sia_karamalegos_okusux.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/dev_canonical_link_sia_karamalegos_okusux.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/dev_canonical_link_sia_karamalegos_okusux.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/dev_canonical_link_sia_karamalegos_okusux.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/dev_canonical_link_sia_karamalegos_okusux.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Post on Dev.to with linked 'Originally published at sia.codes' text that goes to the my original post" width="1818" height="1592" />
<figcaption>Unlike many publishers, Dev.to will highlight where the article is originally published</figcaption>
</figure>
<p>To set the canonical URL, when you're editing the post, you'll see a small hexagonal button next to the "Save changes" button. Click the hexagon to get to the canonical URL settings.</p>
<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/dev_canonical_settings_sia_karamalegos_b2zhsc.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/dev_canonical_settings_sia_karamalegos_b2zhsc.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/dev_canonical_settings_sia_karamalegos_b2zhsc.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/dev_canonical_settings_sia_karamalegos_b2zhsc.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/dev_canonical_settings_sia_karamalegos_b2zhsc.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" loading="lazy" alt="Save changes button followed by a hexagonal icon and then a Revert changes link." width="1950" height="430" />
<figcaption>Click the hexagonal icon to add the canonical URL.</figcaption>
</figure>
<p>Eventually, learn more about SEO and optimize the timing on when you syndicate. In the beginning, you don't need to worry about it.</p>
<h2 id="but-i-want-dark-mode%2C-social-share-images%2C-a-great-design%2C-etc.">But I want dark mode, social share images, a great design, etc. <a class="direct-link" href="https://sia.codes/posts/build-that-blog-already/#but-i-want-dark-mode%2C-social-share-images%2C-a-great-design%2C-etc.">#</a></h2>
<p>Feature wishlist creep can hold you back from accomplishing anything. I know because <a href="https://sia.codes/posts/how-to-build-a-website/">I've been there</a>.</p>
<p>Just build and deploy a minimally-viable-blog (MVB). Like right now. Today. If you don't, you'll never be motivated enough to improve it. Let the shame work to your advantage.</p>
<h3 id="forms">Forms <a class="direct-link" href="https://sia.codes/posts/build-that-blog-already/#forms">#</a></h3>
<p>Many of us want a way for people to be able to contact us through our website. As a woman in tech, I don't necessarily want to share my email address so openly. In the old days, we would need a server or a third-party widget to handle contact forms.</p>
<p>Today, Netlify includes form handling for up to 100 submissions per month for free! Implementing it is mostly setting a <code>netlify</code> attribute on the form then setting up notifications in Netlify:</p>
<pre class="language-html"><code class="language-html"><span class="highlight-line"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>form</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>contact<span class="token punctuation">"</span></span> <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>POST<span class="token punctuation">"</span></span> <span class="token attr-name">netlify</span><span class="token punctuation">></span></span></span></code></pre>
<p>Netlify automatically filters out most spam, and you can add a honeypot field to help filter out bots. Check out the <a href="https://docs.netlify.com/forms/setup/">docs</a> for more info.</p>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/build-that-blog-already/#conclusion">#</a></h2>
<p>Your blog isn't the goddess Athena. It's not going to <a href="https://www.greekmythology.com/Myths/The_Myths/Birth_of_Athena/birth_of_athena.html">pop fully formed and all grown up out of your head</a>. You aren't Zeus.</p>
<p>Stop procrastinating, and build your minimally-viable-blog today. Yes, today! Deploy it! Yes, right now! Before you close that tab!</p>
Building a Serverless E-commerce Store with Stripe, Netlify, & 11ty2022-05-16T00:00:00Zhttps://sia.codes/posts/serverless-ecommerce-store/<figure>
<img src="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_680/v1607719366/sia.codes/ecommerce_v5tosj.jpg" srcset="https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_300/v1607719366/sia.codes/ecommerce_v5tosj.jpg 300w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_600/v1607719366/sia.codes/ecommerce_v5tosj.jpg 600w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_928/v1607719366/sia.codes/ecommerce_v5tosj.jpg 928w, https://res.cloudinary.com/siacodes/image/upload/q_auto,f_auto,w_1856/v1607719366/sia.codes/ecommerce_v5tosj.jpg 1856w" sizes="(min-width: 980px) 928px, calc(95.15vw + 15px)" alt="Livestream promo image with headshots of Sia and Matt Link and the title of the livestream 'Building a Jamstack store with Netlify and Stripe'" importance="high" width="1280" height="720" />
<!-- <figcaption></figcaption> -->
</figure>
<p>A lot of e-commerce solutions exist, but many of them charge a monthly fee. Creators may or may not be able to afford those fees depending on their sales volume. How could we build a site with the lowest hosting costs possible?</p>
<p><a href="https://twitter.com/mattling_dev">Matthew Ling</a> from Stripe and I brainstormed how to do this and then built it in a series of livestreams. This article is a collection of all the resources we created:</p>
<ul>
<li>Architecting the ecommerce site (the meetup talk and slides)</li>
<li>Building the website (the livestream videos)</li>
<li>Concepts (the companion articles)</li>
</ul>
<p>We built this live on <a href="https://sia.studio/">sia.studio</a>, the beginning of my creative website. This series shows how we added the shop with products to sell as digital downloads. The files are emailed to the customer upon successful payment.</p>
<p>This first step is understanding how all the pieces fit together...</p>
<h2 id="architecting-a-serverless-e-commerce-site">Architecting a serverless e-commerce site <a class="direct-link" href="https://sia.codes/posts/serverless-ecommerce-store/#architecting-a-serverless-e-commerce-site">#</a></h2>
<p>Sometimes the biggest challenge is coming up with an architecture that will work. In my talk for <a href="https://11tymeetup.dev/events/ep-6-async-shortcodes-and-serverless-e-commerce/">THE Eleventy Meetup</a>, I showed how I weaved in all the moving parts to make it work, using:</p>
<ul>
<li><strong>11ty</strong> - static site generator</li>
<li><strong>Github</strong> - origin repo</li>
<li><strong>Netlify</strong> - static site hosting and serverless functions (and more!)</li>
<li><strong>Stripe</strong> - products, checkout, email payment receipts (and more!)</li>
<li><strong>AWS</strong> - S3 signed URLs for unique expiring download links</li>
<li><strong>Sendgrid</strong> - email download links</li>
</ul>
<p>Here are the <a href="https://docs.google.com/presentation/d/13ICudJnSRmUrBwlo6CcGevPQgOfnsfPGLa0OXl9_ixI/edit?usp=sharing">slides</a> from the meetup, and the recording is below:</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="DWrWWhDNi4w" style="background-image: url('https://i.ytimg.com/vi/DWrWWhDNi4w/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Architecting an Eleventy serverless e-commerce site</span>
</button>
</lite-youtube>
<h2 id="creating-and-rendering-a-product-catalog">Creating and rendering a product catalog <a class="direct-link" href="https://sia.codes/posts/serverless-ecommerce-store/#creating-and-rendering-a-product-catalog">#</a></h2>
<p>In our first livestream, we demonstrated how to model a product catalog in Stripe using Products and Prices and how to query and render them using Stripe-node and 11ty:</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="N7qSVXyHlCA" style="background-image: url('https://i.ytimg.com/vi/N7qSVXyHlCA/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Ep 1: Building a Jamstack store with Netlify and Stripe</span>
</button>
</lite-youtube>
<p>The source code for sia.studio is all public, and you can see both the <a href="https://github.com/siakaramalegos/sia.studio">current live version</a> and the <a href="https://github.com/siakaramalegos/sia.studio/tree/livestream">livestream branch</a> for posterity.</p>
<p>Matt wrote a companion article which you can find here: <a href="https://dev.to/stripe/building-an-ecommerce-store-13-managing-products-prices-with-examples-in-ruby-iba?signin=true">Building an eCommerce Store 1/3: Managing Products & Prices (with examples in Ruby)</a>.</p>
<aside>
Are you finding this post helpful? If so,
<a href="https://sia.codes/posts/serverless-ecommerce-store/#inform">sign up for my newsletter</a> to be notified of new posts!
</aside>
<h2 id="using-serverless-functions-for-checkout">Using serverless functions for checkout <a class="direct-link" href="https://sia.codes/posts/serverless-ecommerce-store/#using-serverless-functions-for-checkout">#</a></h2>
<p>In the second episide, we leveraged Netlify serverless functions to create Checkout Sessions using the Stripe API and collect payments from our customers.</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="rxSm3THX748" style="background-image: url('https://i.ytimg.com/vi/rxSm3THX748/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Ep 2: Building a Jamstack store with Netlify and Stripe</span>
</button>
</lite-youtube>
<p>Matt's companion article is <a href="https://dev.to/stripe/building-an-ecommerce-store-23-checkout-flows-35k7">Building an eCommerce Store 2/3: Checkout flows</a>.</p>
<h2 id="stripe-webhooks-for-order-fulfillment">Stripe webhooks for order fulfillment <a class="direct-link" href="https://sia.codes/posts/serverless-ecommerce-store/#stripe-webhooks-for-order-fulfillment">#</a></h2>
<p>In the last episode, we used Netlify serverless function to build a webhook endpoint to listen and react to payment events such as a Checkout Session completing to fulfill our orders. We used AWS to create a "signed URL" for an expiring download link. Then, we email that link to the customer using Sendgrid.</p>
<script src="https://sia.codes/javascript/lite-yt-embed.js"></script>
<lite-youtube videoid="9g8UTYR5dH0" style="background-image: url('https://i.ytimg.com/vi/9g8UTYR5dH0/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">Play Video: Ep 3: Building a Jamstack store with Netlify and Stripe</span>
</button>
</lite-youtube>
<p>Matt's final article goes in depth on Stripe's webhook endpoints: <a href="https://dev.to/stripe/building-an-ecommerce-store-33-webhook-endpoints-and-fulfillment-260j">Building an eCommerce Store 3/3: Webhook endpoints and fulfillment</a></p>
<h2 id="conclusion-and-next-steps">Conclusion and next steps <a class="direct-link" href="https://sia.codes/posts/serverless-ecommerce-store/#conclusion-and-next-steps">#</a></h2>
<p>This was a fun build. In the future, I'd love to build out a full shopping cart functionality that also allows for multiple copies of the same item. I'd also like to explore a way to offer physical goods that doesn't rely on large inventory since Stripe does not support drop shipping.</p>
<p>What would you like to see next? We're considering adding on to this series and would love to hear your thoughts!</p>
How I converted speaking content to written with AI2024-02-08T00:00:00Zhttps://sia.codes/posts/converting-transcripts-to-blogs-with-ai/<p>If you've ever approached a blank page with an idea for a long blog post, you'll understand me when I say that it can be daunting. I can't count how many times I've though to myself "maybe I should go get a snack" while staring at that blank page. We'll do anything to procrastinate that step.</p>
<p>This time, I decided to try something new. I recently published a massive blog post about the <a href="https://performance.shopify.com/blogs/blog/debugging-common-causes-for-slow-loading-in-shopify-liquid-storefronts">common causes of poor performance for Shopify sites</a>. I used it as a learning experience for using ChatGPT to make that first, painful blank page step "easier" ("easy" being in the eyes of the beholder).</p>
<p>This post is more of a story about that learning experience. It doesn't include a lot of technical how-to steps but more the thinking and concepts around the approach. I hope it is helpful or at a minimum slightly entertaining.</p>
<h2 id="how-i-personally-feel-about-ai">How I personally feel about AI <a class="direct-link" href="https://sia.codes/posts/converting-transcripts-to-blogs-with-ai/#how-i-personally-feel-about-ai">#</a></h2>
<p>Before we get started, let me state that I'm currently not a fan of LLM and image generation AI tools. For one, I think the creators whose content they were trained off of should have been asked permission first. Secondly, it's created a massive wave of bad content and sometimes misinformation. If you haven't noticed yet, internet search results have gotten pretty shitty, first with SEO farmed content and now with the AI tools to do this at a scale never seen before.</p>
<p>All that being said, I can't just stick my head in the sand and ignore what is going on. For one, my company's leadership is a huge proponent of AI, and I still need an income. For another, I need to better understand how it works and where it works better/worse so that I can engage in the community with more informed views. AI is not going away. And, I do believe it is possible to make it a better tool in the future.</p>
<h2 id="the-human-intelligence-(hi-not-ai)-learning-process">The human intelligence (HI not AI) learning process <a class="direct-link" href="https://sia.codes/posts/converting-transcripts-to-blogs-with-ai/#the-human-intelligence-(hi-not-ai)-learning-process">#</a></h2>
<p>This all began begrudgingly in an internal, very informal training for ChatGPT. It started out with learning the basics of prompts. I'm not going to lie, these first prompts gave me some fun output:</p>
<ol>
<li>Tell me about lego</li>
<li>How do I build a LEGO city?</li>
<li>Can you recommend a good first building for my lego city? I like reading, movies, and dogs.</li>
</ol>
<p>However, my <a href="https://www.instagram.com/siabuildsbricks/">LEGO city hobby</a> was not going to get me very far in a practical use case of ChatGPT.</p>
<p>For the second part, we saw examples of how other people at my company have been using ChatGPT. Some of those examples were intriguing but most were not relevant to my own work activities. I'm a web performance expert so I help merchants make their websites faster. Part of this is creating content to help teach them to do it.</p>
<p>When it came to applying my learning, I first thought about getting it to help write a web perf blog post. That was... terrible. The training data on technical concepts, especially a niche area of web dev, is very poor and the outputs essentially sound like the worst SEO farmed version of a web performance blog regurgitating the same dated strategies that aren't usually the primary culprits for bad performance.</p>
<p>Remember when I said that some of the examples from fellow employees were intriguing? I noticed a pattern of more people using it to process meeting transcripts or folders worth of text content into summarized points. So, not asking it to create "original" content but to process existing content into another format. This makes sense when you think about the nature of LLMs. They were trained on what word should come after another word or patterns of how we put words together.</p>
<aside>While asking GPT to summarize existing content had fewer errors or hallucinations, still be on your guard. My last employee review included a GPT summary of peer employee feedback which miscategorized one of my strengths as one of my weaknesses. In other words, the sentiment analysis was incorrect. Luckily, I have a manager who read through the original content and caught this error.</aside>
<p>Thus, I finally had an idea with promise... I recorded a web performance workshop session and wanted to convert part of it into a blog post. First, in true developer style, I had to <a href="https://en.wiktionary.org/wiki/bikeshedding">bike shed</a>/<a href="https://en.wiktionary.org/wiki/yak_shaving">yak shave</a> a little and create a <a href="https://projects.sia.codes/gong-transcript/">bookmarklet to copy the meeting transcript from Gong</a>, but that's a story for another day.</p>
<h2 id="creating-the-article-draft%3A-trial-and-error">Creating the article draft: trial and error <a class="direct-link" href="https://sia.codes/posts/converting-transcripts-to-blogs-with-ai/#creating-the-article-draft%3A-trial-and-error">#</a></h2>
<p>You may be wondering, "Sia, why not just use the transcript directly?" May I present to you, Exhibit A:</p>
<blockquote>
<p>Sia: You start interacting so you can.<br />
Sia: Wait until after interaction, I make it feels a little bit like a hack but also it's a.<br />
Sia: When all these things pop up like I came to your site to see what you are serving. And now you're telling me to spin a wheel for.<br />
Sia: I haven't even yet.<br />
Sia: Looked at your product.<br />
Sia: Like I feel like they're strategic decisions that should.<br />
Sia: Change anyways, but you can also argue for performance reasons.<br />
Sia: And…<br />
Sia: Delay them at least until.</p>
</blockquote>
<p>That is a hot mess.</p>
<p>So how did I convert this hot mess into a reasonable first draft? First, I tried to get it to write the article for me with one prompt:</p>
<blockquote>
<p>Can you rewrite this transcript as a blog article?</p>
</blockquote>
<p>My first obstacle was that I found myself in an endless do loop where GPT asked me for the transcript, I gave it the transcript, then it asked me what I wanted to do with it starting the cycle again. I was using GPT-4 which apparently couldn't parse the quantity of data I gave it - about 30-40 minutes of lecture-style speaking.</p>
<p>When I switched to GPT-4-32K, it was able to handle the volume of "data" from the transcript. Then I hit my second obstacle. The intro paragraph was dumb GPT speak, I got a few paragraphs of decent summary of the beginning of my talk, then it went way off script and started giving me GPT-style "original" content for web perf optimization. Not what I wanted.</p>
<p>Then I asked GPT, "What did I say about x?" for each of the sub-topics I wanted more detail on and it did pretty good at that! It kept cutting off the output mid-sentence, and later I learned that I should tell it to "continue". 🤦🏻♀️ Regardless, I finally had something workable.</p>
<h2 id="conclusion">Conclusion <a class="direct-link" href="https://sia.codes/posts/converting-transcripts-to-blogs-with-ai/#conclusion">#</a></h2>
<p>Finally, I had a decent first draft of a conversion from my transcript to an article. Did it save time? Probably not due to all the experimentation. However, I might use it in the future when I have another transcript I want to convert to written content. Just to get through that initial mental block of an empty page.</p>