<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/css" href="https://www.aaron-gustafson.com/c/feed.min.css" ?><feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:amg="https://www.aaron-gustafson.com.com/amg-dtd/"><title>Aaron Gustafson: Content tagged web development</title><subtitle>The latest 20 posts and links tagged web development.</subtitle><id>https://www.aaron-gustafson.com</id><link href="https://www.aaron-gustafson.com/feeds/web-development.xml" rel="self"/><link href="https://www.aaron-gustafson.com"/><author><name>Aaron Gustafson</name><uri>https://www.aaron-gustafson.com</uri></author><updated>2026-04-29T12:30:00Z</updated><entry><id>https://www.aaron-gustafson.com/notebook/links/ai-assisted-coding-transforms-pdf-to-web-app-using-nys-design-system/</id><title type="html"><![CDATA[🔗 AI-assisted coding transforms PDF to web app using NYS Design System]]></title><link href="https://www.aaron-gustafson.com/notebook/links/ai-assisted-coding-transforms-pdf-to-web-app-using-nys-design-system/" rel="alternate" type="text/html" /><link href="https://www.linkedin.com/posts/plasticmind_designsystem-ai-civictech-share-7420295564430450688-0RM5/" rel="related" type="text/html" /><published>2026-04-29T12:30:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a compelling illustration of what becomes possible when a design system is structured well enough to be usable by both people and machines.</p><p>What’s exciting to me here is not “AI turned a PDF into an app” so much as what made that possible: components, tokens, utilities, and enough design-system clarity to give the model something useful to work with. That’s a big part of the story so many people miss. If your system is inconsistent, vague, or poorly documented, AI is just going to amplify that mess.</p>]]></content><amg:twitter><![CDATA[A well-structured design system is not just a delivery mechanism for consistency; it’s also critical infrastructure for AI-assisted coding.]]></amg:twitter><amg:summary><![CDATA[<p>This is a compelling illustration of what becomes possible when a design system is structured well enough to be usable by both people and machines.</p>]]></amg:summary><summary type="html"><![CDATA[<p>This is a compelling illustration of what becomes possible when a design system is structured well enough to be usable by both people and machines.</p>]]></summary><category term="AI/ML" /><category term="pattern libraries" /><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://static.licdn.com/aero-v1/sc/h/c45fy346jw096z9pbphyyhdz7" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/the-incredible-overcomplexity-of-the-shadcn-radio-button/</id><title type="html"><![CDATA[🔗 The Incredible Overcomplexity of the Shadcn Radio Button]]></title><link href="https://www.aaron-gustafson.com/notebook/links/the-incredible-overcomplexity-of-the-shadcn-radio-button/" rel="alternate" type="text/html" /><link href="https://paulmakeswebsites.com/writing/shadcn-radio-button/" rel="related" type="text/html" /><published>2026-04-29T12:15:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p><em>It’s just a radio button.</em></p><p>Paul’s teardown is a good illustration of how far modern front-end practice can drift from the simple, robust platform primitives we already have.</p><blockquote><p>If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.</p></blockquote><p>Reading this reminded me of a pair of pieces I’ve linked to before: <a href="/links/2015-09-01-making-radio-buttons-and-checkboxes-easier-to-use/">Making radio buttons and checkboxes easier to use</a> and <a href="/links/2022-12-11-there-can-be-only-one-options-for-building-choose-one-fields/">There can be only one: Options for building “choose one” fields</a>.</p><p>We have spent an astonishing amount of time and energy overcomplicating a control HTML already gives us, often in the name of developer experience or styleability. More often than not, all we’re really doing is trading resilience for ceremony.</p>]]></content><amg:twitter><![CDATA[It’s just a radio button. We really don’t need two component libraries, ARIA role remapping, and client-side JS to do what HTML already does.]]></amg:twitter><amg:summary><![CDATA[<p>It’s just a radio button. Paul’s teardown is a good illustration of how far modern front-end practice can drift from the simple, robust platform primitives we already have.</p>]]></amg:summary><summary type="html"><![CDATA[<p>It’s just a radio button. Paul’s teardown is a good illustration of how far modern front-end practice can drift from the simple, robust platform primitives we already have.</p>]]></summary><category term="HTML" /><category term="accessibility" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/under-the-hood-of-mdns-new-frontend/</id><title type="html"><![CDATA[🔗 Under the hood of MDN’s new frontend]]></title><link href="https://www.aaron-gustafson.com/notebook/links/under-the-hood-of-mdns-new-frontend/" rel="alternate" type="text/html" /><link href="https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/" rel="related" type="text/html" /><published>2026-04-24T12:05:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a fascinating look at how MDN rebuilt its frontend around server-rendered HTML, web components, and a clear understanding of where interactivity actually belongs (and how to deliver it).</p><p>I particularly appreciated how plainly this post describes a mismatch a lot of teams create for themselves: wrapping largely-static content in an app shell, then shipping a pile of JavaScript just to re-assert what the server already knew. MDN’s new approach feels refreshing, despite being grounded in age-old best practices: Keep the content first-class, treat interactivity as optional (and isolated), and only ship what the page actually needs.</p><p>There’s a lot to like here, but more than anything I love seeing MDN embrace an architecture that’s truly aligned with the platform it documents.</p>]]></content><amg:twitter><![CDATA[This is a fascinating architecture write-up from MDN: fewer abstractions, less shipped JavaScript, and interactivity where it actually belongs.]]></amg:twitter><amg:summary><![CDATA[<p>This is a fascinating look at how MDN rebuilt its frontend around server-rendered HTML, web components, and a clear understanding of where interactivity actually belongs.</p>]]></amg:summary><summary type="html"><![CDATA[<p>This is a fascinating look at how MDN rebuilt its frontend around server-rendered HTML, web components, and a clear understanding of where interactivity actually belongs.</p>]]></summary><category term="web components" /><category term="web development" /><category term="progressive enhancement" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/slidevars/</id><title type="html"><![CDATA[🔗 slideVars]]></title><link href="https://www.aaron-gustafson.com/notebook/links/slidevars/" rel="alternate" type="text/html" /><link href="https://codepen.github.io/slideVars/" rel="related" type="text/html" /><published>2026-04-23T12:20:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a lovely little utility from Chris Coyier that turns CSS custom properties into a live control panel.</p><p>It’s one of those tools that immediately makes me want to crack open a demo and start playing. What’s especially nice is that it doesn’t ask you to invent some parallel configuration universe if you don’t want to; it can just inspect the variables you already have and give you a UI for experimenting with them. That makes it feel less like a framework and more like a really practical enhancement to the way many of us already work.</p>]]></content><amg:twitter><![CDATA[slideVars feels like the kind of tiny, practical tool that makes design and front-end iteration a whole lot more fun.]]></amg:twitter><amg:summary><![CDATA[<p>A lovely little utility from Chris Coyier that turns CSS custom properties into a live control panel.</p>]]></amg:summary><summary type="html"><![CDATA[<p>A lovely little utility from Chris Coyier that turns CSS custom properties into a live control panel.</p>]]></summary><category term="CSS" /><category term="developer tools" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/why-we-teach-our-students-progressive-enhancement/</id><title type="html"><![CDATA[🔗 Why we teach our students progressive enhancement]]></title><link href="https://www.aaron-gustafson.com/notebook/links/why-we-teach-our-students-progressive-enhancement/" rel="alternate" type="text/html" /><link href="https://cydstumpel.nl/why-we-teach-our-students-progressive-enhancement/" rel="related" type="text/html" /><published>2026-02-06T19:10:02Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Excellent post and primer on progressive enhancement. I particularly love this framing:</p><blockquote><p>progressive enhancement is not about preventing failure, it’s about defining what must always work.</p></blockquote><p>I also want one of those ”Wat als de JavaScript uit staat?“ stickers.</p>]]></content><amg:twitter><![CDATA[“[P]rogressive enhancement is not about preventing failure, it’s about defining what must always work.”]]></amg:twitter><category term="progressive enhancement" /><category term="web development" /><category term="web design" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cydstumpel.nl/wp-content/uploads/og-images/og-work-1900.jpg?v=1772487917" /></entry><entry><id>https://www.aaron-gustafson.com/publications/books/learning-web-design-sixth-edition/</id><title type="html"><![CDATA[📗 Learning Web Design]]></title><link href="https://www.oreilly.com/library/view/learning-web-design/9781098137670/" rel="alternate" type="text/html" /><published>2025-05-01T00:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Do you want to build web pages but have no prior experience? This friendly guide is the perfect place to start. You’ll begin at square one, learning how the web and web pages work, and then steadily build from there. By the end of the book, you’ll have the skills to create a simple site with multicolumn pages that adapt for mobile devices.</p>]]></content><category term="accessibility" /><category term="CSS" /><category term="HTML" /><category term="JavaScript" /><category term="progressive enhancement" /><category term="responsive web design" /><category term="web design" /><category term="web development" /><category term="web standards" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/complaining-about-designers-fiddling-with-figma-solves-nothing/</id><title type="html"><![CDATA[✍🏻 Complaining About Designers Fiddling with Figma Solves Nothing]]></title><link href="https://www.aaron-gustafson.com/notebook/complaining-about-designers-fiddling-with-figma-solves-nothing/" rel="alternate" type="text/html" /><published>2025-03-24T16:11:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p><p>First off, I agree with his core message: designers should understand the medium they are working in, be it web or an OS-specific UI. That’s where my agreements end, however.</p><blockquote><p>They pat themselves on the back, believing they’ve mastered digital design. Meanwhile, a developer glances at the file, sighs, and codes the button in five minutes.</p></blockquote><p>Condescention toward designers aside, this also assumes a high degree of competency and understanding of nuance on the engineering side which — in my experience — is not guaranteed. I cannot tell you how often I’ve designed an interface that calls for a button to submit a form and I get back a <code>div</code> with a click handler that can’t accept focus and has no design considerations for hover, focus, etc. In other words, if I provided a design with a single button on it, I get that button exactly. Sometimes exactly to those explicit dimensions, text overflow be damned.</p><p>There needs to be an acknowledgement and appreciation that designers know their craft and (generally) have good reasons for being overly prescriptive. Do some designers stray into realms of self-indulgence… absolutely! But so do engineers.</p><p>I think where we need to get to is a place where handoff provides enough information to inform the engineer of the totality of what’s needed. Sure, to a seasoned engineer that may seem overly detailed, but to a junior engineer it can be incredibly helpful in learning all of the considerations of something as seemingly simple as a button. And even a seasoned engineer might learn a thing (or three) from annotations regarding accessibility and such. Communication is the key to success here.</p><p>Also, some prototyping in a tool like Figma can help with catching issues — like keyboard traps, for instance — before they are codified into code. Making changes in Figma or other design tools is generally a lot cheaper that doing it in code. Back of the napkin math — which is generally agreed upon across the industry — is that catching &amp; fixing an accessibility issue in design costs about $100. In development, it’s like $1,000. If it sneaks into production, it’s gonna be more like $10,000.</p><p>To be clear, I’m not disagreeing with the piece in its entirety — designers should learn their medium (which is coding to a degree but also UX expectations) and Figma’s <strong>not</strong> the medium — but I think there’s more nuance to it. I also dislike the us vs. them framing; we’re all in this together.</p>]]></content><amg:twitter><![CDATA[Michael F. Buckley posted a somewhat imflamatory piece to the UX Collective blog over on Medium and I had some strong reactions to it I wanted to share.]]></amg:twitter><amg:summary><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p>]]></amg:summary><summary type="html"><![CDATA[<p>Michael F. Buckley posted <a href="https://uxdesign.cc/figmas-not-a-design-tool-it-s-a-rube-goldberg-machine-for-avoiding-code-2a24f11add5d">a somewhat imflamatory piece to the UX Collective blog over on Medium</a> and I had some strong reactions to it I wanted to share. You should read it first before continuing.</p>]]></summary><category term="design" /><category term="web development" /><category term="industry" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/on-crowdstrike-dependencies-and-building-robust-products-on-the-web/</id><title type="html"><![CDATA[✍🏻 On CrowdStrike, dependencies, and building robust products on the web]]></title><link href="https://www.aaron-gustafson.com/notebook/on-crowdstrike-dependencies-and-building-robust-products-on-the-web/" rel="alternate" type="text/html" /><published>2024-07-25T17:20:26Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I have no opinion on CrowdStrike as a company or service. I’ve never used their products. In fact, prior to <a href="https://wikipedia.org/wiki/2024_CrowdStrike_incident">the incident last week</a>, I had only a passing familiarity with their name — likely from headlines in the tech press I’d scrolled past at some point in time. I now have a vague understanding of what they do, but that’s only based on what I learned about <a href="https://www.crowdstrike.com/wp-content/uploads/2024/07/CrowdStrike-PIR-Executive-Summary.pdf">the cause of the incident</a>. In reflecting on this unfortunate incident, I can’t help but think of the lesson it holds for web designers and developers.</p><p>The incident was caused when a bug in CrowdStrike’s code made it out into production. The results were catastrophic: It caused roughly 8.5 million servers to crash. Hospitals weren’t able to serve the people that needed them. Airline passengers were stranded. People couldn’t access their money in banks. Folks in distress could not get the emergency services they needed. On top of that, the financial fallout is estimated to be somewhere around US$10 billion.</p><p>Bugs happen. I’d hate to be the person who was responsible for that particular bug (or the people in the quality assurance team that should have caught it), but the reality is that none of us who write code write <em>perfect</em> code. We all make mistakes and sometimes those mistakes make it into production. Other times, the code we write works perfectly during development and testing, but causes an unexpected issue in production. Sometimes only in very specific “edge case” circumstances that we didn’t have the foresight to consider.</p><p>Which brings me to the lesson I took away from the CrowdStrike incident: minimize the impact dependencies can have on your customers’ ability to complete critical tasks. In other words, <a href="https://www.smashingmagazine.com/2016/05/developing-dependency-awareness/">develop dependency awareness</a>.</p><p>The web is a hostile operating environment. Sometimes network connections are slow to resolve or time out completely, which may cause issues with your JavaScript, may result in broken images or videos, or could make it so your user never receives your CSS. Sometimes 3rd party scripts ship bugs that can hose your site completely. Sometimes a customer’s browser plugin can wreak havoc on your site by adjusting markup or injecting code. If any (or all) of those things were to happen, could your customers still accomplish their key tasks? Could they even understand your site at all?</p><p>This is why it’s so critical to start with a fully-functional website that relies only on semantic, accessible HTML and regular ol’ links and form submissions. They aren’t sexy, but they’re solid. Then <em>progressively enhance</em> that experience to improve things when the CSS is downloaded. And improve some more when your JavaScript executes properly. Build an awareness for the kinds of dependencies you have in your code so you can ensure there is always a fallback.</p><p>When I think about building robust websites like this, I often think of the Chrysler Imperial. The 1964-1966 model is one of the few cars that has been outright banned from entering demolition derby events. It is just too well built. It just takes the hits and keeps on driving. We should aspire to that kind of resilience in the websites we build.</p><p>Bugs happen. Can your site withstand them? Or will you let the failure of a single dependency (a.k.a., site fragility) ruin your customers’ day?</p><p><a href="https://www.youtube.com/watch?v=-9GGDOUDLhc&amp;start=7&amp;end=17">https://www.youtube.com/watch?v=-9GGDOUDLhc&amp;start=7&amp;end=17</a></p>]]></content><amg:twitter><![CDATA[The #CrowdStrike meltdown last week should be a cautionary tale for #WebDesigners and #WebDevleopers.]]></amg:twitter><amg:summary><![CDATA[<p>I have no opinion on CrowdStrike as a company or service. I’ve never used their products. In fact, prior to <a href="https://wikipedia.org/wiki/2024_CrowdStrike_incident">the incident last week</a>, I had only a passing familiarity with their name — likely from headlines in the tech press I’d scrolled past at some point in time. I now have a vague understanding of what they do, but that’s only based on what I learned about <a href="https://www.crowdstrike.com/wp-content/uploads/2024/07/CrowdStrike-PIR-Executive-Summary.pdf">the cause of the incident</a>. In reflecting on this unfortunate incident, I can’t help but think of the lesson it holds for web designers and developers.</p>]]></amg:summary><summary type="html"><![CDATA[<p>I have no opinion on CrowdStrike as a company or service. I’ve never used their products. In fact, prior to <a href="https://wikipedia.org/wiki/2024_CrowdStrike_incident">the incident last week</a>, I had only a passing familiarity with their name — likely from headlines in the tech press I’d scrolled past at some point in time. I now have a vague understanding of what they do, but that’s only based on what I learned about <a href="https://www.crowdstrike.com/wp-content/uploads/2024/07/CrowdStrike-PIR-Executive-Summary.pdf">the cause of the incident</a>. In reflecting on this unfortunate incident, I can’t help but think of the lesson it holds for web designers and developers.</p>]]></summary><category term="progressive enhancement" /><category term="hazards" /><category term="CSS" /><category term="JavaScript" /><category term="web design" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/sharing-in-the-age-of-3p-cookie-mageddon/</id><title type="html"><![CDATA[✍🏻 Sharing in the Age of 3p Cookie-mageddon]]></title><link href="https://www.aaron-gustafson.com/notebook/sharing-in-the-age-of-3p-cookie-mageddon/" rel="alternate" type="text/html" /><published>2023-12-15T18:28:35Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Over a decade ago, I wrote up <a href="https://blog.easy-designs.net/archives/dont-sell-out-your-users/">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href="https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p><h2 id="sharing%2C-the-old-fashioned-way" tabindex="-1"><a class="header-anchor" href="#sharing%2C-the-old-fashioned-way" aria-hidden="true">#</a> Sharing, the Old-fashioned Way</h2><p>When it comes to sharing, there are myriad ways to do it. If you’re at all familiar with my work, it should come as no surprise that I always start with a universally-useable and accessible baseline and then <a href="/tags/progressive-enhancement">progressively enhance</a> things from there. Thankfully, every social media site I commonly use (with the exception of the Fediverse) makes this pretty easy by providing a form that accepts inbound content via the query string.<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup> You can <a href="https://www.linkedin.com/cws/share?url=https://www.aaron-gustafson.com/notebook/rebuilding-a-php-app-using-isomorphic-javascript-with-eleventy-and-netlify/">try LinkedIn’s to see it in action</a>.</p><p>Each service is a little different, but all function similarly. I support the following ones in this site:</p><table><caption>Social Media Sites and Their Sharing URLs</caption><thead><tr><th scope="col">Site</th><th scope="col">Destination</th><th scope="col">URL</th><th scope="col">Optional Params</th></tr></thead><tbody><tr><th scope="row">Twitter / X</th><td><a href="https://twitter.com/%EF%BF%BD4%EF%BF%BDintent/tweet%EF%BF%BD5%EF%BF%BD%EF%BF%BD6%EF%BF%BD%EF%BF%BD7%EF%BF%BDurl%EF%BF%BD8%EF%BF%BD%EF%BF%BD9%EF%BF%BD%EF%BF%BD10%EF%BF%BD%EF%BF%BD11%EF%BF%BD%EF%BF%BD12%EF%BF%BD">https://twitter.com/�4�intent/tweet�5��6��7�url�8��9��10��11��12�</a><tr><th scope="row">Hacker News</th><td><a href="https://news.ycombinator.com/%EF%BF%BD4%EF%BF%BDsubmitlink%EF%BF%BD5%EF%BF%BD%EF%BF%BD6%EF%BF%BD%EF%BF%BD7%EF%BF%BDu%EF%BF%BD8%EF%BF%BD%EF%BF%BD9%EF%BF%BD%EF%BF%BD10%EF%BF%BD%EF%BF%BD11%EF%BF%BDt%EF%BF%BD12%EF%BF%BD">https://news.ycombinator.com/�4�submitlink�5��6��7�u�8��9��10��11�t�12�</a> = the title you want to share </td></tr><tr><th scope="row">Facebook</th><td><a href="https://www.facebook.com/%EF%BF%BD4%EF%BF%BDsharer.php%EF%BF%BD5%EF%BF%BD%EF%BF%BD6%EF%BF%BD%EF%BF%BD7%EF%BF%BDu%EF%BF%BD8%EF%BF%BD%EF%BF%BD9%EF%BF%BD%EF%BF%BD10%EF%BF%BD%EF%BF%BD11%EF%BF%BD%EF%BF%BD12%EF%BF%BD">https://www.facebook.com/�4�sharer.php�5��6��7�u�8��9��10��11��12�</a><tr><th scope="row">LinkedIn</th><td><a href="https://www.linkedin.com/cws/share%EF%BF%BD4%EF%BF%BD%EF%BF%BD5%EF%BF%BD%EF%BF%BD6%EF%BF%BDurl%EF%BF%BD7%EF%BF%BD%EF%BF%BD8%EF%BF%BD%EF%BF%BD9%EF%BF%BD%EF%BF%BD10%EF%BF%BD%EF%BF%BD11%EF%BF%BD">https://www.linkedin.com/cws/share�4��5��6�url�7��8��9��10��11�</a><tr><th scope="row">Pinterest</th><td><a href="https://pinterest.com/%EF%BF%BD4%EF%BF%BDpin/create/button/%EF%BF%BD5%EF%BF%BD%EF%BF%BD6%EF%BF%BD%EF%BF%BD7%EF%BF%BDurl%EF%BF%BD8%EF%BF%BD%EF%BF%BD9%EF%BF%BD%EF%BF%BD10%EF%BF%BD%EF%BF%BD11%EF%BF%BDmedia%EF%BF%BD12%EF%BF%BD">https://pinterest.com/�4�pin/create/button/�5��6��7�url�8��9��10��11�media�12�</a> = an image to share<br><code>description</code> = the text you want to share</td></tr></tbody></table><p>Using this information, I created <a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/_includes/partials/post/sharing.njk">a partial template for use on any page in this site</a> (though I mainly use it on blog posts right now). Each link includes useful text content (e.g., “Share on <strong>__</strong>”) and a local SVG of the service’s icon. Here’s a simplified overview of the markup I use:</p><pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>social-links social-links–share<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>social-links__item<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span><span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>{{ SHARE URL }}<span class="token punctuation">”</span></span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>social-link<span class="token punctuation">”</span></span><span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>nofollow<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span><span class="token punctuation">&gt;</span></span>{{ SERVICE ICON }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>b</span><span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">“</span>social-link__text<span class="token punctuation">”</span></span><span class="token punctuation">&gt;</span></span>Share on {{ SERVICE NAME }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>b</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">&gt;</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">&gt;</span></span></code></pre><p>You can check out the baseline experience on this very page by disabling JavaScript.</p><figure id="2023-12-14-01"><p><img src="https://www.aaron-gustafson.com/i/posts/2023-12-15/1.png" alt=""></p><figcaption>My baseline sharing component is a list of icon links.</figcaption></figure><p>It’s worth noting that I have chosen not to enforce opening these links in a new tab. You can do that if you like, but on mobile devices I’d prefer the user just navigate to the share page directly. You may have a different preference, but if you decide to spawn a new tab, be sure your link text lets folks know that’s what will happen. I do include a <a href="https://developer.mozilla.org/docs/Web/HTML/Attributes/rel#nofollow"><code>rel=&quot;nofollow&quot;</code></a> on the link, however, to prevent search spiders from indexing the share forms.</p><p>If you test out these links, you’ll notice many of the target forms will pick up a ton of information from your page automatically. By and large, this info is grabbed from your page’s <a href="https://developers.facebook.com/docs/opengraph/">Open Graph data</a> (<a href="https://developers.facebook.com/docs/sharing/webmasters#markup">stored in <code>meta</code> tags</a>) or <a href="https://www.w3.org/TR/json-ld11/">Linked Data</a> (as <a href="https://json-ld.org/">JSON-LD</a>). You can write that info to your page by hand or use a plugin to generate it for you automatically. There are a ton of options out there if you choose to go the later route (which I’d recommend).</p><h2 id="enhancement-level-1%3A-popup-share" tabindex="-1"><a class="header-anchor" href="#enhancement-level-1%3A-popup-share" aria-hidden="true">#</a> Enhancement Level 1: Popup Share</h2><p>If you played around with any of the various share forms, you probably noticed that they are, by and large, designed as discrete interactions best-suited to a narrow window (e.g., mobile) or popup. To provide that experience, I’ve long-relied on a little bit of JavaScript to launch them in a new, appropriately-sized window:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">function</span><span class="token function">popup</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">var</span> $link <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">;</span><span class="token keyword">while</span><span class="token punctuation">(</span>$link<span class="token punctuation">.</span>nodeName<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">!=</span><span class="token string">“a”</span><span class="token punctuation">)</span><span class="token punctuation">{</span>$link <span class="token operator">=</span> $link<span class="token punctuation">.</span>parentNode<span class="token punctuation">;</span><span class="token punctuation">}</span>e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">var</span> popup <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span>$link<span class="token punctuation">.</span>href<span class="token punctuation">,</span><span class="token string">“share”</span><span class="token punctuation">,</span><span class="token string">“height=500,width=600,status=no,toolbar=no,popup”</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span><span class="token punctuation">{</span>popup<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>e<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">catch</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">var</span> screen_width <span class="token operator">=</span><span class="token string">“visualViewport”</span><span class="token keyword">in</span> window<span class="token operator">?</span> window<span class="token punctuation">.</span>visualViewport<span class="token punctuation">.</span>width<span class="token operator">:</span> window<span class="token punctuation">.</span>innerWidth<span class="token punctuation">,</span>$links <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">“.social-links–share a”</span><span class="token punctuation">)</span><span class="token punctuation">,</span>count <span class="token operator">=</span> $links<span class="token punctuation">.</span>length<span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span>screen_width <span class="token operator">&gt;</span><span class="token number">600</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">while</span><span class="token punctuation">(</span>count<span class="token operator">–</span><span class="token punctuation">)</span><span class="token punctuation">{</span>$links<span class="token punctuation">[</span>count<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“click”</span><span class="token punctuation">,</span> popup<span class="token punctuation">,</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>$links<span class="token punctuation">[</span>count<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“.social-link__text”</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerHTML <span class="token operator">+=</span><span class="token string">&quot; (in a popup)“</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre><p>The first chunk defines a new function called <code>popup()</code> that will act as the event listener. It takes the event (<var>e</var>) as an argument and then finds the associated link (bubbling up through the DOM as necessary in that <code>while</code> loop). Once it finds the link, the function opens a new popup window (using <code>window.open()</code>). Then, to check if the popup was blocked, it attempts (within the <code>try…catch</code>) to focus it. If the focus succeeds, which means the popup wasn’t blocked, the script prevents the link from navigating the user to the <code>href</code> (which is the default behavior, hence <code>e.preventDefault()</code>).</p><p>The second block defines a couple of variables we’ll need. First, it captures the current <var>screen_width</var> using either the window’s <code>visualViewport</code> (if available) or its <code>innerWidth</code> (which is more old school). Next it grabs the social links (<var>$links</var>) and counts them for looping purposes (<var>count</var>).</p><p>The final block is a conditional that checks to see if the <var>screen_width</var> is wider than 600px (an arbitrary width that just feels right… your mileage may vary). If the screen is wider than that threshold, it loops through the links,<sup class="footnote-ref"><a href="#fn2" id="fnref2">2</a></sup> adds the click handler, and adds some text to the link label to let folks know it will open a popup.</p><p>And with that, the first layer of enhancement is complete: Users with JavaScript support who also happen to be using a wider browser window will get the popup share form if the popup is allowed. If the popup isn’t allowed, they’ll default to the baseline experience.</p><h2 id="enhancement-level-2%3A-os-share" tabindex="-1"><a class="header-anchor" href="#enhancement-level-2%3A-os-share" aria-hidden="true">#</a> Enhancement Level 2: OS Share</h2><p>A few years back, browsers began participating in OS-level share activities. On one side, this allowed websites to share some data—URLs, text, files—to other apps on the device via <code>navigator.share()</code>. On the other side of the equation, Progressive Web Apps could advertise themselves—via the Manifest’s <code>share_target</code> member—as being able to receive content shared in this way.</p><p>Sharing a URL and text is <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share#browser_compatibility">really well supported</a>. That said, it’s only been around a few years at this point and some browsers require an additional permission to use the API.<sup class="footnote-ref"><a href="#fn3" id="fnref3">3</a></sup> For these reasons, it’s best to use the API as a progressive enhancement. Thankfully, it’s easy to test for support:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token string">“share”</span><span class="token keyword">in</span> navigator<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// all good!</span><span class="token punctuation">}</span></code></pre><p>For my particular implementation, I’ve decided to swap out the individual links for a single button that, when clicked, will proffer the page’s details over to the OS’s share widget. Here’s the code I use to do that:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">var</span> $links <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">”.social-links–share&quot;</span><span class="token punctuation">)</span><span class="token punctuation">,</span>$parent <span class="token operator">=</span> $links<span class="token punctuation">.</span>parentNode<span class="token punctuation">,</span>$button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">“button”</span><span class="token punctuation">)</span><span class="token punctuation">,</span>title <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">“h1.p-name,title”</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerText<span class="token punctuation">,</span>$description <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">‘meta[name=“og:description”],meta[name=“description”]’</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">,</span>text <span class="token operator">=</span> $description <span class="token operator">?</span> $description<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">“content”</span><span class="token punctuation">)</span><span class="token operator">:</span><span class="token string">“”</span><span class="token punctuation">,</span>url <span class="token operator">=</span> window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href<span class="token punctuation">;</span>$button<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span><span class="token string">“Share &lt;svg&gt;…&lt;/svg&gt;”</span><span class="token punctuation">;</span>$button<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“click”</span><span class="token punctuation">,</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span><span class="token punctuation">{</span>navigator<span class="token punctuation">.</span><span class="token function">share</span><span class="token punctuation">(</span><span class="token punctuation">{</span> title<span class="token punctuation">,</span> text<span class="token punctuation">,</span> url <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>$parent<span class="token punctuation">.</span><span class="token function">insertBefore</span><span class="token punctuation">(</span>$button<span class="token punctuation">,</span> $links<span class="token punctuation">)</span><span class="token punctuation">;</span>$links<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The first block sets up my variables:</p><ul><li><var>$links</var> - A reference to the list (<code>ul</code>) of sharing links;</li><li><var>$parent</var> - the parent container of that list;</li><li><var>$button</var> - the button I’m going to swap in for the links;</li><li><var>title</var> - The page title (either from the page’s <code>h1</code> or <code>title</code> element);</li><li><var>$description</var> - A reference to a <code>meta</code> description element;</li><li><var>text</var> - The text content of that description, if one is found; and</li><li><var>url</var> - The URL to be shared.</li></ul><p>The second block sets up the button by inserting the text “Share” and an SVG share icon and setting an event listener on it that will pass the collected info to <code>navigator.share()</code>.</p><p>The third and final block swaps out the link list for the button.</p><h2 id="putting-it-all-together" tabindex="-1"><a class="header-anchor" href="#putting-it-all-together" aria-hidden="true">#</a> Putting It All Together</h2><p>The final step to putting this all together involves setting up the conditional that determines which enhancement is offered. To keep everything a bit cleaner, I’m also moving each of the enhancements into its own function:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token operator">!</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">window<span class="token punctuation">,</span> document<span class="token punctuation">,</span> navigator</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">function</span><span class="token function">prepForPopup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// popup code</span><span class="token punctuation">}</span><span class="token keyword">function</span><span class="token function">popup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// popup event handler</span><span class="token punctuation">}</span><span class="token keyword">function</span><span class="token function">swapForShareAPI</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// share button code</span><span class="token punctuation">}</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token string">“share”</span><span class="token keyword">in</span> navigator<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token function">swapForShareAPI</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">else</span><span class="token punctuation">{</span><span class="token function">prepForPopup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span><span class="token keyword">this</span><span class="token punctuation">.</span>document<span class="token punctuation">,</span><span class="token keyword">this</span><span class="token punctuation">.</span>navigator<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>With this setup in place, I can provide the optimal experience in browsers that support the web share API and a pretty decent fallback experience to browsers that don’t. And if none of these enhancements can be applied, users can still share my content to the places I’ve identified… no cookies or third-party widgets required.</p><p>You can <a href="https://codepen.io/aarongustafson/pen/eYxajwy">see (and play with) an isolated demo of this interface over on Codepen</a>.</p><hr class="footnotes-sep"><section class="footnotes"><h4 class="hidden">Footnotes</h4><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>Interesting side-note: If you own a form like this on your site, <a href="/notebook/my-own-personal-pwa/">it makes a great share target</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p>Why a reverse <code>while</code> loop? Well, the order of execution doesn’t matter and decrementing <code>while</code> loops are faster in some instances. It’s a micro-optimization that boosts perf on older browsers and lower-end chipsets. <a href="#fnref2" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p>Like many modern APIs, it also requires a secure connection (HTTPS). <a href="#fnref3" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content><amg:twitter><![CDATA[Are you still relying on third party share widgets? You should really stop that. Here’s how…]]></amg:twitter><amg:summary><![CDATA[<p>Over a decade ago, I wrote up <a href="https://blog.easy-designs.net/archives/dont-sell-out-your-users/">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href="https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p>]]></amg:summary><summary type="html"><![CDATA[<p>Over a decade ago, I wrote up <a href="https://blog.easy-designs.net/archives/dont-sell-out-your-users/">detailed instructions on how to enable users to share your content on social media without allowing them to be tracked by every social media site via cookies</a>. In a few short weeks <a href="https://developers.google.com/privacy-sandbox/blog/cookie-countdown-2023oct">“third party” cookies will get the boot in Chromium-based browsers</a>. If you’re still relying on third party share widgets on your site, your users may start seeing problems. Now is a good time to replace them with code that Just Works™. Here’s how…</p>]]></summary><category term="HTML" /><category term="privacy" /><category term="progressive enhancement" /><category term="the web" /><category term="user experience" /><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2023-12-15/hero.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/widgets/</id><title type="html"><![CDATA[✍🏻 Widgets!]]></title><link href="https://www.aaron-gustafson.com/notebook/widgets/" rel="alternate" type="text/html" /><published>2023-10-09T22:38:54Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>It was a long time coming, but I finally had a chance to put the work I did on <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p><h2 id="where-it-all-started" tabindex="-1"><a class="header-anchor" href="#where-it-all-started" aria-hidden="true">#</a> Where it all started</h2><p>I had <a href="https://web.archive.org/web/20200929174844/https://discourse.wicg.io/t/noodling-on-an-idea-projections-for-web-apps/3900">the original idea for “projections”</a> way back in 2019. Inspired by <a href="https://en.wikipedia.org/wiki/Dashboard_(macOS)#Widget_functions_and_capabilities">OS X’s Dashboard Widgets</a> and <a href="https://en.wikipedia.org/wiki/Adobe_AIR">Adobe AIR</a>, I’d begun to wonder if it might be possible to <em>project</em> a component from a website into those kinds of surfaces. Rather than building a bespoke widget that connected to an API, I thought it made sense to leverage an installed PWA to manage those “projections.” I shared the idea at TPAC that year and got some interest from a broad range of folks, but didn’t have much time to work on the details until a few years later.</p><p>In the intervening time, I kept working through the concept in my head. I mean in an ideal world, the widget would just be a responsive web page, right? But if that were the case, what happens when every widget loads the entirety of React to render their stock ticker? That seemed like a performance nightmare.</p><p>In my gut, I felt like the right way to build things would be to have a standard library of widget templates and to enable devs to flow data into them via a Service Worker. Alex Russell suggested I model the APIs on how Notifications are handled (since they serve a similar function) and I was off to the races.</p><p>I drafted <a href="https://github.com/aarongustafson/pwa-widgets">a substantial proposal for my vision of how PWA widgets should work</a>. Key aspects included:</p><ul><li>A declarative way to define and configure a widget from within the Web App Manifest;</li><li>A progressively enhanced pathway for devs to design a widget that adapts to its host environment, from using predefined templates to using custom templates to full-blown web-based widgets (with rendering akin to an <code>iframe</code>);</li><li>A collection of recommended stock templates that implementors should offer to support most widget types;</li><li>Extensibility to support custom templates using any of a variety of templating languages; and</li><li>A complete suite of tools for managing widgets and any associated business logic within a Service Worker.</li></ul><h2 id="widgets-became-a-reality" tabindex="-1"><a class="header-anchor" href="#widgets-became-a-reality" aria-hidden="true">#</a> Widgets became a reality</h2><p>After continuing to gently push on this idea with colleagues across Microsoft (and beyond), I discovered that the Windows 11 team was looking to open up the new Widget Dashboard to third-party applications. I saw this as an opportunity to turn my idea into a reality. After working my way into the conversation, I made a solid case for why PWAs needed to be a part of that story and… it worked! (It no doubt helped that companies including Meta, Twitter, and Hulu were all invested in PWA as a means of delivering apps for Windows.)</p><p>While the timeline for implementation didn’t allow us to tackle the entirety of my proposal, we did carve out the pieces that made for a compelling MVP. This allowed us to show what’s possible, see how folks use it, and plan for future investment in the space.</p><p>Sadly, it meant tabling two features I really loved:</p><ul><li><strong>Stock/predefined templates</strong>. A library of lightly theme-able, consistent, cross-platform templates based on common data structures (e.g., RSS/Atom, iCal) would make it incredibly simple for devs to build a widget. If implemented well, devs might not even need to write a single line of business logic in their Service Worker as the browser could pick up all of the configuration details from the Manifest.</li><li><strong>Configurable widget instances.</strong> Instead of singleton widgets, these would allow you to define a single widget type and replicate it for different use cases. For example, a widget to follow a social media user’s profile could be defined once and the individual instances could be configured with the specific account to be followed.</li></ul><p>I’m sincerely hopeful these two features eventually make their way to us as I think they truly unlock the power of the widget platform. Perhaps, with enough uptake on the current implementation, we can revisit these in the not-too-distant future.</p><hr><p>To test things out, I decided to build two widgets for this site:</p><ol><li>Latest posts</li><li>Latest links</li></ol><p>Both are largely the same in terms of their setup: They display a list of linked titles from this site.</p><h2 id="designing-my-widget-templates" tabindex="-1"><a class="header-anchor" href="#designing-my-widget-templates" aria-hidden="true">#</a> Designing my widget templates</h2><p>Given that they were going to be largely identical, I made <a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json">a single “feed” template for use in both widgets</a>. The templating tech I used is called <a href="https://adaptivecards.io">Adaptive Cards</a>, which is what Windows 11 uses for rendering.</p><p>Adaptive Card templates are relatively straightforward JSON:</p><pre class="language-json" tabindex="0"><code class="language-json">&amp;#<span class="token number">123</span>;<span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“AdaptiveCard”</span><span class="token punctuation">,</span><span class="token property">“$schema”</span><span class="token operator">:</span><span class="token string">“<a href="http://adaptivecards.io/schemas/adaptive-card.json">http://adaptivecards.io/schemas/adaptive-card.json</a>”</span><span class="token punctuation">,</span><span class="token property">“version”</span><span class="token operator">:</span><span class="token string">“1.6”</span><span class="token punctuation">,</span><span class="token property">“body”</span><span class="token operator">:</span> &amp;#<span class="token number">91</span>;&amp;#<span class="token number">123</span>;<span class="token property">“$data”</span><span class="token operator">:</span><span class="token string">“$&amp;#123;take(items,5)&amp;#125;”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“Container”</span><span class="token punctuation">,</span><span class="token property">“items”</span><span class="token operator">:</span> &amp;#<span class="token number">91</span>;&amp;#<span class="token number">123</span>;<span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“TextBlock”</span><span class="token punctuation">,</span><span class="token property">“text”</span><span class="token operator">:</span><span class="token string">“&amp;#91;$&amp;#123;title&amp;#125;&amp;#93;($&amp;#123;url&amp;#125;)”</span><span class="token punctuation">,</span><span class="token property">“wrap”</span><span class="token operator">:</span><span class="token boolean">true</span><span class="token punctuation">,</span><span class="token property">“weight”</span><span class="token operator">:</span><span class="token string">“Bolder”</span><span class="token punctuation">,</span><span class="token property">“spacing”</span><span class="token operator">:</span><span class="token string">“Padding”</span><span class="token punctuation">,</span><span class="token property">“height”</span><span class="token operator">:</span><span class="token string">“stretch”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">93</span>;<span class="token punctuation">,</span><span class="token property">“height”</span><span class="token operator">:</span><span class="token string">“stretch”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">93</span>;<span class="token punctuation">,</span><span class="token property">“backgroundImage”</span><span class="token operator">:</span> &amp;#<span class="token number">123</span>;<span class="token property">“url”</span><span class="token operator">:</span><span class="token string">“<a href="https://www.aaron-gustafson.com/i/background-logo.png">https://www.aaron-gustafson.com/i/background-logo.png</a>”</span><span class="token punctuation">,</span><span class="token property">“verticalAlignment”</span><span class="token operator">:</span><span class="token string">“Bottom”</span><span class="token punctuation">,</span><span class="token property">“horizontalAlignment”</span><span class="token operator">:</span><span class="token string">“Center”</span>&amp;#<span class="token number">125</span>;&amp;#<span class="token number">125</span>;</code></pre><p>What this structure does is:</p><ol><li>Create a container into which I will place the content;</li><li>Extract the first five <code>items</code> from the data being fed into the template (more on that in a moment);</li><li>Loop through each <code>item</code> and<ul><li>create a text block,</li><li>populate its content with Markdown to generate a linked title (using the <code>title</code> and <code>url</code> keys from the <code>item</code> object)</li><li>Set some basic styles to make the text bold, separate the titles a little and make them grow to fill the container; then, finally</li></ul></li><li>Set a background on the widget.</li></ol><p>The way Adaptive Cards work is that they flow JSON data into a template and render that. The variable names in the template map directly to the incoming data structure, so are totally up to you to define. As these particular widgets are feed-driven and <a href="https://www.aaron-gustafson.com/feeds/">this site already supports JSONFeed</a>, I set up the widgets to flow the appropriate feed into each and used the keys that were already there. For reference, here’s a sample JSONFeed <code>item</code>:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“id”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“title”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“summary”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“content_html”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“url”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">,</span><span class="token property">“tags”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token property">“date_published”</span><span class="token operator">:</span><span class="token string">“…”</span><span class="token punctuation">}</span></code></pre><p>If you want to tinker with Adaptive Cards and make your own, you can do so with <a href="https://adaptivecards.io/designer/">their Designer tool</a>.</p><h3 id="defining-the-widgets-in-the-manifest" tabindex="-1"><a class="header-anchor" href="#defining-the-widgets-in-the-manifest" aria-hidden="true">#</a> Defining the widgets in the Manifest</h3><p>With a basic template created, the next step was to set up the two widgets in my Manifest. As they both function largely the same, I’ll just focus on the definition for one of them.</p><p>First off, defining widgets in the Manifest is done via the <code>widgets</code> member, which is an array (much like <code>icons</code> and <code>shortcuts</code>). Each widget is represented as an object in that array. Here is the definition for the “latest posts” widget:</p><pre class="language-json" tabindex="0"><code class="language-json"><span class="token punctuation">{</span><span class="token property">“name”</span><span class="token operator">:</span><span class="token string">“Latest Posts”</span><span class="token punctuation">,</span><span class="token property">“short_name”</span><span class="token operator">:</span><span class="token string">“Posts”</span><span class="token punctuation">,</span><span class="token property">“tag”</span><span class="token operator">:</span><span class="token string">“feed-posts”</span><span class="token punctuation">,</span><span class="token property">“description”</span><span class="token operator">:</span><span class="token string">“The latest posts from Aaron Gustafson’s blog”</span><span class="token punctuation">,</span><span class="token property">“template”</span><span class="token operator">:</span><span class="token string">“feed”</span><span class="token punctuation">,</span><span class="token property">“ms_ac_template”</span><span class="token operator">:</span><span class="token string">“/w/feed.ac.json”</span><span class="token punctuation">,</span><span class="token property">“data”</span><span class="token operator">:</span><span class="token string">“/feeds/latest-posts.json”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“application/json”</span><span class="token punctuation">,</span><span class="token property">“auth”</span><span class="token operator">:</span><span class="token boolean">false</span><span class="token punctuation">,</span><span class="token property">“update”</span><span class="token operator">:</span><span class="token number">21600</span><span class="token punctuation">,</span><span class="token property">“icons”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">{</span><span class="token property">“src”</span><span class="token operator">:</span><span class="token string">“/i/icons/webicon-rss.png”</span><span class="token punctuation">,</span><span class="token property">“type”</span><span class="token operator">:</span><span class="token string">“image/png”</span><span class="token punctuation">,</span><span class="token property">“sizes”</span><span class="token operator">:</span><span class="token string">“120x120”</span><span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token property">“screenshots”</span><span class="token operator">:</span><span class="token punctuation">[</span><span class="token punctuation">{</span><span class="token property">“src”</span><span class="token operator">:</span><span class="token string">“/i/screenshots/widget-posts.png”</span><span class="token punctuation">,</span><span class="token property">“sizes”</span><span class="token operator">:</span><span class="token string">“387x387”</span><span class="token punctuation">,</span><span class="token property">“label”</span><span class="token operator">:</span><span class="token string">“The latest posts widget”</span><span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">}</span></code></pre><p>Breaking this down:</p><ol><li><code>name</code> and <code>short_name</code> act much like these keys in the root of the Manifest as well as in <code>shortcuts</code>: The <code>name</code> value is used as the name for the widget unless there’s not enough room, in which case <code>short_name</code> is used.</li><li>You can think of <code>tag</code> as analogous to <code>class</code> in HTML sense. It’s a way of labeling a widget so you can easily reference it later. Each widget instance will have a unique id created by the widget service, but that instance (or all instances, if the widget supports multiple instances) can be accessed via the <code>tag</code>. But more on that later.</li><li>The <code>description</code> key is used for marketing the widget within a host OS or digital storefront. It should accurately (and briefly) describe what the widget does.</li><li>The <code>template</code> key is not currently used in the Windows 11 implementation but refers to the expected standard library widget template provided by the system. As a template library is not currently available, the <code>ms_ac_template</code> value is used to provide a URL to get the custom Adaptive Card (hence “ac”) template. The “ms_” prefix is there because it’s expected that this would be a Microsoft-proprietary property. It follows <a href="https://www.w3.org/TR/appmanifest/#proprietary-extensions">the guidance for extending the Manifest</a>.</li><li>The <code>data</code> and <code>type</code> keys define the path to the data that should be fed into the template for rendering by the widget host and the MIME of the data format it’s in. The Windows 11 implementation currently only accepts JSON data, but the design of widgets is set up to allow for this to eventually extend to other standardized formats like RSS, iCal, vCard, and such.</li><li><code>update</code> is an optional configuration member allowing you to set how often you’d like the widget to update, in seconds. Developers currently need to add the logic for implementing this into their Service Worker, but this setup allows the configuration to remain independent of the JavaScript code, making it easier to maintain.</li><li>Finally, <code>icons</code> and <code>screenshots</code> allow us to define how the widget shows up in the widget host and how it is promoted for install.</li></ol><p>When someone installs my site as a PWA, the information about the available widgets gets ingested by the browser. The browser then determines, based on the provided values and its knowledge of the available widget service(s) on the device, which widgets should be offered. On Windows 11, this information is <a href="https://learn.microsoft.com/en-us/windows/apps/develop/widgets/implement-widget-provider-cs#update-the-package-manifest">routed into the AppXManifest that governs how apps are represented in Windows</a>. The Windows 11 widget service can then read in the details about the available widgets and offer them for users to install.</p><figure id="2023-10-09-01"><p><img src="https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-promotion.gif" alt=""></p><figcaption>An animated capture of Windows 11’s widget promotion surface, showing 2 widgets available from this site’s PWA.</figcaption></figure><h2 id="adding-widget-support-to-my-service-worker" tabindex="-1"><a class="header-anchor" href="#adding-widget-support-to-my-service-worker" aria-hidden="true">#</a> Adding widget support to my Service Worker</h2><p>As I mentioned earlier, all of the plumbing for widgets is done within a Service Worker and is modeled on the Notifications API. I’m not going to exhaustively detail how it all works, but I’ll give you enough detail to get you started.</p><p>First off, widgets are exposed via the <code>self.widgets</code> interface. Most importantly, this interface lets you access and update any instances of a widget connected to your PWA.</p><h3 id="installing-a-widget" tabindex="-1"><a class="header-anchor" href="#installing-a-widget" aria-hidden="true">#</a> Installing a widget</h3><p>When a user chooses to install a widget, that emits a “widgetinstall” event in your Service Worker. You use that to kickoff the widget lifecycle by gathering the template and data needed to instantiate the widget:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetinstall”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Installing &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">initializeWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>The event argument comes in with details of the specific widget being instantiated (as <code>event.widget</code>). In the code above, you can see I’ve logged the widget’s <code>tag</code> value to the console. I pass the widget information over to my <code>initializeWidget()</code> function and it updates the widget with the latest data and, if necessary, sets up a <a href="https://developer.mozilla.org/docs/Web/API/Web_Periodic_Background_Synchronization_API">Periodic Background Sync</a>:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">initializeWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">await</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">await</span><span class="token function">registerPeriodicSync</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>The code for my <code>updateWidget()</code> function is as follows:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">updateWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">const</span> template <span class="token operator">=</span><span class="token keyword">await</span><span class="token punctuation">(</span><span class="token keyword">await</span><span class="token function">fetch</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>msAcTemplate<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> data <span class="token operator">=</span><span class="token keyword">await</span><span class="token punctuation">(</span><span class="token keyword">await</span><span class="token function">fetch</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>data<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span><span class="token punctuation">{</span><span class="token keyword">await</span> self<span class="token punctuation">.</span>widgets<span class="token punctuation">.</span><span class="token function">updateByTag</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">,</span><span class="token punctuation">{</span> template<span class="token punctuation">,</span> data <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">catch</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Couldn’t update the widget &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>This function does the following:</p><ol><li>Get the template for this widget</li><li>Get the data to flow into the template</li><li>Use the <code>self.widgets.updateByTag()</code> method to push the <var>template</var> and <var>data</var> to the widget service to update any widget instances connected to the widget’s <code>tag</code>.</li></ol><p>As I mentioned, I also have code in place to take advantage of Periodic Background Sync if/when it’s available and the browser allows my site to do it:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">registerPeriodicSync</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">let</span> tag <span class="token operator">=</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token string">“update”</span><span class="token keyword">in</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">)</span><span class="token punctuation">{</span>registration<span class="token punctuation">.</span>periodicSync<span class="token punctuation">.</span><span class="token function">getTags</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">tags</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token comment">// only one registration per tag</span><span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>tags<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>tag<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">{</span>periodicSync<span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span>tag<span class="token punctuation">,</span><span class="token punctuation">{</span><span class="token literal-property property">minInterval</span><span class="token operator">:</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>update<span class="token punctuation">,</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><p>This function also receives the widget details and:</p><ol><li>Looks to see if the widget <code>definition</code> (from the Manifest) includes an <code>update</code> member. If it has one, it…</li><li>Checks to see if there’s already a Periodic Background Sync that is registered for this tag. If none exists, it…</li><li>Registers a new Periodic Background Sync using the <code>tag</code> value and a minimum interval equal to the <code>update</code> requested.</li></ol><p>The <code>update</code> member, as you may recall, is the frequency (in seconds) you’d ideally like the widget to be updated. In reality, you’re at the mercy of the browser as to when (or even if) your sync will run, but that’s totally cool as there are other ways to update widgets as well.<sup class="footnote-ref"><a href="#fn1" id="fnref1">1</a></sup></p><h3 id="uninstalling-a-widget" tabindex="-1"><a class="header-anchor" href="#uninstalling-a-widget" aria-hidden="true">#</a> Uninstalling a widget</h3><p>When a user uninstalls a widget, your Service Worker will receive a “widgetuninstall” event. Much like the “widgetinstall” event, the argument contains details about that widget which you can use to clean up after yourself:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetuninstall”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Uninstalling &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">uninstallWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>Your application may have different cleanup needs, but this is a great time to clean up any unneeded Periodic Sync registrations. Just be sure to check the length of the widget’s <code>instances</code> array (<code>widget.instances</code>) to make sure you’re dealing with the last instance of a given widget <em>before</em> you unregister the sync:</p><pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">async</span><span class="token keyword">function</span><span class="token function">uninstallWidget</span><span class="token punctuation">(</span><span class="token parameter">widget</span><span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">if</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>instances<span class="token punctuation">.</span>length <span class="token operator">===</span><span class="token number">1</span><span class="token operator">&amp;&amp;</span><span class="token string">“update”</span><span class="token keyword">in</span> widget<span class="token punctuation">.</span>definition<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">await</span> self<span class="token punctuation">.</span>registration<span class="token punctuation">.</span>periodicSync<span class="token punctuation">.</span><span class="token function">unregister</span><span class="token punctuation">(</span>widget<span class="token punctuation">.</span>definition<span class="token punctuation">.</span>tag<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">}</span></code></pre><h3 id="refreshing-your-widgets" tabindex="-1"><a class="header-anchor" href="#refreshing-your-widgets" aria-hidden="true">#</a> Refreshing your widgets</h3><p>Widget platforms may periodically freeze your widget(s) to save resources. For example, they may do this when widgets are not visible. To keep your widgets up to date, they will periodically issue a “widgetresume” event. If you’ve modeled your approach on the one I’ve outlined above, you can route this event right through to your <code>updateWidget()</code> function:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetresume”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string"><code>&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Resuming &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;widget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;</code></span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="actions" tabindex="-1"><a class="header-anchor" href="#actions" aria-hidden="true">#</a> Actions</h3><p>While I don’t want to get too into the weeds here, I do want to mention that widgets can have predefined user actions as well. These actions result in “widget click” events being sent back to the Service Worker so you can respond to them:</p><pre class="language-js" tabindex="0"><code class="language-js">self<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">“widgetclick”</span><span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span><span class="token operator">=&gt;</span><span class="token punctuation">{</span><span class="token keyword">const</span> widget <span class="token operator">=</span> event<span class="token punctuation">.</span>widget<span class="token punctuation">;</span><span class="token keyword">const</span> action <span class="token operator">=</span> event<span class="token punctuation">.</span>action<span class="token punctuation">;</span><span class="token keyword">switch</span><span class="token punctuation">(</span>action<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">// Custom Actions</span><span class="token keyword">case</span><span class="token string">“refresh”</span><span class="token operator">:</span>event<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span><span class="token function">updateWidget</span><span class="token punctuation">(</span>widget<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">break</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>For a great example of how a widget can integrate actions, you should check out <a href="https://microsoftedge.github.io/Demos/pwamp/">the demo PWAmp project</a>. <a href="https://github.com/MicrosoftEdge/Demos/blob/main/pwamp/sw-widgets.js">Their Service Worker widget code</a> is worth a read.</p><h2 id="result!" tabindex="-1"><a class="header-anchor" href="#result!" aria-hidden="true">#</a> Result!</h2><p>With all of these pieces in place, I was excited to see my site showing up in the Widget Dashboard in Windows 11.</p><figure id="2023-10-09-02"><p><img src="https://www.aaron-gustafson.com/i/posts/2023-10-09/widgets-in-windows.jpg" alt=""></p><figcaption>A screenshot of Windows 11 showing the Widget Dashboard overlaying the desktop with this site installed as a PWA to the right. The “latest posts” and “latest links” widgets are shown.</figcaption></figure><p>You can view the full source code on GitHub:</p><ul><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/w/feed.ac.json">“Feed” Adaptive Card Template</a></li><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/static/manifest.json#L158-L237">Widget definitions in the Manifest</a></li><li><a href="https://github.com/aarongustafson/aaron-gustafson.com/blob/main/src/_javascript/serviceworker/widgets.js">Widgets code in my Service Worker</a></li></ul><hr><p>I’m quite hopeful this will be the first of many places PWA-driven widgets will appear. If you’s like to see them supported elsewhere, be sure to tell your browser and OS vendor(s) of choice. The more they hear from their user base that this feature is needed, the more likely we are to see it get implemented in more places.</p><h2 id="addendum%3A-gotchas" tabindex="-1"><a class="header-anchor" href="#addendum%3A-gotchas" aria-hidden="true">#</a> Addendum: Gotchas</h2><p>In wiring this all up, I ran into a few current bugs I wanted to flag so you can avoid them:</p><ul><li>The <code>icons</code> member won’t accept SVG images. This should eventually be fixed, but it was keeping my widgets from appearing as installable.</li><li>The <code>screenshots</code> members can’t be incredibly large. I’m told you should provide square screenshots no larger than 500px ×500px.</li></ul><hr class="footnotes-sep"><section class="footnotes"><h4 class="hidden">Footnotes</h4><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>Have you checked out <a href="https://developer.mozilla.org/docs/Web/API/Server-sent_events/Using_server-sent_events">Server Events</a>? <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content><amg:twitter><![CDATA[I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. It’s pretty exciting!]]></amg:twitter><amg:summary><![CDATA[<p>It was a long time coming, but I finally had a chance to put the work I did on <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p>]]></amg:summary><summary type="html"><![CDATA[<p>It was a long time coming, but I finally had a chance to put the work I did on <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/tree/main/PWAWidgets">a widgets proposal for PWAs</a> into practice on my own site. I’m pretty excited about it!</p>]]></summary><category term="progressive web apps" /><category term="experiments" /><category term="JavaScript" /><category term="Microsoft" /><category term="this site" /><category term="user experience" /><category term="web development" /><category term="web standards" /><category term="Windows" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/posts/2023-10-09/hero1.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/appearances/podcasts/2023-08-28-building-resilient-web-apps-progressive-enhancement-unveiled/</id><title type="html"><![CDATA[🎧 Building Resilient Web Apps: Progressive Enhancement Unveiled]]></title><link href="https://www.youtube.com/watch?v=-ZqsLwWM7YE" rel="alternate" type="text/html" /><published>2023-08-28T00:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Let’s delve into the world of progressive enhancement for web applications. From dissecting common pitfalls to offering invaluable tips, we unravel the art of building robust and inclusive web experiences.</p>]]></content><category term="progressive enhancement" /><category term="accessibility" /><category term="JavaScript" /><category term="progressive web apps" /><category term="web design" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/design-for-developers/</id><title type="html"><![CDATA[✍🏻 Design for Developers]]></title><link href="https://www.aaron-gustafson.com/notebook/design-for-developers/" rel="alternate" type="text/html" /><published>2023-07-18T16:19:37Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>One of my favorite web designers, <a href="https://stephaniestimac.com/">Stephanie Stimac</a>, asked me to write the foreword for her amazing new book, <a href="http://mng.bz/zXqB"><cite>Design for Developers</cite></a>. With her permission, and Manning’s, I’m reprinting it here.</p><blockquote><p>I’ve worked on the web for a long time. When I say a long time, I mean a long time. As in <em>my first browser was on the command line</em> old. As in <em>my first back-end development was a CGI script</em> old. With my nearly three decades of building dozens upon dozens of projects for the web, the one skill I’ve found most valuable is the ability to bridge the gaps between the worlds of design, user experience, and development.</p><p>Each of these fields attract practitioners from different education and/or hobbyist backgrounds. This can be a point of tension, as folks in each camp tend to see their own field as having the greatest influence on a web project’s success. The reality is that web exists in the nexus of these fields, relying equally on what each of these practices bring to the table. I have found that the most valuable people in any web project are those that understand enough of each field to be able to operate in that nexus, ensuring each team is communicating effectively with the other teams and that everyone is working together toward a shared goal.</p><p>Which brings me to Stephanie Stimac’s <cite>Design for Developers</cite>. This book is an invaluable resource for developers looking to level up in their understanding of what it takes to build for the web today. It provides a crash course in design and user experience in a way that will both improve your fluency with these topics and give you the necessary skills to become productive in these spaces as well. Beyond that superficial perspective, however, this book provides the foundation for leveling up your career and becoming the person who thrives in the space between design, user experience, and development.</p><p>Stephanie is the perfect person to provide you with these insights as well. Not only is she an excellent communicator, she also has years of working experience as a designer, developer, user experience practitioner, and an educator. She’s even worked behind the scenes on web browsers and the web standards they implement, giving her an incredible depth of understanding when it comes to the web and the tools we rely on to build it.</p><p>Whether you’re a longtime developer looking to grow a new branch on your skill tree or you’re starting out and looking to get a solid foundation in what it takes to build for the web, <cite>Design for Developers</cite> is sure to provide you with a wealth of knowledge to help you on your way.</p></blockquote>]]></content><amg:twitter><![CDATA[One of my favorite web designers, @seaotta, asked me to write the foreword for her amazing new book, Design for Developers. With her permission, and Manning’s, I’ve reprinted it on my site.]]></amg:twitter><amg:summary><![CDATA[<p>One of my favorite web designers, <a href="https://stephaniestimac.com/">Stephanie Stimac</a>, asked me to write the foreword for her amazing new book, <a href="http://mng.bz/zXqB"><cite>Design for Developers</cite></a>. With her permission, and Manning’s, I’m reprinting it here.</p>]]></amg:summary><summary type="html"><![CDATA[<p>One of my favorite web designers, <a href="https://stephaniestimac.com/">Stephanie Stimac</a>, asked me to write the foreword for her amazing new book, <a href="http://mng.bz/zXqB"><cite>Design for Developers</cite></a>. With her permission, and Manning’s, I’m reprinting it here.</p>]]></summary><category term="design" /><category term="web design" /><category term="web development" /><category term="CSS" /><category term="HTML" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.aaron-gustafson.com/i/books/design-for-developers.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/read-only-web-apps/</id><title type="html"><![CDATA[🔗 Read-only web apps]]></title><link href="https://www.aaron-gustafson.com/notebook/links/read-only-web-apps/" rel="alternate" type="text/html" /><link href="https://adactio.com/journal/20113" rel="related" type="text/html" /><published>2023-06-07T20:42:50Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love Jeremy’s proposed compromise on JavaScript in web apps:</p><blockquote><p>Your app should work in a read-only mode without JavaScript.</p></blockquote>]]></content><amg:twitter><![CDATA[I love @adactio’s proposed compromise on web apps and JavaScript: “Your app should work in a read-only mode without JavaScript.”]]></amg:twitter><category term="progressive enhancement" /><category term="JavaScript" /><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://adactio.com/images/photo-300.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/-news-from-wwdc23-webkit-features-in-safari-17-beta/</id><title type="html"><![CDATA[🔗 News from WWDC23: WebKit Features in Safari 17 beta]]></title><link href="https://www.aaron-gustafson.com/notebook/links/-news-from-wwdc23-webkit-features-in-safari-17-beta/" rel="alternate" type="text/html" /><link href="https://webkit.org/blog/14205/news-from-wwdc23-webkit-features-in-safari-17-beta/" rel="related" type="text/html" /><published>2023-06-07T20:35:53Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I’m very excited to see Apple roll out greater support for PWAs (though I’d bet good money on them never using that term publicly) in macOS Safari! I sincerely hope this is the beginning of many good things to come.</p>]]></content><amg:twitter><![CDATA[I’m very excited to see Apple roll out greater support for #PWAs in macOS Safari! ]]></amg:twitter><category term="progressive web apps" /><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://webkit.org/wp-content/uploads/Whats-new-in-web-apps-1024x663.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/publications/books/design-for-developers/</id><title type="html"><![CDATA[📗 Design for Developers]]></title><link href="http://mng.bz/zXqB" rel="alternate" type="text/html" /><published>2023-05-30T17:00:00Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>Whether you’re a longtime developer looking to grow a new branch on your skill tree or you’re starting out and looking to get a solid foundation in what it takes to build for the web, <cite>Design for Developers</cite> is sure to provide you with a wealth of knowledge to help you on your way.</p>]]></content><category term="web design" /><category term="web development" /><category term="CSS" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/how-to-skip-automatic-netlify-build-when-committing-to-github/</id><title type="html"><![CDATA[🔗 How to Skip Automatic Netlify Build When Committing to GitHub]]></title><link href="https://www.aaron-gustafson.com/notebook/links/how-to-skip-automatic-netlify-build-when-committing-to-github/" rel="alternate" type="text/html" /><link href="https://radu.link/skip-netlify-build/" rel="related" type="text/html" /><published>2023-03-13T15:58:01Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>If you auto-build with every push, did you know you can tell Netlify to skip building a given commit? Just add “[skip netlify]” to your commit message.</p><p>That’s gonna be super useful for something I have planned!</p>]]></content><amg:twitter><![CDATA[If you auto-build with every push, did you know you can tell #Netlify to skip building a given commit? Just add '[skip netlify]' to your commit message.]]></amg:twitter><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/when-javascript-fails/</id><title type="html"><![CDATA[🔗 When JavaScript Fails]]></title><link href="https://www.aaron-gustafson.com/notebook/links/when-javascript-fails/" rel="alternate" type="text/html" /><link href="https://medium.com/@jason.godesky/when-javascript-fails-52eef47e90db" rel="related" type="text/html" /><published>2023-03-10T19:51:05Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This piece does a great job of relaying the reality of JavaScript failures on the web and makes a strong case for why progressive enhancement is such an important approach. Jason’ illustrations of who is affected by JavaScript failures and how are particularly compelling.</p><p>I also love how succinctly he nails this section:</p><blockquote><p>So, if progressive enhancement is no more expensive to create, future-proof, provides us with technical credit, and ensures that our users always receive the best possible experience under any conditions, why has it fallen by the wayside?</p><p>Because before, when you clicked on a link, the browser would go white for a moment.</p><p>JavaScript frameworks broke the browser to avoid that momentary loss of control. They then had to recreate everything that the browser had provided for free: routing, history, the back button, accessibility features, the ability for search engines to read the page, et cetera iterum ad infinitum. Coming up with solutions to these problems has been the fixation of the JavaScript community for years now, and we do have serviceable solutions for all of these — but all together, they create the incredibly complex ecosystem of modern-day JavaScript that so many JavaScript developers bemoan and lament.</p><p>All to avoid having a browser refresh for a moment.</p></blockquote>]]></content><amg:twitter><![CDATA[An excellent reflection on JavaScript and progressive enhancement.]]></amg:twitter><category term="progressive enhancement" /><category term="JavaScript" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/a-practical-guide-to-progressive-enhancement-in-2023/</id><title type="html"><![CDATA[🔗 A Practical Guide to Progressive Enhancement in 2023]]></title><link href="https://www.aaron-gustafson.com/notebook/links/a-practical-guide-to-progressive-enhancement-in-2023/" rel="alternate" type="text/html" /><link href="https://medium.com/@jason.godesky/a-practical-guide-to-progressive-enhancement-in-2023-52c740c3aff3" rel="related" type="text/html" /><published>2023-03-10T19:30:41Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love it when people share their approaches to progressive enhancement. I don’t know that I would take the same approach, but it was really interesting to read Jason’s rationale and to see the comparisons between his original React project and the progressively enhanced one.</p>]]></content><amg:twitter><![CDATA[I love it when people share their approaches to progressive enhancement.]]></amg:twitter><category term="progressive enhancement" /><category term="JavaScript" /><category term="web development" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/redefining-developer-experience/</id><title type="html"><![CDATA[🔗 Redefining Developer Experience]]></title><link href="https://www.aaron-gustafson.com/notebook/links/redefining-developer-experience/" rel="alternate" type="text/html" /><link href="https://begin.com/blog/posts/2023-02-28-redefining-developer-experience" rel="related" type="text/html" /><published>2023-03-08T17:35:16Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>This is a rather lengthy piece on developer experience from Cole Peters (who works on the Enhance framework). He makes some excellent points, including this one:</p><blockquote><p>I believe ‘good developer experience’ needs to be re-imagined as the enabling of developers to deliver reliable and first rate end user experiences — for as many users as possible, and for as long as possible. With this goal in mind, I think we’d be hard pressed to find a better starting point than the fundamentals of the web platform — that is, web browsers, HTTP, HTML, CSS, and JavaScript. These technologies have existed for decades in a near total state of backwards compatibility, while also consistently evolving to offer web developers ever more powerful techniques to deliver exceptional user experiences.</p></blockquote>]]></content><amg:twitter><![CDATA[Excellent discussion of #DeveloperExperience in the context of #WebDevelopment.]]></amg:twitter><category term="web development" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://begin.com/_public/blog/post-assets/cole-dev-ex/analogue-life-japanese-scissors-ca64a66343.jpg" /></entry><entry><id>https://www.aaron-gustafson.com/notebook/links/nhsd-apply-for-care-id-alpha-assessment/</id><title type="html"><![CDATA[🔗 NHSD Apply for Care ID alpha assessment]]></title><link href="https://www.aaron-gustafson.com/notebook/links/nhsd-apply-for-care-id-alpha-assessment/" rel="alternate" type="text/html" /><link href="https://digitalhealth.blog.gov.uk/2023/03/03/nhsd-apply-for-care-id-alpha-assessment/" rel="related" type="text/html" /><published>2023-03-08T17:29:45Z</published><content type="html" xml:base="https://www.aaron-gustafson.com"><![CDATA[<p>I love that the UK government is making digital product assessments, like this one for the in-development Care ID Assessment tool, public. There’s a lot to be learned from critiques like this.</p><p>In particular, I was impressed with how they held the line on the importance of robustness in tools like this (from <a href="https://digitalhealth.blog.gov.uk/2023/03/03/nhsd-apply-for-care-id-alpha-assessment/#choose-the-right-tools-and-technology">the “Choose the right tools and technology” section</a>):</p><blockquote><p>Before their reassessment, the team needs to … allow users who have issues using services with Javascript or have Javascript disabled. The team must build services for all users and cannot depend on client-side Javascript.</p></blockquote><p>Which yielded results in their reassessment:</p><blockquote><p>The panel was impressed that:</p><ul><li>the team has worked around the limitations for progressive enhancement of the service, resulting from the use of a serverless Single Page Application (SPA) architecture, which is a historical technology choice inherited from NHS loginthe team has ensured the service now works for all applicants without requiring JavaScript to be enabled</li><li>the team has used the third party Paycasso identity verification mobile application to automate many parts of the process for validating an identity document, presenting a significant improvement versus the current remote identity check process via video link</li><li>the team has used the no JavaScript route through the service which re-uses the business logic for the SPA route despite the UI forms being separately maintained for both routes</li></ul></blockquote><p>The assessment also provides guidance for further improvements to be made. Love this!</p>]]></content><amg:twitter><![CDATA[I love that the UK government is making digital product assessments, like this one for the in-development Care ID Assessment tool, public. There’s a lot to be learned from critiques like this.]]></amg:twitter><category term="web development" /><category term="progressive enhancement" /><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://g.twimg.com/Twitter_logo_blue.png" /></entry></feed>