<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alistair Shepherd</title>
    <description>The latest articles on Forem by Alistair Shepherd (@accudio).</description>
    <link>https://forem.com/accudio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F462974%2Fb349f7e2-fcf3-4313-b44d-646bacaf174a.jpg</url>
      <title>Forem: Alistair Shepherd</title>
      <link>https://forem.com/accudio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/accudio"/>
    <language>en</language>
    <item>
      <title>Selling a small front-end web project — what I learned</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/selling-a-small-front-end-web-project-what-i-learned-4g8k</link>
      <guid>https://forem.com/accudio/selling-a-small-front-end-web-project-what-i-learned-4g8k</guid>
      <description>&lt;p&gt;In 2017, I made a small web app for interacting with the Spotify API — mostly to learn some new technologies. Surprisingly it took off, getting quite a lot of visitors despite my lack of interest in developing it further or monetising it.&lt;/p&gt;

&lt;p&gt;Earlier this year someone reached out to me asking about my plans for the project and whether I would be interested in selling it. Fast forward a few weeks and I have sold my first web project and learnt a lot in the meantime!&lt;/p&gt;

&lt;p&gt;I didn't find much information online about the process of selling a project, particularly one without any monetisation and at a small scale. This is my attempt at helping others who may end up in this position.&lt;/p&gt;

&lt;p&gt;That's the TLDR; stick around for more detail!&lt;/p&gt;

&lt;h2&gt;
  
  
  About My Top for Spotify
&lt;/h2&gt;

&lt;p&gt;So the project is called &lt;a href="https://mytopspotify.com" rel="noopener noreferrer"&gt;My Top for Spotify&lt;/a&gt;. It's a pretty small web app built on React that allows a user to connect their Spotify account and see what the Spotify API says about their top artists and songs over a few different time periods.&lt;/p&gt;

&lt;p&gt;In early 2017 I was looking to move from freelance development into a permanent role, and loads of jobs were looking for React experience. I'd done the standard hello world and to-do app, but wanted something live, real-world, and app-like for the experience and a portfolio piece. I built it with Create React App and a server based on AWS Lambda and Serverless. To be honest I really don't like the tech stack, and it was the start of my dislike of those two products — but that's not what this post is about!&lt;/p&gt;

&lt;p&gt;I cracked it out in a few weeks, the functionality is pretty straightforward and the design has some cool CSS details but overall unpolished. But it was just a demo for me and a few friends so who cares right? I named it My Top for Spotify, purchased a .com and .io domain and deployed, shared it around some friends and family and that was that.&lt;/p&gt;

&lt;p&gt;Quite quickly it started to get quite a lot of traffic, ranking highly on search engines, and being mentioned in blogs and YouTube videos. I believe this is because just a few months earlier Spotify released their first Spotify Wrapped — an annual review of your Spotify listening. This seemed to drive loads of interest in seeing this kind of data, and my little experiment ended up top of the search results for queries like "See top Spotify tracks".&lt;/p&gt;

&lt;p&gt;And then I basically did nothing! The app got about 4 million annual visitors in those first few years, and I just left it. I considered further work and monetisation but didn't have much interest in that — particularly not in turning it into a business. I paid the hosting and domain costs for it myself, did the odd urgent fix or upgrade and otherwise let it be. After that initial deploy I probably spent about 10 hours on it total over a period of 7 years.&lt;/p&gt;

&lt;p&gt;That's where it sat as of May 2024, getting about 1–2 million views a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking about selling
&lt;/h2&gt;

&lt;p&gt;I received an email out of the blue earlier this year, asking if I had any plans for the project and if I would consider selling it. I was pretty confident this was a spam email to be honest — perhaps I'm too suspicious! Regardless, I replied with mild interest and frankly saying it was something I hadn't even considered.&lt;/p&gt;

&lt;p&gt;What followed was a lot of emails back-and-forth, discussing the project and their interest. The whole process went pretty quick, even despite a time difference between the me in the UK and the buyers in Australia. Within six weeks of that initial email we were making the transfer.&lt;/p&gt;

&lt;p&gt;After those first few emails I became very aware I had no idea what I was doing. Searching online brought up some guides and articles, but only a handful that were relevant to a small-scale project that doesn't earn any money. &lt;a href="https://training.kalzumeus.com/newsletters/archive/selling_software_business" rel="noopener noreferrer"&gt;Patrick McKenzie&lt;/a&gt; has a good article which I would suggest reading, especially for selling a small SaaS.&lt;/p&gt;

&lt;p&gt;I am also fortunate to have some great people in my 'network' that gave me advice, including my parents, folk I work with at Series Eight, and online friends on Mastodon and Discord. A particularly huge thank you to &lt;a href="https://narrativ.es/@janl" rel="noopener noreferrer"&gt;Jan Lehnardt&lt;/a&gt;, who kindly spent some time on a call taking me through his experience and helping me work through my priorities and the risks involved. &lt;a href="https://kilianvalkhof.com" rel="noopener noreferrer"&gt;Kilian Valkhof&lt;/a&gt; also gave me much-appreciated advice from selling a similar size of project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sell, and my priorities
&lt;/h2&gt;

&lt;p&gt;The main reason I was willing to sell My Top Spotify was because it was going to waste under me. I invested close to nothing into it — AWS and domain costs and the odd hour when there was an SSL cert issue or Netlify needed an update.&lt;/p&gt;

&lt;p&gt;I had no intention of doing anything further with it, and there is now more competition in the area — companies actually investing in their products to add new features and gain more visibility. That resulted in a decline in popularity to other tools investing in features and marketing. Ultimately I didn't give or receive much value, and people willing to invest time and money could do a lot more with it.&lt;/p&gt;

&lt;p&gt;Because I wasn't planning on selling it originally, that gave me a bit of power over negotiations and the decision — at least in my head. If I decided that I wasn't happy with the arrangement I wouldn't be any worse off — so I could be picky.&lt;/p&gt;

&lt;p&gt;On being picky, it was very important for me to consider my priorities for the project and for the sale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The buyer should appear to care about the project;&lt;/li&gt;
&lt;li&gt;I had to be confident it wouldn't be immediately strip-mined of value or users exploited maliciously;&lt;/li&gt;
&lt;li&gt;A fair offer (more on that shortly);&lt;/li&gt;
&lt;li&gt;No major risk from me — money before permanent transfers;&lt;/li&gt;
&lt;li&gt;Any additional fees covered by the buyer – common but not standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those were the principles I decided I wasn't willing to compromise on, and I think that was extremely helpful for me to focus on.&lt;/p&gt;

&lt;p&gt;I've seen and heard of web projects being strip-mined of value, becoming riddled with ads, or even injecting malware. I really worried about that, and it was the main thing that held me back.&lt;/p&gt;

&lt;p&gt;As for the money and risk, as someone new to all this making sure I wasn't about to screw up was really important. Jan was fantastic helping me work through the risks I was and wasn't comfortable with.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I value an unmonetised side project?
&lt;/h2&gt;

&lt;p&gt;Okay so I had a stable project with lots of visitors, great search engine ranking, low outgoings, and a pretty promising future.&lt;/p&gt;

&lt;p&gt;So what is it worth?&lt;/p&gt;

&lt;p&gt;Wherever I looked online, the first step to answering that was "how much does it earn?". At a big fat zero that makes it tricky! I tried loads of domain evaluators — ranging from $100–$100k — and the calculators of various project 'marketplaces' do not handle that situation well at all. I could find essentially no good blogs or details that accounted for selling a free project based on potential rather than revenue.&lt;/p&gt;

&lt;p&gt;Eventually I gave up and decided to ask the buyer "How much are you going to pay for it?", and see what happens. Their suggestion was what we based our final amount on — assuming the site become advertising-funded and basing the value on &lt;a href="https://adsense.google.com/start/#calculator" rel="noopener noreferrer"&gt;Google's AdSense estimator&lt;/a&gt;. With some negotiation we agreed on how many years to include, and to using an average monthly pageviews from the previous year of my analytics to account for seasonal shifts.&lt;/p&gt;

&lt;p&gt;I'm not a salesperson or valuator so I'm sure others would use a different method accounting for repeat visitors, SEO potential, backlinks, domain value and much more. Probably with WAY different numbers. Crucially however I am really happy with this method. The key thing for me is that it's very clear, independent, and objective.&lt;/p&gt;

&lt;p&gt;We negotiated details but in the end with the AdSense estimator I could easily weigh up the comparison between implementing ads myself and receiving ongoing funds, or receiving them up-front without the need of ongoing maintenance. I'm sure that there was potential for further valuation but this method felt fair to me and the buyer.&lt;/p&gt;

&lt;p&gt;I'm not going to detail any of the final numbers we agreed as that's not the point of this. If you're selling something similarly unmonetised then I'd recommend having a discussion with the buyer and see where it goes. The AdSense tool could be handy to reference if that fits the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanics of selling
&lt;/h2&gt;

&lt;p&gt;I've decided I'm selling, we've agreed a price, what happens next?&lt;/p&gt;

&lt;p&gt;There were a few things I was unsure about at this point: do we need to use a broker; do I need a solicitor to review contracts; how do we handle AUD → GBP payment?&lt;/p&gt;

&lt;p&gt;I had absolutely no idea if a broker would be needed, what it costed and involved, but lots of places online swear by them. Both Jan and Kilian talked to me about the risks vs costs, so combined with the principles I detailed above there were a couple options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use a broker for the domain names, but the buyer pays for it;&lt;/li&gt;
&lt;li&gt;I transfer everything non-permanent — codebase, re-assign DNS, environment variables — and then only transfer permanent domain ownership once I've received payment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Obviously this depends on the buyer but this second option worked out great for us. It reduced the complexity and fees whilst giving me control and some protection against the risk of things going wrong. From the buyer's perspective there was still some risk but by transferring everything non-permanent I'd proven I was serious about following through.&lt;/p&gt;

&lt;p&gt;The contracts discussion was pretty easy also, I didn't need a solicitor and went through it myself — asking a couple friends to check there wasn't anything I was missing. Once again your mileage may vary.&lt;br&gt;&lt;br&gt;
I asked for a few additions — making it clear I wasn't liable for future support, clarifying trademark details, and making sure I was able to write this blog post! All were agreed and easily added.&lt;/p&gt;

&lt;p&gt;And finally for international payments, turns out that's an easy one. The buyer took my UK bank account details and used Wise to transfer the amount agreed over at the market exchange rate. Nice and easy!&lt;/p&gt;

&lt;p&gt;Once those questions were worked out it went pretty smoothly — the code was already open-source so I had no issue handing that over before we'd signed, what had real value was the domain name. I detailed the tech stack and how it was built, wrote some documentation and tips on ongoing maintenance.&lt;/p&gt;

&lt;p&gt;We hopped on a call to finalise the tech setup, answer any technical questions, share environment variables, sign the contract, and finally switch the DNS. Once that was all sorted they sent the payment, and when I confirmed that in my bank account I started the domain transfer.&lt;/p&gt;

&lt;p&gt;And that was it! We had a few emails in the following weeks to confirm everything was going well, share Google Search Console and Analytics access and that was that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it feel?
&lt;/h2&gt;

&lt;p&gt;It feels good! It was a good experience to go through, I feel like I'm doing good by the project that I had basically abandoned, I appreciate the money of course, and it's also a much appreciated new marker of success for me. I'll be sewing on my "website sale" scout badge and putting "Founder with one major exit" on my CV now 😉.&lt;/p&gt;

&lt;p&gt;I feel quite fortunate that the people who have bought the project were very helpful and friendly throughout, and seem genuinely keen to develop and improve the project. I'm sure that not all sales go as well and smoothly so I am thankful for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  My key takeaways
&lt;/h2&gt;

&lt;p&gt;If you take anything from this post — and for my own future reference — here are my takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work out your priorities, they help you make judgements about process and value;&lt;/li&gt;
&lt;li&gt;You don't necessarily need a broker, you may be able do it without one;&lt;/li&gt;
&lt;li&gt;Unmonetised projects are hard to value, ask for offers. The AdSense Estimator may be helpful;&lt;/li&gt;
&lt;li&gt;Consider adding analytics to projects. It doesn't have to be GA, but page views at minimum are essential for a sale;&lt;/li&gt;
&lt;li&gt;Make sure contracts make clear your support/responsibility post-sale;&lt;/li&gt;
&lt;li&gt;Remember that ultimately as soon as it's sold it's out of your control;&lt;/li&gt;
&lt;li&gt;Don't use an io domain! Read &lt;a href="https://www.beep.blog/io/" rel="noopener noreferrer"&gt;.io considered harmful&lt;/a&gt;, but also they're more expensive than they're worth for side projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What now?
&lt;/h2&gt;

&lt;p&gt;For My Top for Spotify, I don't really know! There has clearly been some hard work and new functionality like global stats already, and I've been told that they're working hard to evolve and improve it.&lt;br&gt;&lt;br&gt;
I think I'll check in once in a while for the sake of curiosity, but I'm happy to otherwise move on. I wish the new team working on the site all the best with it. If you use Spotify I'd encourage you to check it out at &lt;a href="https://mytopspotify.com" rel="noopener noreferrer"&gt;mytopspotify.com&lt;/a&gt; if you're interested.&lt;/p&gt;

&lt;p&gt;For me nothing changes. I have no interest in building things with only value in mind, so it'll continue to be things for myself and for fun!&lt;/p&gt;




&lt;p&gt;If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and &lt;a href="//mailto:alistair@accudio.com"&gt;send me an email&lt;/a&gt; at &lt;a href="mailto:alistair@accudio.com"&gt;alistair@accudio.com&lt;/a&gt; or &lt;a href="https://front-end.social/@accudio" rel="noopener noreferrer"&gt;contact me on Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.alistairshepherd.uk/writing/selling-web-project/" rel="noopener noreferrer"&gt;Selling a small front-end web project — what I learned&lt;/a&gt; appeared first on &lt;a href="https://alistairshepherd.uk" rel="noopener noreferrer"&gt;alistairshepherd.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Proxying an Image CDN with Cloudflare workers</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sat, 03 Aug 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/proxying-an-image-cdn-with-cloudflare-workers-4ek4</link>
      <guid>https://forem.com/accudio/proxying-an-image-cdn-with-cloudflare-workers-4ek4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This &lt;a href="https://www.alistairshepherd.uk/writing/cloudflare-worker-image-proxy/" rel="noopener noreferrer"&gt;post appeared on my blog&lt;/a&gt; back in July, but am adding here so you nice folks on dev.to can see it also! I'll be posting here more consistently in future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A short intro and code on how you can use Cloudflare workers on Cloudflare's free tier to proxy an image CDN like Cloudinary, CloudImage or Imgix. Using this you can load optimised images via the primary origin, support the Vary header for format conversion, and use Cloudflare's cache for those images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;CDNs are extremely good for web performance, and if you can deliver your HTML via a CDN you can benefit from extremely fast from-cache load times and rely on a smaller server.&lt;/p&gt;

