This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan
<p>The web is a powerful thing, with the right capabilities you can create tools and services that can be deployed without a central authority and can also deeply integrate with the peoples devices.</p>
<p>I love being able to tinker and scratch an itch quickly.</p>
<p>The itch in question was that I'm a heavy user of .new domains and I wanted a quick way to have access to them on my Android home screen so that I can start a task without having to find the app, go through its menus and then create the "new" thing.</p>
<p>If you're not well versed in .new domains, they're great. Want to create an email? <a href="https://mail.new/">mail.new</a>. Want a doc? <a href="https://docs.new">docs.new</a>.... etc etc. The .new domains are simple shortcuts to deep-linked functionality.</p>
<p><a href="https://shortcut.cool">shortcut.cool</a> is the demo I built. It lets you create any number of shortcuts that you can then install as a PWA onto your homescreen.</p>
<figure><img src="https://paul.kinlan.me/images/2020-12-14-creating-a-quick-launcher-for-android-using-the-web-0.jpeg" alt="Shortcut new"></figure>
<h2 id="how-does-it-work">How does it work?</h2>
<p>Before I get too far into the weeds, at a high-level this is a PWA. The PWA has a small service worker and a web app manifest. The manifest defines how your PWA should be installed and launched, and it is also what defines the list of shortcuts that you might want to be on your home screen (checkout <a href="https://web.dev/app-shortcuts/">shortcuts</a> in the web app manifest). </p>
<p>The entire site is a configuration tool for custom web app manifests.</p>
<p>At a deeper level, the demo is a small nodeJS express service that takes user configuration that is passed in from the query string, decodes it, and returns a valid manifest file as follows:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">"/:actions/manifest.json"</span>, (<span style="color:#a6e22e">request</span>, <span style="color:#a6e22e">response</span>) => {
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">buff</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Buffer</span>.<span style="color:#a6e22e">from</span>(<span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">actions</span>, <span style="color:#e6db74">"base64"</span>);
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">actions</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">buff</span>.<span style="color:#a6e22e">toString</span>(<span style="color:#e6db74">"ascii"</span>));
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">shortcuts</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">actions</span>.<span style="color:#a6e22e">map</span>(({ <span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">url</span> }) => {
<span style="color:#66d9ef">return</span> {
<span style="color:#a6e22e">name</span>,
<span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">actions</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/launch?url=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">url</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
<span style="color:#a6e22e">icons</span><span style="color:#f92672">:</span> [
{
<span style="color:#a6e22e">src</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"images/ic_launcher.png"</span>,
<span style="color:#a6e22e">sizes</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"192x192"</span>,
},
],
};
});
<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>({
<span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"shortcut.cool"</span>,
<span style="color:#a6e22e">short_name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"shortcut.cool"</span>,
<span style="color:#a6e22e">background_color</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"#E91E63"</span>,
<span style="color:#a6e22e">theme_color</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"#E91E63"</span>,
<span style="color:#a6e22e">display</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"standalone"</span>,
<span style="color:#a6e22e">icons</span><span style="color:#f92672">:</span> [
{
<span style="color:#a6e22e">sizes</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"192x192"</span>,
<span style="color:#a6e22e">src</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"images/ic_launcher.png"</span>,
<span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"image/png"</span>,
<span style="color:#a6e22e">purpose</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"maskable"</span>,
},
{
<span style="color:#a6e22e">sizes</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"512x512"</span>,
<span style="color:#a6e22e">src</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"images/web_hi_res_512.png"</span>,
<span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"image/png"</span>,
<span style="color:#a6e22e">purpose</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"any"</span>,
},
],
<span style="color:#a6e22e">shortcuts</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">shortcuts</span>,
<span style="color:#a6e22e">start_url</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">actions</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/`</span>,
});
});
</code></pre></div><p>If you're familiar with nodeJS you can see that the architecture of this site is not too complex, but it does require some explaining. The site has a basic user interface that takes in the user's desired configuration of site names and launch URLs.</p>
<figure><img src="https://paul.kinlan.me/images/2020-12-14-creating-a-quick-launcher-for-android-using-the-web-1.jpeg" alt="The UI"></figure>
<p>When the user hits 'Create Launcher' the site encodes the configuration into the URL with the format <code>/[base64 encoding of url and name pairs]/</code>.</p>
<figure><img src="https://paul.kinlan.me/images/2020-12-14-creating-a-quick-launcher-for-android-using-the-web-2.jpeg" alt="The URL"></figure>
<p>With this URL format, each distinct configuration has its own custom directory, its own service worker and manifest for its scope, with all of the required images relative to the new PWA. The launch url for each shortcut is also locked to the PWA - and is a simple redirect based on a <code>url</code> parameter.</p>
<p>This method of encoding the data in the URL worked well because it meant I don't have to store any state in on the server, which means I don't have to have user accounts or databases, and when the URL changes the updated manifest and service worker mean that Chrome will offer to install this new experience.</p>
<p>It does have a couple of drawbacks. By using a URL to encode the data, it can be quite brittle. While I don't think there's a major issue, encoding and rendering user inputted data is a recipe for potential security issues and although I've tested it fairly well, it's, well, a known area of potential concern. And finally, you can't update your existing installed PWA - because configurations are distinct PWA's and are unique to the data entered, the one you create will always be the way you configured it. Any change you make to the configuration will be considered a new PWA.</p>
<p>Anyway, I love how the web let's me mess about like this and try simple new services. If you want to have a look at how I built it, the code is up on my <a href="https://github.com/PaulKinlan/quick.new">github</a>.</p>
This content originally appeared on Modern Web Development with Chrome and was authored by Paul Kinlan