<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>thirty-four</title>
        <link>https://jeffy.info</link>
        <description>thirty-four is crafted with love in Brooklyn, by Jeff Posnick.</description>
        <lastBuildDate>Thu, 18 Sep 2025 01:06:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>thirty-four</title>
            <url>https://jeffy.info/static/34.png</url>
            <link>https://jeffy.info</link>
        </image>
        <copyright>Creative Commons Attribution 4.0 International License</copyright>
        <item>
            <title><![CDATA[Which Mastodon servers are the accounts you follow on?]]></title>
            <link>/2022/11/19/following-mastodon-servers.html</link>
            <guid>/2022/11/19/following-mastodon-servers.html</guid>
            <pubDate>Sat, 19 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Maybe you want to switch to a server your friends are on!]]></description>
            <content:encoded><![CDATA[<p>Thinking about
<a href="https://blog.joinmastodon.org/2019/06/how-to-migrate-from-one-server-to-another/">switching Mastodon servers</a>?
Maybe you'd like to join a server that's popular among the accounts you
currently follow?</p>
<p>Enter an account below (your own, or someone else's),
and get a sorted list of servers of the (first ~1000) accounts that it follows.</p>
<base target="_blank">
<form id="lookup">
	<label for="account">Mastodon Account:</label>
	<input type="text" id="account" placeholder="user@server" />
	<input type="submit" value="Lookup" />
</form>
<p id="status"></p>
<table id="results"></table>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[How do you test a service worker, anyway?]]></title>
            <link>/2022/08/25/testing-a-service-worker.html</link>
            <guid>/2022/08/25/testing-a-service-worker.html</guid>
            <pubDate>Thu, 25 Aug 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Opinions about how and what to test in your progressive web app.]]></description>
            <content:encoded><![CDATA[<h2 id="wait%2C-should-i-be-testing-my-service-worker%3F" tabindex="-1"><a class="header-anchor" href="#wait%2C-should-i-be-testing-my-service-worker%3F">Wait, should I be testing my service worker?</a></h2>
<p>If you're building a <a href="https://web.dev/progressive-web-apps/">progressive web app</a>, and you're not exercising your <a href="https://developer.chrome.com/docs/workbox/service-worker-overview/">service worker</a>'s behavior, you might have a gap in your overall test coverage. How high this ranks on the near-infinite list of things you <em>could</em> be testing depends on a number of things, including how important your service worker's behavior is to your core user experience.</p>
<p>If you're using <a href="https://developer.chrome.com/docs/workbox/">Workbox</a>, with mostly out-of-the-box default configuration options for your service worker's implementation, you might feel comfortable without a dedicated test suite, with the understanding that Workbox itself has an extensive <a href="https://github.com/GoogleChrome/workbox/tree/v6/test">set of tests</a> for common usage patterns.</p>
<p>But if you're writing a service worker from scratch, if you're using one of Workbox's more exotic plugins or custom strategies, or if your service worker's behavior is particularly important to your overall user experience, developing a test suite dedicated to your service worker can give you an important peace of mind.</p>
<h3 id="what-kind-of-service-workers-are-we-talking-about%3F" tabindex="-1"><a class="header-anchor" href="#what-kind-of-service-workers-are-we-talking-about%3F">What kind of service workers are we talking about?</a></h3>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">service worker runtime environment</a> has undergone an explosion of usage recently, with adaptations of the original environment spring up on the <a href="https://workers.js.org/">server</a>, or in <a href="https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/">Manifest v3 extensions</a>. This blog post discusses on tests that are important for &quot;traditional&quot; service-worker-in-the-browser use cases, focusing on caching and offline use cases.</p>
<h2 id="what-should-i-use-to-run-my-tests%3F" tabindex="-1"><a class="header-anchor" href="#what-should-i-use-to-run-my-tests%3F">What should I use to run my tests?</a></h2>
<p>I recommend writing your tests against Microsoft's <a href="https://playwright.dev/">Playwright</a> environment, though if you have an existing framework set up for integration/end-to-end testing, you can probably get away with using that instead.</p>
<p>I've found Playwright to be the best supported, most modern choice for running tests directly in &quot;real&quot; browser runtimes, across Chrome, Safari, and Firefox. While cross-browser compatibility for the service worker runtime is generally much better than it was in the past, if you're taking the time to test your service worker, you might as well ensure that there aren't edge cases due to, e.g., bugs in IndexedDB or reliance on service worker features that aren't yet in all three browser engines.</p>
<p>Some service worker tests might rely on, say, triggering a series of navigations and then checking cache state, and in my experience, Playwright has done a good job of orchestrating that type of behavior in a consistent fashion across browsers. This means fewer flaky tests, or tests that need to be skipped in specific browsers.</p>
<p>Throughout this post, I'll include inline examples that assume usage of Playwright.</p>
<h2 id="waiting-for-the-right-moment" tabindex="-1"><a class="header-anchor" href="#waiting-for-the-right-moment">Waiting for the right moment</a></h2>
<p>A common pain point, leading to flakiness, retries, and awkward hardcoded calls to <code>setTimeout()</code>, is due to the asynchronous nature of most things having to do with service workers and the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage">cache storage API</a>. Throughout your test suite, you'll find yourself needing to delay execution of the next bit of code until some previous async operation has completed.</p>
<p>Here are a few scenarios where timing comes into play, along with tips on how to wait for the right moment.</p>
<h3 id="testing-state-after-installation" tabindex="-1"><a class="header-anchor" href="#testing-state-after-installation">Testing state after installation</a></h3>
<p>A service worker might precache a set of URLs during <a href="https://web.dev/service-worker-lifecycle/#install">installation</a>, or add data to IndexedDB. To confirm that a given page registers a service worker which in turns performs those installation activities, it's important to delay examining the state until installation is complete. The simplest way to do this is to wait until the <a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/ready"><code>navigator.serviceWorker.ready</code> promise</a> resolves, at which point you're guaranteed that there is a service worker that's completed installation (and activation, though testing post-activation state is less common).</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> {test, expect} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@playwright/test&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;post-install state&#x27;</span>, <span class="hljs-keyword">async</span> ({baseURL, page}) =&gt; {
	<span class="hljs-comment">// Navigate to a page which registers a service worker.</span>
	<span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;/&#x27;</span>);

	<span class="hljs-comment">// await the navigator.serviceWorker.ready promise.</span>
	<span class="hljs-keyword">const</span> swURL = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-keyword">const</span> registration = <span class="hljs-keyword">await</span> navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-property">ready</span>;
		<span class="hljs-keyword">return</span> registration.<span class="hljs-property">active</span>?.<span class="hljs-property">scriptURL</span>;
	});
	<span class="hljs-comment">// Confirm the expected service worker script installed.</span>
	<span class="hljs-title function_">expect</span>(swURL).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">`<span class="hljs-subst">${baseURL}</span>sw.js`</span>);

	<span class="hljs-comment">// Now you&#x27;re ready to check cache or IndexedDB state.</span>
});
</code></pre>
<h3 id="testing-network-request-interception" tabindex="-1"><a class="header-anchor" href="#testing-network-request-interception">Testing network request interception</a></h3>
<p>If you're interested in testing <a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch_event"><code>fetch</code> handler</a> behavior, i.e. how your service worker responds to network requests from a client page, waiting on <code>navigator.serviceWorker.ready</code> is not a good idea. That promise resolves as soon as there's an active service worker, but even if you're calling <a href="https://web.dev/service-worker-lifecycle/#clientsclaim"><code>clients.claim()</code></a> within your service worker's <a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/activate_event"><code>activate</code> handler</a>, there's a small gap of time before an active service worker takes control of existing clients. If you don't account for this, you may end up triggering a network request that isn't intercepted by your service worker, leading to tests that don't exercise the code you expect.</p>
<p>While there's <a href="https://github.com/w3c/ServiceWorker/issues/799">no built-in equivalent</a> to <code>navigator.serviceWorker.ready</code> that resolves when the current page is controlled by a service worker, you can explicitly <a href="https://github.com/w3c/ServiceWorker/issues/799#issuecomment-165499718">create a promise</a> that accomplishes the same thing.</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> {test, expect} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@playwright/test&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;fetch handler behavior&#x27;</span>, <span class="hljs-keyword">async</span> ({page}) =&gt; {
	<span class="hljs-comment">// Navigate to a page which registers a service worker.</span>
	<span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;/&#x27;</span>);

	<span class="hljs-comment">// await a promise that resolves when the page is controlled.</span>
	<span class="hljs-comment">// Ensure you include clients.claim() in your activate handler!</span>
	<span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
			<span class="hljs-keyword">if</span> (navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-property">controller</span>) {
				<span class="hljs-comment">// If we&#x27;re already controlled, resolve immediately.</span>
				<span class="hljs-title function_">resolve</span>();
			} <span class="hljs-keyword">else</span> {
				<span class="hljs-comment">// Otherwise, resolve after controllerchange fires.</span>
				navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;controllerchange&#x27;</span>, <span class="hljs-function">() =&gt;</span>
					<span class="hljs-title function_">resolve</span>(),
				);
			}
		});
	});

	<span class="hljs-comment">// Now calls to page.evaluate() which make network requests will</span>
	<span class="hljs-comment">// be intercepted by the service worker&#x27;s fetch handler.</span>
});
</code></pre>
<h3 id="testing-logic-outside-of-respondwith()" tabindex="-1"><a class="header-anchor" href="#testing-logic-outside-of-respondwith()">Testing logic outside of respondWith()</a></h3>
<p>Every <code>fetch</code> handler needs to include <a href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith"><code>fetchEvent.respondWith()</code></a>. Once this method is called, response headers and a promise for the response body is made available to the controlled client that triggered the <code>fetch</code> event.</p>
<p>However, your <code>fetch</code> handler might perform other activities independent of the call to <code>fetchEvent.respondWith()</code>—for instance, cleaning up outdated cache entries after caching a new response. (If these activities are asynchronous, they would normally be wrapped in a call to <a href="https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil"><code>fetchEvent.waitUntil()</code></a> to ensure that the service worker is kept alive until they complete.)</p>
<p>Most developers won't have logic that executes in a <code>fetch</code> handler outside the <code>fetchEvent.respondWith()</code> that they need to test. But if you do find yourself needing to do so, be careful not to <em>only</em> wait on the client's <code>fetch()</code> that triggered the behavior to resolve. A client's <code>fetch()</code> will resolve immediately after the controlling service worker's <code>fetch</code> handler calls <code>fetchEvent.respondWith()</code>, and that might happen before the other asynchronous work is completed.</p>
<p>So, what do you do if you want to delay your test execution until after additional code has run? One solution is to have a client wait for a <code>message</code> event, triggered by the service worker calling <code>postMessage()</code> upon completion of the additional work. (Since this <code>postMessage()</code> is only useful for test orchestration, you might want to wrap it in an <code>if()</code> statement that checks for a specific <a href="https://playwright.dev/docs/test-parameterize#env-files"><code>process.env</code> value</a>, and is minimized away if that value isn't set.)</p>
<pre><code class="hljs language-js"><span class="hljs-comment">// Example service worker code:</span>
self.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;fetch&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-comment">// When this executes, the client&#x27;s fetch() promise resolves.</span>
  e.<span class="hljs-title function_">respondWith</span>(...);

  <span class="hljs-keyword">const</span> <span class="hljs-title function_">additionalWork</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params"></span>) =&gt; {
    <span class="hljs-comment">// Perform cache cleanup, etc.</span>
    <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">await</span> clients.<span class="hljs-title function_">get</span>(e.<span class="hljs-property">clientId</span>);
    <span class="hljs-comment">// Send a message back to the client to signal completion.</span>
    client.<span class="hljs-title function_">postMessage</span>({...});
  };
  e.<span class="hljs-title function_">waitUntil</span>(<span class="hljs-title function_">additionalWork</span>());
});
</code></pre>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> {test, expect} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@playwright/test&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;extra fetch handler behavior&#x27;</span>, <span class="hljs-keyword">async</span> ({page}) =&gt; {
	<span class="hljs-comment">// Navigate to a page which registers a service worker.</span>
	<span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;/&#x27;</span>);

	<span class="hljs-comment">// await a promise that resolves when the page is controlled.</span>
	<span class="hljs-comment">// Ensure you include clients.claim() in your activate handler!</span>
	<span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-keyword">const</span> [message, response] = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([
			<span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
				navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-title function_">addEventListener</span>(
					<span class="hljs-string">&#x27;message&#x27;</span>,
					<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> <span class="hljs-title function_">resolve</span>(e.<span class="hljs-property">data</span>),
					{<span class="hljs-attr">once</span>: <span class="hljs-literal">true</span>},
				);
			}),
			<span class="hljs-title function_">fetch</span>(<span class="hljs-string">&#x27;/url/to/test&#x27;</span>),
		]);
		<span class="hljs-comment">// Optionally do something to serialize response or message</span>
		<span class="hljs-comment">// and return it from the page.evaluate().</span>
	});

	<span class="hljs-comment">// Now the service worker&#x27;s fetch handler has finished everything.</span>
});
</code></pre>
<h2 id="cache-expectations" tabindex="-1"><a class="header-anchor" href="#cache-expectations">Cache expectations</a></h2>
<p>In addition to the async-related nuances we just talked about, another stumbling block when writing service worker tests is how to ensure that a client's cache ends up in the expected state.</p>
<h3 id="getting-the-current-global-cache-state" tabindex="-1"><a class="header-anchor" href="#getting-the-current-global-cache-state">Getting the current global cache state</a></h3>
<p>There's no <a href="https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage">cache storage API</a> method that will return the full state of all caches in a single go. Additionally, the code that interrogates the cache will need to run inside of Playwright's <a href="https://playwright.dev/docs/evaluating"><code>page.evaluate()</code> method</a>, so only <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description">serializable</a> values can be returned to the <code>test()</code> execution context—<a href="https://developer.mozilla.org/en-US/docs/Web/API/Request"><code>Request</code> objects</a> are used as cache storage keys, and they are not serializable. This means that getting the current cache state to use within an <code>expect()</code> assertion is a multi-step process.</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> {test, expect} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@playwright/test&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;service worker install caching&#x27;</span>, <span class="hljs-keyword">async</span> ({baseURL, page}) =&gt; {
	<span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;/&#x27;</span>);

	<span class="hljs-comment">// Wait until the service worker has finished installing.</span>
	<span class="hljs-keyword">const</span> swURL = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-keyword">const</span> registration = <span class="hljs-keyword">await</span> navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-property">ready</span>;
		<span class="hljs-keyword">return</span> registration.<span class="hljs-property">active</span>?.<span class="hljs-property">scriptURL</span>;
	});
	<span class="hljs-title function_">expect</span>(swURL).<span class="hljs-title function_">toBe</span>(<span class="hljs-string">`<span class="hljs-subst">${baseURL}</span>sw.js`</span>);

	<span class="hljs-keyword">const</span> cacheContents = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-keyword">const</span> cacheState = {};
		<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> cacheName <span class="hljs-keyword">of</span> <span class="hljs-keyword">await</span> caches.<span class="hljs-title function_">keys</span>()) {
			<span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">await</span> caches.<span class="hljs-title function_">open</span>(cacheName);
			<span class="hljs-keyword">const</span> reqs = <span class="hljs-keyword">await</span> cache.<span class="hljs-title function_">keys</span>();
			<span class="hljs-comment">// Use the req.url string value, not an unserializable Request.</span>
			<span class="hljs-comment">// sort() allows the array to be used for stable comparisons.</span>
			cacheState[cacheName] = reqs.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">req</span>) =&gt;</span> req.<span class="hljs-property">url</span>).<span class="hljs-title function_">sort</span>();
		}
		<span class="hljs-keyword">return</span> cacheState;
	});

	<span class="hljs-comment">// cacheContents now contains a mapping of cache names to an</span>
	<span class="hljs-comment">// sorted array of URL strings contained in the cache.</span>
	<span class="hljs-title function_">expect</span>(cacheContents).<span class="hljs-title function_">toEqual</span>({
		<span class="hljs-string">&#x27;precache-v1&#x27;</span>: [
			<span class="hljs-string">`<span class="hljs-subst">${baseURL}</span>assets/app.js`</span>,
			<span class="hljs-string">`<span class="hljs-subst">${baseURL}</span>assets/index.css`</span>,
			<span class="hljs-comment">// etc.</span>
		],
	});
});
</code></pre>
<h3 id="comparisons-of-hashed-urls" tabindex="-1"><a class="header-anchor" href="#comparisons-of-hashed-urls">Comparisons of hashed URLs</a></h3>
<p>Checking cache state using strict equality assertions can be a problem if your web app caches <a href="https://bundlers.tooling.report/hashing/">hashed URLs</a>. While it's possible to update your test expectations every time any of your assets' contents change, you risk noise from failing tests if you ever forget.</p>
<p>A more flexible approach is to replace the hash portion of your URLs with a stable placeholder, and then compare those normalized URLs against strings that also use the same placeholder. Since I couldn't find an existing module that would accomplish this, and since I already had a <a href="%E2%80%8B%E2%80%8B/smart-caching-hashes.html">different use case</a> for the same functionality, I published the <a href="https://github.com/jeffposnick/remove-filename-hash"><code>remove-filename-hash</code> module</a> to package up the reusable logic.</p>
<p><code>remove-filename-hash</code> is flexible enough to deal with hashes using any character set, of any length, found anywhere in a URL or filename string. It works in any runtime environment that supports RegExp match indices.</p>
<p>Since a sample that illustrates the full cache comparison test is fairly long, you can take a look at a <a href="https://github.com/jeffposnick/yt-playlist-notifier/blob/396624c7f3471ad2bf713b87579be60572ba0c64/tests/sw.spec.ts">representative Playwright test on GitHub</a> for inspiration.</p>
<h2 id="service-worker-update-flow" tabindex="-1"><a class="header-anchor" href="#service-worker-update-flow">Service worker update flow</a></h2>
<p>Adding logic to your progressive web app to reacts to <a href="https://web.dev/service-worker-lifecycle/#updates">service worker updates</a> can be a key part of a good user experience. However, triggering this logic in a test suite can be a challenge.</p>
<p>If you are running a custom HTTP server as part of your test suite, you can write runtime logic in that server that will respond with a slightly different version of your service worker JavaScript file each time it's requested—perhaps by appending a commented-out incremented counter to the end of the response. Any byte-for-byte difference between the current service worker file and the latest network response is sufficient to trigger the service worker update.</p>
<p>But if you're just using a static HTTP server, or if you'd prefer not to write custom server-side logic, your best bet for triggering a service worker update is to register two different service worker URLs sequentially.</p>
<p><em>Note: It's normally a <a href="https://web.dev/service-worker-lifecycle/#avoid-url-change">best-practice</a> to keep your service worker URL unchanged, but explicitly triggering an update is an exception to that.</em></p>
<h3 id="simulating-an-update" tabindex="-1"><a class="header-anchor" href="#simulating-an-update">Simulating an update</a></h3>
<p>Here's an example of triggering a service worker update inside of a Playwright test; assume that the web page being tested already registers the service worker URL <code>/sw.js</code>, and that you have an updated service worker (potentially with different logic, if you'd like) available at <code>/sw-updated.js</code>.</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> {test, expect} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@playwright/test&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;fetch handler behavior&#x27;</span>, <span class="hljs-keyword">async</span> ({page}) =&gt; {
	<span class="hljs-comment">// Navigate to a page which registers /sw.js</span>
	<span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;/&#x27;</span>);

	<span class="hljs-comment">// Ensure you include clients.claim() in your activate handler!</span>
	<span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-keyword">async</span> () =&gt; {
		<span class="hljs-comment">// Wait until the initial /sw.js controls the page.</span>
		<span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
			<span class="hljs-keyword">if</span> (navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-property">controller</span>) {
				<span class="hljs-title function_">resolve</span>();
			} <span class="hljs-keyword">else</span> {
				navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-title function_">addEventListener</span>(
					<span class="hljs-string">&#x27;controllerchange&#x27;</span>,
					<span class="hljs-function">() =&gt;</span> <span class="hljs-title function_">resolve</span>(),
					{<span class="hljs-attr">once</span>: <span class="hljs-literal">true</span>},
				);
			}
		});

		<span class="hljs-comment">// At this point, you can set up listeners for whatever events/state</span>
		<span class="hljs-comment">// changes you care about waiting for, potentially creating new promises</span>
		<span class="hljs-comment">// (as with the above example) to confirm that they take place.</span>
		navigator.<span class="hljs-property">serviceWorker</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;/sw-updated.js&#x27;</span>);
		<span class="hljs-comment">// After calling register() with the updated URL, await the promises</span>
		<span class="hljs-comment">// you previously created to ensure that the changes happened.</span>
	});

	<span class="hljs-comment">// Any other test logic goes here.</span>
});
</code></pre>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Smarter runtime caching of hashed assets]]></title>
            <link>/2021/10/10/smart-caching-hashes.html</link>
            <guid>/2021/10/10/smart-caching-hashes.html</guid>
            <pubDate>Sun, 10 Oct 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Clean caches and graceful fallbacks via a custom Workbox plugin.]]></description>
            <content:encoded><![CDATA[<h2 id="what-problem-are-we-trying-to-solve%3F" tabindex="-1"><a class="header-anchor" href="#what-problem-are-we-trying-to-solve%3F">What problem are we trying to solve?</a></h2>
<p>This is a follow-up to my recent <a href="https://jeffy.info/2021/07/26/precaching-vs-runtime-caching.html">precaching vs. runtime caching</a> post. I'd like to examine one of the advantages of precaching, as well as two of the disadvantages of runtime caching, and see if can make runtime caching more compelling by addressing them both.</p>
<p>Excerpting an advantage of precaching from my earlier blog post:</p>
<p><em>Allows you to &quot;upgrade&quot; from one version of cached subresources to another atomically. (Useful if, for instance, your templates and JavaScript rendering logic both need to be cached in concert with each other.)</em></p>
<p>And a disadvantage of runtime caching:</p>
<p><em>Assuming your URLs do include versioning information like hashes, there is no built-in mechanism for automatically expiring previous revisions of a given resource once a new one is in the cache. (I've been playing around with solutions via a <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/revisionedAssetsPlugin.ts">Workbox plugin</a>, though.)</em></p>
<p>(That last bit about the Workbox plugin is a spoiler for the rest of this post.)</p>
<p>So how can we use runtime caching and still get a reasonable best-effort coordination of cached resources without atomic cache upgrades? And how can we avoid cluttering up our runtime cache storage with dozens of versioned copies of the same logical assets, each with a different hash in their URL?</p>
<h2 id="naming-conventions" tabindex="-1"><a class="header-anchor" href="#naming-conventions">Naming conventions</a></h2>
<p>All of the techniques in this blog post assume that you've adopted a consistent convention for translating &quot;unversioned&quot; filenames, like <code>app.js</code>, into a versioned equivalent containing a hash, like <code>app.34abf34a.js</code> or <code>app.78dd13ee.js</code>. If you're using a build tool that can do versioning, like <a href="https://rollupjs.org/guide/en/#outputassetfilenames">Rollup</a> or <a href="https://webpack.js.org/configuration/output/#template-strings">webpack</a>, you're probably familiar with using the placeholder <code>[hash]</code> to specify where you want that hash inserted.</p>
<p>The exact tool you use to add these hashes doesn't matter as much as adopting a consistent naming format across all your URLs, with a fixed number of characters in the hash. We'll be writing code that will need to translate from a hashed URL to the underlying &quot;unversioned&quot; filename (i.e. <code>app.34abf34a.js</code> =&gt; <code>app.js</code>), so if you have the flexibility to do so, try to optimize your hash delimiters for ease of parsing.</p>
<p>For example, I've used the equivalent of <code>[hash:8]~[name].[ext]</code> for the versioned URLs on the current incarnation of my blog. An &quot;unversioned&quot; file named <code>page.js</code> might have a hashed filename of <code>8GlAOC2Y~page.js</code> (I'm <a href="https://github.com/jeffposnick/jeffy-info/blob/48c4db2e27721f72dcbdd86972a77a49efccd937/src/build/lib.ts#L144-L150">using <code>base64url</code> encoding</a> for the hash, instead of hex, but that's an implementation detail.) I find it easier to extract the original name from the hashed filename if the hash is always at the start, and the <code>~</code> character offers a visual hint to anyone inspecting the URLs about where the hash ends.</p>
<p>Using this approach, translating a hashed URL into the original &quot;unversioned&quot; filename is as simple as taking the final portion of the URL's path and calling <code>substring()</code>, passing in the number of initial characters that need to be skipped.</p>
<h2 id="cleaning-up-old-revisions" tabindex="-1"><a class="header-anchor" href="#cleaning-up-old-revisions">Cleaning up old revisions</a></h2>
<p>Now that we have a well-defined way to translate from a hashed URL to the corresponding original filename, there's a straightforward approach to ensuring that we don't indefinitely cache out of date versions of the same asset. The steps are:</p>
<ul>
<li>Find the &quot;unversioned&quot; filename for the URL that was just cached.</li>
<li>Iterate through all the current cache keys, and find the &quot;unversioned&quot; filename for each.</li>
<li>If the filenames match, delete the previously cached entry.</li>
</ul>
<p>We're operating on the assumption that the newly cached hashed URL is the definitive version that should be kept around, and hopefully there isn't any other cache entries that refer to the soon-to-be-deleted hashed URL. (If there are, we'll cover how to handle that in the next section.)</p>
<h2 id="dealing-with-cache-misses-due-to-versioning" tabindex="-1"><a class="header-anchor" href="#dealing-with-cache-misses-due-to-versioning">Dealing with cache misses due to versioning</a></h2>
<p>The other drawback of runtime caching to address is cache misses triggered by one resource referring to a outdated versioned URL of a subresource. You might encounter this if, for example, you cache HTML documents at runtime, and a user revisits a very old page that still refers to your CSS and JS files using hashes that have long since been purged from your servers and local caches.</p>
<p>This is a solved problem for precaching (usually! assuming <a href="https://pawll.glitch.me/">you're careful about when you call <code>skipWaiting()</code></a>!), where you can deploy compatible versions of your HTML, JS, and CSS altogether, versioned alongside your service worker.</p>
<p>So how should you deal with it when using runtime caching? The answer is a little bit of <code>¯\_(ツ)_/¯</code> sprinkled in with some best-effort fallback logic.</p>
<p>Before implementing this, you should ask yourself whether you're actually comfortable using anything other than the exact version of a subresource that a page asks for. That answer might depend on what type of subresource is being requests—using an outdated CSS file or versioned image is likely to be a lot &quot;safer&quot; than using an old JS file containing crucial business logic. You should only adopt fallback logic when you're comfortable with that risk.</p>
<p>Assuming you are comfortable with falling back to a different version, being able to translate a versioned URL into the underlying logical resource makes things straightforward:</p>
<ul>
<li>Attempt to read the requested version of a given resource from some combination of the cache or network, depending on which runtime caching strategy you're using.</li>
<li>If that fails for any reason, get the &quot;unversioned&quot; filename for the URL you're requesting.</li>
<li>Iterate through all the cache keys, getting the &quot;unversioned&quot; filename of each entry.</li>
<li>If there's a match, use that cached response to fulfill the original request.</li>
<li>Otherwise, the request can't be fulfilled.</li>
</ul>
<h2 id="packaging-this-up" tabindex="-1"><a class="header-anchor" href="#packaging-this-up">Packaging this up</a></h2>
<p>I've been using this logic for my current service worker setup, packaged in a <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/revisionedAssetsPlugin.ts">standalone Workbox plugin</a>.</p>
<p>If you've never tried writing a Workbox plugin before, we've got some <a href="https://developers.google.com/web/tools/workbox/guides/using-plugins">basic info in our docs</a>, and I recorded a video for last year's Chrome Dev Summit with more examples:</p>
<iframe class="youtube-embed" src="https://www.youtube.com/embed/jR9-aDWZeSE" allowfullscreen frameborder="0" loading="lazy"></iframe>
<p>The plugin takes advantage of the <code>cachedResponseWillBeUsed</code>, <code>cacheDidUpdate</code>, and <code>handlerDidError</code> strategy lifecycle methods to trigger all of the steps described above.</p>
<h3 id="why-not-officially-release-the-plugin%3F" tabindex="-1"><a class="header-anchor" href="#why-not-officially-release-the-plugin%3F">Why not officially release the plugin?</a></h3>
<p>While I think the code in the plugin works as intended, I'm still not happy about the ergonomics of using it. Specifically, there's a lot of logic hardcoded in it related to the naming conventions that I'm using for my hashed URLs, and that won't work for sites that do something different, like include hashes in the middle of their filenames.</p>
<p>What's needed to clean this up a bit is to allow the plugin to take in a function that will translate from versioned filenames to unversioned, allowing folks to use this without requiring them to adopt a specific naming convention. My current usage assumes you can just do <code>hashedFilename.substring(HASH_CHARS + 1)</code>, but folks might need to use a regular expression to obtain the original filename, or split on specific delimiter characters.</p>
<p><em><strong>Update</strong>: I've released a standalone module, <a href="https://github.com/jeffposnick/remove-filename-hash"><code>remove-filename-hash</code></a>, that offers flexible removal of hashes from URLs.</em></p>
<p>In the meantime, feel free to borrow the code from <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/revisionedAssetsPlugin.ts">that plugin</a> and adapt the logic by hand to accommodate your current naming conventions.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Precaching vs. runtime caching]]></title>
            <link>/2021/07/26/precaching-vs-runtime-caching.html</link>
            <guid>/2021/07/26/precaching-vs-runtime-caching.html</guid>
            <pubDate>Mon, 26 Jul 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Some pros and cons to help you make an educated choice.]]></description>
            <content:encoded><![CDATA[<h2 id="first%2C-some-background" tabindex="-1"><a class="header-anchor" href="#first%2C-some-background">First, some background</a></h2>
<p>Service workers and the Cache Storage API offer some very low-level primitives that developers are expected to build on top of. Some common recipes that work well are outlined in &quot;<a href="https://web.dev/offline-cookbook/">The Offline Cookbook</a>.&quot; Developers who want a higher-level package for those strategies often turn to the <a href="https://developers.google.com/web/tools/workbox/">Workbox libraries</a>. Generally speaking, Workbox divides up caching into two approaches.</p>
<p>There's precaching, where build tools (like <a href="https://developers.google.com/web/tools/workbox/modules/workbox-cli"><code>workbox-cli</code></a> or <a href="https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin"><code>workbox-webpack-plugin</code></a>) generate a manifest of URLs and revisions, which is then fed into the <a href="https://developers.google.com/web/tools/workbox/modules/workbox-precaching"><code>workbox-precaching</code></a> runtime module. Based on the precache manifest, <code>workbox-precaching</code> keeps your cache up to date and sets up a route to serve precached URLs using a cache-first strategy.</p>
<p>There's also runtime caching, where you use a mix of <a href="https://developers.google.com/web/tools/workbox/modules/workbox-routing"><code>workbox-routing</code></a> and <a href="https://developers.google.com/web/tools/workbox/modules/workbox-strategies"><code>workbox-strategies</code></a> (along with any <a href="https://developers.google.com/web/tools/workbox/guides/using-plugins">plugins</a>).</p>
<p>Workbox will let you go all-in on either precaching of runtime caching, if you want, but you can also use both types of caching within the same service worker. (If you do, the order in which you register routes is imports; your should include <code>precacheAndRoute()</code> before any additional <code>registerRoute()</code> statements to ensure that anything in your precache manifest ends up being served via the precache strategy!)</p>
<p>In order to help developers decide which URLs should be precached and which should be runtime cached, here's a rough rundown of pros and cons of each approach. We'll formalize this information as part of an upcoming, long-overdue revamp of the <a href="https://developers.google.com/web/tools/workbox/">Workbox documentation</a>, but I recently wrote this up for a different purpose, so I figured I'd make it public!</p>
<h2 id="precaching-pros" tabindex="-1"><a class="header-anchor" href="#precaching-pros">Precaching Pros</a></h2>
<ul>
<li>Cache-first is &quot;safe&quot; with URLs that don't contain hashes, since the revision information is generated at build time and maintained out-of-band in the precache manifest.</li>
<li>Allows you to &quot;upgrade&quot; from one version of cached subresources to another atomically. (Useful if, for instance, your templates and JavaScript rendering logic both need to be cached in concert with each other.)</li>
<li>You can easily cache resources that are needed for subsequent pages, instead of waiting for the service worker to take control and intercept the request for runtime caching. (You can work around this somewhat by <a href="https://developers.google.com/web/tools/workbox/modules/workbox-window#example-cache-urls">explicitly adding items to a runtime cache</a>.)</li>
</ul>
<h2 id="precaching-cons" tabindex="-1"><a class="header-anchor" href="#precaching-cons">Precaching Cons</a></h2>
<ul>
<li>Requires a build step.</li>
<li>Unconditionally caches everything during service worker installation. If you include URLs for infrequently used subresources in the precache manifest, or large resources like images, that can be wasteful, as you're caching things that will never be read.</li>
<li>Precaching resources that update frequently can lead to a lot of &quot;cache churn&quot;. This is fine for subresources that are commonly used, but the worst case scenario is that you've precached an infrequently <em>used</em> subresource that's <em>updated</em> frequently, compounding the wasted bytes described in the previous point.</li>
</ul>
<h2 id="runtime-caching-pros" tabindex="-1"><a class="header-anchor" href="#runtime-caching-pros">Runtime Caching Pros</a></h2>
<ul>
<li>Works well for URLs whose hash can't be determined at build time. (This is an issue for most dynamic or server-rendered content.)</li>
<li>Flexibility to use either a network-first or cache-first strategy, giving you control over the freshness vs. speed tradeoff.</li>
<li>Allows the cache lifetimes of different subresources to exist independent of each other, based on routing rules, and cache expiration plugins.</li>
</ul>
<h2 id="runtime-caching-cons" tabindex="-1"><a class="header-anchor" href="#runtime-caching-cons">Runtime Caching Cons</a></h2>
<ul>
<li>Users might end up with a broken offline experience if the page relies on resources that haven't been cached yet.</li>
<li>If a given URL doesn't include hash or versioning info, it's generally not safe to use a cache-first strategy. The best your can do is stale-while-revalidate, to ensure that the cached resources gets updated eventually.</li>
<li>There are no atomic updates of cached resources. One JavaScript subresource may have been updated recently via a stale-while-revalidate strategy, while your templates weren't, and that can lead to a mismatch or incorrect assumption about interoperability.</li>
<li>Assuming your URLs <em>do</em> include versioning information like hashes, there is no built-in mechanism for automatically expiring previous revisions of a given resource once a new one is in the cache. (I've been playing around with solutions via <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/revisionedAssetsPlugin.ts">a Workbox plugin</a>, though.)</li>
</ul>
<h2 id="takeaways" tabindex="-1"><a class="header-anchor" href="#takeaways">Takeaways</a></h2>
<p>My normal go-to is to precache as much as possible, as most of my web apps have a build process already, they normally are limited in size (say, under 1 megabyte for <em>all</em> of the app content), and I don't churn my cached resources that frequently. Whenever I do mix in runtime caching, it's normally for things that can't be versioned at build time (like live API results) or for images, which I'd rather not force everyone to download during service worker installation.</p>
<p>During my recent <a href="/2021/07/17/sw-rendering.html">blog infrastructure rewrite</a>, though, I've ended up going entirely with runtime caching, instead of precaching. I don't know if there's a technical reason for that choice as much as giving me an opportunity to experience some of the pain points that come with runtime caching, like how to expire old versions of hashed URLs.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Writing your build scripts in TypeScript]]></title>
            <link>/2021/07/25/esrun.html</link>
            <guid>/2021/07/25/esrun.html</guid>
            <pubDate>Sun, 25 Jul 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[esrun to the rescue!]]></description>
            <content:encoded><![CDATA[<p><em>Note: I've since switched to <a href="https://github.com/lukeed/tsm"><code>tsm</code></a> instead of <code>@digitak/esrun</code> in some of my projects. They both use esbuild under the hood, and either should work well.</em></p>
<h2 id="continued-blog-infra-rewrite" tabindex="-1"><a class="header-anchor" href="#continued-blog-infra-rewrite">Continued blog infra rewrite</a></h2>
<p>I had a lot of fun getting full <a href="https://jeffy.info/2021/07/17/sw-rendering.html">service worker rendering</a> working for this blog, and have continued to noodle on a number of improvements since then.</p>
<p>Some of them are bigger than others (I'm looking forward to writing a dedicated post about the <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/revisionedAssetsPlugin.ts">Workbox plugin I built</a> that improves runtime caching for hashed URLs!), but a quick one is a plug for the <a href="https://github.com/digital-loukoum/esrun"><code>@digitak/esrun</code> module</a>.</p>
<h2 id="esbuild-is-great-at-build-time" tabindex="-1"><a class="header-anchor" href="#esbuild-is-great-at-build-time">esbuild is great at build time</a></h2>
<p>I've been <em>very</em> happy using <a href="https://esbuild.github.io/"><code>esbuild</code></a> extensively in my blog's <a href="https://github.com/jeffposnick/jeffy-info/tree/cf-worker/src/build">build process</a>. It's provided (almost) zero-config TypeScript transpilation and bundling that runs <strong>so much faster</strong> that others tool with similar feature sets.</p>
<p>One thing that bothered me, though, was that my build scripts produce a number of assets that are consumed at runtime by my HTML and JavaScript, but because my build process didn't understand TypeScript types, I didn't have a lot of confidence that all the data structures matched up, and that if I refactored code in one place, I'd remember to update my generated assets.</p>
<p>Writing my build scripts in TypeScript would solve that, but adding in <em>another</em> build step to transpile my scripts before I could start my <em>real</em> build process sounded clunky. I know that <a href="https://github.com/TypeStrong/ts-node"><code>ts-node</code></a> and a few other options have been around for a while, but I haven't heard great things about their speed.</p>
<h2 id="esrun-is-great-at-runtime" tabindex="-1"><a class="header-anchor" href="#esrun-is-great-at-runtime">esrun is great at runtime</a></h2>
<p>Some more Googling led me to <code>@digitak/esrun</code>, which is a light wrapper on top of <code>esbuild</code> that will automatically transpile TypeScript source files before running them as command line scripts.</p>
<p>After adding <code>@digitak/esrun</code> to <code>devDependencies</code>, and rewriting my build scripts in TypeScript, my <code>package.json</code>'s <code>build</code> script went from</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
	<span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
		<span class="hljs-attr">&quot;build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;node src/build/main.js&quot;</span>
	<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>to</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
	<span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
		<span class="hljs-attr">&quot;build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;esrun src/build/main.ts&quot;</span>
	<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>That's all it took! I now have confidence that comes from <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/shared/types.ts">sharing types</a> between my build and runtime environments, and the increase in build time due to transpilation is completely negligible.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Service worker rendering, in the cloud and in the browser]]></title>
            <link>/2021/07/17/sw-rendering.html</link>
            <guid>/2021/07/17/sw-rendering.html</guid>
            <pubDate>Sat, 17 Jul 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Cloudflare Workers + Workbox = isomorphic rendering]]></description>
            <content:encoded><![CDATA[<p><em>You can watch me chat with Luke Edwards about this architecture <a href="https://cloudflare.tv/event/6ZJ5mEjrgcnCtBXBsUtyqV">on Cloudflare.tv</a>!</em></p>
<h2 id="tl%3Bdr" tabindex="-1"><a class="header-anchor" href="#tl%3Bdr">tl;dr</a></h2>
<p>This site is now rendered entirely on-demand via service workers!</p>
<p>The first time you visit, you'll get HTML rendered in the cloud using <a href="https://workers.cloudflare.com/">Cloudflare Workers</a>. For each subsequent page you visit, a local in-browser service worker generates equivalent HTML, taking advantage of local caching to render as much HTML as possible without blocking on the network.</p>
<p>Both service worker environments share the majority of the same <a href="https://developers.google.com/web/tools/workbox/">Workbox</a> code for routing and streaming response generation.</p>
<p>You can see everything that changed from my previous setup in <a href="https://github.com/jeffposnick/jeffy-info/compare/627e88f1ce0a583d08bd5a31638da1f60bec74d6...3af1650a414c09c4fc92e66b26e4d66622af70e6">this diff</a>.</p>
<h2 id="previously%2C-on-blog-infra" tabindex="-1"><a class="header-anchor" href="#previously%2C-on-blog-infra">Previously, on blog infra</a></h2>
<p>This site previously used a custom <a href="https://www.11ty.dev/">11ty</a> setup, with static HTML generated at build time, using <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> templates and Markdown pages. (It was migrated from a long-ago <a href="https://jekyllrb.com/">Jekyll</a> config.)</p>
<p>After your first visit, a <a href="https://developers.google.com/web/tools/workbox/">Workbox</a>-powered service worker took over, and cached all of the templates and page data. HTML for future navigations are generated by the SW, independent of the network, using the same templating logic that 11ty uses at build time.</p>
<p>The templates were cached independently from the page content, so I could, e.g., update the site header template, and that's the only thing that needs to be invalidated—already cached content could stay as-is.</p>
<p>It was served from <a href="https://firebase.google.com/docs/hosting">Firebase static hosting</a>.</p>
<p>This all worked well!</p>
<p>But, the disconnect between how 11ty and the service worker generated the HTML bothered me. They shared templates, but static HTML &quot;routing&quot; happens via file system layout, while the service worker needed its own independent routing configuration. Also, running Nunjucks inside of a service worker <em>works</em>, but it's not exactly elegant, or lightweight. And generating the site's full HTML at build time is fine for a site with a modest amount of content, but I could see it being an issue for larger sites, or for sites that rely on database or API lookups to populate their HTML.</p>
<h2 id="service-workers-everywhere" tabindex="-1"><a class="header-anchor" href="#service-workers-everywhere">Service workers everywhere</a></h2>
<p>I've been fascinated by the new crop of cloud runtime environments that expose a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">service worker</a> API (or some modified version of it). <a href="https://workers.cloudflare.com/">Cloudflare Workers</a> seems like the most mature of them, and it looks like the recently formed <a href="https://deno.com/blog/the-deno-company">Deno Company</a> is <a href="https://github.com/denoland/deno/issues/5957">working</a> on something similar. Running the service worker API in the cloud opens the door for a closer sharing routing and HTML generation logic with the browser's service worker—<a href="https://en.wikipedia.org/wiki/Isomorphic_JavaScript">isomorphic</a> rendering, if you will. (It's cool if you won't.)</p>
<p>A recent <a href="https://twitter.com/lukeed05/status/1415036180765106176">tweet</a> from <a href="https://twitter.com/lukeed05">Luke Edwards</a> about his new, lightweight <a href="https://handlebarsjs.com/">Handlebars</a>-compatible templating library, <a href="https://github.com/lukeed/tempura">Tempura</a>, reminded me that Luke was now working on the Cloudflare DevRel team, and that I have been <a href="https://twitter.com/jeffposnick/status/1379525749514850305">intending</a> to explore this space further.</p>
<p>Well, no time like this weekend!</p>
<h3 id="build-setup" tabindex="-1"><a class="header-anchor" href="#build-setup">Build setup</a></h3>
<p>Some of the work involved swapping out the 11ty build infrastructure in favor of a <a href="https://github.com/jeffposnick/jeffy-info/tree/cf-worker/src/build">bespoke build process</a>. At some point I might re-add 11ty, because I ultimately think that I just ended up recreating most of what it already does. But while prototyping this, I needed the level of control offered by generating <em>exactly</em> the build artifacts I wanted from my <a href="https://github.com/jeffposnick/jeffy-info/tree/cf-worker/site/posts">source Markdown files</a>.</p>
<p>The nice thing is that, for the most part, my existing posts didn't need to change—they remain Markdown documents with some <a href="https://jekyllrb.com/docs/front-matter/">frontmatter</a> metadata, just like 11ty (and before that, Jekyll) expected.</p>
<h3 id="shared-service-worker-code" tabindex="-1"><a class="header-anchor" href="#shared-service-worker-code">Shared service worker code</a></h3>
<p>Most of the magic takes place in the <a href="https://github.com/jeffposnick/jeffy-info/tree/cf-worker/src/service-worker/shared">shared service worker code</a>, which handles routing using a <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/URLPatternMatcher.ts">wrapper</a> on top of the upcoming <a href="https://web.dev/urlpattern/"><code>URLPattern</code> API</a>, along with some logic to handle requests for static assets like images, CSS, JS, or JSON files.</p>
<h3 id="streaming%2C-sequential-templates" tabindex="-1"><a class="header-anchor" href="#streaming%2C-sequential-templates">Streaming, sequential templates</a></h3>
<p>Each matching route triggers a sequence of <a href="https://github.com/jeffposnick/jeffy-info/tree/cf-worker/site/templates">Tempura templates</a>, with each rendered template streaming its partial HTML immediately in environments that support constructing <a href="https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams"><code>ReadableStream</code>s</a>, courtesy of the <a href="https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-streams"><code>workbox-streams</code> library</a>.</p>
<p>Here's an <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/shared/common.ts">adapted</a> snippet of code:</p>
<pre><code class="hljs language-js"><span class="hljs-title function_">registerRoute</span>(
	<span class="hljs-keyword">new</span> <span class="hljs-title class_">URLPatternMatcher</span>({<span class="hljs-attr">pathname</span>: <span class="hljs-string">&#x27;/(.*).html&#x27;</span>}).<span class="hljs-property">matcher</span>,
	<span class="hljs-title function_">streamingStrategy</span>(
		[
			<span class="hljs-function">() =&gt;</span> <span class="hljs-title class_">Templates</span>.<span class="hljs-title class_">Start</span>({site}),

			<span class="hljs-keyword">async</span> ({event, params}) =&gt; {
				<span class="hljs-keyword">const</span> post = params.<span class="hljs-property">pathname</span>.<span class="hljs-property">groups</span>[<span class="hljs-number">0</span>];
				<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">loadStatic</span>(event, <span class="hljs-string">`/static/<span class="hljs-subst">${post}</span>.json`</span>);
				<span class="hljs-keyword">if</span> (response?.<span class="hljs-property">ok</span>) {
					<span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> response.<span class="hljs-title function_">json</span>();
					<span class="hljs-keyword">return</span> <span class="hljs-title class_">Templates</span>.<span class="hljs-title class_">Page</span>({site, ...json});
				}
				<span class="hljs-keyword">return</span> <span class="hljs-title class_">Templates</span>.<span class="hljs-title class_">Error</span>({site});
			},

			<span class="hljs-function">() =&gt;</span> <span class="hljs-title class_">Templates</span>.<span class="hljs-title class_">End</span>({site}),
		],
		{<span class="hljs-string">&#x27;content-type&#x27;</span>: <span class="hljs-string">&#x27;text/html&#x27;</span>},
	),
);
</code></pre>
<p>This initial HTML response always comes from the <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/site/templates/start.hbs">first template</a> immediately, without blocking on any data retrieval, so you should see consistently fast renders of each page's header. What's displayed right away is roughly equivalent to what you'd see if you were using an <a href="https://developers.google.com/web/fundamentals/architecture/app-shell">App Shell</a>.</p>
<p>Unlike with an App Shell approach, it's easy to set up a completely different sequence of templates for different routes. You're not forced to always respond with a single, hardcoded placeholder HTML document.</p>
<h3 id="different-approaches-to-static-assets" tabindex="-1"><a class="header-anchor" href="#different-approaches-to-static-assets">Different approaches to static assets</a></h3>
<p>The some significant difference between the CloudFlare and browser service workers is how they load static assets.</p>
<p>The CloudFlare Workers runtime supports loading static assets via a <a href="https://github.com/cloudflare/kv-asset-handler"><code>kv-asset-handler</code> helper library</a>.</p>
<p>Here's an abbreviated snippet of <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/cf-sw.ts">that code</a>:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">const</span> <span class="hljs-title function_">loadStatic</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params">event, urlOverride</span>) =&gt; {
	<span class="hljs-keyword">const</span> options = urlOverride
		? {
				<span class="hljs-attr">mapRequestToAsset</span>: <span class="hljs-function">(<span class="hljs-params">request: Request</span>) =&gt;</span> {
					<span class="hljs-keyword">const</span> absoluteURLString = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(urlOverride, request.<span class="hljs-property">url</span>).<span class="hljs-property">href</span>;
					<span class="hljs-keyword">return</span> <span class="hljs-title function_">mapRequestToAsset</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Request</span>(absoluteURLString, request));
				},
		  }
		: {};

	<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-title function_">getAssetFromKV</span>(event, options);
};
</code></pre>
<p>The browser runtime relies on Workbox's caching and routing to load those assets. A <a href="https://developers.google.com/web/tools/workbox/modules/workbox-strategies#stale-while-revalidate">stale-while-revalidate strategy</a> ensures that the service worker can render all the content quickly if you've previously visited the same page.</p>
<p>Workbox's <a href="https://developers.google.com/web/tools/workbox/modules/workbox-broadcast-update"><code>BroadcastUpdatePlugin</code></a> will notify any open <code>window</code> clients when an update is found for cached content during the revalidate step. I took the blunt-force approach of reloading the entire page when this happens, but a more nuanced approach would involve showing a message on the screen, prompting the user to reload if they would like to see new content.</p>
<p>You <a href="https://github.com/jeffposnick/jeffy-info/blob/cf-worker/src/service-worker/service-worker.ts">end up</a> with code like:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> swrStrategy = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StaleWhileRevalidate</span>({
	<span class="hljs-attr">cacheName</span>: <span class="hljs-string">&#x27;static&#x27;</span>,
	<span class="hljs-attr">plugins</span>: [<span class="hljs-keyword">new</span> <span class="hljs-title class_">BroadcastUpdatePlugin</span>()],
});

<span class="hljs-keyword">const</span> <span class="hljs-title function_">loadStatic</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params">event, urlOverride</span>) =&gt; {
	<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> swrStrategy.<span class="hljs-title function_">handle</span>({
		event,
		<span class="hljs-attr">request</span>: urlOverride || event.<span class="hljs-property">request</span>.<span class="hljs-property">url</span>,
	});
};
</code></pre>
<p>An alternative would be to use <a href="https://developers.google.com/web/tools/workbox/modules/workbox-build">Workbox's build tools</a> to generate a precache manifest of all the JSON files needed to render every page on the site. (This is what I've done with previous iterations of this blog.) I decided to go with runtime caching approach instead, in the interest of minimizing the amount of data transferred during service worker installation. This means that you can't navigate to every page on this blog while offline—only to the pages you've previously visited. If making the entirety of your site work offline after the first visit is important to your use case, precaching everything will let you do that!</p>
<h2 id="what's-next%3F" tabindex="-1"><a class="header-anchor" href="#what's-next%3F">What's next?</a></h2>
<p>Unlike some of my <a href="https://developers.google.com/web/updates/2018/05/beyond-spa">previous efforts</a> in this space, I'm pretty confident that this setup isn't <em>too</em> out there. It seems like web developers are comfortable with embracing service workers in the cloud, and Workbox has been going strong for over six years now.</p>
<p>Moreover, while I implemented this architecture on blog site with Markdown files, this same approach could apply equally well to a site that relied on database or API calls to populate each page's content. The only thing that needs to change is the logic that retrieves data to populate each page's template. And because that logic only needs to be written in one place, inside the shared service worker code, you don't have to worry about it getting out of sync between the cloud and browser.</p>
<p>I'm excited to see if more folks use <a href="https://github.com/jeffposnick/jeffy-info">my current setup</a> as inspiration, and am happy to chat with anyone interested in turning this into a reusable started kit!</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Using npm modules inside of Apps Script]]></title>
            <link>/2021/07/12/npm-from-apps-script.html</link>
            <guid>/2021/07/12/npm-from-apps-script.html</guid>
            <pubDate>Mon, 12 Jul 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Bundlers: the source of, and solution to, all of JavaScript's problems.]]></description>
            <content:encoded><![CDATA[<p>I recently was processing some data using <a href="https://developers.google.com/apps-script">Apps Script</a>, and needed to parse out <a href="https://en.wikipedia.org/wiki/Second-level_domain">second-level domain</a> info from a bunch of URLs. This is definitely <a href="https://twitter.com/jeffposnick/status/1401218570093305863"><strong>not</strong> the job for regular expressions</a>, but it's perfect for an <code>npm</code> module, <a href="https://www.npmjs.com/package/psl"><code>psl</code></a>, that uses the <a href="https://publicsuffix.org/">public suffix list</a>.</p>
<p>But while Apps Script has come a long way, and features lots of ES2015+ goodness nowadays, it's not possible to pull in arbitrary code from <code>npm</code> and run it directly.</p>
<p>To work around this, I created the following <code>index.js</code> file locally, exporting the interface that I wanted to call from Apps Script:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> psl <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;psl&#x27;</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title class_">Url</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;url-parse&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">parseHostname</span>(<span class="hljs-params">sourceURL</span>) {
	<span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Url</span>(sourceURL);
	<span class="hljs-keyword">return</span> psl.<span class="hljs-title function_">parse</span>(url.<span class="hljs-property">hostname</span>);
}
</code></pre>
<p>Then, I installed the necessary dependencies from <code>npm</code>, and bundled this code up with <a href="https://esbuild.github.io/"><code>esbuild</code></a>:</p>
<pre><code class="hljs language-sh">npm init -y
npm install --save-dev psl url-parse punycode
npx esbuild index.js --bundle --global-name=psl --outfile=psl.js
</code></pre>
<p>I manually copied the contents of the <code>psl.js</code> file into a <code>psl.gs</code> file, alongside my main <code>Code.gs</code> file in the Apps Script editor. (This would be annoying if the bundled output changed frequently, but doing it once by hand wasn't a problem.)</p>
<p>Apps Script will automatically make the contents of all <code>.gs</code> files in a project visible in the same global scope, so I could now write code like</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> {sld} = psl.<span class="hljs-title function_">parseHostname</span>(url);
</code></pre>
<p>inside of my main <code>Code.gs</code> file.</p>
<h2 id="caveats" tabindex="-1"><a class="header-anchor" href="#caveats">Caveats</a></h2>
<p>The Apps Script runtime environment still has a bunch of quirks when compared to Node, so don't expect all of your bundled code to work as-is. Polyfills may be needed (I used <a href="https://www.npmjs.com/package/url-parse"><code>url-parse</code></a>, for instance, since the native <code>URL</code> object isn't available in Apps Script.)</p>
<p>Once you copy over the bundled code to Apps Script, it's never going to be updated, so make sure you're prepared to rebundle if and when there are any security or feature updates to the modules you're using.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Debugging a specific service worker request]]></title>
            <link>/2021/03/11/debugging-sw-request.html</link>
            <guid>/2021/03/11/debugging-sw-request.html</guid>
            <pubDate>Thu, 11 Mar 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[The power of DevTools conditional breakpoints.]]></description>
            <content:encoded><![CDATA[<h2 id="conditional-breakpoints" tabindex="-1"><a class="header-anchor" href="#conditional-breakpoints">Conditional breakpoints</a></h2>
<p>Chrome DevTools has <a href="https://developers.google.com/web/updates/2015/07/set-a-breakpoint-based-on-a-certain-condition">supported conditional breakpoints</a> since, at least, July of 2015, but I'm embarrassed to admit that I've never tried them until now.</p>
<h2 id="service-worker-debugging" tabindex="-1"><a class="header-anchor" href="#service-worker-debugging">Service worker debugging</a></h2>
<p>I found myself (as I often do...) trying to track down exactly why a service worker was <a href="https://github.com/GoogleChrome/workbox/issues/2767#issuecomment-796854731">behaving in a certain way</a> when processing incoming requests in its <code>fetch</code> handler(s). If this were code for my own project, I'd (no judgment!) throw in a ton of <code>console.log()</code> statements and make sense of what got spit out on the Console after redeploying. But since I was looking at someone else's code, that debugging technique wasn't feasible. (Well, <a href="https://dev.to/apvarun/chrome-devtools-logging-without-console-log-2b7i">actually it is</a>, but I needed something more powerful and less noisy than a bunch of logging.)</p>
<p>Instead, I remembered that conditional breakpoints could be set at runtime, and decided to give them try.</p>
<h2 id="what-to-look-for%2C-and-break-on" tabindex="-1"><a class="header-anchor" href="#what-to-look-for%2C-and-break-on">What to look for, and break on</a></h2>
<p>In this case, I wanted to start debugging at the very start of a <code>fetch</code> event handler, but only if the handler were processing an incoming request for a specific type of URL. I was dealing with <a href="https://www.cityfurniture.com/browse/dining-room">code</a> generated by <a href="https://developers.google.com/web/tools/workbox/">Workbox</a>, and it was minimized, so finding the exact place to start debugging was a bit of a challenge.</p>
<p>Taking DevTools up on its offer to pretty-print the source code was the first step:</p>
<p><img src="/static/images/pretty-print.webp" alt="Pretty-print link in DevTools"></p>
<p>Next, I searched the source code for the string <code>fetch</code>. Even though most of the symbols were minimized, string constants like <code>addEventListener('fetch', ...)</code> were still intact, so finding that is reliable.</p>
<p>Once I found the right spot, I right-clicked on the line number where I wanted to conditionally enter the debugger:</p>
<p><img src="/static/images/conditional-breakpoint.webp" alt="Setting a conditional breakpoint"></p>
<p>There were a <em>lot</em> of different requests being made, but I knew that the one I cared about had a URL containing <code>common</code>. I also knew that even through the variable was renamed to <code>e</code> during minification, the parameter passed to a <code>fetch</code> event listener is a <a href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent"><code>FetchEvent</code></a>. That, in turn has a <a href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/request"><code>request</code></a>, and <em>that</em> has a <code>url</code> property string. So <code>e.request.url</code> would give me access to a URL that I could conditionally match:</p>
<p><img src="/static/images/breakpoint-criteria.webp" alt="The criteria used"></p>
<p>If I were trying to debug something different, like tracing the logic used for all image requests, I could use different <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request#properties">properties</a> of the <code>Request</code>, leading to a condition like <code>e.request.destination === 'image'</code>.</p>
<h3 id="note" tabindex="-1"><a class="header-anchor" href="#note">Note</a></h3>
<p>In this particular case, there were actually two <code>fetch</code> event handlers, which is a <a href="https://stackoverflow.com/questions/45257602/sharing-fetch-handler-logic-defined-across-multiple-service-workers#:~:text=Multiple%20fetch%20Event%20Handlers">valid thing</a> to have! This is an implementation detail that might change in the future, but Workbox currently creates one for its precache logic, and one for its runtime logic. So what I actually had to do was assign conditional breakpoints inside both of those handlers to figure out which one was running.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Testing multiple versions of a module dependency]]></title>
            <link>/2020/10/01/testing-multiple-webpack-versions.html</link>
            <guid>/2020/10/01/testing-multiple-webpack-versions.html</guid>
            <pubDate>Thu, 01 Oct 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[module-alias ftw]]></description>
            <content:encoded><![CDATA[<h2 id="the-problem" tabindex="-1"><a class="header-anchor" href="#the-problem">The problem</a></h2>
<p><code>webpack</code> is preparing its <a href="https://webpack.js.org/migrate/5/">v5.0.0 release</a>, and as the maintainer of the <a href="https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin"><code>workbox-webpack-plugin</code></a>, I want to make sure we work well with the new version. But at the same time, we can't abandon compatibility with <code>webpack</code> v4.x.</p>
<p>We've got an <a href="https://github.com/GoogleChrome/workbox/tree/v6/test/workbox-webpack-plugin">extensive test suite</a> that previously assumed there would be a single version of <code>webpack</code> that we tested against, so how can we make sure that we can run our individual tests against multiple versions of <code>webpack</code>, without modifying any of the <code>require('webpack')</code> statements in <code>workbox-webpack-plugin</code>? (The same applies other plugins that are exercised alongside <code>workbox-webpack-plugin</code> in our test suite, like <code>html-webpack-plugin</code>.)</p>
<h2 id="the-solution" tabindex="-1"><a class="header-anchor" href="#the-solution">The solution</a></h2>
<h3 id="step-1%3A-install-multiple-package-versions" tabindex="-1"><a class="header-anchor" href="#step-1%3A-install-multiple-package-versions">Step 1: Install multiple package versions</a></h3>
<p><code>npm</code> v6.9.0 <a href="https://npm.community/t/release-npm-6-9-0/5911">added support</a> for package aliases. This allows you to install multiple versions of the same <code>npm</code> package under different <code>node_modules/</code> subdirectory names.</p>
<p>In my case, I ran:</p>
<pre><code class="hljs language-text">npm install --save-dev webpack-v4@npm:webpack
npm install --save-dev webpack-v5@npm:webpack@5.0.0-rc.3
</code></pre>
<p>After running that, my <code>package.json</code> included:</p>
<pre><code class="hljs language-text">&quot;devDependencies&quot;: {
  &quot;webpack-v4&quot;: &quot;npm:webpack@^4.44.2&quot;,
  &quot;webpack-v5&quot;: &quot;npm:webpack@^5.0.0-rc.3&quot;
}
</code></pre>
<p>and I had local <code>node_modules/webpack-v4/</code> and <code>node_modules/webpack-v5/</code> directories.</p>
<h3 id="step-2%3A-use-module-alias-to-override-require()" tabindex="-1"><a class="header-anchor" href="#step-2%3A-use-module-alias-to-override-require()">Step 2: Use module-alias to override require()</a></h3>
<p><a href="https://github.com/ilearnio/module-alias"><code>module-alias</code></a> makes overriding <code>require()</code> behavior extremely easy. I was worried that I would need to start modifying the <code>NODE_PATH</code> environment variable or something like that, but <code>module-alias</code> is all you need.</p>
<p>Here's an approximation of how I used it:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;path&#x27;</span>);
<span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;module-alias&#x27;</span>).<span class="hljs-title function_">addAlias</span>(
	<span class="hljs-comment">// Replace with the &quot;real&quot; name of the module.</span>
	<span class="hljs-string">&#x27;webpack&#x27;</span>,
	<span class="hljs-comment">// Replace with the actual local versioned directory path.</span>
	path.<span class="hljs-title function_">resolve</span>(<span class="hljs-string">&#x27;node_modules&#x27;</span>, <span class="hljs-string">&#x27;webpack-v4&#x27;</span>),
);
</code></pre>
<p>This aliasing will be in effect for a <code>require()</code> that's performed in any submodules as well, for the lifetime of the process. If you need to clear it out at any point before the process exits, you can use:</p>
<pre><code class="hljs language-js"><span class="hljs-comment">// Replace with the name of the module you&#x27;re aliasing.</span>
<span class="hljs-keyword">delete</span> <span class="hljs-built_in">require</span>.<span class="hljs-property">cache</span>[<span class="hljs-built_in">require</span>.<span class="hljs-title function_">resolve</span>(<span class="hljs-string">&#x27;webpack&#x27;</span>)];
</code></pre>
<h3 id="solution-in-context" tabindex="-1"><a class="header-anchor" href="#solution-in-context">Solution in context</a></h3>
<p><a href="https://github.com/GoogleChrome/workbox/pull/2641">This PR</a> includes the full set of changes that I made for <code>webpack</code> v5 compatibility, including the dependency changes described above.</p>
<h2 id="thanks" tabindex="-1"><a class="header-anchor" href="#thanks">Thanks</a></h2>
<p>Huge thanks to <a href="https://twitter.com/_developit">Jason</a>, for walking me through this solution. I wanted to get this all written up somewhere permanent in case others have the same problem.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Performance auditing an eCommerce site]]></title>
            <link>/2020/04/06/live-site-audit.html</link>
            <guid>/2020/04/06/live-site-audit.html</guid>
            <pubDate>Mon, 06 Apr 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[...let's do it live!]]></description>
            <content:encoded><![CDATA[<h2 id="alan's-weekly-ecommerce-livestreams" tabindex="-1"><a class="header-anchor" href="#alan's-weekly-ecommerce-livestreams">Alan's weekly eCommerce livestreams</a></h2>
<p>My teammate <a href="https://alankent.me/">Alan</a>'s been running a weekly livestream on <a href="https://www.youtube.com/channel/UCyQwDaXnT7wMBBqIaAfmY7g">his YouTube channel</a>, focusing on topics of interest to eCommerce websites. He asked me to come on this week and perform a site performance audit.</p>
<p>Since I'm currently homebound due to Coronavirus concerns in NYC, I haven't had the chance to perform any <a href="https://jeffy.info/2017/11/10/post-cds-perf-links.html">in-person site audits</a> in a while. Going through the process via a livestream was a great opportunity!</p>
<h2 id="the-site-review" tabindex="-1"><a class="header-anchor" href="#the-site-review">The site review</a></h2>
<p>The review process took about 50 minutes (I'd recommend watching at 2x playback speed...) and covered some relevant findings from WebPageTest.org and Lighthouse.</p>
<p>We choose the site, <a href="https://threddies.com/">https://threddies.com/</a>, &quot;at random&quot; based on folks who <a href="https://twitter.com/akent99/status/1245466347502333952">volunteered</a>. I think it ended up being fairly representative of the types of issues a lot of sites encounter.</p>
<iframe class="youtube-embed" src="https://www.youtube.com/embed/l4bIT3CXCpc" allowfullscreen frameborder="0" loading="lazy"></iframe>
<p><em>Lightly edited from the notes I took during the review.</em></p>
<h3 id="tools-used%3A" tabindex="-1"><a class="header-anchor" href="#tools-used%3A">Tools used:</a></h3>
<ul>
<li><a href="https://webpagetest.org/easy">&quot;Easy&quot; mode on WebPageTest</a> (<a href="https://webpagetest.org/result/200402_FD_f791a96a04046aaa9d7583914ba1c952/">site results</a>)</li>
<li><a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a></li>
</ul>
<h3 id="observations" tabindex="-1"><a class="header-anchor" href="#observations">Observations</a></h3>
<ul>
<li>11 seconds for main content to load — what's loaded beforehand?</li>
<li>How many of the widgets/analytics tools can be <a href="https://web.dev/native-lazy-loading/">lazily-loaded</a> (or potentially removed, if appropriate)?</li>
<li>HTTP cache expiration <a href="https://web.dev/reliable/">best practices</a>.</li>
<li>Use the <a href="https://developers.google.com/web/tools/chrome-devtools/coverage">Coverage panel</a> in Chrome Dev Tools to evaluate how much of your JS/CSS in the critical request path is being used</li>
<li>Q: Can they get away with loading Google Maps static images instead of Google Maps widget? Answer: <a href="https://developers.google.com/maps/documentation/maps-static/intro">yes, they can</a>.</li>
<li>Always click on the &quot;Learn More&quot; link in Lighthouse for additional context!</li>
<li>Zach Leatherman's <a href="https://www.zachleat.com/web/comprehensive-webfonts/">guides to loading web fonts</a> are great.</li>
</ul>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Exposing headers on CORS responses]]></title>
            <link>/2019/07/22/exposing-cors-headers.html</link>
            <guid>/2019/07/22/exposing-cors-headers.html</guid>
            <pubDate>Mon, 22 Jul 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Access-Control-Expose-Headers is your friend.]]></description>
            <content:encoded><![CDATA[<h2 id="cors-and-its-discontents" tabindex="-1"><a class="header-anchor" href="#cors-and-its-discontents">CORS and its discontents</a></h2>
<p>The concept of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> requests comes up a lot in my professional life. Much of the time, it's in the context of why a given response is <a href="https://stackoverflow.com/questions/39109789/what-limitations-apply-to-opaque-responses">opaque</a>, and how to make that response non-opaque so that it plays nicely with service workers and the Cache Storage API.</p>
<p>Fortunately, many popular third-party APIs and hosts support CORS nowadays, and solving your basic CORS-related mystery normally boils down to, say, adding in the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes"><code>crossorigin</code> attribute</a> to your <code>&lt;img&gt;</code> tags.</p>
<h2 id="when-cors-is-not-enough" tabindex="-1"><a class="header-anchor" href="#when-cors-is-not-enough">When CORS is not enough</a></h2>
<p>But while enabling CORS is enough to get back basic information about an HTTP response—like its status code, or access to its body—there's still some information that's locked down by default. The headers exposed on a CORS response, for instance, are limited to the following six <a href="https://developer.mozilla.org/en-US/docs/Glossary/Simple_response_header">&quot;simple&quot; response headers</a>:</p>
<ul>
<li><code>Cache-Control</code></li>
<li><code>Content-Language</code></li>
<li><code>Content-Type</code></li>
<li><code>Expires</code></li>
<li><code>Last-Modified</code></li>
<li><code>Pragma</code></li>
</ul>
<p>Some of those headers can come in handy when accessed inside of a service worker, but there's one in particular that can be useful, but isn't exposed by default: <code>Date</code>.</p>
<p>In particular, if you're using <a href="https://developers.google.com/web/tools/workbox/modules/workbox-cache-expiration#restrict_the_age_of_cached_entries">Workbox's cache expiration</a> logic and you provide a <code>maxAgeSeconds</code> parameter, the <code>Date</code> of the cached response <a href="https://github.com/GoogleChrome/workbox/blob/b0825d74d81264e7b4537ed170dd60de638561ba/packages/workbox-expiration/src/Plugin.ts#L176-L195">is checked</a> against the difference between the current time and <code>maxAgeSeconds</code>. If the <code>Date</code> is too old, then the cached response will end up being ignored.</p>
<p>But... this logic only works if there's a <code>Date</code> header exposed on the response. By default, that won't be the case for a CORS response.</p>
<h2 id="exposition" tabindex="-1"><a class="header-anchor" href="#exposition">Exposition</a></h2>
<p>The workaround, as with so many things related to CORS, involves fiddling with HTTP response headers. You'll either need access to the underlying HTTP server yourself, or you'll need to reach out to your CDN/API provider asking them to make the change.</p>
<p>Setting <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers"><code>Access-Control-Expose-Headers: Date</code></a> will permit the <code>Date</code> response header to be visible to your web app's code, and you could include any additional headers there in a comma-separated list.</p>
<p>If you're using your own Express-based web server, the <a href="https://www.npmjs.com/package/corser"><code>corser</code> middleware</a> looks pretty reasonable for setting up a working configuration. Their docs include a <a href="https://www.npmjs.com/package/corser#getting-a-response-header-returns-refused-to-get-unsafe-header-x">recipe</a> for configuring the exposed response headers.</p>
<h2 id="live-demo" tabindex="-1"><a class="header-anchor" href="#live-demo">Live demo</a></h2>
<p>Here's a quick demonstration, separate from service workers, and using the fantastic <a href="https://httpbin.org">https://httpbin.org</a> service to control the <code>Access-Control-Expose-Headers</code> response header that's returned in a simulated API response. (They support CORS by default, so nothing needs to be done to enable that.)</p>
<p>Check out the log messages in the JavaScript console to see which headers are visible in the response.</p>
<p><button id="request-without-aceh">Without Access-Control-Expose-Headers: Date</button></p>
<p><button id="request-with-aceh">With Access-Control-Expose-Headers: Date</button></p>
<script class="visible">async function logResponseHeaders(url) {
  // mode: 'cors' is the default, but let's just be explicit here.  
  const response = await fetch(url, {mode: 'cors'});
  console.log(`Response headers for ${url}:\n`, ...response.headers);
}

document.querySelector('#request-without-aceh').addEventListener(
  'click',
  () => logResponseHeaders('https://httpbin.org/response-headers')
);

document.querySelector('#request-with-aceh').addEventListener(
  'click',
  () => logResponseHeaders('https://httpbin.org/response-headers?Access-Control-Expose-Headers=Date')
);</script>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[That time I used a generator]]></title>
            <link>/2019/03/22/that-time-i-used-a-generator.html</link>
            <guid>/2019/03/22/that-time-i-used-a-generator.html</guid>
            <pubDate>Fri, 22 Mar 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[They're there for a reason!]]></description>
            <content:encoded><![CDATA[<h2 id="backstory" tabindex="-1"><a class="header-anchor" href="#backstory">Backstory</a></h2>
<p>I moved over to work on Google's Web DevRel team way back in 2014, and one of
the first tasks that I took on was writing short update articles and code
samples for new web platform features. These are... somewhat embarrassing to
look back on, so I won't link to many here, but one of the first I put together
covered
<a href="https://developers.google.com/web/updates/2014/10/Generators-the-Gnarly-Bits">generators</a>.
I didn't have a huge amount to say about generators, so the general approach
used in the article was to link to some more
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*">canonical resources</a>,
while calling out a couple of interesting &quot;gotchas&quot; that I thought could add
some value.</p>
<p>So I wrote that, moved on, and then pretty much forgot that generators existed for the next 4 years.</p>
<h2 id="refactoring" tabindex="-1"><a class="header-anchor" href="#refactoring">Refactoring</a></h2>
<p>That takes us to a few months ago, when I was working on a <a href="https://github.com/GoogleChrome/workbox/issues/1793">rewrite</a>
of the <code>workbox-precaching</code> module as part of the <a href="https://github.com/GoogleChrome/workbox/releases/tag/v4.0.0">Workbox v4</a>
release. This gave me an opportunity to revisit some code that hadn't been
touched in a while.</p>
<h3 id="original-logic" tabindex="-1"><a class="header-anchor" href="#original-logic">Original logic</a></h3>
<p>The actual code isn't super-relevant (I'll link to the before and after below,
for those who are interested), but the main points were:</p>
<ul>
<li>It was code to compare a string against few possible matches.</li>
<li>Calculating each possible match is (somewhat) expensive.</li>
<li>The code returned <code>true</code> as soon as it found a match.</li>
<li>If none of the conditions matched, it returned <code>false</code>.</li>
</ul>
<p>The original code looked something like:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> originalUrl = <span class="hljs-string">&#x27;...&#x27;</span>;
<span class="hljs-keyword">const</span> urlToMatch = <span class="hljs-string">&#x27;...&#x27;</span>;

<span class="hljs-keyword">const</span> urlVariation1 = <span class="hljs-title function_">generateVariation1</span>(originalUrl);
<span class="hljs-keyword">if</span> (urlToMatch === urlVariation1) {
	<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-keyword">const</span> urlVariation2 = <span class="hljs-title function_">generateVariation2</span>(originalUrl);
<span class="hljs-keyword">if</span> (urlToMatch === urlVariation2) {
	<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-comment">// etc.</span>

<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
</code></pre>
<p>I'm not a huge fan of a repeated sequence of <code>if(...) { return ... }</code> statements
aesthetically, and structuring code like that can make it harder to understand
that each test case is effectively doing the same thing.</p>
<h3 id="refactoring-without-generators" tabindex="-1"><a class="header-anchor" href="#refactoring-without-generators">Refactoring without generators</a></h3>
<p>One potential refactoring to emphasize the repeated logic could be:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> originalUrl = <span class="hljs-string">&#x27;...&#x27;</span>;
<span class="hljs-keyword">const</span> urlToMatch = <span class="hljs-string">&#x27;...&#x27;</span>;

<span class="hljs-keyword">const</span> urlVariations = [
	generateVariation1,
	generateVariation2,
	<span class="hljs-comment">// etc.</span>
].<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">generateVariation</span>) =&gt;</span> <span class="hljs-title function_">generateVariation</span>(originalUrl));

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> urlVariation <span class="hljs-keyword">of</span> urlVariations) {
	<span class="hljs-keyword">if</span> (urlToMatch === urlVariation) {
		<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
	}
}

<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
</code></pre>
<p>I like that version of the code from an aesthetic point of view, but one
downside is that you end up running each of the <code>generateVariationN()</code> functions
ahead of time. If a variation early in the list ends up matching, you've ended
up running (potentially expensive) code for nothing.</p>
<h3 id="refactoring-with-generators" tabindex="-1"><a class="header-anchor" href="#refactoring-with-generators">Refactoring with generators</a></h3>
<p>So! This is when I remembered that generators were A Thing, and could come in
handy in this use case.</p>
<p>Generators are
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#Generator_functions">iterable</a>,
so it could be dropped right in to a
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of"><code>for...of</code></a>
loop.</p>
<p>Generators only run when their <code>next()</code> value is requested: they'll execute
until a <code>yield</code> keyword is encountered, at which point they pause and control
goes back to whatever triggered the iteration. If we yield the results of our
potentially expensive functions one at a time inside of a generator, we don't
have to worry about executing functions whose results won't actually be needed.
And we still get to structure the code that uses the values as a loop rather
than a sequence of <code>if(...) { return ...; }</code> statements. It's the best of both
worlds!</p>
<p>Using a generator and a <code>for...of</code> loop gives us code that looks something like:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">function</span>* <span class="hljs-title function_">generateVariations</span>(<span class="hljs-params">originalUrl</span>) {
	<span class="hljs-comment">// You could put these yields inside a loop, too!</span>
	<span class="hljs-keyword">yield</span> <span class="hljs-title function_">generateVariation1</span>(originalUrl);
	<span class="hljs-keyword">yield</span> <span class="hljs-title function_">generateVariation2</span>(originalUrl);
	<span class="hljs-comment">// etc.</span>
}

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> urlVariation <span class="hljs-keyword">of</span> <span class="hljs-title function_">generateVariations</span>(originalUrl)) {
	<span class="hljs-keyword">if</span> (urlToMatch === urlVariation) {
		<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
	}
}

<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
</code></pre>
<h3 id="the-actual-changes-in-workbox" tabindex="-1"><a class="header-anchor" href="#the-actual-changes-in-workbox">The actual changes in Workbox</a></h3>
<p>If you're curious, the original code in Workbox v3 is
<a href="https://github.com/GoogleChrome/workbox/blob/d27aafbdf164f051a883965058e6eb4c0df3a052/packages/workbox-precaching/_default.mjs#L76-L130">here</a>.
The v4 code has been split up into modules for the new
<a href="https://github.com/GoogleChrome/workbox/blob/f1164254b8abdd12c5c601ee7e7fc7d73fffd979/packages/workbox-precaching/utils/generateURLVariations.mjs#L23-L55">generator</a>
and the
<a href="https://github.com/GoogleChrome/workbox/blob/f1164254b8abdd12c5c601ee7e7fc7d73fffd979/packages/workbox-precaching/utils/getCacheKeyForURL.mjs#L25-L35">code that loops over the generated values</a>.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Styling inline scripts]]></title>
            <link>/2019/03/14/styling-inline-scripts.html</link>
            <guid>/2019/03/14/styling-inline-scripts.html</guid>
            <pubDate>Thu, 14 Mar 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Show what you run when demoing JavaScript.]]></description>
            <content:encoded><![CDATA[<p>Following up on <a href="https://twitter.com/jeffposnick/status/1106217552190062598">this tweet</a>, I remembered that I have a blog(!) and that I can use it to host and run arbitrary JavaScript and CSS(!).</p>
<p>Here's a rather contrived example of how you could use CSS to make certain <code>&lt;script&gt;</code> tags visible, and use <code>&lt;details&gt;</code> + <code>&lt;summary&gt;</code> to hide them by default.</p>
<script class="visible">console.log('This is visible.');</script>
<details>
  <summary>View Inline Script</summary>
  <script class="visible">console.log('This is visible, but hidden inside of details.');</script>
</details>
<script>
console.log('This is not visible.');
</script>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Service workers in create-react-app v2]]></title>
            <link>/2018/10/10/sw-in-c-r-a.html</link>
            <guid>/2018/10/10/sw-in-c-r-a.html</guid>
            <pubDate>Wed, 10 Oct 2018 00:00:00 GMT</pubDate>
            <description><![CDATA[There are many alternatives, and something has to be the default.]]></description>
            <content:encoded><![CDATA[<p>Spurred on by <a href="https://twitter.com/AdamRackis/status/1050176700150108160">this Twitter thread</a>, I wanted to share some thoughts in a longer forum than Twitter would allow.</p>
<p>Here's my thinking:</p>
<p>These are all equally valid end-states, based on the tradeoffs that make sense for each developers' use case:</p>
<p><code>1)</code> Precache HTML in a SW, and get the benefits of navigation being reliably fast.</p>
<p><code>1a)</code> Use <code>skipWaiting: false</code> to ensure that precached assets are updated eventually, once all the existing tabs controlled by the old SW are closed. A recipe like <a href="https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users">this one</a>, which could ideally be added to <code>c-r-a</code>, can make this a better UX.</p>
<p><code>1b)</code> Use <code>skipWaiting: true</code> to ensure that precached assets are updated immediately, with the understanding that this can mess up lazy-loading. (Maybe you already have fallback logic to deal with flaky lazy loading.)</p>
<p><code>2)</code> Use a SW that doesn't precache your HTML, but potentially offers some other benefits, like runtime caching of subresources, or displays a custom &quot;you're offline&quot; web page.</p>
<p><code>3)</code> Don't use a SW at all.</p>
<p>Ideally developers would have the flexibility to choose which state to be in. The SW integration in <code>c-r-a</code> is tricky, because the configuration is (for obviously valid reasons) locked down to the points where choosing being those options is non-trivial. That makes it important to ship with the right default behavior, one that will strike a balance between providing benefits to the user with minimal negative side effects.</p>
<p><code>c-r-a</code> v1 shipped with <code>1b)</code> by default, with the only &quot;escape hatch&quot; to change some runtime code and avoid registering a SW. In retrospect, that wasn't a great default.</p>
<p><code>c-r-a</code> v2 shipped with <code>3)</code> by default, with developers who explicitly opt-in ending up in <code>1a)</code>. I think that's a saner/safer default, but it might not be the right fit for all developers.</p>
<p><code>2)</code> is interesting, and I don't want to say that that kind of SW is inappropriate. It's just that it really requires a level of SW configuration that you can't default to in a locked-down environment like <code>c-r-a</code>.</p>
<p>If developers <a href="https://github.com/facebook/create-react-app/issues/5359">have the ability</a> to configure Workbox without needing to <code>eject</code>, I think they'll have the tools they need to choose any of those end states.</p>
<p>I haven't done a great job of updating the <code>c-r-a</code> docs to explain all that, and that's on me to take care of. (Update: <a href="https://github.com/facebook/create-react-app/pull/5410">here's a PR</a> that will hopefully help.)</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Something Brewing]]></title>
            <link>/2018/09/30/something-brewing.html</link>
            <guid>/2018/09/30/something-brewing.html</guid>
            <pubDate>Sun, 30 Sep 2018 00:00:00 GMT</pubDate>
            <description><![CDATA[Apparently, the whole point of this blog is to blog about blogging architecture.]]></description>
            <content:encoded><![CDATA[<h2 id="motivation" tabindex="-1"><a class="header-anchor" href="#motivation">Motivation</a></h2>
<p>Following up from my &quot;<a href="https://developers.google.com/web/updates/2018/05/beyond-spa">Beyond SPAs: alternative architectures for your PWA</a>&quot; talk at Google I/O this year, I've been investigating ways of bringing all of the benefits folks associate with PWAs (network-independent loading, rich metadata provided by a web app manifest) to a more traditional, &quot;blog-y&quot; site architecture.</p>
<p><a href="https://twitter.com/mathias">Mathias</a> was visiting NYC last week, and I gave him some advice about the service worker for the relaunched <a href="https://v8.dev/">V8 developer blog</a>. He ended up going with <a href="https://www.11ty.io/">11ty</a> for building the blog, along with <a href="https://mozilla.github.io/nunjucks/">Nunjucks templates</a>. I figured it was worth giving that set of tools a try, and see what migrating my existing, <a href="https://jekyllrb.com/">Jekyll</a> (+ <a href="https://shopify.github.io/liquid/">Liquid templates</a>) blog setup would be.</p>
<p>And, while I was at it, I wanted to see what it would look like to implement all of the site build + templating infrastructure inside of a service worker, to give a more robust offline-first experience than what Mathias was able to get for the V8 blog. (Just the index page was being cached.)</p>
<h2 id="the-present" tabindex="-1"><a class="header-anchor" href="#the-present">The present</a></h2>
<p>I've moved most of the templates over to use Nunjucks, though I believe I've lost a few filters and date formatting options that I had with Liquid.</p>
<p>Using 11ty instead of Jekyll to orchestrate the build was very straightforward, after moving a few directories around.</p>
<p>I've created a <a href="https://github.com/jeffposnick/jeffposnick.github.io/blob/6d9e5631a2eb9dd5083b1ee18090e890fa128672/generate-sw.js">helper script</a> to take care of the service worker metadata generation. This basically handles a lot of the same processing that 11ty does as part of a build, but spits out the data in JSON files that could then be cached and read from the service worker using the Cache Storage API.</p>
<p>The service worker itself, not surprisingly, uses <a href="https://developers.google.com/web/tools/workbox/">Workbox</a> under the hood for precaching and runtime caching, with a custom <a href="https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-routing#~handlerCallback"><code>handlerCallback</code></a> to trigger the Nunjucks rendering.</p>
<p>Nunjucks rendering worked very well—there's an official build of Nunjucks which runs in the <a href="https://mozilla.github.io/nunjucks/api.html#browser-usage">browser</a> (including in a service worker), and that really made it much easier to set this up. I've struggled in the past to implement service worker templating using <a href="https://github.com/sirlantis/liquid-node">libraries</a> that weren't really meant to run in the browser, or did not have <a href="https://mozilla.github.io/nunjucks/api.html#writing-a-loader">pluggable loaders</a> (to pull in templates using the Cache Storage API). Wrapping callback-style asynchronicity with promises meant that everything played nicely. I would love to see if, in the future, Nunjucks could be replaced by something a bit more lightweight—perhaps <a href="https://twitter.com/jeffposnick/status/1046093468341276673">native JavaScript template literals</a>.</p>
<p>Check out the <a href="/service-worker.js">latest deployed version</a> of this site's service worker if you want to learn more—I'm expecting to clean it up quite a bit as I find some more time. One pretty neat bit worth excerpting is:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">postHandler</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params">{params}</span>) =&gt; {
	<span class="hljs-keyword">const</span> site = <span class="hljs-keyword">await</span> <span class="hljs-title function_">initSiteData</span>();
	<span class="hljs-keyword">const</span> cachedResponse = <span class="hljs-keyword">await</span> caches.<span class="hljs-title function_">match</span>(
		<span class="hljs-string">`/_posts/<span class="hljs-subst">${params.join(<span class="hljs-string">&#x27;-&#x27;</span>)}</span>.json`</span>,
		{
			<span class="hljs-attr">cacheName</span>: workbox.<span class="hljs-property">core</span>.<span class="hljs-property">cacheNames</span>.<span class="hljs-property">precache</span>,
		},
	);
	<span class="hljs-keyword">const</span> context = <span class="hljs-keyword">await</span> cachedResponse.<span class="hljs-title function_">json</span>();
	context.<span class="hljs-property">site</span> = site;
	context.<span class="hljs-property">content</span> = context.<span class="hljs-property">html</span>;
	<span class="hljs-keyword">const</span> html = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
		nunjucksEnv.<span class="hljs-title function_">render</span>(context.<span class="hljs-property">layout</span>, context, <span class="hljs-function">(<span class="hljs-params">error, html</span>) =&gt;</span> {
			<span class="hljs-keyword">if</span> (error) {
				<span class="hljs-keyword">return</span> <span class="hljs-title function_">reject</span>(error);
			}
			<span class="hljs-keyword">return</span> <span class="hljs-title function_">resolve</span>(html);
		});
	});
	<span class="hljs-keyword">const</span> headers = {<span class="hljs-string">&#x27;content-type&#x27;</span>: <span class="hljs-string">&#x27;text/html&#x27;</span>};
	<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(html, {headers});
};

workbox.<span class="hljs-property">routing</span>.<span class="hljs-title function_">registerRoute</span>(
	<span class="hljs-keyword">new</span> <span class="hljs-title class_">RegExp</span>(<span class="hljs-string">&#x27;/(\\d{4})/(\\d{2})/(\\d{2})/(.+)\\.html&#x27;</span>),
	postHandler,
);
</code></pre>
<h2 id="caching-strategies" tabindex="-1"><a class="header-anchor" href="#caching-strategies">Caching strategies</a></h2>
<p>All of the site metadata, templates and content (a whole 8 blog posts!) are precached, which adds up to only ~77kb for my blog. If I were working on a larger site, I'd consider only precaching the site metadata and templates, and using a runtime caching strategy for the posts' JSON.</p>
<p>I've got a cache-first strategy set up for the few images that I use on this blog.</p>
<p>After the service worker's installed, subsequent navigations should be fulfilled entirely from the cache, making the site work fully offline and, more importantly, load reliably fast. (The fact that it's all just some ugly static HTML and CSS helps with the speed, too.)</p>
<h2 id="the-future" tabindex="-1"><a class="header-anchor" href="#the-future">The future</a></h2>
<p>I'd love to get some of the logic that I've put into the custom build scripts into the core of the 11ty project, if that makes sense, or at least packaged up into a standalone set of helpers.</p>
<p>I need to refactor a bunch of that code to stop hardcoding things like paths, as well as try to support custom 11ty filters and plugins within the service worker.</p>
<p>As mentioned, I'm still evaluating whether Nunjucks is the right templating engine to use, but its syntax does seem familiar enough for folks coming from Liquid templates, or Jinja.</p>
<p>The actual service worker code needs to be refactored a bit, and I'd like to make it flexible enough to load any <code>_data</code> you're using, not just the <code>site.json</code>.</p>
<p>I'm not minimizing anything right now, and the build process is generally a bunch of things stuck together with tap.</p>
<h2 id="alternatives" tabindex="-1"><a class="header-anchor" href="#alternatives">Alternatives</a></h2>
<p>There are a host of other options for folks to consider right now—it's actually a really great time for PWA-y static blogs and site generators. I like what <a href="https://www.gatsbyjs.org/">Gatsby's doing</a>, especially with v2 (featuring <a href="https://www.gatsbyjs.org/packages/gatsby-plugin-offline/">Workbox-powered offline support</a>). <a href="https://vuepress.vuejs.org/">Vuepress</a> is similarly vue-pressive, and features Workbox integration as well.</p>
<p>But if you're not interested in building your site's structure using React or Vue, and are coming from a more Jekyll-y starting point, I'm kind of excited about the approach I'm using here.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[CDS Perf Review Clinic Takeaways]]></title>
            <link>/2017/11/10/post-cds-perf-links.html</link>
            <guid>/2017/11/10/post-cds-perf-links.html</guid>
            <pubDate>Fri, 10 Nov 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[A linkdump by any other name....]]></description>
            <content:encoded><![CDATA[<h2 id="cds-'17-retrospective" tabindex="-1"><a class="header-anchor" href="#cds-'17-retrospective">CDS '17 Retrospective</a></h2>
<p><a href="https://developer.chrome.com/devsummit/">Chrome Dev Summit 2017</a> is behind us.
I spent most of the run-up to the event putting together my
<a href="https://www.youtube.com/watch?v=DtuJ55tmjps">&quot;Workbox: Flexible PWA Libraries&quot; talk</a>,
but beyond that, I was also involved in <a href="https://twitter.com/jeffposnick/status/922899094053330944">running on-site Performance Review
Clinics</a> that
attendees could sign up for.</p>
<p>It's always a privilege to meet with the developers who are building web apps
that I use every day, and to lend whatever guidance I could offer. Looking into
the performance of a large group of production websites gives insights that you
wouldn't get from just examining a site as a one-off. Patterns start to emerge
about common areas for improvement.</p>
<p>I wanted to publicly share a few high-level groupings based on those patterns,
and identify a handful of links for each area that contain useful guidance. It's
by no means exhaustive, but I can say that each of these links contains
solutions to a real-world performance issue that we identified during the
Performance Review Clinics.</p>
<h2 id="linkdump!" tabindex="-1"><a class="header-anchor" href="#linkdump!">Linkdump!</a></h2>
<p>Huge thanks go out to all the authors, many of whom I'm lucky enough to work
with.</p>
<h3 id="overall-migration%2Fend-to-end-case-study" tabindex="-1"><a class="header-anchor" href="#overall-migration%2Fend-to-end-case-study">Overall Migration/End to End Case Study</a></h3>
<ul>
<li><a href="https://medium.com/dev-channel/treebo-a-react-and-preact-progressive-web-app-performance-case-study-5e4f450d5299">A React And Preact Progressive Web App Performance Case Study: Treebo</a></li>
</ul>
<h3 id="loading-and-prioritization" tabindex="-1"><a class="header-anchor" href="#loading-and-prioritization">Loading and Prioritization</a></h3>
<ul>
<li><a href="https://developers.google.com/web/fundamentals/performance/http2/">Introduction to HTTP/2</a></li>
<li><a href="https://developers.google.com/web/fundamentals/performance/resource-prioritization">Resource Prioritization – Getting the Browser to Help You</a></li>
<li><a href="https://www.html5rocks.com/en/tutorials/speed/script-loading/">Deep dive into the murky waters of script loading</a></li>
</ul>
<h3 id="javascript" tabindex="-1"><a class="header-anchor" href="#javascript">JavaScript</a></h3>
<ul>
<li><a href="https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/">Can You Afford It?: Real-world Web Performance Budgets</a></li>
<li><a href="https://medium.com/reloading/javascript-start-up-performance-69200f43b201">JavaScript Start-up Performance</a></li>
</ul>
<h3 id="css" tabindex="-1"><a class="header-anchor" href="#css">CSS</a></h3>
<ul>
<li><a href="https://github.com/addyosmani/critical"><code>critical</code>: Extract &amp; Inline Critical-path CSS in HTML pages</a></li>
<li><a href="https://jakearchibald.com/2016/link-in-body/">The future of loading CSS</a></li>
<li><a href="https://github.com/filamentgroup/loadCSS/"><code>loadCss</code>: A function for loading CSS asynchronously</a></li>
</ul>
<h3 id="caching" tabindex="-1"><a class="header-anchor" href="#caching">Caching</a></h3>
<ul>
<li><a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">HTTP Caching</a></li>
<li><a href="https://jakearchibald.com/2016/caching-best-practices/">Caching best practices &amp; max-age gotchas</a></li>
<li><a href="https://developers.google.com/web/fundamentals/primers/service-workers/">Service Workers: an Introduction</a></li>
<li><a href="https://developers.google.com/web/tools/workbox/">Workbox: JavaScript libraries for Progressive Web Apps</a></li>
</ul>
<h3 id="images%2Fmultimedia-content" tabindex="-1"><a class="header-anchor" href="#images%2Fmultimedia-content">Images/Multimedia Content</a></h3>
<ul>
<li><a href="https://images.guide/">Essential Image Optimization</a></li>
<li><a href="https://deanhume.com/home/blogpost/lazy-loading-images-using-intersection-observer/10163">Lazy Loading Images Using Intersection Observer</a></li>
<li><a href="https://css-tricks.com/icon-fonts-vs-svg/">Inline SVG vs Icon Fonts [CAGEMATCH]</a></li>
<li><a href="https://css-tricks.com/content-jumping-avoid/">Content Jumping (and How To Avoid It)</a></li>
<li><a href="https://www.labnol.org/internet/light-youtube-embeds/27941/">A Better Method for Embedding YouTube Videos on your Website</a></li>
</ul>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Hosting Setup, Revisited]]></title>
            <link>/2017/02/24/hosting-setup-revisited.html</link>
            <guid>/2017/02/24/hosting-setup-revisited.html</guid>
            <pubDate>Fri, 24 Feb 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[Goodbye gh-pages + Cloudflare, hello Firebase Hosting.]]></description>
            <content:encoded><![CDATA[<p><em>All of the info in this post is long out of date. See <a href="/2021/07/17/sw-rendering.html">Service worker rendering, in the cloud and in the browser</a> for the latest setup.</em></p>
<h2 id="background" tabindex="-1"><a class="header-anchor" href="#background">Background</a></h2>
<p>As I <a href="https://jeffy.info/2014/11/28/hosting-setup.html">detailed</a>a few years ago when I started up this blog, I used a combination of <a href="https://help.github.com/categories/github-pages-basics/">GitHub Pages</a> + <a href="https://www.cloudflare.com/">Cloudflare</a> for a basic HTTPS static site deployment, pointed at by the <code>jeffy.info</code> domain I bought from <a href="https://domains.google.com/registrar">Google Domains</a>.</p>
<p>I'd be meaning to try something different for a while now, since</p>
<ul>
<li>I'm not taking advantage of GitHub Page's automatic Jekyll build anymore (relying instead on a custom <a href="https://github.com/jeffposnick/jeffposnick.github.io/blob/d0bfdf81b7f1ddc29a299d98919f506f5366182b/gulpfile.js">build process</a> that, among other things, generates a service worker for this site).</li>
<li>I'm not happy about the lack of control GitHub Pages offers over HTTP caching, in which everything is served with a <code>max-age</code> of 10 minutes. I really wanted to serve my <a href="https://jeffy.info/service-worker.js"><code>/service-worker.js</code></a> with <a href="http://stackoverflow.com/a/38854905/385997">HTTP caching disabled entirely</a>.</li>
<li>After experimenting with some of the various rewrites and optimizations that Cloudflare's proxy servers could perform, I'd turned all of them off, preferring instead to take any steps needed to minimize resources during the build process.</li>
</ul>
<p>For <a href="https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/">whatever reason</a>, last night seemed like a good night to actually switch to something else.</p>
<h2 id="migrating-to-firebase-hosting" tabindex="-1"><a class="header-anchor" href="#migrating-to-firebase-hosting">Migrating to Firebase Hosting</a></h2>
<p>I work with the folks responsible for <a href="https://firebase.google.com/docs/hosting/">Firebase Hosting</a>, so I won't pretend that this was an unbiased decision, but I'm quite glad that I decided to switch things over to them.</p>
<h3 id="deploying-to-jeffy-info.firebaseapp.com" tabindex="-1"><a class="header-anchor" href="#deploying-to-jeffy-info.firebaseapp.com">Deploying to jeffy-info.firebaseapp.com</a></h3>
<p>The first step was getting my site deployed to a subdomain under <code>firebaseapp.com</code>, to make sure that everything looked okay there. Given that I just was serving the entirety of a static <code>build/</code> folder, this was low-drama. A basic <a href="https://github.com/jeffposnick/jeffposnick.github.io/blob/d0bfdf81b7f1ddc29a299d98919f506f5366182b/firebase.json"><code>firebase.json</code></a> configuration was enough to specify my public <code>build/</code> directory, and also configure the cache policy I wanted for <code>/service-worker.js</code> at the same time:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
	<span class="hljs-attr">&quot;hosting&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
		<span class="hljs-attr">&quot;public&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;build&quot;</span><span class="hljs-punctuation">,</span>
		<span class="hljs-attr">&quot;headers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
			<span class="hljs-punctuation">{</span>
				<span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/service-worker.js&quot;</span><span class="hljs-punctuation">,</span>
				<span class="hljs-attr">&quot;headers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
					<span class="hljs-punctuation">{</span>
						<span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Cache-Control&quot;</span><span class="hljs-punctuation">,</span>
						<span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;no-cache&quot;</span>
					<span class="hljs-punctuation">}</span>
				<span class="hljs-punctuation">]</span>
			<span class="hljs-punctuation">}</span>
		<span class="hljs-punctuation">]</span>
	<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>Things looked fine when deployed to <code>jeffy-info.firebaseapp.com</code>, so the next step was to switch over the DNS entries for <code>jeffy.info</code> to point to Firebase Hosting's IP addresses, and <a href="https://firebase.google.com/docs/hosting/custom-domain#wait-for-ssl-certificate-provisioning">wait for Firebase Hosting to generate a new HTTPS certificate</a> for me.</p>