&lt;p&gt;Cloudflare is not without it's problems, but it offers a free tier that gives you extremely good CDN capabilities for zero cost. In my opinion there are better options for bigger usecases, but for a small site the free Cloudflare tier is great.&lt;/p&gt;

&lt;p&gt;One thing notaby absent from Cloudflare's free tier is image transformation. I find that super frustrating because loads of companies offer a free tier of image transformation but not Cloudflare, so if you want to keep costs down you're going to have to use a different company. If you're not familiar with Image CDNs and why you should use one, check out my &lt;a href="https://dev.to/speaking/imagecdns/"&gt;talk on the subject&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With an Image CDN you will probably get a URL something like &lt;a href="http://kdnfwe948owqq.example.com" rel="noopener noreferrer"&gt;kdnfwe948owqq.example.com&lt;/a&gt; that you can load your images through. Now you're loading your images through a different origin however, there is a performance implication where a new connection has to be established to that origin which may delay image loads.&lt;/p&gt;

&lt;p&gt;Wouldn't it be great if we could use our image CDN, but make it use our primary origin that matches our website? It's a little tricky but turns out that's possible to do in the free tier of Cloudflare using Cloudflare workers!&lt;/p&gt;

&lt;h2&gt;
  
  
  How
&lt;/h2&gt;

&lt;p&gt;This code is mostly taken from &lt;a href="https://github.com/wesbos/cloudflare-cloudinary-proxy" rel="noopener noreferrer"&gt;Wes Bos&lt;/a&gt; and &lt;a href="https://github.com/wilsonhou/cloudflare-image-proxy" rel="noopener noreferrer"&gt;Wilson Hou&lt;/a&gt; on GitHub, thanks to them for their work.&lt;/p&gt;

&lt;p&gt;First thing is to set up a Worker. You can do this via a command-line but to be honest for this I just use the admin.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your Cloudflare Dashboard and "Workers &amp;amp; Pages"&lt;/li&gt;
&lt;li&gt;"Create", "Create Worker", name it, and then "Deploy"&lt;/li&gt;
&lt;li&gt;Now lets' assign and test it&lt;/li&gt;
&lt;li&gt;Assign it to a URL by going to your cloudflare zone/website and to "Workers Routes"&lt;/li&gt;
&lt;li&gt;"Add Route", choose the worker you've just created and create a route pattern

&lt;ul&gt;
&lt;li&gt;This needs to include the domain name and uses regex to specify patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.example.com/images/*" rel="noopener noreferrer"&gt;www.example.com/images/*&lt;/a&gt; will make the worker serve all requests under the &lt;code&gt;/images/&lt;/code&gt; path&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;"Save" and confirm requests to &lt;code&gt;/images/whatever&lt;/code&gt; return "Hello world!"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we need to add the proxying code. To add this we go back to the worker we created and click "Edit code". This opens an online editor with text field, debugging tools and the "Deploy" button in the top-right.&lt;/p&gt;

&lt;p&gt;Add the below code, customise the &lt;code&gt;destination&lt;/code&gt; variable and add any path manipulation you need. Then deploy and you should be golden!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://kdnfwe948owqq.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;serveAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// if this is already in the cache return that&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;
  &lt;span class="c1"&gt;// make any path manipulation here, eg removing a prefix&lt;/span&gt;
  &lt;span class="c1"&gt;// path = path.replace('/images', '')&lt;/span&gt;

  &lt;span class="c1"&gt;// request the URL with path and URL params&lt;/span&gt;
  &lt;span class="c1"&gt;// include request headers for content negotiation/auto-format&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// add caching header, configured here for 1-year&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`public, max-age=31536000`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// vary header so cache respects content-negotiation/auto-format&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// create response and add to the cache if successful&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// get the response&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;serveAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// if not a successful status code return response text&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;399&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Things to note
&lt;/h2&gt;

&lt;p&gt;This works pretty well, there are a couple of things to note however.&lt;/p&gt;

&lt;p&gt;The free Cloudflare tier includes &lt;a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits" rel="noopener noreferrer"&gt;limits on Workers&lt;/a&gt;, so depending on the size of your site you may run into these. It's also account-level not website-level so keep that in mind. At time of writing it's pretty high at "100,000 requests/day, 1000 requests/min". You'll likely run into that only if you have a busy site or many using this technique.&lt;/p&gt;

&lt;p&gt;This also creates another layer of caching in your image chain, which depending on your setup and if your images are mutable could be tricky. This would probably already be the case for the image CDN however, so you'd be needing to clear two caches instead of one. Using permanent image URLs and ensuring modifications create a copy instead of modifying the original is a great way to avoid this being an issue and better for client caching also.&lt;/p&gt;




&lt;p&gt;If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and &lt;a href="//mailto:alistair@accudio.com"&gt;send me an email&lt;/a&gt; at &lt;a href="mailto:alistair@accudio.com"&gt;alistair@accudio.com&lt;/a&gt; or &lt;a href="https://front-end.social/@accudio" rel="noopener noreferrer"&gt;contact me on Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.alistairshepherd.uk/writing/cloudflare-worker-image-proxy/" rel="noopener noreferrer"&gt;Proxying an Image CDN with Cloudflare workers&lt;/a&gt; appeared first on &lt;a href="https://alistairshepherd.uk" rel="noopener noreferrer"&gt;alistairshepherd.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>serverless</category>
    </item>
    <item>
      <title>JS library compilation to browser, esm and cjs using esbuild</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Fri, 19 Jul 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/js-library-compilation-to-browser-esm-and-cjs-using-esbuild-o0e</link>
      <guid>https://forem.com/accudio/js-library-compilation-to-browser-esm-and-cjs-using-esbuild-o0e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This &lt;a href="https://www.alistairshepherd.uk/writing/js-library-bundling-esbuild/" rel="noopener noreferrer"&gt;post appeared on my blog&lt;/a&gt; back in July, but am adding here so you nice folks on dev.to can see it also! I'll be posting here more consistently in future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a quick post, public but mostly for myself about how to do simple JS library compilation and bundling with &lt;a href="https://esbuild.github.io" rel="noopener noreferrer"&gt;esbuild&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I have a few JS libraries that are shipped via npm, and can be used with an ESM &lt;code&gt;import&lt;/code&gt;, CommonJS &lt;code&gt;require&lt;/code&gt;, or a browser &lt;code&gt;script&lt;/code&gt; element. I've found it pretty tricky in the past working out how to compile to all of these correctly from my ESM source, and the internet hasn't been very helpful either.&lt;/p&gt;

&lt;p&gt;What I want is a setup that will produce all three of those with no huge legwork, handle bundling, sourcemaps, minification for me, convert modern syntax to es6, and have a dev mode that will re-compile on source change.&lt;/p&gt;

&lt;p&gt;Esbuild is ideal for this circumstance as it has a simple API, all of those features and is extremely fast! I'm not saying this is the ideal setup but it works pretty well for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  build.js
&lt;/h2&gt;

&lt;p&gt;This is the build script, called with &lt;code&gt;node build.js&lt;/code&gt; for a production build and &lt;code&gt;node build.js --watch&lt;/code&gt; for dev mode. Pop these in your &lt;code&gt;package.json&lt;/code&gt; scripts if you like.&lt;/p&gt;

&lt;p&gt;You define the formats you want and the esbuild options within &lt;code&gt;buildAll&lt;/code&gt;, I find this works well in most instances. This will get &lt;code&gt;src/index.js&lt;/code&gt; and &lt;code&gt;src/script.js&lt;/code&gt; (for browsers, imports index and runs it) and output to the &lt;code&gt;dist/&lt;/code&gt; directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;buildAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/script.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neutral&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node10.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.js`&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Building &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`./dist/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`./dist/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  package.json
&lt;/h2&gt;

&lt;p&gt;To deploy this via npm and potentially load the browser version from a CDN like JSDelivr, we need to set up our &lt;code&gt;package.json&lt;/code&gt; correctly. The following snippet is what I've found works well for modern versions of node. &lt;code&gt;exports&lt;/code&gt; defines which version of the file to use based on how it's imported, Node will change which version is used based on whether we &lt;code&gt;require&lt;/code&gt; or &lt;code&gt;import&lt;/code&gt;. We set &lt;code&gt;main&lt;/code&gt; to the &lt;code&gt;script.js&lt;/code&gt; file so it's picked up by all major CDNs I've tried.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/script.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/cjs.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/esm.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/esm.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and &lt;a href="//mailto:alistair@accudio.com"&gt;send me an email&lt;/a&gt; at &lt;a href="mailto:alistair@accudio.com"&gt;alistair@accudio.com&lt;/a&gt; or &lt;a href="https://front-end.social/@accudio" rel="noopener noreferrer"&gt;contact me on Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.alistairshepherd.uk/writing/js-library-bundling-esbuild/" rel="noopener noreferrer"&gt;JS library compilation to browser, esm and cjs using esbuild&lt;/a&gt; appeared first on &lt;a href="https://alistairshepherd.uk" rel="noopener noreferrer"&gt;alistairshepherd.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>npm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Simple, fast build tooling with live reload for a non-framework website</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sun, 14 Jul 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/simple-fast-build-tooling-with-live-reload-for-a-non-framework-website-41d6</link>
      <guid>https://forem.com/accudio/simple-fast-build-tooling-with-live-reload-for-a-non-framework-website-41d6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This &lt;a href="https://www.alistairshepherd.uk/writing/simple-build-tooling/" rel="noopener noreferrer"&gt;post appeared on my blog&lt;/a&gt; back in July, but am adding here so you nice folks on dev.to can see it also! I'll be posting here more consistently in future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found a nice build tooling setup that can be integrated into non-framework websites where some part isn't controlled by the build tool. WordPress, Craft, Kirby or other CMS' are a good example. My setup uses Parcel, livereload and only a few node scripts to work. Jump to the details or stick around for the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Yesterday I started a new project for a family member, a simple WordPress website built with a custom from-scratch theme. One of the first thing to sort out is build tooling to bundle my CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;Most build tools now seem to be geared around the 'in fashion' technologies like React, Vue etc. I come back to build tooling relatively often, wondering if the tech world has understood yet that making 'index.html' a requirement for your build tool to work makes it useless to a huge number of developers.&lt;/p&gt;

&lt;p&gt;If you weren't aware, many build tools want to own your entire website build including HTML and all assets. That works great when your whole website runs through your build, but for dynamic sites that have their own asset loading systems — like WordPress and countless other CMS' — that just doesn't work. Whilst many build tools do offer an alternative it is often second-rate, complex or doesn't have the same functionality I expect from a modern development setup.&lt;/p&gt;

&lt;p&gt;That leads me onto my day yesterday. I wanted to work out finally how I can get a good modern development setup on this WordPress site. My requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bundles my Scss, JS, inlines small images, imports from npm with tree-shaking. Standard build stuff.&lt;/li&gt;
&lt;li&gt;Uses versioned/hashed asset names to make caching easier and gives me those names in a way I can use in PHP.&lt;/li&gt;
&lt;li&gt;Fast. I don't want to wait for builds.&lt;/li&gt;
&lt;li&gt;Instant hot switching of CSS without need for a reload.&lt;/li&gt;
&lt;li&gt;Automatic reloading of my browser when JS, HTML or PHP changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turns out this was harder to achieve than I expected. Let's go through the candidates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://laravel-mix.com" rel="noopener noreferrer"&gt;Laravel Mix&lt;/a&gt; is what I'm used to from work and is what a lot of the PHP community uses. Simple, declarative way of bundling assets based on Webpack. It does have Browsersync out-of-the-box for automatic reloading, but it need to proxy your site to do that and I've found it a bit inconsistent. When using Mix I generally manually reload as it's more reliable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://webpack.js.org" rel="noopener noreferrer"&gt;Webpack&lt;/a&gt; is awful and I hate it. The configuration system is terrible and indecipherable and in my opinion it is always the wrong decision for a build tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vitejs.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; is the hot new kid on the block. It's very much designed for frameworks or for if it's also handling your index.html. It is possible to make it work with WordPress etc, but I haven't seen a setup that looks great and their documentation is pretty poor if you opt for that route.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rollupjs.org" rel="noopener noreferrer"&gt;RollUp&lt;/a&gt; looks good and I'd like to try it, but I find it quite difficult to actually get what I want from it. I've played with it a few times and just couldn't find a setup that was as flexible as I wanted. That may just be me not understanding right but that I can't work out how to get a simple CSS/JS bundler set up is not a good sign.&lt;/p&gt;

&lt;p&gt;A custom system with node scripts and importing packages? It would probably work but there is a lot to deal with that build tools have already been through. Inlining a 600byte SVG into my CSS file is something I want but don't want to have to build myself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://parceljs.org" rel="noopener noreferrer"&gt;Parcel&lt;/a&gt; was what I ended up settling on. It's a little like Vite where it's built for the simple, no config, index.html setup, but I find it's a lot more versatile and easy to use in other circumstances, and it's docs are a lot better for non-standard usecases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The set up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 0. Installation
&lt;/h3&gt;

&lt;p&gt;If you're following along with implementing this yourself, you'll need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; parcel livereload pug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the commands below you'll be able to use them from npm scripts no problems, but if you're running on the command line yourself add &lt;code&gt;npx&lt;/code&gt; in front of each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Asset bundling
&lt;/h3&gt;

&lt;p&gt;Parcel handles compilation of most stuff pretty easily and out of the box using &lt;code&gt;link&lt;/code&gt; and &lt;code&gt;script&lt;/code&gt; tags, so I'm starting there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- src/assets.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./css/main.scss"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./js/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run the production build with &lt;code&gt;parcel build ./src/assets.html --public-url /dist/&lt;/code&gt;. This will bundle and import my &lt;code&gt;main.scss&lt;/code&gt; and &lt;code&gt;main.js&lt;/code&gt; files, and output &lt;code&gt;dist/assets.html&lt;/code&gt; that includes the HTML to those versioned assets, which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/dist/assets.50a966ac.css"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/dist/assets.fd9c92df.js"&lt;/span&gt; &lt;span class="na"&gt;defer=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can import &lt;code&gt;/dist/assets.html&lt;/code&gt; into the output of my page — for example using PHPs &lt;code&gt;file_get_contents&lt;/code&gt;. We can change the path used in the output URLs if needed with the &lt;code&gt;--public-url&lt;/code&gt; parameter — for my WordPress theme I use &lt;code&gt;--public-url /wp-content/themes/theme/dist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we have asset bundling with URL hashing that we can import into our templates. The build is also really fast. Nice!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Parcel reloading and dev server
&lt;/h3&gt;

&lt;p&gt;Okay so we've got a build, but it's one-off and doesn't do anything about reloading. Next step is setting Parcel up to reload. Although most guides will push you down Parcel's serve mode to serve your assets here, the way to go is actually using watch mode, with this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;parcel watch ./src/assets.html &lt;span class="nt"&gt;--public-url&lt;/span&gt; /dist/ &lt;span class="nt"&gt;--hmr-host&lt;/span&gt; localhost &lt;span class="nt"&gt;--hmr-port&lt;/span&gt; 1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will run a watcher that compiles the assets and html file whenever CSS or JS files change. It also runs an HMR (Hot Module Reloading) server on &lt;code&gt;localhost:1234&lt;/code&gt; and injects some code into &lt;code&gt;main.js&lt;/code&gt; that will check if any assets or the page needs reloaded.&lt;/p&gt;

&lt;p&gt;As we're importing &lt;code&gt;assets.html&lt;/code&gt; into our site which Parcel is auto-generating, this will pick up the changes between build and dev mode with no further changes. Run the dev parcel command, refresh the page and now we have hot CSS replacement and reloading on JS changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3. Livereload for HTML and PHP changes
&lt;/h3&gt;

&lt;p&gt;Whilst Parcel is handling the reloads of CSS and JS nicely, it doesn't do any updates when the HTML or PHP changes. I spent a while going down a rabbit hole of trying to work out if I could piggyback on Parcel's HMR system but found no way of doing that so we're using &lt;a href="https://www.npmjs.com/package/livereload" rel="noopener noreferrer"&gt;livereload&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Run livereload with this command, which watches HTML and PHP files for changes and triggers a reload. I'm also excluding the dist directory as that would result in a double refresh when Parcel builds &lt;code&gt;assets.html&lt;/code&gt;. You can add other extensions here like your templating language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;livereload &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'html,php'&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; dist/ &lt;span class="nt"&gt;-p&lt;/span&gt; 1235
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll also need to add the below script to your page to communicate with livereload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:1235/livereload.js?snipver=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we have both Parcel and livereload running we have a hot replacement of CSS and a page reload on JS, HTML and PHP changes! All very fast and works pretty well. One final thing...&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. Automatic addition of livereload script
&lt;/h3&gt;

&lt;p&gt;One problem with what we have now is we need a way to include the livereload script when it's needed in development, but not in the live site. This seemed tricky at first, perhaps we change an environment variable or something we can pick up in the site code? Turns out there's an easier way!&lt;/p&gt;

&lt;p&gt;We're likely going to want to run Parcel and Livereload together during development, and Parcel is already building the &lt;code&gt;assets.html&lt;/code&gt; differently depending on dev/production. So we can use Parcel to inject the livereload script only in development.&lt;/p&gt;

&lt;p&gt;Parcel can't use functional logic in HTML files, but it can use it in files that build to HTML, most notably Pug! Pug is a templating language that compiles to HTML and includes stuff like if statements etc. So we can rename &lt;code&gt;src/assets.html&lt;/code&gt; to &lt;code&gt;src/assets.pug&lt;/code&gt; and add to our asset markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link href="./css/main.scss" rel="stylesheet" /&amp;gt;
&amp;lt;script type="module" src="./js/main.js"&amp;gt;&amp;lt;/script&amp;gt;
if process.env.NODE_ENV !== 'production'
    &amp;lt;script src="http://localhost:1235/livereload.js?snipver=1"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will only include the livereload snippet if the &lt;code&gt;NODE_ENV&lt;/code&gt; environment variable is not &lt;code&gt;production&lt;/code&gt;, which means it won't be included in our production build when we run &lt;code&gt;parcel build ...&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts and the complete code
&lt;/h2&gt;

&lt;p&gt;I'm really happy with this dev setup! It ticks all of my boxes from earlier, it's relatively simple and flexible, and brings a really nice modern dev setup to sites like WordPress or other CMS builds.&lt;/p&gt;

&lt;p&gt;There are definitely some limitations, beyond the standard ones when you don't process HTML with your build tool. The main trickiness I've found is that whilst CSS and JS in the &lt;code&gt;head&lt;/code&gt; works great it's a bit faffy to have more structure. You can generate more html files with alternative assets so you could have &lt;code&gt;head.html&lt;/code&gt;, &lt;code&gt;footer.html&lt;/code&gt;, &lt;code&gt;about-page.html&lt;/code&gt;. That does get a little messy and verbose however, and image processing within the build would also require the same treatment.&lt;/p&gt;

&lt;p&gt;However, I don't believe I've seen a good development setup with this kind of website that handles this better. A purely up-front build tool like Laravel Mix has the same problems, and none of the 'fancier' tools seem to have a better solution either.&lt;/p&gt;

&lt;p&gt;So yeah, I think this is what I'll be using for my own sites going forward, certainly with CMS', I'll see how it compares against my other methods for 11ty sites in future!&lt;/p&gt;

&lt;p&gt;If you're just looking to copy+paste, run the &lt;code&gt;npm install&lt;/code&gt; command and copy the &lt;code&gt;assets.pug&lt;/code&gt; from above. This is my &lt;code&gt;package.json&lt;/code&gt; scripts, with &lt;code&gt;npm-run-all&lt;/code&gt; for running both Parcel and livereload at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-p dev:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev:parcel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parcel watch ./src/assets.pug --public-url /dist/ --hmr-host localhost --hmr-port 1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev:livereload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"livereload -e 'html,php' -p 1235"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parcel build ./src/assets.pug --public-url /dist/"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and &lt;a href="//mailto:alistair@accudio.com"&gt;send me an email&lt;/a&gt; at &lt;a href="mailto:alistair@accudio.com"&gt;alistair@accudio.com&lt;/a&gt; or &lt;a href="https://front-end.social/@accudio" rel="noopener noreferrer"&gt;contact me on Mastodon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.alistairshepherd.uk/writing/simple-build-tooling/" rel="noopener noreferrer"&gt;Simple, fast build tooling with live reload for a non-framework website&lt;/a&gt; appeared first on &lt;a href="https://alistairshepherd.uk" rel="noopener noreferrer"&gt;alistairshepherd.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Interop Priority Game 2024</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sat, 18 Nov 2023 15:20:59 +0000</pubDate>
      <link>https://forem.com/accudio/interop-priority-game-2024-43c8</link>
      <guid>https://forem.com/accudio/interop-priority-game-2024-43c8</guid>
      <description>&lt;p&gt;The other day &lt;a href="https://bkardell.com/"&gt;Brian Kardell&lt;/a&gt; asked about &lt;a href="https://github.com/web-platform-tests/interop"&gt;Interop project&lt;/a&gt; prioritisation on his blog and mastodon. As he asks in his blog &lt;a href="https://bkardell.com/blog/PriorityGame.html"&gt;"Let's Play a Game"&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's Interop 24 planning time! Let's play a game: Tell me what you prioritize, or don't and why?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Interop Project is an collaboration across the browser communities to focus on making various web APIs interoperable, standard, and bug-free across the Blink, Webkit and Gecko engines. It's been very productive over the past few years to fix interop bugs and to release new features like container queries and subgrid in a co-ordinated way.&lt;/p&gt;

&lt;p&gt;Each year, the Interop Project accepts proposals for what should be included in the project, and once whittled down carefully considers how to prioritise working on these various important or exciting proposals.&lt;/p&gt;

&lt;p&gt;So Brian has come up with a game! Ask developers to look through the full list, sort some of them and maybe explain a little bit of why they made the decisions they have. I found this a really interesting exercise and a great way of getting up-to-date with some of the things upcoming to the web platform. Please consider looking at the proposals, voting to what you like the look of and maybe doing your own prioritisation like this!&lt;/p&gt;

&lt;h2&gt;
  
  
  My top fifteen interop proposals
&lt;/h2&gt;

&lt;p&gt;My order comes from my personal priorities, which are in Accessibility, Performance, and making the kind of websites I work on easier/more fun to build. Those are mostly public, lightweight websites with a bit of creativity. I don't know all the proposals super in-depth but I think enough to make a quick judgement of "oh yeah I want that".&lt;/p&gt;

&lt;p&gt;There are over 90 proposals in total and pretty much all of them seem like they would be useful — it makes sorting them tricky! The "game" is to prioritise them to your own order, and if you have your own opinions make your own list!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;a href="https://github.com/web-platform-tests/interop/issues/568"&gt;display: contents accessibility&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;All of the accessibility-related improvements and fixes hit the top of my list. I work in accessibility, that was always going to happen and I personally feel like stuff that is "nice to have" for developers should come after reducing barriers and making the web more accessible. This one is both nice to have and an accessibility improvement so tops the list.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;display: contents&lt;/code&gt; is a very useful CSS property that allows an element to basically remove it's own element box and allow it's child nodes to participate in a higher level. That's particularly handy for wrapping an element in a semantic container—like &lt;code&gt;li&lt;/code&gt;—but allowing children to sit within the grid or flex layout of the parent.&lt;/p&gt;

&lt;p&gt;Unfortunately, it's hampered by buggy and unreliable accessibility. In several browsers using &lt;code&gt;display: contents&lt;/code&gt; will remove an element from the accessibility tree and therefore is best to avoid. It's my first priority as it's a great feature that is currently unusable, and an accessibility issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;a href="https://github.com/web-platform-tests/interop/issues/512"&gt;Accessibility issues with display properties&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Another accessibility one, and this is related to setting &lt;code&gt;display&lt;/code&gt; properties on particular elements. In some circumstances changing the display property of certain elements can remove or break accessibility for those elements. These circumstances are all over the web, and it would be good to ensure people relying on accessibility technology aren't impacted negatively.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;a href="https://github.com/web-platform-tests/interop/issues/526"&gt;Accessibility (computed role + accname)&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The final accessibility proposal in Brian's list, this is standardising and making accessibility roles and names more consistent. It'll make it easier to build complex interfaces in an accessible way, and improve the experience for people using accessibility technology. Sounds great.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;a href="https://github.com/web-platform-tests/interop/issues/437"&gt;View Transitions Level 1&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;View Transitions are absolutely amazing, and I think they're going to a monumental addition to the web. In short, page transitions but also so much more! Particularly for building flashy, creative websites it makes it easier to produce something that feels great without needing to ship a huge amount of JS to do so.&lt;/p&gt;

&lt;p&gt;Now this proposal is for Level 1, so only the 'SPA API' that does same-document transitions. Cross-document transitions are in the Level 2 module which is still in draft. That said, there's a lot of amazing things you can do with the SPA API and by getting Level 1 out it'll encourage work on Level 2 and I want that as soon as possible!&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;a href="https://github.com/web-platform-tests/interop/issues/430"&gt;JPEG XL image format&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;JPEG XL looks to be a fantastic upcoming image format with great high-fidelity compression, fast encoding/decoding, backwards compatibility with JPEG, and lots of great image format features that make it look like a great candidate for the canonical image format for most-use cases.&lt;/p&gt;

&lt;p&gt;From the perspective of web performance I'm really excited for the potential of JPEG XL.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;a href="https://github.com/web-platform-tests/interop/issues/439"&gt;Scroll-driven animations&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I've been playing with Scroll Driven animations recently and found them a great API that makes it super easy to implement really cool animations linked to the scroll, with performance that's almost impossible to achieve otherwise and only a handful of lines of CSS.&lt;/p&gt;

&lt;p&gt;I've worked on projects with complex scroll animations that are basically impossible to maintain. The mess of timelines in JS are a nightmare, and the CSS API and use of native &lt;code&gt;@keyframe&lt;/code&gt; animations is fantastic in comparison. From a performance perspective it also removes the need to reach for a 100+kB JS library for a simple scroll effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;a href="https://github.com/web-platform-tests/interop/issues/529"&gt;Text fragments&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Now this one is surprisingly high! Text fragments make it possible to link to certain text from a page and highlight it, using a format like &lt;code&gt;#:~:text=Now%20this%20one%20is%20surprisingly%20high!&lt;/code&gt;. Fairly regularly I want to link someone to a specific part of a page/article, but there are no in-built ID anchor links nearby. With this I can manually construct a URL that links straight to where I want it.&lt;/p&gt;

&lt;p&gt;I already use it to link people who I know are using chromium-based browsers, but would love for it to be possible in all browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. &lt;a href="https://github.com/web-platform-tests/interop/issues/520"&gt;CSS Multi-Column Layout block element breaking&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I feel like issues and bugs with multi-column layout have been a constant through my career in web since Chrome added support in 2016. Every other time I try and use them I give up and instead use a less ideal Grid, Flex or JS-based layout. Or tell the designer "Sorry, the web can't do columns properly" which is completely ridiculous.&lt;/p&gt;

&lt;p&gt;It's about time CSS multi-column is sorted so it's reliable enough to use consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. &lt;a href="https://github.com/web-platform-tests/interop/issues/422"&gt;text-box-trim&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Text box trim allows trimming the space around text, so you can rely on padding and margins to sit flush with the text glyphs. In some designs you want a really neat alignment between a heading and graphic, currently that's tricky without resorting to fiddling with line-heights or &lt;a href="https://css-tricks.com/magic-numbers-in-css/"&gt;"magic numbers"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a design-led agency, this is definitely something our designers are looking forward to and would make heading design more flexible and easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. &lt;a href="https://github.com/web-platform-tests/interop/issues/513"&gt;Unit division and multiplication for mixed units of the same type within calc()&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Currently in the CSS &lt;code&gt;calc&lt;/code&gt; function division can only be done by unitless numbers. If we were able to divide by value with a unit it would open the way to strip units and to compare the scale of values with different units.&lt;/p&gt;

&lt;p&gt;This isn't one I run into often—hence it's position at 10—but there's been a handful of times it's come up as something that would make CSS SO much easier. There is a cool but nasty hack using &lt;code&gt;tan(atan2())&lt;/code&gt; but otherwise the workaround are annoying and either involve duplication or JS.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. &lt;a href="https://github.com/web-platform-tests/interop/issues/433"&gt;CSS style container queries&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;If you need to change multiple properties in CSS at once with something manual, your best bet is adding or removing CSS classes. Which requires server-side or JS logic, and can get a real mess. Style container queries allows modifying CSS properties depending on the value of a single custom property. Basically a custom if statement within CSS.&lt;/p&gt;

&lt;p&gt;This is an awesome feature and super handy. Despite that, it's lower down this list for me as it doesn't really solve problems I have with the utility-first CSS methodology I use at work.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. &lt;a href="https://github.com/web-platform-tests/interop/issues/420"&gt;CSS Nesting&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is another one that is super exciting for certain methodologies and ways of writing CSS, but it doesn't really match up with how I do. At work I write utility-first CSS that doesn't really benefit from nesting, and on my own projects I lean hard into BEM-style nesting which needs pre-processed. Ah, how I wish they'd added BEM-style nesting natively but no cigar.&lt;/p&gt;

&lt;p&gt;It's extremeley powerful, a great addition to the language, and I know some people super excited for this. Maybe I will be if I have another look at how I structure CSS, but that's why it's not top of the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  13. &lt;a href="https://github.com/web-platform-tests/interop/issues/517"&gt;CSS background-clip&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is particularly around inconsistencies with how clipping text works with background-clip. Clipping backgrounds to text is a super neat feature that can produce some really cool looking effects, particularly combined with images. It can be pretty finicky though so it would be great if it were more consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  14. &lt;a href="https://github.com/web-platform-tests/interop/issues/561"&gt;text-wrap: balance&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;A way of preventing widows and generally improving readability across lines in paragraphs. Sounds good, and people who care about typography will love it. Definitely handy, I'll use it when it's available, but it's a relatively minor issue to me.&lt;/p&gt;

&lt;h3&gt;
  
  
  15. &lt;a href="https://github.com/web-platform-tests/interop/issues/562"&gt;text-wrap: pretty&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Finally we have &lt;code&gt;text-wrap: pretty&lt;/code&gt;, which provides a better layout for short blocks of text considering where to break lines. Just adding it to major headings can easily make them look a little nicer. Like &lt;code&gt;text-wrap: balance&lt;/code&gt;, it'll be handy and I'll use it but I'm not clamouring for it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Honourable mentions
&lt;/h2&gt;

&lt;p&gt;These are all on my radar and of interest, but less of a priority. That may be because I don't know enough, I don't use them, am unsure quite where to place them, or they have less convenient but full-featured alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/563"&gt;requestIdleCallback&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/501"&gt;Declarative Shadow DOM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/521"&gt;attr() support extended capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/464"&gt;Web Share API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/553"&gt;details and summary elements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/web-platform-tests/interop/issues/442"&gt;CSS element() function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/571"&gt;scrollbar-width&lt;/a&gt;, &lt;a href="https://github.com/web-platform-tests/interop/issues/417"&gt;scrollbar-color&lt;/a&gt;, &lt;a href="https://github.com/web-platform-tests/interop/issues/419"&gt;scrollbar-gutter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/541"&gt;font-size-adjust&lt;/a&gt;, &lt;a href="https://github.com/web-platform-tests/interop/issues/542"&gt;size-adjust&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Notable items low on my list
&lt;/h2&gt;

&lt;p&gt;The point of the "game" is prioritisation, and I thought it would be interesting to look at what proposals are popular that I don't prioritise. Clearly lots of people want them so it's definitely not a case of them not bing valuable, just not relevant to me for whatever reason!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/423"&gt;Popover&lt;/a&gt; — honestly, I've never had difficulty implementing "popovers" and I'm not sure I love the API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/522"&gt;WebXR&lt;/a&gt; — Nothing on the API, I've just gone off VR and never really 'got' AR.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/421"&gt;Custom Media Queries&lt;/a&gt; — I actually prefer the API of the style queries 'workaround'.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/web-platform-tests/interop/issues/573"&gt;Allowing &amp;lt;hr&amp;gt; inside of &amp;lt;select&amp;gt;&lt;/a&gt; — Kinda just a meh version of &lt;code&gt;optgroup&lt;/code&gt;? See &lt;a href="https://adrianroselli.com/2023/10/splitting-within-selects.html"&gt;Adrian Roselli's Splitting within Selects&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
      <category>browsers</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Simple, cheap GeoIP API using Netlify Edge functions</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sun, 30 Apr 2023 23:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/simple-cheap-geoip-api-using-netlify-edge-functions-3f25</link>
      <guid>https://forem.com/accudio/simple-cheap-geoip-api-using-netlify-edge-functions-3f25</guid>
      <description>&lt;p&gt;Need to look up a users' approximate location based on their IP address? Don't want to opt for a third-party GeoIP service or integrate it into your backend?&lt;/p&gt;

&lt;p&gt;Turns out that &lt;a href="https://netlify.com"&gt;Netlify&lt;/a&gt; makes it super easy to set up a simple GeoIP service for yourself!&lt;/p&gt;

&lt;p&gt;If you just want the code you can find the repo at &lt;a href="https://github.com/Accudio/netlify-geoip"&gt;github.com/Accudio/netlify-geoip&lt;/a&gt; and demo at &lt;a href="https://accudio-geoip.netlify.app/"&gt;accudio-geoip.netlify.app&lt;/a&gt;. You can fork that repository and deploy it to your own Netlify account to use yourself!&lt;/p&gt;

&lt;p&gt;I have also published a very similar post (almost identical to be honest, it's mostly copied) about how to do the &lt;a href="https://dev.to/writing/vercel-geoip"&gt;same with Vercel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read on for a deeper explanation, and let me know if you have any thoughts or issues!&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;For a couple projects I'm currently working on, recently I had need for a Geolocation API. Nothing too major, just getting a users very rough location based on their IP address, to tailor their default experience of language, currency, or laws.&lt;/p&gt;

&lt;p&gt;There are a TON of Geolocation API services with various pricing, trustworthiness and privacy/tracking policies. I looked at a few, but the per-lookup pricing and lack of certainty around trusting a third-party with our users' IP addresses was a bit of a deterrent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify and Geolocation
&lt;/h2&gt;

&lt;p&gt;If you haven't heard of Netlify before, it's a hosting company that specialises in JAMStack sites. I use it for this website and a lot of my personal projects, and it's a great platform for static sites, JavaScript-based frameworks and serverless/edge functions.&lt;/p&gt;

&lt;p&gt;It's the serverless and edge functions that are the key to this setup. Serverless and edge functions allow us to run a node.js script on each request, responding dynamically. Serverless functions run on centralised servers (they're pretty badly named!), Edge functions are a bit more restrictive and run directly on the CDN nodes allowing for a potentially faster or lighter response.&lt;/p&gt;

&lt;p&gt;These functions can be combined with &lt;a href="https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object"&gt;Netlify's &lt;code&gt;context&lt;/code&gt; object&lt;/a&gt; for geolocation information. We can send that data back on the request in a JSON format, and then use that within our front-end JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;For my own later reference and potentially yours, I'm going through the full process of setting up a simple Edge function on Netlify!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialising and installing Netlify CLI
&lt;/h3&gt;

&lt;p&gt;First we need to initialise our repo, npm project and install the Netlify CLI for local development.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;netlify-geoip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;netlify-geoip
git init
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;netlify-cli &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Trying out an edge function
&lt;/h3&gt;

&lt;p&gt;In Netlify projects edge functions are placed within the &lt;code&gt;netlify/edge-functions/&lt;/code&gt; directory by default, so let's create an &lt;code&gt;netlify/edge-functions/geoip.js&lt;/code&gt;. Within it, we're going to put the very basics of a edge function that has a text response, and specify Netlify should serve it as the root request &lt;code&gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// netlify/edge-functions/geoip.js&lt;/span&gt;
&lt;span class="c1"&gt;// Specify that this function should run on the path `/`&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// We export the function that runs on each request&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Respond to the request with the content "hello world!"&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test our function, we can run &lt;code&gt;netlify dev&lt;/code&gt; to run the Netlify development server. Now, if you visit the dev URL in your browser — &lt;a href="http://localhost:8888"&gt;probably &lt;code&gt;localhost:8888&lt;/code&gt;&lt;/a&gt; you should see "hello world!".&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Geolocation bit
&lt;/h3&gt;

&lt;p&gt;Now let's amend our &lt;code&gt;geoip.js&lt;/code&gt; file to include the Geolocation bits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// netlify/edge-functions/geoip.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The context parameters includes details about the current request,&lt;/span&gt;
    &lt;span class="c1"&gt;// including the geolocation information and client IP address&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once again we can test this with &lt;code&gt;netlify dev&lt;/code&gt;, you may need to restart the development server to get the latest changes. If you visit the preview URL you'll get the Geolocation data and your IP address in a JSON format! Neat!&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross Origin Resource Sharing
&lt;/h3&gt;

&lt;p&gt;If we try to call this on a different website with JavaScript, we're going to run into CORS issues. CORS — Cross Origin Resource Sharing — is a way browsers prevent websites from using a browser to access content they shouldn't have access to, like resources from a local network. This means as things currently stands, a browser won't let us access the content from our API request with &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To allow us to use the API within JavaScript in a browser, we need to tell the browser to allow CORS. We can do this by adding some HTTP Headers via the second argument of &lt;code&gt;Response.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// Add a second parameter to `Response.json`&lt;/span&gt;
        &lt;span class="c1"&gt;// where we can provide our CORS headers&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET,OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could be more specific with your CORS headers, but for a simple API like ours this will do fine. These two lines allow all origins to access the API, and only the GET and OPTIONS methods.&lt;/p&gt;

&lt;p&gt;That is one thing to note however, the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header allows all origins to make a request to the API. In most cases that might be okay, but you may want to prevent other sites from using your API, especially if you start hitting Netlify's usage limits.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin"&gt;whitelist a single origin&lt;/a&gt; by adding it to the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header instead of &lt;code&gt;*&lt;/code&gt;. For multiple origins you could also dynamically read the &lt;code&gt;Origin&lt;/code&gt; header and use that to allow or disallow a request. I haven't run into that problem yet though, so consider that a further exercise for the reader!&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy and test!
&lt;/h3&gt;

&lt;p&gt;We can deploy the API to Netlify with &lt;code&gt;netlify deploy --build --prod&lt;/code&gt;, or link the project via the Netlify website to a Git repo on GitHub, GitLab or similar. Now access the API at your Netlify URL, &lt;a href="https://accudio-geoip.netlify.app"&gt;for example &lt;code&gt;accudio-geoip.netlify.app&lt;/code&gt;&lt;/a&gt; and there we go!&lt;/p&gt;

&lt;p&gt;This is the result I get when visiting that URL (IP obfuscated for privacy):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Newbury"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"United Kingdom"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subdivision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ENG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"England"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Europe/London"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;51.3195&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-1.4146&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XX.XX.XX.X"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's definitely not perfect, to start I'm in Edinburgh, Scotland not Newbury, England! City and Subdivision should maybe be taken with a pinch of salt, but that's something I run into with GeoIP systems all over the web so it's clearly not just Netlify. (interestingly, my &lt;a href="https://dev.to/writing/vercel-geoip"&gt;Vercel post&lt;/a&gt; had similar but slightly different results)&lt;/p&gt;

&lt;p&gt;For the purposes of country though it's accurate, and the City and Subdivision may be helpful to set a default that a user can later change.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Using the API within JavaScript
&lt;/h3&gt;

&lt;p&gt;We can use this within JavaScript on another website like so, but keep in mind you may need to switch from using &lt;code&gt;await&lt;/code&gt; to &lt;code&gt;.then()&lt;/code&gt; depending on your setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geoRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://accudio-geoip.netlify.app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;geoRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>browsers</category>
    </item>
    <item>
      <title>Simple, cheap GeoIP API using Vercel Edge functions</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sat, 29 Apr 2023 23:00:00 +0000</pubDate>
      <link>https://forem.com/accudio/simple-cheap-geoip-api-using-vercel-edge-functions-30oc</link>
      <guid>https://forem.com/accudio/simple-cheap-geoip-api-using-vercel-edge-functions-30oc</guid>
      <description>&lt;p&gt;Need to look up a users' approximate location based on their IP address? Don't want to opt for a third-party GeoIP service or integrate it into your backend?&lt;/p&gt;

&lt;p&gt;Turns out that &lt;a href="https://vercel.com"&gt;Vercel&lt;/a&gt; makes it super easy to set up a simple GeoIP service for yourself!&lt;/p&gt;

&lt;p&gt;If you just want the code you can find the repo at &lt;a href="https://github.com/Accudio/vercel-geoip"&gt;github.com/Accudio/vercel-geoip&lt;/a&gt; and demo at &lt;a href="https://accudio-geoip.vercel.app/"&gt;accudio-geoip.vercel.app&lt;/a&gt;. You can fork that repository and deploy it to your own Vercel account to use yourself!&lt;/p&gt;

&lt;p&gt;I have also published a very similar post (almost identical to be honest, it's mostly copied) about how to do the &lt;a href="https://dev.to/writing/netlify-geoip"&gt;same with Netlify&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read on for a deeper explanation, and let me know if you have any thoughts or issues!&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;For a couple projects I'm currently working on, recently I had need for a Geolocation API. Nothing too major, just getting a users very rough location based on their IP address, to tailor their default experience of language, currency, or laws.&lt;/p&gt;

&lt;p&gt;There are a TON of Geolocation API services with various pricing, trustworthiness and privacy/tracking policies. I looked at a few, but the per-lookup pricing and lack of certainty around trusting a third-party with our users' IP addresses was a bit of a deterrent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vercel and Geolocation Headers
&lt;/h2&gt;

&lt;p&gt;If you haven't heard of Vercel before, it's a hosting company that specialises in JAMStack sites, similar to Netlify. It's a good platform for static sites, JavaScript-based frameworks and serverless/edge functions.&lt;/p&gt;

&lt;p&gt;It's the serverless and edge functions that are the key to this setup. Serverless and edge functions allow us to run a node.js script on each request, responding dynamically. Serverless functions run on centralised servers (they're pretty badly named!), Edge functions are a bit more restrictive and run directly on the CDN nodes allowing for a potentially faster or lighter response.&lt;/p&gt;

&lt;p&gt;These functions can be combined with &lt;a href="https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country_"&gt;Vercel's HTTP headers with geolocation information&lt;/a&gt;. We can send that data back on the request in a JSON format, and then use that within our front-end JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;As most of the examples of Vercel's functions rely on Next.js, it's a bit tricky to find how to set up functions without it. For my own later reference and to avoid you having to go through the same research, I'm going through the full process!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialising
&lt;/h3&gt;

&lt;p&gt;First we need to initialise our repo, npm project and install the Vercel packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;vercel-geoip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;vercel-geoip
git init
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; vercel
npm i @vercel/edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Trying out an edge function
&lt;/h3&gt;

&lt;p&gt;In Vercel projects functions are placed within an &lt;code&gt;api/&lt;/code&gt; directory, so let's create an &lt;code&gt;api/index.js&lt;/code&gt; file. This would run on any requests to &lt;code&gt;/api/&lt;/code&gt;. Within it, we're going to put the very basics of a edge function that has a basic text response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api/index.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Specify this function as an edge function rather than a serverless function&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// We export the function that runs on each request, which receives the `request`&lt;/span&gt;
&lt;span class="c1"&gt;// parameter with data about the current request. We'll use this later&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// respond to the request with the content "hello world!"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test our function, we can run &lt;code&gt;npx vercel dev&lt;/code&gt; to run the Vercel development server. This will ask you to link the project to your Vercel account and some details about the project. You can leave those details as default.&lt;/p&gt;

&lt;p&gt;Now, if you visit the dev URL in your browser and add &lt;code&gt;/api&lt;/code&gt; — &lt;a href="https://localhost:5000/api"&gt;probably &lt;code&gt;localhost:5000/api&lt;/code&gt;&lt;/a&gt; you should see "hello world!".&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Geolocation bit
&lt;/h3&gt;

&lt;p&gt;Now let's amend our &lt;code&gt;index.js&lt;/code&gt; file to include the Geolocation bits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api/index.js&lt;/span&gt;
&lt;span class="c1"&gt;// Import the geolocation and ipAddress helpers&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipAddress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vercel/edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The geolocation helper pulls out the geoIP headers from the&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="c1"&gt;// The IP helper does the same function for the user's IP address&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

    &lt;span class="c1"&gt;// Output the Geolocation data and IP address as a JSON object, and&lt;/span&gt;
    &lt;span class="c1"&gt;// set the content type to make it easier to handle when requested&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this won't work in the dev server as Vercel doesn't inject the geolocation headers there, but if you open the function at least it shouldn't error. You can get a preview deployment to test it on the Vercel servers by running &lt;code&gt;npx vercel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you visit the &lt;code&gt;/api&lt;/code&gt; route on your preview URL you'll get the Geolocation data of your IP address! Neat!&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross Origin Resource Sharing
&lt;/h3&gt;

&lt;p&gt;If we try to call this on a different website with JavaScript, we're going to run into CORS issues. CORS — Cross Origin Resource Sharing — is a way browsers prevent websites from using a browser to access content they shouldn't have access to, like resources from a local network. This means as things currently stands, a browser won't let us access the content from our API request with &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To allow us to use the API within JavaScript in a browser, we need to tell the browser to allow CORS. We can do this by adding some HTTP Headers, via a &lt;code&gt;vercel.json&lt;/code&gt; config file in root of our project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Access-Control-Allow-Origin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Access-Control-Allow-Methods"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET,OPTIONS"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is taken from Vercel's &lt;a href="https://vercel.com/guides/how-to-enable-cors"&gt;"How can I enable CORS on Vercel?" guide&lt;/a&gt;. Since this is a relatively straightforward API we don't really need a lot of the parameters in that article, so I've simplified it to allowing all origins, and only the GET and OPTIONS methods.&lt;/p&gt;

&lt;p&gt;There is one thing to note with the above code however, the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header allows all origins to make a request to the API. In most cases that might be okay, but you may want to prevent other sites from using your API, especially if you start hitting Vercel's usage limits.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin"&gt;whitelist a single origin&lt;/a&gt; by adding it to the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header instead of &lt;code&gt;*&lt;/code&gt;. You could also include the CORS headers within the edge function depending on the requesting Origin for multiple origins. I haven't run into that problem yet though, so consider that a further exercise for the reader!&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Root rewrite (optional)
&lt;/h3&gt;

&lt;p&gt;The final touch is a rewrite so we can hit our API at the root URL &lt;code&gt;/&lt;/code&gt;, instead of having to include &lt;code&gt;api/&lt;/code&gt; on every request. With Vercel we can do that with a few more lines to &lt;code&gt;vercel.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rewrites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Deploy and test!
&lt;/h3&gt;

&lt;p&gt;We can deploy the API to Vercel with &lt;code&gt;npx vercel --prod&lt;/code&gt;, or link the project via the Vercel website to a Git repo on GitHub, GitLab or similar. Access the API at the Vercel URL, &lt;a href="https://accudio-geoip.vercel.app"&gt;for example &lt;code&gt;accudio-geoip.vercel.app&lt;/code&gt;&lt;/a&gt; and there we go!&lt;/p&gt;

&lt;p&gt;This is the result I get when visiting that URL (IP obfuscated for privacy):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Loughborough"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"countryRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ENG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"lhr1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"52.7681"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"-1.2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"XX.XX.XX.X"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's definitely not perfect, to start I'm in Edinburgh, Scotland not Loughborough, England! City and Country Region should maybe be taken with a pinch of salt, but that's something I run into with GeoIP systems all over the web so it's clearly not just Vercel. (interestingly, my &lt;a href="https://dev.to/writing/netlify-geoip"&gt;Netlify post&lt;/a&gt; had similar but slightly different results)&lt;/p&gt;

&lt;p&gt;For the purposes of country though it's accurate, and the City and Region may be helpful to set a default that a user can later change.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Using the API within JavaScript
&lt;/h3&gt;

&lt;p&gt;We can use this within JavaScript on another website like so, but keep in mind you may need to switch from using &lt;code&gt;await&lt;/code&gt; to &lt;code&gt;.then()&lt;/code&gt; depending on your setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geoRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://accudio-geoip.vercel.app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;geoRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>browsers</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Searching for a Mastodon app for Android</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sun, 26 Mar 2023 12:06:15 +0000</pubDate>
      <link>https://forem.com/accudio/searching-for-a-mastodon-app-for-android-ddc</link>
      <guid>https://forem.com/accudio/searching-for-a-mastodon-app-for-android-ddc</guid>
      <description>&lt;p&gt;I've been using the Fediverse via Mastodon since October 2022, as a pretty much complete replacement for Twitter. I'm loving it! The community feels a lot more friendly and accessible than Twitter has done in a long time and I'm very happy to be out of the Twitter shitshow.&lt;/p&gt;

&lt;p&gt;You can find me at &lt;a href="https://mastodon.scot/@accudio"&gt;accudio@mastodon.scot&lt;/a&gt; and if you're not on the Fediverse yet come join us!&lt;/p&gt;

&lt;p&gt;Now onto the actual topic of this post, trying out various Mastodon or Fediverse apps for Android! I've been using &lt;a href="https://twidere.com"&gt;Twidere&lt;/a&gt; since I started, originally because it supported both Twitter and Mastodon and therefore allowed me to transition slowly. I removed Twitter after a few weeks but kept the app as it's decent. Unfortunately however lately I've had it crashing upon seeing particular posts. That means I have to clear the cache and data before re-using it — not ideal!&lt;/p&gt;

&lt;p&gt;Hence my search for a new app, but first...&lt;/p&gt;

&lt;h2&gt;
  
  
  How I use social media and 'must-haves'
&lt;/h2&gt;

&lt;p&gt;I used Twitter — and use Mastodon — in a very particular way. Normally I check in maybe 2-3 times a day, depending on how busy I am. When I start my app, I want it to be exactly where I left off when I last used it. I then go through 'catching up' on what's been posted since I last checked, and once I'm done I finish and get back to work/play. When I next check it I continue from where I finished.&lt;/p&gt;

&lt;p&gt;I have a pretty curated feed so this doesn't tend to take me very long and means I don't miss anything from the folks I really care about. I use mutes, hide retweets and separate RSS feeds to keep track of others in a less instant form.&lt;/p&gt;

&lt;p&gt;This way of using Twitter and Mastodon is ideal for me, and is the most healthy relationship I've had with Social Media as once I'm 'caught up' there's no more scrolling to do!&lt;/p&gt;

&lt;p&gt;I have just one 'must-have' for a Mastodon app (beyond the very basics of loading posts) and from how I use it that'll probably be pretty clear! I need the app to keep track where I am in my feed, no exceptions. I don't mind if loading more posts is automatic or manual, but it has to happen around my scroll position so I don't lose track of where I am. Any other features are a bonus!&lt;/p&gt;

&lt;h2&gt;
  
  
  The contendors
&lt;/h2&gt;

&lt;p&gt;First, thank you so much to &lt;a href="https://mastodon.scot/@accudio/110057755928969826"&gt;everyone who gave me recommendations when I asked&lt;/a&gt; last week, most of these suggestions came from those recommendations.&lt;/p&gt;

&lt;p&gt;If there are any good ones you know of that I haven't mentioned then please let me know!&lt;/p&gt;

&lt;h3&gt;
  
  
  Tusky
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tusky.app"&gt;Tusky&lt;/a&gt; was the most common recommendation I got and it's pretty good! Modern design, very quick, great support for Mastodon features like Polls, editing, displaying of alt text, all of which is beyond Twidere.&lt;/p&gt;

&lt;p&gt;Unfortunately, when I'm scrolling up and tap "Load More" it pins me to the top of the new posts rather than the bottom, requiring me to scroll back down to find where I was beforehand. I tried to stick with it for a bit because I really like the other features but it just frustrated me unfortunately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Husky
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://husky.adol.pw"&gt;Husky&lt;/a&gt; is a fork of Tusky and has pretty similar features, mainly focused around Pleroma servers. I don't use a Pleroma server so most of these aren't too major for me as a Mastodon user.&lt;/p&gt;

&lt;p&gt;Sadly it faces the same issue as Tusky with loading new posts&lt;/p&gt;

&lt;h3&gt;
  
  
  Official Mastodon app
&lt;/h3&gt;

&lt;p&gt;I like the design and feel of the &lt;a href="https://f-droid.org/packages/org.joinmastodon.android/"&gt;official Mastodon app&lt;/a&gt; but compared to Tusky and Husky the comparitive lack of customisation kinda sucks. That said, if it can load posts in the way I want it to then that's nothing...&lt;/p&gt;

&lt;p&gt;Once again, no it can't. Tapping "Load more posts" pins you to the top (newest) of the loaded posts rather than the bottom (oldest). Another fail :(&lt;/p&gt;

&lt;h3&gt;
  
  
  Megalodon and Moshidon
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://sk22.github.io/megalodon/"&gt;Megalodon&lt;/a&gt; is a fork of the official Mastodon app that adds support for some pretty handy features like a Federated timeline, more control over posting and image descriptiong viewing. &lt;a href="https://lucasggamerm.github.io/moshidon/"&gt;Moshidon&lt;/a&gt; is a fork of Megalodon which also adds support for remote local timelines.&lt;/p&gt;

&lt;p&gt;Both are cool but suffer from the same issue as the official app when it comes to loading posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mastodon website
&lt;/h3&gt;

&lt;p&gt;I don't necessarily need an app really, I'm perfectly happy to use a website or Progressive Web App instead if that works better. That said, thanks to Gogle being anti-competitive, monopolistic jerks my phone doesn't support Progressive Web Apps properly. So I'm restricted to the Mastodon website of my server as viewed through my browser, no PWA install superpowers.&lt;/p&gt;

&lt;p&gt;And this ends up worse than the apps unfortunately! If I load it up having not used it in a few hours the page loads fresh and I'm placed at the top of my feed with the latest post. Absolutely no can do unfortunately.&lt;/p&gt;

&lt;p&gt;I can't test how it works as a proper PWA, but giving it the benefit of the doubt I'm blaming this one on Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elk
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://elk.zone"&gt;Elk&lt;/a&gt; is a third-party website and PWA for a few different Fediverse servers that is pretty great for customisation and accessibility.&lt;/p&gt;

&lt;p&gt;It's another casualty of Google it seems, loaded at the latest post losing track of where I was. It's a great website though so I might switch my desktop use to it, but it doesn't work for my mobile use.&lt;/p&gt;

&lt;h3&gt;
  
  
  FediLab
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://fedilab.app"&gt;FediLab&lt;/a&gt; is the best shot yet. It seems a bit rougher than the other apps but works extremely well, with most of the customisation I'd want and loads of super handy features like showing Alt text, automatic privacy-friendly translation, and support for alternative frontends for sites like YouTube, Twitter and Instagram.&lt;/p&gt;

&lt;p&gt;This is the first app that copes with my style of browsing the feed! It keeps track of my place in the feed and when I scroll up there's a "Fetch more messages" button that lets me choose whether I want to be placed at the newest first or the oldest — ideal!&lt;/p&gt;

&lt;p&gt;I've been using FediLab for about a week, and it's sadly not the success story I hoped it would be. In theory it handles keeping track of scroll position fine but in practice it's not perfect. One in every 3 or 4 opens of the app it will misplace me by a certain amount up or down, making me spend time working out where I was before. Sometimes I think it's just not saving my position during an entire session and the next time I'll be back to where I started.&lt;/p&gt;

&lt;p&gt;I really want to stick with it because it's a really solid app and nice experience but after a week of use I don't think I can.&lt;/p&gt;

&lt;h3&gt;
  
  
  Twidere
&lt;/h3&gt;

&lt;p&gt;So that leaves me... exactly where I started? Even with the crashing and all of the Mastodon features it's missing, &lt;a href="https://twidere.com"&gt;Twidere&lt;/a&gt; meets my requirement of how I use the app the best. It's tracking of scroll position is rock-solid and that part of it hasn't had any issues at ll whilst I've been using it.&lt;/p&gt;

&lt;p&gt;In terms of the crashing, it seems to happen about once every 4-5 days on average, and I haven't put the time into working out exactly what's causing it. To fix it, I need to clear the data and cache of the app and restart it. I'm still logged in at that point, but it's lost all of my customisation. However, there's a way to export your settings to a file so I can re-import that after it's crashed to fairly quickly get back to where I was.&lt;/p&gt;

&lt;p&gt;It's a faff, and means when it does crash I lose where I was but that's a lot less frequently for any of the other apps.&lt;/p&gt;

&lt;p&gt;I'll miss all of the features of the others, when I want to vote in a poll for example I have to open the post in my browser, copy the URL and paste it into my home server. A bloody pain, but I don't use polls that often. I think the thing that I miss the most is not being able to see an image's alt text within the app, making it hard to keep my my policy of "No alt no boost".&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally
&lt;/h2&gt;

&lt;p&gt;So there we go, that's a summary of how I switched my Mastodon app usage from Twidere to Twidere! If I've missed a setting on any of the apps that would make them work better, or you have any other suggestions then please let me know!&lt;/p&gt;

&lt;p&gt;And once again, join me on the Fediverse at &lt;a href="https://mastodon.scot/@accudio"&gt;accudio@mastodon.scot&lt;/a&gt;, and if you aren't on the Fediverse then come join us! &lt;a href="https://joinmastodon.org"&gt;joinmastodon.org&lt;/a&gt; is a great place to get started.&lt;/p&gt;

</description>
      <category>mastodon</category>
      <category>fediverse</category>
      <category>android</category>
    </item>
    <item>
      <title>GrapheneOS as my daily-driver mobile OS</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Fri, 20 Jan 2023 17:37:51 +0000</pubDate>
      <link>https://forem.com/accudio/grapheneos-as-my-daily-driver-mobile-os-84m</link>
      <guid>https://forem.com/accudio/grapheneos-as-my-daily-driver-mobile-os-84m</guid>
      <description>&lt;p&gt;In November 2022 I dropped my previous phone. Actually 'drop' maybe isn't accurate, it was more of a drop-kick into a stone floor. It was pretty beat-up already honestly, this Xiaomi Mi 9T had been dropped countless times and the back was covered in sellotape to keep the glass in. My &lt;em&gt;beautiful&lt;/em&gt; drop-kick was a bit too much though, and the screen died completely.&lt;/p&gt;

&lt;p&gt;I had already been considering getting a new phone for a bit however, as my trusty 9T certainly had it's problems. I'll get onto some of the software issues later, but also the pop-up selfie camera only popped up about 30% of the time (thanks to a Snowboarding accident). I regularly got strange looks as my phone whirred unhealthily and I shook it upside-down to try to get the camera out.&lt;/p&gt;

&lt;p&gt;So at this point I was looking for a new smartphone, and for anyone who isn't a big fan of either Apple or Google as I am you'll be familiar with the dilemma. Android is a privacy and security hell-hole where you're expected to get a new phone almost every half an hour. iOS is possible &lt;em&gt;slightly&lt;/em&gt; better for privacy but expects you to sell at least 3 organs to get the cash to buy a phone. Also &lt;a href="https://dev.to/writing/cma-mobile-browsers-mir/"&gt;fuck the browser ban&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://mastodon.neilzone.co.uk/@neil"&gt;Neil Brown&lt;/a&gt;, I found &lt;a href="https://grapheneos.org"&gt;GrapheneOS&lt;/a&gt;, a mobile OS which promises to solve all my problems. I've since been using it for a couple months on a Google Pixel 6 and it's been fantastic. This post is about my experience with it to share the love!&lt;/p&gt;

&lt;h2&gt;
  
  
  What is GrapheneOS?
&lt;/h2&gt;

&lt;p&gt;So &lt;a href="https://grapheneos.org"&gt;GrapheneOS&lt;/a&gt; is Android... kinda. It's basically the open-source version of Android but with loads of added security and privacy functionality. It's open-source, officially supports all the most recent Google Pixel devices and can be fairly easily installed to replace the default Pixel Android.&lt;/p&gt;

&lt;p&gt;By default it doesn't include Google apps or services for security and privacy reasons, so Google doesn't have constant access to your device for their nefarious purposes. It does however support android apps, and you can install Google services in a limited way to ensure maximum compatibility while ensuring it doesn't have full control/access.&lt;/p&gt;

&lt;p&gt;It sounded fantastic, Android app compatibility including for apps that need Google services, whilst a priority on security and privacy. That's exactly what I want from my phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  My previous mobile OS
&lt;/h2&gt;

&lt;p&gt;So on my previous couple of phones I tried to do something like what GrapheneOS promises. I had a de-googled version of Android, with the &lt;a href="https://microg.org"&gt;microg&lt;/a&gt; project adding support for apps that needed Google services.&lt;/p&gt;

&lt;p&gt;Unfortunately in practice it didn't work great. Props to all devs involved in making it happen, but so many apps didn't work on it. Some intentionally: "Your phone is insecure, fuck off", some just crashing.&lt;/p&gt;

&lt;p&gt;I had to carry a second phone with me that had 'normal' Android on it for my banking apps, most takeaway/food delivery apps, on holidays I was relying on others to order an Uber for me, and mobile gaming was pretty much completely out. There were often workarounds and alternatives for some of these but it was regularly a huge effort just to install an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  GrapheneOS installation and set-up
&lt;/h2&gt;

&lt;p&gt;Graphene has a very fancy web-based UI for installing itself onto devices and extremely good instructions and documentation. It was probably the easiest OS install/flash I've ever dealt with, mobile or otherwise.&lt;/p&gt;

&lt;p&gt;Compatibility with different computers seems a bit iffy, I couldn't get it working on my Windows 11 desktop (driver issues probably) and work Macbook (USB C-C cable not being right maybe?) but it worked fine on my personal Windows 10 laptop. If you have issues try different computers you have access to and different cables.&lt;/p&gt;

&lt;p&gt;You basically just go through all the steps, doing what you're told and clicking the buttons when prompted. I did have some cases where it seemed to stop at random points so I had to re-do some steps when they didn't finish but eventually they all worked and I got it installed.&lt;/p&gt;

&lt;p&gt;Set-up was pretty standard Android, minus all of Google's shitty questions about tracking and such. Overall very easy to get installed and set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Profiles
&lt;/h2&gt;

&lt;p&gt;GrapheneOS has full separate user profiles and encourages users to utilise these to isolate different apps from each other to increase privacy and security. On Android every app can see what other apps you have installed on that user and potentially interact with them, so if you split your apps across different users it limits how much each app knows and can potentially affect.&lt;/p&gt;

&lt;p&gt;It also allows you to more easily control what apps are running when. If a user is not 'logged in', none of the apps in it can run in the background.&lt;/p&gt;

&lt;p&gt;I got really confused about how to set this up at first. I understood the concept but didn't really get the details about what was being suggested? Looking at what other people did confused me further as it varied so much. Some people would have an 'Instagram' user, others would not use the default user at all, I didn't really get what extent I should be using user accounts.&lt;/p&gt;

&lt;p&gt;Once I played with it a bit it started to make more sense. I'd suggest thinking about them as different 'contexts'. I ended up with this user structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main profile — my 'normal'. Has stuff I use often and anything I want running in the background;&lt;/li&gt;
&lt;li&gt;Social Media — social media apps, allowing me to only engage with them when I explicitly want to;&lt;/li&gt;
&lt;li&gt;Google Services — this is where I have Google Play services installed for apps that need it. Food delivery apps, games and misc apps I need that can't run in my main profile;&lt;/li&gt;
&lt;li&gt;Work — I like being able to have easy-access to my work email and Slack, if I'm out and running late for example. This allows me to keep it out of mind unless necessary;&lt;/li&gt;
&lt;li&gt;Private — stuff that I want behind an extra later of security and a different PIN code. Includes banking apps and anything I wouldn't want my parents to be able to switch to!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My thoughts on Graphene after a few months
&lt;/h2&gt;

&lt;p&gt;I really like GrapheneOS. Quite often software intended for people who are security/privacy minded compromises a lot on usability but the user experience of Graphene is fantastic. It has a handful of issues—more on that in a sec—but none are major enough to override all of the problems it solves with the mobile OS market.&lt;/p&gt;

&lt;p&gt;Some of the things I love about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user profiles are fantastic for app control;&lt;/li&gt;
&lt;li&gt;Flawless compatibility with &lt;em&gt;almost&lt;/em&gt; every android app I've tried;&lt;/li&gt;
&lt;li&gt;Improved security;&lt;/li&gt;
&lt;li&gt;Nice additional android settings and controls;&lt;/li&gt;
&lt;li&gt;OS updates that happen in the background that have never had an issue or changed anything unexpectedly (more than I can say about Google and Apple);&lt;/li&gt;
&lt;li&gt;Far better privacy—I noticed how Google ads knew considerably less about me and my life after switching;&lt;/li&gt;
&lt;li&gt;The camera works as well as stock;&lt;/li&gt;
&lt;li&gt;Automatic fully-encrypted app backups;&lt;/li&gt;
&lt;li&gt;Gesture-based navigation works brilliantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In terms of battery life and performance, it's been pretty much exactly as normal Android when I tried it before installing Graphene. Performance seems the exact same, and the battery life might even be a bit better with less tracking and more control over background apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues with Graphene
&lt;/h2&gt;

&lt;p&gt;There's not many issues, but as I said it's not perfect.&lt;/p&gt;

&lt;p&gt;The most notable for me is that Facebook Messenger calls often don't come through to me. Even if I have the app open, someone will call me and nothing even comes up until the 'didn't answer' message appears. Messages work and I can call people fine, but until I come up with a solution I have to occasionally check my missed calls. It's not a terrible arrangement, my friends and family know to phone me if it's urgent.&lt;/p&gt;

&lt;p&gt;I've also found a handful of apps that don't work, with Google Play Services or without. So far there's been three, all random games from the Play Store that crash on startup. None I'm that fussed about yet so I haven't done any debugging. It is a very small number compared to the total number of apps that work great.&lt;/p&gt;

&lt;p&gt;I normally have an always-on VPN and have occasionally had issues with connecting to the internet with it on. This might be my Wireguard VPN client but I didn't have any problems on my last phone. Toggling it off and on tends to sort it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Pixel 6
&lt;/h2&gt;

&lt;p&gt;This post is mostly about GrapheneOS, as the software is what I really care about. If you're planning on buying a phone for Graphene though, I'll mention my experience with the Pixel 6.&lt;/p&gt;

&lt;p&gt;I bought a refurbished device rather than new for climate reasons and to not give money to Google directly. After the first device being a store model stolen from an o2 store and unusable, the second one was in perfect nick and as new.&lt;/p&gt;

&lt;p&gt;It's a bit big, I think I maybe should have gone for the Pixel 6a as that's slightly smaller, but I manage it okay with fairly big hands. It is slippy so I'd suggest getting a skin or case for it. I've got a &lt;a href="https://www.spigen.com/products/pixel-6-case-liquid-air"&gt;Spigen Liquid Air&lt;/a&gt; which isn't too thick and offers a bit of protection whilst still feeling pretty premium. I don't like the shape of the back with the big raised camera, but the case makes that a bit less major.&lt;/p&gt;

&lt;p&gt;The camera quality is good, I'm not sure it's up to all the hype from the many adverts I've seen but it's better than my previous phones.&lt;/p&gt;

&lt;p&gt;The fingerprint sensor is pretty crummy unfortunately, the worst I've used before. It works, but fairly often I have to try multiple times to get in. I originally wondered if this was Graphene, but from some searching it seems to be the phone hardware.&lt;/p&gt;

&lt;p&gt;Overall, it's a decent phone for a decent price. A good one to go for if you're buying a new phone for Graphene. I wouldn't recommend it without Graphene though, it's not worth the Google spyware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm a big fan of GrapheneOS and it's pretty much nailed the perfect mobile OS for me at the moment. I've got more control over my phone, how I use it, how apps run on it, and who it reports back to than I ever have before.&lt;/p&gt;

&lt;p&gt;I would highly recommend it for anyone considering a new phone, especially if you're considering privacy, security or control over your device.&lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href="https://mastodon.scot/@accudio"&gt;message me&lt;/a&gt; or &lt;a href="//mailto:alistair@accudio.com"&gt;email me&lt;/a&gt; if you have any questions about it! I'd be happy to help. 👋&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>SVG generative mountain ridge dividers</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Fri, 29 Apr 2022 09:46:16 +0000</pubDate>
      <link>https://forem.com/accudio/svg-generative-mountain-ridge-dividers-2j4d</link>
      <guid>https://forem.com/accudio/svg-generative-mountain-ridge-dividers-2j4d</guid>
      <description>&lt;p&gt;This is another post about the build of this website. Check out the other posts in the &lt;a href="https://alistairshepherd.uk/writing/tag/my-site/"&gt;my site tag&lt;/a&gt; on my website.&lt;/p&gt;

&lt;p&gt;Today, I wanted to write a little about the section dividers used on my site. These ones:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GQPjNKlY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpauftqjwprz8y236afl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GQPjNKlY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpauftqjwprz8y236afl.png" alt="Two website section dividers, in the style of a 2D mountain ridge" width="800" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've done any game development they may seem familiar, they're nothing particularly new! They are however a neat thing you can do with SVG and I love those!&lt;/p&gt;

&lt;p&gt;For people who just want to dig into a demo, here you go!&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/accudio/embed/VwyRjaj?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you want more of an explanation then here we go!&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea: Interesting section dividers
&lt;/h2&gt;

&lt;p&gt;You may have noticed, but this website has a bit of a theme. Pat yourself on the back if you guessed that it's a mountain/landscape theme.&lt;/p&gt;

&lt;p&gt;The header and colour changes were the entire basis for my new site, and the colour scheme was to be very simple but impactful. It felt fairly natural that the background of page section should vary between different colours within the theme to separate them. What didn't feel natural though was the hard straight line between them. I played around with curved lines, skewed them, added wobble, but none seemed to feel quite right.&lt;/p&gt;

&lt;p&gt;At this point my sister suggested I use a mountain ridge, matching the style of the header. I initially produced a simple SVG manually and inserted it between each section.&lt;/p&gt;

&lt;p&gt;I liked how this looked, but when two were visible on screen at once it looked a bit silly them being identical (recreation below).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pB04XCm8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ye8an9twof6s23nzon08.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pB04XCm8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ye8an9twof6s23nzon08.jpg" alt="Two matching hilly dividers, either side of content. Looks a bit weird" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't really want to manually create more, although it would have been a quick workaround it didn't feel like it was really a solution. My temporary solution was to manipulate the one I did have, using &lt;code&gt;transform&lt;/code&gt; to flip, rotate or scale it so it looked slightly different each time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7QcmeJ0s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dkknvmcxi21s1lijwws.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7QcmeJ0s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dkknvmcxi21s1lijwws.jpg" alt="Two matching hilly dividers, either side of content. The top one has been flipped and rotated, but still looks a bit weird" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It bugged me that my site just had the one ridge design, but I didn't really like any of the solutions I came up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terrain Generation
&lt;/h2&gt;

&lt;p&gt;Some time later, I read an article from the Joy of Computing newsletter about terrain generation in game development. I really like the Joy of Computing, although I don't have much time for keeping up with the wider programming industry, their newsletters are cool projects or posts about different areas I don't normally follow like Game Development, DevOps, Hardware or Networking to name a few.&lt;/p&gt;

&lt;p&gt;Although the post was not really relevant to me, it made me realise that terrain generation was exactly what I needed! A method to create unique 'ridges' generated every time I needed a new divider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with points
&lt;/h2&gt;

&lt;p&gt;The output format was pretty easy, it had to be SVG. That way I could generate it ahead of time and embed it in the document and not need to rely on client-side JavaScript or outputting a large image file. For my use-case I basically needed a shape with variable top and cover the below area to match the background colour.&lt;/p&gt;

&lt;p&gt;I needed a way to convert however I generate the points of the line to an SVG &lt;code&gt;path&lt;/code&gt; format. My input array in most cases was in the format &lt;code&gt;[ [ x, y ], ... ]&lt;/code&gt;, acting as a programmatic dot-to-dot. Turns out that although the &lt;code&gt;path&lt;/code&gt; syntax seems a bit complex, when you're building it ends up making a lot of sense. SVG has different 'commands' which do certain things with a few parameteres. Check out the path syntax on MDN for them all, but we're mostly interested in &lt;code&gt;L&lt;/code&gt; which draws a line to the specified absolute point. With a &lt;code&gt;viewBox&lt;/code&gt; that matches our generation coordinate system we can convert it like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// convert points into SVG path&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;convertPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// add first M (move) command to go to the first point&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`M &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

  &lt;span class="c1"&gt;// iterate through points adding L (line) commands to path&lt;/span&gt;
  &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;` L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// close path down from the last point to bottom-right, bottom-left, then back to start&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;` L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Z`&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Attempt 1 — Random
&lt;/h2&gt;

&lt;p&gt;In my keenness, I jumped straight in with my first thoughts. I use &lt;code&gt;Math.random&lt;/code&gt; to work out where the next position is and keep going until I've done the whole width:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/accudio/embed/qBpvNYN?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ah. Not quite what I was going for, less like a mountain ridge and more like a bed of nails. Maybe the issue is that I'm used fixed intervals, so I tried random intervals too:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/accudio/embed/vYpPKaJ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Yeah, that looks really cool! Not what I'm wanting though - it has too much randomness and most of the time it just doesn't make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2 — Midpoint Displacement
&lt;/h2&gt;

&lt;p&gt;After my first attempt, I actually did some research on terrain generation. I wanted something very simple I could implement myself in JavaScript and very fast.&lt;/p&gt;

&lt;p&gt;I discovered the Midpoint Displacement Algorithm which seems to fit the bill perfectly. It's a simple algorithm and isn't very often used in modern games thanks to a lack of sudden steep inclines, overhangs and such, but for a mostly rolling ridge as I wanted it's perfect.&lt;/p&gt;

&lt;p&gt;A short summary is how it works is by drawing out a straight line, and then splitting it into two segments at the midpoint. We then take that midpoint and 'displace' it—move it upwards or downwards— by a random amount. We then take the two segments and do the same thing, splitting them in two on a midpoint and displacing that midpoint. Each iteration, we reduce the amount each midpoint can move so as the segments get smaller we get finer and finer detail.&lt;/p&gt;

&lt;p&gt;If you're interested in the theory behind it or the implementation I would recommend reading &lt;a href="https://bitesofcode.wordpress.com/2016/12/23/landscape-generation-using-midpoint-displacement/"&gt;"Landscape generation using midpoint displacement" by Bites of Code&lt;/a&gt;. This is a great article about implementing this in Python, and it explains whats happening and why really well. I found it when I was implementing it myself, and most of my code is a JS adaptation of their Python code.&lt;/p&gt;

&lt;p&gt;I made a few tweaks and voila! Check out the demo for the code and result:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/accudio/embed/VwyRjaj?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This works really well, and generates extremely quickly. You can play with the variables at the top of the file to change the dimensions, fiedlity and roughness.&lt;/p&gt;

&lt;p&gt;By running the output SVG through SVGO it ends up being pretty small too! This is exactly the method you see around my site at the time of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3 — Noise?
&lt;/h2&gt;

&lt;p&gt;I did make a third attempt, using Simplex noise to generate a terrain map with higher fidelity, cliffs, overhangs and flatter regions. I didn't get very far with it however, as I didn't particularly like the effect for the divider—it pulled away too much attention. It was also significantly slower to generate the SVG was quite a lot larger so I ended up ditching it and sticking with attempt 2.&lt;/p&gt;

&lt;p&gt;It is very fun to play with terrain generation though so I'd love to play with this some more in future!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Here's the final demo of the divider, as used on my site:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/accudio/embed/VwyRjaj?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I implemented this server-side with &lt;a href="https://github.com/Accudio/alistair-shepherd/blob/main/src/_includes/utils/shortcodes/divider.js"&gt;an Eleventy Shortcode&lt;/a&gt;, but as it's JavaScript you could easily use it on the client instead. That's what I've done in the demos throughout this post.&lt;/p&gt;

&lt;p&gt;There are so many examples of where web designers and developers can learn from game design and development. Video games have so many examples of unique, creative and interesting challenges and solutions in their design and development that we could learn from. This is definitely a case where a fairly standard technique used by game developers can be used for creative result on the web.&lt;/p&gt;

&lt;p&gt;Now go have a play and implement something like this yourself! Look at any games you play, or find out a little bit about an industry you aren't as familiar with and see if there's anything you can learn from to make more creative and cool websites!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>svg</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Managing tracking consent with Cead Consent</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Sun, 10 Apr 2022 10:17:24 +0000</pubDate>
      <link>https://forem.com/accudio/managing-tracking-consent-with-cead-consent-1j92</link>
      <guid>https://forem.com/accudio/managing-tracking-consent-with-cead-consent-1j92</guid>
      <description>&lt;p&gt;An issue many businesses and sites will have to deal with is cookie and tracking consent on their websites. The web however is plagued with a huge number of intrusive trackers, and terrible, frustrating and often illegal consent dialogs.&lt;/p&gt;

&lt;p&gt;Many websites implement a notice that doesn't allow opt-out, some offer an option that does nothing, whilst others only offer an opt-out solution - conveniently after they've collected all of your data.&lt;/p&gt;

&lt;p&gt;Cead (pronounced kee-yed) is a cookie and tracking consent manager that is simple, lightweight, easy to implement and free. It's designed to help you implement a simple Accept or Deny dialog that will actually enable or disable tracking.&lt;/p&gt;

&lt;p&gt;Cead is primarily created in response an increase in unsolicited web surveillance, but also to assist with meeting the standards of regulation including the EU GDPR &amp;amp; ePrivacy and California's CCPA. As privacy legislation becomes more strict it's important that solutions offer compliant opt-in and opt-out controls which Cead offers at it's core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The problem&lt;/li&gt;
&lt;li&gt;A possible fix?&lt;/li&gt;
&lt;li&gt;
Cead Consent

&lt;ol&gt;
&lt;li&gt;Using Cead Consent to manage Google Tag Manager&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Tracking on the web has long been a difficult topic. The interests of business owners, SEO teams, Ad vendors, site users and lawmakers become almost impossible to resolve and frequently ignore each other.&lt;/p&gt;

&lt;p&gt;I'm of the opinion that a site should have no tracking. This site has no analytics or anything, because your browsing is your own business. Check out &lt;a href="https://css-tricks.com/aint-no-party-like-a-third-party/"&gt;Jeremy Keith's "Ain’t No Party Like a Third Party"&lt;/a&gt; for his insight on third-party scripts.&lt;/p&gt;

&lt;p&gt;I find however that is an impossible stance to maintain when building sites for other people. They are often used to tracking metrics to evaluate their success, generate leads or target their services.&lt;/p&gt;

&lt;p&gt;I've worked in agencies where I've seen and worked on a lot of websites for a variety of clients. They vary in purpose, build, location, size and much more, but one thing almost all have in common is they handle tracking terribly.&lt;/p&gt;

&lt;p&gt;This may be familiar to you, but if not let me demonstrate the situation. We build a site for a client and add Google Analytics to it - pretty standard. Google Analytics has an easy way to allow people to opt-out by setting a global variable so we integrate a wee popup that allows the user to opt out.&lt;/p&gt;

&lt;p&gt;That works great until the client gets an SEO expert who wants to track conversions better. They ask you to add a couple more scripts and you dutifully do so, but these have no way to opt out so all you can do is add them.&lt;/p&gt;

&lt;p&gt;Later on, they want to add more scripts so they either ask for a text box to add them arbitrarily, install plugins, or install a Tag Manager.&lt;/p&gt;

&lt;p&gt;Before long, the site has 5 analytics scripts, 10 conversion trackers and a screen recorder. These may not respect the user's privacy settings or have a way to opt out, and the website could slow to a crawl.&lt;/p&gt;

&lt;p&gt;Some developers will give up at the beginning of this process and instead of asking consent put a message saying "This site uses cookies and tracks you. Deal with it or fuck off".&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is an issue
&lt;/h3&gt;

&lt;p&gt;There are two reasons why this is a problem. Ethical and legal.&lt;/p&gt;

&lt;p&gt;Ethically, if this is your site you are stalking your users - standing 2 metres behind them as they peruse your store. The level of what is acceptable here can be debated, but tracking someones every move without their ability to consent to this is not justified. Place yourself in the shoes of someone who is being tracked across the web by several trackers, without any knowledge that potentially every interaction and details about their computer and location are being harvested and stored. It's hard to dispute in those circumstances.&lt;/p&gt;

&lt;p&gt;This is also illegal in many jurisdictions. Consumer privacy laws like GDPR and ePrivacy in the EU, and CCPA and similar in American states requires some level of consent to web tracking. I'm not a lawyer so contact one for proper advice, but this gist is at minimum you &lt;strong&gt;need&lt;/strong&gt; a way for users to be able to meaningfully opt out of tracking.&lt;/p&gt;

&lt;h3&gt;
  
  
  The opt-out issue
&lt;/h3&gt;

&lt;p&gt;The big problem with the requirement to offer an opt out is that this is &lt;strong&gt;very&lt;/strong&gt; hard to do.&lt;/p&gt;

&lt;p&gt;As I mentioned earlier, some scripts like Google Analytics offer a method to opt out. This still isn't ideal as you're loading a tracking script and then checking if you're allowed to run it, but it at least gives you some control.&lt;/p&gt;

&lt;p&gt;However that one of few tracking scripts I have come across that allows a way to opt out. Lots of other scripts will happily run as soon as they load, without regard for consequences. Even those that do have methods to opt-out, may be individual for each service and be a nightmare to manage.&lt;/p&gt;

&lt;p&gt;Developers can deal with this by dynamically adding scripts under certain conditions, but clients will want to add their own and may not consider the consequences.&lt;/p&gt;

&lt;p&gt;As developers we're left in a difficult position. Laws require that tracking can be opt-out, but we have no way to do so.&lt;/p&gt;

&lt;h2&gt;
  
  
  A possible fix?
&lt;/h2&gt;

&lt;p&gt;The way to fix this is to be in control of all tracking scripts, and then load them ourselves in response to a consent status.&lt;/p&gt;

&lt;p&gt;There are many solutions to do this as investors have monopolised on businesses grappling with the issue of tracking and consent.&lt;/p&gt;

&lt;p&gt;Some large 'privacy-focused' corporations offer pricey 'hosted consent solutions' that supposedly solve all your problems. However when I load the site of one, my browser tells me it's blocked 14 trackers.&lt;br&gt;
If you've ever been annoyed by a cookie popup, it's probably a solution like this. A big annoying popup that makes opting out difficult and will send all your preferences to a tracking service to track your consent.&lt;/p&gt;

&lt;p&gt;My opinion is that some of these companies are morally corrupt. Tracking the consent of users on a remote server is still tracking and they charge extortionate fees to fix a problem their own investors created.&lt;/p&gt;

&lt;p&gt;I think the fix is a lot easier. Our webpage only runs tracking scripts when we say so. That's why I made Cead Consent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cead Consent
&lt;/h2&gt;

&lt;p&gt;Cead Consent is a small library designed to solve the issue of tracking consent by controlling when scripts can run on the client-side. By making a tiny modification to tracking scripts we can load them on-demand in response to consent status.&lt;/p&gt;

&lt;p&gt;It is designed to be extremely simple, easy to use and lightweight, and I'll give you a quick demo of how you would use it to solve the problem of consent.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/accudio/cead-consent"&gt;the GitHub repo&lt;/a&gt; for full instructions on &lt;a href="https://github.com/accudio/cead-consent#installation"&gt;installation&lt;/a&gt; and &lt;a href="https://github.com/accudio/cead-consent#managing-tracking-scripts-and-images"&gt;usage&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using Cead Consent to manage Google Tag Manager
&lt;/h3&gt;

&lt;p&gt;First we need to install Cead. It can either be loaded from a CDN or installed via &lt;code&gt;npm&lt;/code&gt;, here I'll use the CDN to make it easier. We need to add a CSS file, a JavaScript file, and a little bit of HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/cead-consent@1/dist/cead.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Hi! Could we please enable some services and cookies to improve your experience and our website?&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cead__btns"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cead__btn cead__btn--decline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;No, thanks.&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cead__btn cead__btn--accept"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Okay!&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;&lt;span class="c"&gt;&amp;lt;!-- your page content --&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/cead-consent@1/dist/browser.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although Cead consent can be used with all sorts of tracking scripts or pixels, I feel it's at it's best when combined with a tag manager like Google Tag Manager.&lt;/p&gt;

&lt;p&gt;We manage tracking scripts (and images) by modifying their code slightly so they'll only run when Cead allows them to. When used with a Tag Manager the client or SEO teams can add as many scripts as they'd like to Google Tag Manager and we need to modify only one script for Cead.&lt;/p&gt;

&lt;p&gt;When you copy your script from Google Tag Manager, it will look something like this (with a different GTM_MEASUREMENT_ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googletagmanager.com/gtm.js?id=GTM_MEASUREMENT_ID&amp;amp;l=dataLayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that last line, the &lt;code&gt;&amp;lt;script async src="..."&amp;gt;&lt;/code&gt;? All we need to do is change the &lt;code&gt;src&lt;/code&gt; attribute to &lt;code&gt;data-src&lt;/code&gt;, and add the &lt;code&gt;data-cead&lt;/code&gt; attribute, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gtm.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dataLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googletagmanager.com/gtm.js?id=GTM_MEASUREMENT_ID&amp;amp;l=dataLayer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;cead&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! With the installation of Cead and that small change to the script tag we've made it so users can choose to consent to tracking or not and their choice is respected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Although the best situation is to avoid adding tracking to sites where possible, it often isn't possible. The best situation then is to use a lightweight, simple consent manager that won't frustrate users, will respect their consent choices and is free and open-source.&lt;/p&gt;

&lt;p&gt;Cead has more options including managing &lt;a href="https://github.com/accudio/cead-consent#inline-scripts"&gt;inline scripts&lt;/a&gt;, &lt;a href="https://github.com/accudio/cead-consent#image-pixels"&gt;tracking 'pixels'&lt;/a&gt;, an &lt;a href="https://github.com/accudio/cead-consent#options"&gt;'opt-out mode'&lt;/a&gt;, &lt;a href="https://github.com/accudio/cead-consent#managing-cookies"&gt;cookie removal&lt;/a&gt; and more. Check out the documentation on the &lt;a href="https://github.com/accudio/cead-consent"&gt;GitHub repo&lt;/a&gt; to see all it can do!&lt;/p&gt;




&lt;p&gt;If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and leave me a comment, &lt;a href="//mailto:alistair@accudio.com"&gt;send me an email&lt;/a&gt; at &lt;a href="mailto:alistair@accudio.com"&gt;alistair@accudio.com&lt;/a&gt; or &lt;a href="https://twitter.com/accudio"&gt;contact me on Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://alistairshepherd.uk/writing/cead-consent/"&gt;Managing tracking consent with Cead Consent&lt;/a&gt; appeared first on &lt;a href="https://alistairshepherd.uk"&gt;alistairshepherd.uk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>privacy</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Personal site stack for alistairshepherd.uk</title>
      <dc:creator>Alistair Shepherd</dc:creator>
      <pubDate>Thu, 23 Dec 2021 16:56:41 +0000</pubDate>
      <link>https://forem.com/accudio/personal-site-stack-for-alistairshepherduk-4l9l</link>
      <guid>https://forem.com/accudio/personal-site-stack-for-alistairshepherduk-4l9l</guid>
      <description>&lt;p&gt;I've had a very chaotic few months at the end of 2021, with work, moving house and my &lt;a href="https://alistairshepherd.uk/speaking/jamstack-imagecdns/"&gt;first steps into giving tech talks&lt;/a&gt;! That has meant that all the blog posts and side projects I planned were left behind, but with some free time before Christmas I thought I'd squeeze a quick and easy post out!&lt;/p&gt;

&lt;p&gt;I hope 2021 has been kind and merciful to you, and you have a good holiday period if you're taking one! Thank you so much for reading and supporting me/my work, it means a huge amount! ❤️&lt;/p&gt;

&lt;p&gt;So I'm finishing off this year with a very festive post... my website tech stack?! 🌲&lt;/p&gt;




&lt;p&gt;In about a month this site will be a year old! Recently a few people have asked me about the tech stack so it's about time I put it and my thinking down properly. I'm really enjoying it!&lt;/p&gt;

&lt;p&gt;The entire build is designed to make it as easy as possible for me to work on new content and tweaks after months or years of not touching it. It's been great so far and I find it easier to work on when I come back to it than other platforms I've worked on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary / Table of Contents
&lt;/h2&gt;

&lt;p&gt;Here's a TLDR if you're not interested in the reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Host — Netlify with Vercel backup ☁️&lt;/li&gt;
&lt;li&gt;
Static site generator — Eleventy 🎈🐀&lt;/li&gt;
&lt;li&gt;
Tasks/build — Gulp 🥤&lt;/li&gt;
&lt;li&gt;
CSS — 'Vanilla' Scss with Gorko 🎨&lt;/li&gt;
&lt;li&gt;
JavaScript — Vanilla JS with esbuild and Barba client-side routing ⚡&lt;/li&gt;
&lt;li&gt;
Media — CloudImage Image CDN 🖼️&lt;/li&gt;
&lt;li&gt;
Fonts — Red Hat Display and Literata, optimised with Glyphhanger 🕴️&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hosting
&lt;/h2&gt;

&lt;p&gt;For Jamstack hosting I really like &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt;. I think their product is brilliant and love how easy it makes deploying and hosting a website. It has tons of features, great documentation and I like their company ethos, principles and staff. My site is primarily hosted with them under the free plan.&lt;/p&gt;

&lt;p&gt;However in case they have a major incident or I disagree with their direction, I have a backup copy of my site ready to go with &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt;. If I needed to switch to them, all I'd need to do is update the DNS and it would be done within a few hours if needed. I don't anticipate needing to, but when it's so easy and free to have a backup website I like having the option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static Site Generator
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://www.11ty.dev/"&gt;Eleventy&lt;/a&gt; as my Static Site Generator (SSG) for data manipulation and HTML generation. My site is fairly simple, all I really need is handlebars-style templating, markdown support and reusable JS snippets for custom functionality.&lt;/p&gt;

&lt;p&gt;This would give me a lot of SSG options but I had a few priorities that were important. I wanted static HTML without any client-side JavaScript, flexibility with data and structure, easily extendable with JavaScript, and to be in full control of the output. Eleventy was the ideal tool for the job here.&lt;/p&gt;

&lt;p&gt;At the time of creation it was a year from stable release but already provided a quick, extendable platform that is easy to work on and has been more stable than at least 4 other major SSGs I've worked with!&lt;/p&gt;

&lt;p&gt;I write my blog posts in Markdown but the rest of the site uses &lt;a href="https://mozilla.github.io/nunjucks/"&gt;Nunjucks&lt;/a&gt; for templating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tasks/build
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://gulpjs.com/"&gt;gulp&lt;/a&gt; for my build process and tasks as it provides me a lot of freedom with how I want to run tasks and implement builds.&lt;/p&gt;

&lt;p&gt;Many people consider gulp to be dated/dead but honestly I much prefer it to many of the big 'build tools' used in development at the moment. Webpack, Rollup and Parcel seem great at first but I've had difficulties with configuration or needed to use gulp alongside them for custom processes.&lt;/p&gt;

&lt;p&gt;A year on I would consider simplifying further and using simple node scripts instead of gulp. These would have the benefits of simplicity and stability over time—I would also appreciate fewer dependencies. For getting off the ground quickly though gulp has the edge for me, with a still huge ecosystem and so many previous projects I can pull gulp tasks from.&lt;/p&gt;

&lt;p&gt;A key feature is I can write my own tasks in JS and don't need to prescribe to a config system. The tasks for this site are fairly standard but for long-term maintenance it's useful being able to write my own without having to learn a new 'plugin' syntax. I can also implement builds using whatever tools I like—an example being choosing to use esbuild for JS bundles.&lt;/p&gt;

&lt;p&gt;It's not as fancy as some of the latest tools like Vite and Snowpack, but in reality I don't need HMR or instant refreshes for a simple site. And although it's not at the cutting-edge, the API and project stability is helpful for coming back to an older project.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS
&lt;/h2&gt;

&lt;p&gt;The CSS is mostly handcrafted without any libraries so I can have full control over the structure and performance. I write in &lt;a href="https://sass-lang.com/"&gt;Sass (Scss flavour)&lt;/a&gt; as I'm used to many of the utilities and conveniences it provides like importing partials, concatenated nesting and variables.&lt;/p&gt;

&lt;p&gt;I say 'mostly handcrafted' as I use the utility class generator &lt;a href="https://github.com/hankchizljaw/gorko"&gt;Gorko&lt;/a&gt; to generate classes for spacing, sizing and colours. Utility classes are great for simple rules like changing &lt;code&gt;display&lt;/code&gt; or spacing, both for convenience and performance reasons. For anything that has more than a couple utility classes though that becomes a 'layout' or 'block' that is written using BEM-like classes (eg &lt;code&gt;.nav__link&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The structure I follow is a variation of Andy Bell's &lt;a href="https://cube.fyi/"&gt;CUBE CSS&lt;/a&gt; with Layout, Utilities and Blocks. For performance optimisation I have a 'critical' CSS file that includes CSS important to display the top of pages correctly, and this is embedded in the &lt;code&gt;head&lt;/code&gt; of every page. I use an 11ty transform with &lt;a href="https://github.com/FullHuman/purgecss"&gt;PurgeCSS&lt;/a&gt; to strip out any unused rules for each page. This makes the first load of each page as fast as possible, and then the rest of the styles are loaded in a 'main' CSS file that is there for lower down the page and cached for subsequent navigations.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript
&lt;/h2&gt;

&lt;p&gt;As I prioritise performance, almost all JavaScript is hand-written and vanilla — no frameworks. This allows me to include only what is needed, which thanks to the capabilities of modern browsers ends up being pretty small. Not including libraries and frameworks improves performance on all devices and means my code is more maintainable in future.&lt;/p&gt;

&lt;p&gt;I really like &lt;a href="https://github.com/evanw/esbuild"&gt;esbuild&lt;/a&gt; for transforming source JS, it's extremely fast, very simple and the &lt;a href="https://github.com/ym-project/gulp-esbuild"&gt;gulp-esbuild&lt;/a&gt; plugin is easy to use. I have it set up to turn a set of modules into a single bundle for performance reasons, minify for production, generate sourcemaps and transform modern syntax to a list of supported browsers.&lt;/p&gt;

&lt;p&gt;The only library I use is &lt;a href="https://barba.js.org/"&gt;Barba&lt;/a&gt; for Client Side Routing (CSR) to maintain the state of the landscape and themes seamlessly across pages. Although I don't normally care much for page transitions and client-side navigations, I couldn't come up with a native solution that was quick, wasn't jarring and maintained the effect. I broke my 'no libraries' rule here as client-side routing isn't easy to get right. Barba does a decent job, is fairly small and I load it separate from the main bundle with low priority to avoid a performance hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media
&lt;/h2&gt;

&lt;p&gt;I'm a big proponent of using Image CDNs, it makes the build process simpler and quicker and further development easier. For more of my thoughts see my recent talk &lt;a href="https://alistairshepherd.uk/speaking/jamstack-imagecdns/"&gt;Making Assets fly on the Jamstack with Image CDNs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://www.cloudimage.io"&gt;CloudImage&lt;/a&gt; for this site as I like it's simplicity and the free tier is generous enough to cover my few images. The performance is good but I'd like to see better, including AVIF support. &lt;a href="https://imgix.com/"&gt;Imgix&lt;/a&gt; and &lt;a href="https://cloudinary.com/"&gt;Cloudinary&lt;/a&gt; both perform better but I'm happy with CloudImage for the moment.&lt;/p&gt;

&lt;p&gt;I've written a custom Eleventy shortcode with a few parameters to generate &lt;code&gt;src&lt;/code&gt; and &lt;code&gt;srcset&lt;/code&gt; attributes to do what I need. This would make it easy to switch to a different provider if I wanted. To avoid the performance impact of using a different origin I &lt;a href="https://github.com/Accudio/alistair-shepherd/blob/92dbe295c402e4645ee463dc3e762fddfd673420/netlify.toml#L19"&gt;proxy CloudImage requests through Netlify using redirects&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://fonts.google.com/specimen/Red+Hat+Display"&gt;Red Hat Display&lt;/a&gt; for titles and &lt;a href="https://fonts.google.com/specimen/Literata"&gt;Literata&lt;/a&gt; for the body. Both are on Google Fonts and open source so I can download and manipulate them freely.&lt;/p&gt;

&lt;p&gt;To mitigate the performance effect of custom fonts I host them myself and preload the font files. I also reduce their size by subsetting them to US ASCII characters using &lt;a href="https://github.com/zachleat/glyphhanger"&gt;Glyphhanger&lt;/a&gt; which cuts their size almost in half. Thanks to &lt;a href="https://twitter.com/hankchizljaw"&gt;Andy Bell&lt;/a&gt; for the font combo!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm really happy with this stack and I'm hopeful it'll stand the test of time. My previous Next.js personal site was an absolute nightmare of dependency updates a year on from launching so we're doing better than that.&lt;/p&gt;

&lt;p&gt;If you're interested in any specific implementations the &lt;a href="https://github.com/Accudio/alistair-shepherd"&gt;source code is public on GitHub&lt;/a&gt;. Note that it's not open-source and licensed for re-use, but if you're looking for a similar setup I'd encourage taking a look and learning from the code!&lt;/p&gt;

&lt;p&gt;Whilst you're here check out my other posts about how I built this site, about the dynamic functionality of the landscape and colour themes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://alistairshepherd.uk/writing/parallax-svg-landscape-1/"&gt;Making a Parallax SVG Landscape - new site part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alistairshepherd.uk/writing/parallax-svg-landscape-2/"&gt;SVG Landscape with live colour theming - new site part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for reading, best wishes to you and yours, and take care!&lt;/p&gt;

</description>
      <category>eleventy</category>
      <category>javascript</category>
      <category>jamstack</category>
      <category>css</category>
    </item>
  </channel>
</rss>