<h3 id="configuring-google-domains-dns" tabindex="-1"><a class="header-anchor" href="#configuring-google-domains-dns">Configuring Google Domains DNS</a></h3>
<p>This was arguably the hardest part of the migration, primarily because it's a change that might take up to an hour (or whatever the previous <a href="https://en.wikipedia.org/wiki/Time_to_live#DNS_records">DNS TTL</a> was) to go into effect. During that interval things might look broken in several different ways. But patience was called for, and things just started working once the DNS configuration changes and new certificate went into affect.</p>
<p>Assuming you're using Firebase Hosting + Google Domains, the relevant bits to look for are switching the Name Servers back to use Google's defaults, like so:</p>
<p><img src="/static/images/2017-02-24/google-domain-name-servers.png" alt="Google Domains Name Servers"></p>
<p>And then also adding in an <a href="https://en.wikipedia.org/wiki/List_of_DNS_record_types"><code>A</code> record</a> for the <code>@</code> entry, with the IP addresses provided by Firebase Hosting:</p>
<p><img src="/static/images/2017-02-24/google-domain-a-records.png" alt="A records"></p>
<h2 id="let's-encrypt-ftw" tabindex="-1"><a class="header-anchor" href="#let's-encrypt-ftw">Let's Encrypt FTW</a></h2>
<p>One of the original reasons I went with a Cloudflare proxy in front of my GitHub Pages deployment was because it was a simple way to get an HTTPS certificate for a custom domain. It's 2017 and <a href="https://letsencrypt.org/">Let's Encrypt</a> is very much a thing, and it was great to see that the HTTPS certificate that Firebase Hosting automatically generated for <code>https://jeffy.info</code> came from Let's Encrypt:</p>
<p><img src="/static/images/2017-02-24/lets-encrypt-cert.png" alt="Let's Encrypt HTTP certificate"></p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Offline-first for Your Templated Site (Part Two)]]></title>
            <link>/2017/01/24/offline-first-for-your-templated-site-part-2.html</link>
            <guid>/2017/01/24/offline-first-for-your-templated-site-part-2.html</guid>
            <pubDate>Tue, 24 Jan 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[Full-page caching vs. App Shell vs. service worker rendering.]]></description>
            <content:encoded><![CDATA[<p><em>This is the second part of a planned three-part series.</em></p>
<p><em>Part <a href="https://jeffy.info/2016/11/02/offline-first-for-your-templated-site-part-1.html">one</a> covered some basic terminology an setup that's referenced throughout this post.</em></p>
<p><em>Part three will dive into a specific service worker implementation that can be
used to provide an offline-first experience for a Jekyll-based site.</em></p>
<p><em>Folks who can't wait for part three are welcome to check out the
<a href="https://jeffy.info">https://jeffy.info</a>
service worker <a href="https://github.com/jeffposnick/jeffposnick.github.io/tree/work/src">implementation</a>
ahead of time.</em></p>
<p><em>I also covered the material in this blog post in a presentation at the
<a href="https://www.meetup.com/JAMstack-nyc/events/236530076/">{static is} The New Dynamic Meetup</a>:</em></p>
<iframe class="youtube-embed" src="https://www.youtube.com/embed/_kJMjJ1tm6o" allowfullscreen frameborder="0" loading="lazy"></iframe>
<h2 id="decisions%2C-decisions" tabindex="-1"><a class="header-anchor" href="#decisions%2C-decisions">Decisions, decisions</a></h2>
<p>With those preliminary definitions out of the way, we can focus on the question at hand: how do you provide an offline-first experience for your templated site?</p>
<p>I'm going to outline three different approaches, each with their own benefits and drawbacks. Choosing the right approach requires balancing various tradeoffs, and the aim of this post is to walk through the plusses and minuses of each strategies, so that you can make an informed decision about what works best for your site.</p>
<h2 id="option-1%3A-cache-entire-html-documents" tabindex="-1"><a class="header-anchor" href="#option-1%3A-cache-entire-html-documents">Option 1: Cache entire HTML documents</a></h2>
<h3 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works">How it works</a></h3>
<p>This approach uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage">Cache Storage API</a> to keep a copy of the fully rendered HTML document that corresponds to each URL. The documents might be precached when the service worker is installed, or the caches might be populated via a runtime caching strategy that adds to the cache as users browse from page to page.</p>
<h3 id="benefits" tabindex="-1"><a class="header-anchor" href="#benefits">Benefits</a></h3>
<h4 id="service-worker-simplicity" tabindex="-1"><a class="header-anchor" href="#service-worker-simplicity">Service worker simplicity</a></h4>
<p>The service worker needed to implement this type of strategy is <em>relatively</em> straightforward. The <code>fetch</code> handler can check to see whether <code>event.request.mode === 'navigate'</code>, and if so, use a <a href="https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate">stale-while-revalidate strategy</a> to handle the request for the HTML document.</p>
<h4 id="minimal-additional-maintenance" tabindex="-1"><a class="header-anchor" href="#minimal-additional-maintenance">Minimal additional maintenance</a></h4>
<p>While you need to create and deploy a service worker script, there isn't anything additional that you need to deploy and maintain. The same HTML documents that you've previously generated and deployed can continue to be served the same way.</p>
<h4 id="multiple-layout-template-flexibility" tabindex="-1"><a class="header-anchor" href="#multiple-layout-template-flexibility">Multiple layout template flexibility</a></h4>
<p>Instead of a single layout template, some sites might find themselves using multiple layouts to generate the final HTML. For example, all documents served from the URL prefix <code>/blog/</code> might use <code>blog_layout.tmpl</code>, while all documents served from under <code>/news/</code> might use <code>news_layout.tmpl</code>.</p>
<p>Because the complete HTML documents are stored and retrieved from the cache, the service worker doesn't need any special knowledge about which underlying layout was used to generate the page.</p>
<p>That also means that a subset of URLs that aren't generated via templating at all—perhaps a site's <code>about.html</code> page, for instance—can be handled via the same logic used for all the other URLs.</p>
<h3 id="drawbacks" tabindex="-1"><a class="header-anchor" href="#drawbacks">Drawbacks</a></h3>
<h4 id="cache-overhead" tabindex="-1"><a class="header-anchor" href="#cache-overhead">Cache overhead</a></h4>
<p>Let's return to our diagram of the &quot;<a href="https://jeffy.info/2016/11/02/offline-first-for-your-templated-site-part-1.html#the-process-that-smushes-together-the-templates-and-the-contents-and-outputs-a-final-html-document">smushening</a>&quot; process, which takes our template, combines it with each of our individual content files, and then results in a complete HTML document:</p>
<img src="/static/images/2016-11-02/smushening.svg" alt="A layout + content = final pages" class="half-width">
<p>The <code>blog_layout.tmpl</code> file is used to generate each of the final HTML documents, and that means that the size of each HTML file is roughly equal to the size of the template file plus the size of the page-specific content.</p>
<p>If your template file is <code>X</code> bytes, and your have <code>N</code> output HTML files, you're incurring <code>X * N</code> bytes of duplicated cache contents, due to size of the templated portion of each final HTML document.</p>
<p>If <code>X</code> is a fairly small number (a template file of less than 1kb is common) and/or <code>N</code> is small (say, only a few dozen unique HTML files), then the amount of cache storage used is negligible. However, if you're using larger templates, or you know that you have hundreds or even thousands of HTML pages that a user might have cached, the overhead can start impacting those users who are storage constrained.</p>
<h4 id="messy-updates" tabindex="-1"><a class="header-anchor" href="#messy-updates">Messy updates</a></h4>
<p>While the cache overhead isn't a showstopper, the compromises involved in updating previously cached content is more of a concern. Let's assume that we have <code>foo.html</code>, <code>bar.html</code>, and <code>foo_bar.html</code> HTML files stored in our cache.</p>
<p>If we were to make some updates to the <code>foo.md</code> content that's used to populate <code>foo.html</code>, our update strategy is to ensure that <code>foo.html</code> eventually gets refetched from the network. This might happen via a normal <a href="https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate">stale-while-revalidate</a> flow, in which case the new content will only be available the <em>second</em> time a user revisits the page. Or it might happen by explicitly purging the existing <code>foo.html</code> entry from the cache ahead of time, making the new content available for the <em>next</em> visit.</p>
<p>But what if we make an update to <code>blog_layout.tmpl</code>? Maybe we've changed our navigation bar, or updated some header text. The impact of this change ripples beyond a single cache entry—<em>all</em> of our cached HTML files that depended on <code>blog_layout.tmpl</code> are now out of date.</p>
<p>We have the same options for dealing with this as before: let a stale-while-revalidate caching strategy gradually update the stale entries as users revisit pages, or proactively purge the out of date cache content, which in this case could mean invalidating our entire cache. There are serious downsides to each approach, though.</p>
<p>If we rely on a <a href="https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate">stale-while-revalidate</a> runtime caching strategy to gradually update our cache, users will see inconsistencies across page navigations. The changes made to <code>blog_layout.tmpl</code> will only take effect after they revisit pages multiple times. If a user returns to a previously cached page months later, they'll still see your old layout, which can be jarring after they've gotten accustomed to seeing layout changes on fresh pages.</p>
<p>To avoid jarring your users, proactively purging <em>all</em> cached HTML that relies on a modified template is arguably the best approach. But now you're faced with another tradeoff: the performance and offline benefits of caching are diminished if users' caches are invalidated frequently. The effort that you put into implementing a caching strategy is wasted if you have to throw your entire cache away due to even small updates to your site's layout.</p>
<h3 id="real-world-examples" tabindex="-1"><a class="header-anchor" href="#real-world-examples">Real-world examples</a></h3>
<p>A number of my colleagues, including <a href="https://gauntface.com/blog/">Matt Gaunt</a>, <a href="https://paul.kinlan.me">Paul Kinlan</a>, and <a href="https://sgom.es/">Sérgio Gomes</a>, are using this option for their blogs.</p>
<h2 id="option-2%3A-use-an-application-shell-architecture" tabindex="-1"><a class="header-anchor" href="#option-2%3A-use-an-application-shell-architecture">Option 2: Use an Application Shell architecture</a></h2>
<h3 id="how-it-works-1" tabindex="-1"><a class="header-anchor" href="#how-it-works-1">How it works</a></h3>
<p>The Application Shell architecture is covered in great detail in <a href="https://developers.google.com/web/fundamentals/architecture/app-shell">this article</a> by Addy Osmani and Matt Gaunt. My talk from the 2015 Chrome Dev Summit also covers similar ground, for those who prefer video:</p>
<iframe class="youtube-embed" src="https://www.youtube.com/embed/jCKZDTtUA2A" allowfullscreen frameborder="0" loading="lazy"></iframe>
<p>While the Application Shell architecture is often talked about in reference to &quot;web apps,&quot; it's definitely applicable to the types of templated &quot;content sites&quot; that we're talking about here.</p>
<p>The general approach is to repurpose your existing page structure, i.e. what's defined in your <code>blog_layout.tmpl</code> template, to serve as your Application Shell. This should be a valid, standalone HTML file (let's call it <code>shell.html</code>) that includes a placeholder element that the dynamic content gets inserted into at runtime, via client-side templating.</p>
<p>The dynamic content could be the same raw content sources—<code>foo.md</code>, <code>bar.md</code>, etc.—in which case the Application Shell's JavaScript would need to <a href="https://github.com/markdown-it/markdown-it">convert</a> the content to HTML at runtime, prior to inserting it into the DOM. Or you might go through an additional step and convert the raw content into HTML snippets via a server-side process, in which case the content snippets can be inserted directly into the DOM at runtime.</p>
<p>You can use either a precaching or a runtime caching strategy like <a href="https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate">stale-while-revalidate</a> to keep both <code>shell.html</code> and the underlying content up to date, while still serving them cache-first.</p>
<h3 id="benefits-1" tabindex="-1"><a class="header-anchor" href="#benefits-1">Benefits</a></h3>
<h4 id="clean-updates" tabindex="-1"><a class="header-anchor" href="#clean-updates">Clean updates</a></h4>
<p>Your structural HTML (<code>shell.html</code>) and your content are cached independently, so when you make changes to a page's content or to your site's layout elements, cache invalidation is simple and efficient. The only cache entries that need to be updated is either the content itself, or the entry for <code>shell.html</code>. If <code>shell.html</code> does get updated, then the updates will immediately apply to all pages on your site that share that Application Shell, ensuring that your site looks the same as the user moves from page to page. You eliminate the risk of a months-old cached page being shown, jarring a user with an inconsistent layout.</p>
<h4 id="low-overhead-precaching" tabindex="-1"><a class="header-anchor" href="#low-overhead-precaching">Low-overhead precaching</a></h4>
<p>Using this architecture opens the door to precaching more of your site's content. Larger precache coverage means that pages a user hasn't previously navigated to will still work offline and load quickly. There are two reasons why aggressive precaching is more viable:</p>
<p>First, each piece of content can be cached as-is, independent of the App Shell's HTML. The <a href="#cache-overhead">cache overhead</a> concerns with full-page caching don't apply, and as a developer, you don't have to worry about wasting as much bandwidth and space as you would if you were precaching full HTML documents.</p>
<p>Second, and more importantly, <a href="#clean-updates">clean updates</a> mean that you're much less likely to end up throwing away data once it's been precached. The only time you'd have to expire and redownload precached content is if that specific content is updated. A substantial precache payload is makes much less sense if you know that it will all end up expired each time you tweak your site's template.</p>
<h3 id="drawbacks-1" tabindex="-1"><a class="header-anchor" href="#drawbacks-1">Drawbacks</a></h3>
<h4 id="routing-logic-in-your-service-worker" tabindex="-1"><a class="header-anchor" href="#routing-logic-in-your-service-worker">Routing logic in your service worker</a></h4>
<p>In the Application Shell model, your service worker needs to have special logic in place to handle <a href="https://html.spec.whatwg.org/#navigating-across-documents">navigation requests</a>. While the incoming request might be for a URL like <code>https://example.com/2016/12/foo.html</code>, your service worker needs to respond with your cached <code>shell.html</code> document, not with <code>foo.html</code> (which won't be cached in this model). Your Application Shell is then responsible for performing client-side templating and inserting the correct content into the DOM, based on whatever the request URL is.</p>
<p>This works fine when you only have one common layout, defined in <code>shell.html</code>, that's shared by all the pages on your site. But if there's a subset of pages on your site that use completely different layouts, like <code>https://example.com/about.html</code>, your service worker needs to know <em>not</em> to respond to those navigation requests with <code>shell.html</code>.</p>
<p>Your service worker is now an HTTP router, examining incoming navigations requests and serving the right type of response for each URL. If there's a simple URL pattern that can be used to match all of the requests that can be handled with the <code>shell.html</code> layout, then you're in good shape—something like the following might suffice:</p>
<pre><code class="hljs language-js">self.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;fetch&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
	<span class="hljs-keyword">const</span> yearMonthPrefix = <span class="hljs-keyword">new</span> <span class="hljs-title class_">RegExp</span>(<span class="hljs-string">&#x27;/d{4}/d{2}/&#x27;</span>);
	<span class="hljs-keyword">if</span> (event.<span class="hljs-property">request</span>.<span class="hljs-property">mode</span> === <span class="hljs-string">&#x27;navigation&#x27;</span>) {
		<span class="hljs-keyword">if</span> (event.<span class="hljs-property">request</span>.<span class="hljs-property">url</span>.<span class="hljs-title function_">matches</span>(yearMonthPrefix)) {
			<span class="hljs-comment">// Use the Application Shell to handle requests like</span>
			<span class="hljs-comment">// https://example.com/2016/12/foo.html</span>
			event.<span class="hljs-title function_">respondWith</span>(caches.<span class="hljs-title function_">match</span>(<span class="hljs-string">&#x27;shell.html&#x27;</span>));
		} <span class="hljs-keyword">else</span> {
			<span class="hljs-comment">// Use an appropriate runtime caching strategy, like</span>
			<span class="hljs-comment">// stale-while-revalidate, to handle requests like</span>
			<span class="hljs-comment">// https://example.com/about.html.</span>
		}
	} <span class="hljs-keyword">else</span> {
		<span class="hljs-comment">// Use an appropriate runtime caching strategy for</span>
		<span class="hljs-comment">// non-navigation requests, like requests for</span>
		<span class="hljs-comment">// images or other resources.</span>
	}
});
</code></pre>
<p>But if you don't have that level of consistency in your URL structure, of if there's a subset of pages that fall under the <code>/year/month/</code> prefix but use a completely different template, accurately reflecting that routing logic in your service worker gets much trickier and you need to construct a bespoke solution.</p>
<h4 id="duplicated-effort" tabindex="-1"><a class="header-anchor" href="#duplicated-effort">Duplicated effort</a></h4>
<p>If you've already got a templated site, adopting the Application Shell architecture will usually mean duplicating pieces of your existing infrastructure. You need to take your <code>blog_layout.tmpl</code> and convert it into a <code>shell.html</code> file, adding to it the necessary client-side templating logic to populate your shell.</p>
<p>You might also need to add in a build step that takes original Markdown sources and converts them to HTML snippets, in in lieu of having to perform that Markdown conversion inside your Application Shell.</p>
<p>Finally, if your site relies on any sort of custom routing or templating rules that are implemented server-side, that routing logic needs to be moved into your service worker, as explained in the <a href="#routing-logic-in-your-service-worker">previous section</a>.</p>
<p>This duplication means that there are more opportunities for pieces of your site to get out of sync. If you make changes to your <code>blog_layout.tmpl</code>, but the corresponding changes aren't made to <code>shell.html</code>, then browsers which lack service worker support will see one thing, while browsers that have a service worker responding with your Application Shell will see something else. Similar issues could arise if your routing rules need tweaking—they could potentially need to be changed in <em>three</em> places (server-side, client-side in <a href="https://en.wikipedia.org/wiki/Single-page_application">SPA-style</a> JavaScript, and client-side in service worker JavaScript) depending on how complicated your routing needs are. Trisomorphic routing: it's actually A Thing!</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr"><a href="https://twitter.com/adactioJournal">@adactioJournal</a> Shows there are fantastic opportunities, though. I&#39;ve yet to see someone nail &quot;trisomorphic&quot; routing (server + JS + SW).</p>&mdash; Nolan Lawson (@nolanlawson) <a href="https://twitter.com/nolanlawson/status/735469605238509569">May 25, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Proper automation of your build process can ensure that the overhead and risk of duplicated pieces getting out of sync is minimal, but that's one more thing to keep track of.</p>
<h3 id="real-world-examples-1" tabindex="-1"><a class="header-anchor" href="#real-world-examples-1">Real-world examples</a></h3>
<p>The <a href="https://ifixit-pwa.appspot.com/">iFixit PWA</a> sample (the <a href="https://github.com/GoogleChrome/sw-precache/tree/master/app-shell-demo">source</a> of which is part of the <a href="https://github.com/GoogleChrome/sw-precache">sw-precache project</a>) is an Application Shell populated with dynamic content from the <a href="https://www.ifixit.com/api/2.0/doc/">iFixit API</a>.</p>
<h2 id="option-3%3A-service-worker-templating" tabindex="-1"><a class="header-anchor" href="#option-3%3A-service-worker-templating">Option 3: Service worker templating</a></h2>
<h3 id="how-it-works-2" tabindex="-1"><a class="header-anchor" href="#how-it-works-2">How it works</a></h3>
<p>With this approach, you take the logic needed to smush together your templates and content and implement it within your service worker.</p>
<p>This doesn't mean that you'd do away with your site's existing build process, though. You can't assume that when a user visits your site there's going to be an active service worker, so you still need to serve complete HTML pages via your normal web server. But when a user returns to your site using a browser that supports service workers, they no longer have to request those complete HTML pages in order to display your site. They should already have your site's templates cached, and they may or may not have the page's content cached as well. So, at worst, only a very minimal amount of page-specific content needs to be requested, and at best, your service worker can assemble the complete HTML immediately, without having to go to the network at all.</p>
<h3 id="benefits-2" tabindex="-1"><a class="header-anchor" href="#benefits-2">Benefits</a></h3>
<h4 id="clean-updates-and-low-overhead-precaching" tabindex="-1"><a class="header-anchor" href="#clean-updates-and-low-overhead-precaching">Clean updates and low-overhead precaching</a></h4>
<p>Both the <a href="#clean-updates">clean updates</a> and <a href="#low-overhead-precaching">low-overhead precaching</a> benefits of the App Shell model apply here, as well.</p>
<h4 id="no-need-to-adopt-single-page-app-patterns" tabindex="-1"><a class="header-anchor" href="#no-need-to-adopt-single-page-app-patterns">No need to adopt Single Page App patterns</a></h4>
<p>While the App Shell approach might seem familiar to developers who are familiar with writing client-side JavaScript and using the <a href="https://en.wikipedia.org/wiki/Single-page_application">Single Page App</a> (SPA) pattern, not every developer who manages a templated site with be comfortable with that model. You don't have to write and deploy a SPA when you use the service worker templating option, but you end up with most of the same benefits.</p>
<p>(Of course, there's a significant engineering effort required to properly implement service worker templating, but it's a different kind of effort…)</p>
<h3 id="drawbacks-2" tabindex="-1"><a class="header-anchor" href="#drawbacks-2">Drawbacks</a></h3>
<h4 id="a-javascript-friendly-templating-system-is-a-must" tabindex="-1"><a class="header-anchor" href="#a-javascript-friendly-templating-system-is-a-must">A JavaScript-friendly templating system is a must</a></h4>
<p>This approach is only viable if you're using a templating system that has a JavaScript implementation. And because the code will be run inside of a service worker, JavaScript code that requires features specific to the node environment, like filesystem support, won't work.</p>
<p>Fortunately, the JavaScript ecosystem is vibrant, and there's a decent chance that you'd <a href="https://npms.io/search?q=template">find</a> a JavaScript implementation of many templating systems. Running the JavaScript code through <a href="https://github.com/substack/node-browserify">browserify</a> can often smooth over the differences between the node and service worker runtime environments.</p>
<h4 id="heavyweight-service-worker-code" tabindex="-1"><a class="header-anchor" href="#heavyweight-service-worker-code">Heavyweight service worker code</a></h4>
<p>In order to get your templating system working inside of your service worker, you'll almost certainly need to bundle in a number of external dependencies. Compared to a svelte service worker that implements a basic runtime caching strategy, you'll need to transfer more bytes of JavaScript each time your service worker is fetched from the network.</p>
<p>Your service worker is almost certainly going to spend more time executing code, since the &quot;<a href="https://jeffy.info/2016/11/02/offline-first-for-your-templated-site-part-1.html#the-process-that-smushes-together-the-templates-and-the-contents-and-outputs-a-final-html-document">smushening</a>&quot; process that would otherwise be done at build-time is effectively run within a user's browser each time it display a page. The amount of overhead that this adds depends on how efficient your JavaScript templating system is and how powerful each of your users' devices are.</p>
<h4 id="routing-logic-in-your-service-worker-1" tabindex="-1"><a class="header-anchor" href="#routing-logic-in-your-service-worker-1">Routing logic in your service worker</a></h4>
<p>This was covered in detail in the App Shell's drawbacks section; the same routing considerations apply here. The main difference is that instead of serving a cached <code>shell.html</code> file when the route matches, you need to kick off your templating logic when there's a matching navigation request.</p>
<h4 id="duplicated-effort-1" tabindex="-1"><a class="header-anchor" href="#duplicated-effort-1">Duplicated effort</a></h4>
<p>This is also similar to the drawback with using an App Shell. Using this approach requires that you continue to run the &quot;<a href="https://jeffy.info/2016/11/02/offline-first-for-your-templated-site-part-1.html#the-process-that-smushes-together-the-templates-and-the-contents-and-outputs-a-final-html-document">smushening</a>&quot; process like you were previously doing, but then additionally start serving not only the final HTML documents, but also the unprocessed template and content files, since those pieces will need to be fetched and cached by the service worker. You'll need to make sure that whenever a template or content is updated, both the final HTML as well as the raw files are updated on your server.</p>
<p>You'll also probably end up duplicating some of the work that your current build process does to generate metadata about your site. For example, Jekyll maintains it own list of recent posts and uses it to populate the index page for a site. Your service worker needs similar data in order to construct the same page, so writing your own code to <a href="https://github.com/jeffposnick/jeffposnick.github.io/blob/work/gulpfile.js#L34">generate</a> the metadata in a <a href="https://raw.githubusercontent.com/jeffposnick/jeffposnick.github.io/master/posts.json">format</a> that your service worker could consume, and keeping that metadata in sync whenever you update your site, is now required.</p>
<h3 id="real-world-examples-2" tabindex="-1"><a class="header-anchor" href="#real-world-examples-2">Real-world examples</a></h3>
<p>My personal blog, <a href="https://jeffy.info/">https://jeffy.info/</a>, is currently using <a href="https://github.com/jeffposnick/jeffposnick.github.io/tree/work/src">service worker templating</a>. We'll dive into the specifics of how that's implemented in the next part of this series!</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Offline-first for Your Templated Site (Part One)]]></title>
            <link>/2016/11/02/offline-first-for-your-templated-site-part-1.html</link>
            <guid>/2016/11/02/offline-first-for-your-templated-site-part-1.html</guid>
            <pubDate>Wed, 02 Nov 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[Terminology and an exploring the architecture.]]></description>
            <content:encoded><![CDATA[<p><em>This is the first part of a planned three-part series.</em></p>
<p><em><a href="https://jeffy.info/2017/01/24/offline-first-for-your-templated-site-part-2.html">Part two covers</a> different strategies for caching and serving templated sites.</em></p>
<p><em>Part three will dive into a specific service worker implementation that can be
used to provide an offline-first experience for a Jekyll-based site.</em></p>
<p><em>Folks who can't wait for part three are welcome to check out the
<a href="https://jeffy.info">https://jeffy.info</a>
service worker <a href="https://github.com/jeffposnick/jeffposnick.github.io/tree/work/src">implementation</a>
ahead of time.</em></p>
<h2 id="what's-offline-first%3F" tabindex="-1"><a class="header-anchor" href="#what's-offline-first%3F">What's offline-first?</a></h2>
<p>Let's see if I can get away with just embedding a tweet:</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">…&quot;Offline first&quot; is a serving strategy. Get as far as you can with local data before going to the network.<br><br>(serve-)offline-(content-)first.</p>&mdash; Jake Archibald (@jaffathecake) <a href="https://twitter.com/jaffathecake/status/788289680735436800">October 18, 2016</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>That's offline-first.</p>
<h2 id="what's-a-templated-site%3F" tabindex="-1"><a class="header-anchor" href="#what's-a-templated-site%3F">What's a templated site?</a></h2>
<p>What I'm calling a <em>templated site</em> is built using multiple templates, combined with the actual text, images and other resources that make up the site's content, spread across multiple pages. The site's URLs uniquely identify the content specific to a page. The templates provide a structural layout shared across the pages.</p>
<p>This is all a convoluted way of describing what's otherwise known as a &quot;content site&quot;, but that's deliberate: I want to avoid all the baggage that comes along with the &quot;content site&quot; label, especially the implication of what it's not—a &quot;web app&quot;. What I'm describing are considerations for a particular architectural equation: templates + content = your site. If you've got templates, and some source of content, and you mix them together (either ahead of time during a build process, or at runtime on a server), then you've got a templated site! If you want to also call it a web app, cool; if you want to call it a content site, or a blog, or a CMS-powered site, that's totally cool too.</p>
<p>Since it's always fun to draw contrasts, if I did have to hold up something in opposition to a templated site, it would be a <a href="https://en.wikipedia.org/wiki/Single-page_application">single page application</a>. There's a meaningful difference between the way SPAs handle navigations (using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/History">History API</a> to rewrite URLs, and swapping content out of an &quot;<a href="https://developers.google.com/web/updates/2015/11/app-shell">application shell</a>&quot; container), and how templated sites handle navigations (as actual <em><a href="https://html.spec.whatwg.org/multipage/browsers.html#navigate">navigations</a></em>, in which the current DOM is torn down, and built up again based on the new HTML). SPAs fill an important role in the web's ecosystem, and they are the right architecture to use for certain types of projects, but they're different beasts than templated sites.</p>
<p>In case it's not clear from that preamble, what you're reading now, assuming it's being read on https://jeffy.info/, is part of a templated site. Blogs, newspapers, and whole host of sites driven drive by a backend <a href="https://en.wikipedia.org/wiki/Content_management_system">content management system</a> are traditionally deployed as templated sites. If you're reading the syndicated copy of this on https://medium.com/, then I'm going to hazard a guess that the &quot;templated site&quot; label is also appropriate, but I'm not as familiar with how they implement things. <code>¯\_(ツ)_/¯</code></p>
<h2 id="anatomy-of-a-templated-site" tabindex="-1"><a class="header-anchor" href="#anatomy-of-a-templated-site">Anatomy of a templated site</a></h2>
<p>All right: with what's hopefully a non-controversial set of a definitions out of the way, let's take a deeper dive into the different pieces that work together to produce a templated site. I'm going to focus on a specific framework, <a href="https://jekyllrb.com/">Jekyll</a>, which I'm familiar with and that I use on https://jeffy.info/. The general concepts, if not the specific formats used for all the pieces, should apply broadly to other frameworks.</p>
<h3 id="templates!" tabindex="-1"><a class="header-anchor" href="#templates!">Templates!</a></h3>
<p>Yes, templates. Templated sites have templates. Moving along…</p>
<p>Well, okay, there's more to explore here. Templates are usually small chunks of HTML that fill a certain structural role on a page—the header, the sidebar, the footer, etc. There's also likely to be a template that defines the HTML structure for the main bit of a page—the content—and which contains an insertion point where the words are plunked in from the content management system (more on that soon). These various templates are laid out in a… <a href="http://jekyll.tips/jekyll-casts/layouts/">layout</a>, which can be expressed as a template composed of those sub-templates.</p>
<p>Unfortunately, I lack the visual skills of some of my <a href="https://jakearchibald.com/2014/offline-cookbook/#on-install-as-a-dependency">colleagues</a>, but here's a rough sketch of what we're talking about, with the hypothetical smaller templates that work together to form a layout template for blog post:</p>
<img src="/static/images/2016-11-02/blog_layout.svg" alt="Templates come together to form a blog post layout" class="half-width">
<p>The interesting bit here is that each of those smaller templates can be tweaked and modified individually, and sometimes (but not always) don't have to coordinate with each other or with the parent layout. You can imagine a rebranding in which a product name is changed in <code>head.tmpl</code>, or a copyright year is changed in <code>foot.tmpl</code>, without that requiring a larger change to any of the other templates or layouts.</p>
<h3 id="content!" tabindex="-1"><a class="header-anchor" href="#content!">Content!</a></h3>
<p>Next up, let's assume that we have some content (like: this post) that we want to share with the world. That content might live in a database. It might live in individual files—Jekyll uses individual <a href="https://jekyllrb.com/docs/posts/">Markdown files</a>. It might be accessed as JSON returned from a content management system's API. The important bits are the idea that there's a logical separation between the content for one page and the content for another, and the idea that content is independent from the overall page structure that's used to display it.</p>
<p>I don't know how to make diagrams of abstract content interesting, but... here's a depiction of content for you:</p>
<img src="/static/images/2016-11-02/content.svg" alt="An abstract representation of content" class="half-width">
<h3 id="the-process-that-smushes-together-the-templates-and-the-contents-and-outputs-a-final-html-document!" tabindex="-1"><a class="header-anchor" href="#the-process-that-smushes-together-the-templates-and-the-contents-and-outputs-a-final-html-document!">The process that smushes together the templates and the contents and outputs a final HTML document!</a></h3>
<p>We've got some templates that are stitched together to make a layout, and we've got some content. What's left is smushing the layout and the content together to form a unique HTML document corresponding to each piece of source of content.</p>
<p>The specifics of the smushing vary greatly depending on what framework you're using. When you know that you have a potentially huge corpus of content (think: the entire archives of a newspaper), you might opt for a framework that smushes on demand, server-side, in response to a request from a client. When you're dealing with a slightly slimmer corpus (https://jeffy.info/: four posts and counting!), or if you want to avoid running custom code on a server, it can make sense to use a framework that smushes everything ahead of time. The you can serve the final HTML using any run of the mill HTTP server. That's what the Jekyll framework <a href="https://jekyllrb.com/docs/usage/">does</a>.</p>
<p>Here's your requisite diagram:</p>
<img src="/static/images/2016-11-02/smushening.svg" alt="A layout + content = final pages" class="half-width">
<p>The complete HTML documents are then displayed by the browser, blissfully unawares of all the behind-the-scenes architecture that was needed to get to this point.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[create-react-pwa]]></title>
            <link>/2016/08/20/create-react-pwa.html</link>
            <guid>/2016/08/20/create-react-pwa.html</guid>
            <pubDate>Sat, 20 Aug 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[What's it take to turn a create-react-app project into a Progressive Web App?]]></description>
            <content:encoded><![CDATA[<h2 id="tl%3Bdr" tabindex="-1"><a class="header-anchor" href="#tl%3Bdr">tl;dr</a></h2>
<p>If you'd like to turn the output of <a href="https://github.com/facebookincubator/create-react-app"><code>create-react-app</code></a> into a <a href="https://developers.google.com/web/progressive-web-apps/">progressive web app</a> (PWA) with offline support, take a look at the <a href="https://github.com/jeffposnick/create-react-pwa"><code>create-react-pwa</code></a> repo, and in particular, the <a href="https://github.com/jeffposnick/create-react-pwa/compare/starting-point...pwa">GitHub diff</a> of the minimal changes required.</p>
<h2 id="create-react-app..." tabindex="-1"><a class="header-anchor" href="#create-react-app...">create-react-app...</a></h2>
<p><a href="https://facebook.github.io/react/blog/2016/07/22/create-apps-with-no-configuration.html"><code>create-react-app</code></a> solves a problem that many folks (including I) have run into: how can you get started with a straightforward, client-rendered, single page React app and build process, without spending hours researching a list of (sometimes conflicting) dependencies to list in your <code>package.json</code>? It's a project that came out of Facebook's React engineering a few months back, and has since gained some significant mindshare in the web developer community.</p>
<h2 id="...and-its-limitations" tabindex="-1"><a class="header-anchor" href="#...and-its-limitations">...and its limitations</a></h2>
<p>In keeping with the fewer-depencies-are-better-dependencies philosophy behind <code>create-react-app</code>, the maintainers <a href="https://github.com/facebookincubator/create-react-app/issues/192">appear to be vigilant</a> in the fight against scope creep, and are keeping the project lean. That's understandable, and it's left up to end users to decide what additional functionality to layer on top of the project's skeleton.</p>
<p>By design, the core of the build process isn't directly extensible. It's kicked off by <code>npm run build</code> and consists of</p>
<pre><code class="hljs language-json"><span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;react-scripts build&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>But! The opportunity's there to cleanly chain some additional commands after that initial <code>react-scripts build</code>, while keeping the <code>react-scripts</code> black box opaque, giving us the chance to modify the output of the original build process.</p>
<h2 id="an-opportunity-for-pwa-ification" tabindex="-1"><a class="header-anchor" href="#an-opportunity-for-pwa-ification">An Opportunity for PWA-ification</a></h2>
<p>That gets to the heart of my investigation: what's the easiest way to guide developers starting from a fresh <code>create-react-app</code> towards the end goal of deploying a <a href="https://developers.google.com/web/progressive-web-apps/">progressive web app</a>? Specifically, what would it take to add in a <a href="https://developers.google.com/web/fundamentals/primers/service-worker/">service worker</a> that provided performance benefits as well as a meaningful offline experience, as well as a <a href="https://developers.google.com/web/updates/2014/11/Support-for-installable-web-apps-with-webapp-manifest-in-chrome-38-for-Android">web app manifest</a> that contained application metadata?</p>
<h3 id="adding-a-web-app-manifest" tabindex="-1"><a class="header-anchor" href="#adding-a-web-app-manifest">Adding a Web App Manifest</a></h3>
<p>This is just a <a href="https://developer.mozilla.org/en-US/docs/Web/Manifest">standard</a> JSON file with fields containing metadata useful for controlling the &quot;add to homescreen&quot; experience (currently only on Android devices). While each developer needs to choose their metadata wisely, there's nothing particularly challenging here from a technical perspective, and we just need to create the file and get it copied over to the <code>build/</code> output directory as part of the <code>npm run build</code> process.</p>
<h3 id="adding-a-service-worker" tabindex="-1"><a class="header-anchor" href="#adding-a-service-worker">Adding a Service Worker</a></h3>
<p>To handle the service worker, I (not surprisingly) turned to the <a href="https://github.com/GoogleChrome/sw-precache"><code>sw-precache</code></a> project. When added to a build process, it generates a service worker JavaScript file that will automatically version and keep all local static files—HTML, JavaScript, CSS, images, etc.—up to date. Because <em>all</em> of the <code>create-react-app</code> output is local static files (there's no server-side rendering or remote API calls by default, for example), <code>sw-precache</code> can handle everything we need with little configuration needed.</p>
<p>While I've traditionally used <code>sw-precache</code> as a JavaScript module inside of a <a href="http://gulpjs.com/"><code>gulp</code> build</a> process, it also sports a <a href="https://github.com/GoogleChrome/sw-precache#command-line-interface">command-line interface</a>, and it's simple to add in the <code>sw-precache</code> command to the end of the <code>npm run build</code> script chain.</p>
<h3 id="the-user-experience" tabindex="-1"><a class="header-anchor" href="#the-user-experience">The User Experience</a></h3>
<p>Here's what the deployed PWA looks like on Chrome for Android, going through the steps of accepting the Add to Homescreen banner, and then launching it without a network connection.</p>
<iframe class="youtube-embed" src="https://www.youtube.com/embed/nV8sKoVbD5Q?rel=0" frameborder="0" loading="lazy" allowfullscreen></iframe>
<h2 id="try-it-yourself" tabindex="-1"><a class="header-anchor" href="#try-it-yourself">Try it Yourself</a></h2>
<p>The best way to visualize the changes to the build process, as well as the (small) modifications needed to the source code, is to view the <a href="https://github.com/jeffposnick/create-react-pwa/compare/starting-point...pwa">GitHub diff</a> between the starting point and the &quot;final&quot; PWA.</p>
<p>Take &quot;final&quot; with a grain of salt, because while you will end up with a build process that yields a progressive web app, there are many directions you might want to take the starter project, and some of those might entail a more complex service worker implementation. I've tried to cover <a href="https://github.com/jeffposnick/create-react-pwa#what-additional-changes-might-be-needed">some scenarios</a> in the repo's README, including <a href="https://github.com/jeffposnick/create-react-pwa#ive-added-in-react-router-and-now-my-urls-dont-work-offline">steps to take</a> if you're using <code>react-router</code> and need arbitrary URLs to work offline, as well as how you can add in <a href="https://github.com/jeffposnick/create-react-pwa#im-using-cross-origin-apis-or-resources-and-they-arent-working-while-offline">runtime caching strategies</a> for calls to third party APIs and resources.</p>
<h2 id="beyond-create-react-app" tabindex="-1"><a class="header-anchor" href="#beyond-create-react-app">Beyond create-react-app</a></h2>
<p>If you're looking for examples of PWAs build with React that go a bit beyond what <code>create-react-app</code> offers—for instance, that take advantage of server-side rendering or are built on third-party APIs—there are a few examples I could recommend:</p>
<ul>
<li><a href="https://github.com/GoogleChrome/sw-precache/tree/master/app-shell-demo">iFixit PWA</a> (<a href="https://ifixit-pwa.appspot.com/">live deployment</a>)</li>
<li><a href="https://github.com/insin/react-hn">Hacker News PWA</a> (<a href="https://react-hn.appspot.com/">live deployment</a>)</li>
</ul>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[Hosting Setup]]></title>
            <link>/2014/11/28/hosting-setup.html</link>
            <guid>/2014/11/28/hosting-setup.html</guid>
            <pubDate>Fri, 28 Nov 2014 00:00:00 GMT</pubDate>
            <description><![CDATA[A step-by-step guide to hosting a blog on HTTPS-enabled custom domain.]]></description>
            <content:encoded><![CDATA[<p><em>All of the info in this post is long out of date. See <a href="/2021/07/17/sw-rendering.html">Service worker rendering, in the cloud and in the browser</a> for the latest setup.</em></p>
<h2 id="blogging-software" tabindex="-1"><a class="header-anchor" href="#blogging-software">Blogging Software</a></h2>
<p>I decided to go with <a href="http://jekyllrb.com/">Jekyll</a>, which ships templates for blog posts, but makes it simple enough to put together standalone pages.</p>
<h2 id="web-hosting" tabindex="-1"><a class="header-anchor" href="#web-hosting">Web Hosting</a></h2>
<p><a href="https://help.github.com/categories/github-pages-basics/">GitHub Pages</a> is free, which is nice, and (obviously) has first-class support for version control.
GitHub Pages <a href="https://help.github.com/articles/using-jekyll-with-pages/">natively supports Jekyll</a>, which means I just need to add the raw material to source control, and the <code>_site</code> content will be automatically generated.
If you don't care about a custom domain then you can stop at this point—you can serve your site off of https://<em>youraccount</em>.github.io. No effort required for HTTPS support!
<a href="https://github.com/jeffposnick/jeffposnick.github.io">jeffposnick.github.io</a> is the GitHub repo with this site's content, if you want to check out what's going on under the hood.</p>
<h2 id="registering-the-domain" tabindex="-1"><a class="header-anchor" href="#registering-the-domain">Registering the Domain</a></h2>
<p>I registered &quot;jeffy.info&quot; using <a href="https://domains.google.com">Google Domains</a>.
Google Domains is in invite-only beta mode at the moment, but using an alternative domain registrar shouldn't change the process much.
(I was surprised at how easy it was to get a <code>.info</code> domain—for whatever reason, they don't seem to be very popular. Check them out rather than settling for some obscure <code>.com</code>.)</p>
<h2 id="add-a-cname-file-to-github" tabindex="-1"><a class="header-anchor" href="#add-a-cname-file-to-github">Add a <code>CNAME</code> file to GitHub</a></h2>
<p>As per the <a href="https://help.github.com/articles/adding-a-cname-file-to-your-repository/">instructions</a>, I added a <code>CNAME</code> file to the top-level of the repo and pushed to GitHub.</p>
<h2 id="cloudflare-setup" tabindex="-1"><a class="header-anchor" href="#cloudflare-setup">CloudFlare Setup</a></h2>
<p>This was my first time using CloudFlare, so I needed to set up a (free) account.
I told CloudFlare that I wanted it to take control over the &quot;jeffy.info&quot; domain, and I set up two <code>A</code> records for the bare (&quot;apex&quot;) domain &quot;jeffy.info&quot; to resolve to the <a href="https://help.github.com/articles/tips-for-configuring-an-a-record-with-your-dns-provider/">two published IP addresses</a> for GitHub's servers.
I also set up a <code>CNAME</code> record for &quot;www&quot; as an alias of &quot;jeffposnick.github.io&quot;, so that &quot;www.jeffy.info&quot; will resolve.</p>
<p><img src="/static/images/cloudflare_dns_settings.png" alt="CloudFlare settings screenshot"></p>
<h2 id="changing-the-google-domains-dns-settings" tabindex="-1"><a class="header-anchor" href="#changing-the-google-domains-dns-settings">Changing the Google Domains DNS Settings</a></h2>
<p>In order for CloudFlare to start serving traffic for &quot;jeffy.info&quot;, I needed to change the authoritative DNS settings for the domain to point to CloudFlare's severs.
This was straight forward with the Google Domains UI (refer to your registrar's online help if you're using something else):</p>
<p><img src="/static/images/google_domains_custom_dns.png" alt="Google Domains custom DNS settings"></p>
<h2 id="https-only-setup" tabindex="-1"><a class="header-anchor" href="#https-only-setup">HTTPS-only Setup</a></h2>
<p>CloudFlare auto-provisioned a certificate for &quot;jeffy.info&quot; withing a couple of minutes, which was awesome and as painless as promised.
Make sure you use the &quot;Flexible SSL&quot; option, which is required to work with a GitHub Pages backend server.
Using &quot;Flexible SSL&quot; means that the GitHub Pages ⇔ CloudFlare traffic isn't encrypted, but the CloudFlare ⇔ user's browser traffic is.
That's sufficient for my goal of enabling web functionality that requires HTTPS to the end user's browser, but it's probably not a good idea if you're concerned about traffic vulnerabilities in the GitHub Pages ⇔ CloudFlare hop.
I want to make sure that my blog is HTTPS-only, and CloudFlare's page rules interface allowed me to set that up:</p>
<p><img src="/static/images/cloudflare_page_rules.png" alt="HTTPS-only via CloudFlare's page rules"></p>
<h2 id="optimizing-content" tabindex="-1"><a class="header-anchor" href="#optimizing-content">Optimizing Content</a></h2>
<p>Since CloudFlare is a full-fledged CDN, they offer a number of content optimization options. In general, I turned off most of the settings, but I did enable HTML + JS + CSS minification:</p>
<p><img src="/static/images/cloudflare_auto_minify.png" alt="CloudFlare's web optimization settings"></p>
<p>Since I'm relying on GitHub Pages to do my Jekyll's <code>_site</code> build, it seemed easiest to just let CloudFlare handle the minification rather than trying to work out a scripted solution.</p>
<p>CloudFlare offers image minification as well, but only as part of their paid offering. So I'm using <a href="https://www.npmjs.org/package/gulp-imagemin"><code>gulp-imagemin</code></a> locally on all my images, and pointing to the minified versions in posts. Be sure to minify your images!</p>
<h2 id="future-tweaks" tabindex="-1"><a class="header-anchor" href="#future-tweaks">Future Tweaks</a></h2>
<p>I'm not particularly happy with the default Jekyll templates, and have already made a few changes.
I'm sure there will be lots of twiddling with that to come.
I'm also not particularly familiar with the best practices for a build workflow. I've got a basic <code>gulpfile.js</code> that I'm sure will grow over time.</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
        <item>
            <title><![CDATA[thirty-four on the Web (Again)]]></title>
            <link>/2014/11/28/thirty-four-on-the-web-again.html</link>
            <guid>/2014/11/28/thirty-four-on-the-web-again.html</guid>
            <pubDate>Fri, 28 Nov 2014 00:00:00 GMT</pubDate>
            <description><![CDATA[But first, some administrivia.]]></description>
            <content:encoded><![CDATA[<h2 id="about-me" tabindex="-1"><a class="header-anchor" href="#about-me">About Me</a></h2>
<p>I'm <a href="https://twitter.com/jeffposnick">Jeff Posnick</a>.
I've been working on the <a href="https://developers.google.com/careers/">Developer Relations</a> team at Google NYC since 2007, first on the <a href="https://developers.google.com/adwords/api/">Ads APIs</a>, then <a href="https://developers.google.com/youtube/">YouTube</a>, and currently on the <a href="http://www.webplatform.org/">Open Web Platform</a>.</p>
<h2 id="about-this-blog" tabindex="-1"><a class="header-anchor" href="#about-this-blog">About this Blog</a></h2>
<p>I've been blogging <a href="http://thirtyfour.blogspot.com/">elsewhere</a> since 2003.
The other 34 started out using <a href="http://en.wikipedia.org/wiki/Blosxom">Blosxom</a> on a self-hosted web server.
I moved it over to Blogger eventually.</p>
<p>I'm going to keep <a href="http://thirtyfour.blogspot.com/">34</a> around for personal topics.
This blog, <a href="https://jeffy.info/">thirty-four</a>, is going to focus on web technologies.
Serving it via HTTPS, and having full control over the HTML/JavaScript/CSS, means that I'll be able to take advantage of some of the new features being added to the web platform, like those powered by <a href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html">service workers</a>.</p>
<p>Stay tuned for a post detailing this blog's setup (spoiler alert: it's a <a href="http://jekyllrb.com/">Jekyll</a> site hosted on <a href="https://help.github.com/categories/github-pages-basics/">GitHub pages</a>, with the <a href="https://www.cloudflare.com/">CloudFlare CDN</a> providing <a href="http://blog.cloudflare.com/easiest-ssl-ever-now-included-automatically-w/">HTTPS support</a>.)</p>
]]></content:encoded>
            <author>blog@posnick.org (Jeff Posnick)</author>
        </item>
    </channel>
</rss>