<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>CodeMade Blog</title>
  <subtitle>Free software &amp; tech musings</subtitle>
  <link href="https://codemade.net/atom.xml" rel="self" />
  <link href="https://codemade.net/blog/" />
  <updated>2026-08-25T00:00:00Z</updated>
  <id>https://codemade.net/blog/</id>
  <author>
    <name>Loris Bognanni</name>
  </author>
  <entry>
    <title>Experimenting with subagents: strict rules for token-minning</title>
    <link href="https://codemade.net/blog/experimenting-with-subagents/" />
    <updated>2026-08-25T00:00:00Z</updated>
    <id>https://codemade.net/blog/experimenting-with-subagents/</id>
    <content type="html">&lt;p&gt;For the past few weeks, I&#39;ve adopted a new workflow for my AI assisted coding. I&#39;ve settled on a very simple subagent based approach, where a main agent coordinates the work, and two subagents do the actual coding and reviewing.&lt;/p&gt;
&lt;p&gt;The idea is simple but it hides a lot of depth: working with agents has a set of constraints for regular people with regular budgets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bigger and more capable models are more expensive&lt;/li&gt;
&lt;li&gt;Smaller models are cheaper, and getting more capable as time goes on&lt;/li&gt;
&lt;li&gt;Context is expensive, and the more context you have, the more expensive it gets&lt;/li&gt;
&lt;li&gt;Token caching is a thing, but it does have limits: if you pick up an old session, you might have to reprocess all the context, which is expensive&lt;/li&gt;
&lt;li&gt;Models tend to have a &amp;quot;dumb zone&amp;quot; where they get more unreliable as the context grows&lt;/li&gt;
&lt;li&gt;Tool calls tend to generate a lot of text that has to be processed in each follow up message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means that the old approach of &amp;quot;use opus for everything&amp;quot; tends to become impractical when working on larger projects.&lt;/p&gt;
&lt;p&gt;Practically I wanted to migrate a fairly large, completely vibe coded codebase (a level editor for a game I&#39;m working on) from vanilla JavaScript to React with TypeScript, and do so while improving the code quality and maintainability. The project is large enough that it would be impractical to do it all in one go.&lt;/p&gt;
&lt;h2&gt;Subagents to the rescue&lt;/h2&gt;
&lt;p&gt;If you&#39;re on tech twitter for any amount of time, &amp;quot;subagents&amp;quot; are already a thing of the past. The cool kids are now doing &amp;quot;loops&amp;quot; or even &amp;quot;graphs&amp;quot;.&lt;/p&gt;
&lt;p&gt;The cool kids also have infinite budgets and work for the token factories. I only have a &amp;quot;Pro&amp;quot; Claude subscription for personal use, meaning that each token needs to earn its keep. I need to &lt;em&gt;token-minn&lt;/em&gt;, not token-maxx!&lt;/p&gt;
&lt;p&gt;In my &lt;a href=&quot;https://codemade.net/blog/building-for-one&quot;&gt;previous post on the matter&lt;/a&gt;, my experience with subagents was&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I found that the multi-agent system was exceptional at consuming tokens, while producing the same results that I could have achieved with a single agent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This time, I am coming at the sub-agent use case from a different angle: now it&#39;s all about optimizing for cost, by using cheaper models for routine tasks, and optimizing the context they need to do their job.&lt;/p&gt;
&lt;p&gt;To keep things as simple as possible, but not simpler, my process looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A Planning session, done with Opus 5, generates a markdown (e.g. &lt;code&gt;docs/feature-x-plan.md&lt;/code&gt;) file containing the plan for the migration / big feature. The plan is split into several milestones.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;During regular work, I interact with the main agent, which is pointed to a workflow file describing how it will manage the work.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The main agent is given the plan file, and asked to direct a &amp;quot;senior coder&amp;quot; (Opus or Sol, medium reasoning) subagent to break down the milestone into a set of small stories that &lt;em&gt;&amp;quot;a junior engineer can pick up&amp;quot;&lt;/em&gt; and store the result in a markdown file (e.g. &lt;code&gt;docs/feature-x-&amp;lt;milestone&amp;gt;.md&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For each story, the main agent will work through the workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A &amp;quot;coder&amp;quot; (Sonnet or Terra, medium reasoning) subagent is tasked with implementing the next story&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A &amp;quot;reviewer&amp;quot; (Opus or Sol, low reasoning) subagent reviews the change and gives it a thumbs up or down.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The main agent sends the reviewer&#39;s feedback back to the coder and the cycle repeats until the change is approved.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once the change is approved, the main agent updates the milestone doc, commits everything to git and moves on to the next story.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We then update the plan file with the progress, and move on to the next milestone (or stop for the day).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
%%{init: { &quot;flowchart&quot;: { &quot;curve&quot;: &quot;stepAfter&quot; } } }%%  
flowchart TD
    classDef default text-wrap:true,white-space:normal;
    Create_overall_spec__17e7846e[Create overall spec, split in milestones] --&gt; Work_on_next_milesto_e97c058d[Work on next milestone]
    Work_on_next_milesto_e97c058d --&gt; sc
    Senior_coder_Break_d_ebf70296[Break down next milestone]
    Senior_coder_Break_d_ebf70296 --&gt; Select_next_task_f184a500[Select next task]
    Select_next_task_f184a500 --&gt; Assign_coder_to_task_f44d0617[Assign coder to task]
    Assign_coder_to_task_f44d0617 --&gt; c
    Write_code_78fe1ad5[Write code]
    Write_code_78fe1ad5 --&gt; Run_tests_lint_a2bcfc51[&quot;Run tests / lint&quot;]
    Coder_done_review_21d3941a[Coder done, review] --&gt; r
    Reviewer_Review_chan_5375aae9[&quot;Review changes (read only)&quot;]
    Run_tests_lint_a2bcfc51 --&gt; Coder_done_review_21d3941a
    Reviewer_Review_chan_5375aae9 --&gt;|Bugs found| Assign_fixes_to_code_f9ec18ac[Assign fixes to coder] --&gt; c
    Reviewer_Review_chan_5375aae9 --&gt;|Code is fine| Commit_changes_67cd3b29[Commit changes]
    Commit_changes_67cd3b29 --&gt; More_tasks_c5e0d359[More tasks?]
    More_tasks_c5e0d359 --&gt;|yes| Select_next_task_f184a500
    More_tasks_c5e0d359 --&gt;|no| Milestone_done_Updat_358f3c5a[Milestone done. Update spec. &amp; commit]
    Milestone_done_Updat_358f3c5a --&gt; More_milestones_c9982668[More milestones?]
    More_milestones_c9982668 --&gt;|yes| Work_on_next_milesto_e97c058d
    More_milestones_c9982668 --&gt;|no| All_done_ced44dce[All done! 🎉]

    c -. optional: ask for help .-&gt; hw



subgraph sc[&quot;Senior coder subagent&quot;]
    Senior_coder_Break_d_ebf70296
    hw[Help coder agent]
end
subgraph c[&quot;Coder subagent&quot;]
    Write_code_78fe1ad5
    Run_tests_lint_a2bcfc51
end
subgraph r[&quot;Reviewer subagent&quot;]
    Reviewer_Review_chan_5375aae9
end
subgraph &quot;Main agent&quot;
    Work_on_next_milesto_e97c058d
    Select_next_task_f184a500
    Commit_changes_67cd3b29
    Milestone_done_Updat_358f3c5a
    More_milestones_c9982668
    All_done_ced44dce
    Assign_coder_to_task_f44d0617
    Coder_done_review_21d3941a
    Assign_fixes_to_code_f9ec18ac
end

classDef style0 stroke:#C3CFD9,fill:#ffffff,color:#293845
class All_done_ced44dce,Assign_coder_to_task_f44d0617,Assign_fixes_to_code_f9ec18ac,Coder_done_review_21d3941a,Commit_changes_67cd3b29,Create_overall_spec__17e7846e,Milestone_done_Updat_358f3c5a,More_milestones_c9982668,More_tasks_c5e0d359,Select_next_task_f184a500,Work_on_next_milesto_e97c058d style0
style Reviewer_Review_chan_5375aae9 stroke:#2C88D9,fill:#d5e7f7,color:#293845
style Senior_coder_Break_d_ebf70296 stroke:#E8833A,fill:#fae6d8,color:#293845
style hw stroke:#E8833A,fill:#fae6d8,color:#293845
style Write_code_78fe1ad5 stroke:#1AAE9F,fill:#d1efec,color:#293845
style Run_tests_lint_a2bcfc51 stroke:#1AAE9F,fill:#d1efec,color:#293845
linkStyle 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 stroke:#788896
&lt;/pre&gt;
&lt;h3&gt;The devil is in the details, and the details are mad expensive&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/axes-mixing-desk.svg&quot; alt=&quot;the axes mixing desk&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here are some of the things I learned while doing this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cached tokens have a relatively short shelf life. If your Claude session runs out of usage, it might be sitting idle for long enough that the context is no longer cached. In this case, &lt;a href=&quot;https://x.com/quxiaoyin/status/2085408811104534754&quot;&gt;better to start a new session than to reprocess all the past context&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is where instructing the main agent to keep track of where the work is, in the plan file, pays dividends. Now it&#39;s possible to start a new session, or even change the model, and continue where you left off without paying through the nose for context reprocessing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Have the main agent feed the subagents with &lt;strong&gt;exactly the context they need&lt;/strong&gt; to do the job.&lt;/p&gt;
&lt;p&gt;The subagent should not have to go read the plan files, or all of the codebase. It should have this information already distilled and passed to them in the prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do not let the reviewer run tests, linting, or any other automated checks&lt;/strong&gt;. We don&#39;t want to waste tokens on tool calls and checks that the coder already ran while implementing the story. The reviewer is specifically instructed to only look at the code and do nothing else.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A weaker model is perfectly fine for coding &lt;em&gt;if you plan things well&lt;/em&gt;. I settled on Sonnet/Terra at medium effort for the best combination of price, speed, and quality.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I want the reviewer to be a strong model, but I don&#39;t want it to go crazy. The best bang for the buck here is &lt;strong&gt;Opus or Sol&lt;/strong&gt; at &lt;strong&gt;Low&lt;/strong&gt; reasoning effort.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Especially for web software, Claude will want to do some semi-manual browser testing. This is a &lt;strong&gt;very expensive idea&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Automated testing is the way to go; the moment your agent has to look at screenshots or write custom throwaway test scripts, you are wasting tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At the time of writing, Twitter is abuzz with &lt;strong&gt;Luna at Max effort&lt;/strong&gt; for coding, because it&#39;s cheap and the benchmarks show it can do impressive things at extreme reasoning levels.&lt;/p&gt;
&lt;p&gt;In my experience, it does write passable code, but it takes a REALLY, REALLY long time to do it. The reviewer keeps any issue in check, but the coder is so slow that it&#39;s only an option if you&#39;re token poor and time rich, and are extremely patient.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://x.com/mattpocockuk&quot;&gt;Matt Pocock&lt;/a&gt; has an excellent &lt;a href=&quot;https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me&quot;&gt;&lt;code&gt;grill-me&lt;/code&gt;&lt;/a&gt; skill you can use to generate the initial plan for a new feature. It will go to excruciating levels of detail and ask you all sorts of questions about the feature, which is great for getting a solid spec and stopping the LLM from guessing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The results are in&lt;/h3&gt;
&lt;p&gt;This played out over two weeks, each week completing about half of the migration. I&#39;m happy to report that the refactoring was completed successfully!&lt;/p&gt;
&lt;p&gt;The first week was mostly spent trying various models and settings, carefully eyeing the usage bar on the Claude dashboard, panicking when I reached the limit on the 3rd day, using a bunch of extra credits (they would disappear on sept 1st anyway 😇), and then buying a ChatGPT Plus subscription to keep going.&lt;/p&gt;
&lt;p&gt;The second week, my token usage was much more efficient considering the amount of work that was done, thankfully leaving me at both subscriptions not maxed out, and no extra credits purchased.&lt;/p&gt;
&lt;p&gt;I&#39;ve now replicated the same workflow on a different greenfield project, and it has allowed me to get a lot of work done quickly, while keeping a decent handle on the code quality, both in terms of maintainability and correctness.&lt;/p&gt;
&lt;p&gt;An unexpected advantage of this approach vs my usual &amp;quot;single agent&amp;quot; workflow has been letting the agents run on their own, while I do other things. The main agent&#39;s context never gets too big since it&#39;s only a coordinator, and the subagents are by nature ephemeral, so they don&#39;t accumulate context either. This means that I can let the system run for hours without worrying about it running out of context or entering the &amp;quot;dumb zone&amp;quot;.&lt;/p&gt;
&lt;h4&gt;Want to give my workflow a go?&lt;/h4&gt;
&lt;p&gt;I&#39;ve put together a &lt;a href=&quot;https://github.com/LBognanni/simple-subagents-workflow&quot;&gt;starter repo with the workflow files&lt;/a&gt;, feel free to fork it and adapt it to your own needs. The workflow is simple enough that you can use it with any codebase, and the subagents can be swapped out for different models or reasoning levels as you see fit.&lt;/p&gt;
&lt;p&gt;I&#39;d love to hear about what you build with it!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Vibelets: tiny, useful and private web apps</title>
    <link href="https://codemade.net/blog/vibelets/" />
    <updated>2026-08-06T00:00:00Z</updated>
    <id>https://codemade.net/blog/vibelets/</id>
    <content type="html">&lt;p&gt;I&#39;ve added a new section to the homepage - &lt;a href=&quot;https://codemade.net/#vibelets&quot;&gt;Vibelets!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;These are tiny applications that I had an LLM (Usually Claude) build for very specific tasks. The main idea is that they are small, single-task web apps that run entirely in the browser, and that I can trust to not send my data anywhere.&lt;/p&gt;
&lt;p&gt;The classic example is the &lt;a href=&quot;https://codemade.net/vibelets/json-to-yaml/&quot;&gt;JSON to YML&lt;/a&gt; converter. I don&#39;t want to send sensitive json configuration out to some random website, I don&#39;t want my data to be stored in a database, or logged, or even worse, sold to advertisers and scammers.&lt;/p&gt;
&lt;p&gt;Of course, the irony is that I am now hosting these apps here and asking you to trust me!&lt;/p&gt;
&lt;p&gt;But at least this website is &lt;a href=&quot;https://github.com/LBognanni/codemade-site&quot;&gt;completely open source&lt;/a&gt;, and you can verify that it doesn&#39;t in fact have any nasty surprises ☺️&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Email-webhook gets two security upgrades</title>
    <link href="https://codemade.net/blog/email-webhook-now-more-secure/" />
    <updated>2026-07-11T00:00:00Z</updated>
    <id>https://codemade.net/blog/email-webhook-now-more-secure/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://email-webhook.com&quot;&gt;Email-webhook&lt;/a&gt; lets you receive emails straight into
your own application via a webhook.&lt;/p&gt;
&lt;p&gt;I&#39;ve been chipping away at two security things lately, and they landed
around the same time, so here they both are: mail delivered to your address is
now &lt;strong&gt;encrypted in transit&lt;/strong&gt;, and you can now &lt;strong&gt;sign outgoing webhook
requests&lt;/strong&gt; so your endpoint can check they really came from email-webhook.&lt;/p&gt;
&lt;h2&gt;Encrypted inbound mail (STARTTLS)&lt;/h2&gt;
&lt;p&gt;Previously, mail from a sending server to email-webhook crossed the network in
plaintext, which, honestly, is how most email still travels today. I&#39;ve
turned on STARTTLS on the inbound SMTP server, and made it required rather
than optional: every message to your &lt;code&gt;@email-webhook.com&lt;/code&gt; address now has to
arrive over an encrypted connection.&lt;/p&gt;
&lt;p&gt;Basically every real mail provider already speaks TLS: Gmail, Outlook,
Yahoo, Apple Mail, SES, SendGrid, Mailgun, Postmark, all of it. So you
shouldn&#39;t notice a thing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&#39;re forwarding from some old self-hosted box that never got TLS set
up, that delivery will now fail.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Verify your webhooks with HMAC signatures&lt;/h2&gt;
&lt;p&gt;Until now, the only way to lock down your webhook endpoint was a custom
header: an API key or bearer token you&#39;d check on every request.&lt;/p&gt;
&lt;p&gt;That works, but it comes with limitations: if it ever leaks into a log or a proxy,
anyone who has it can replay it forever, and it says nothing about whether the
body of the request is actually genuine.&lt;/p&gt;
&lt;p&gt;So I added a stronger option: &lt;strong&gt;HMAC-SHA256 request signing&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;How it works&lt;/h3&gt;
&lt;p&gt;Open any webhook&#39;s settings and you&#39;ll find a new &lt;strong&gt;Signing secret&lt;/strong&gt; field.
New webhooks get one generated automatically; clear it any time to turn
signing off.&lt;/p&gt;
&lt;p&gt;When a secret is set, every non-&lt;code&gt;GET&lt;/code&gt; delivery includes two extra headers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-Timestamp&lt;/code&gt;: Unix timestamp (seconds) when the request was sent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Signature-256&lt;/code&gt;: hex-encoded HMAC-SHA256 signature&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The signature is computed over &lt;code&gt;{timestamp}.{body}&lt;/code&gt; using your secret as the
HMAC key. The secret itself never travels over the wire, only a signature
derived from it does, so a leaked signature is useless for any other
request. There&#39;s nothing for an attacker to steal by inspecting a single
delivery.&lt;/p&gt;
&lt;h3&gt;Why the timestamp matters&lt;/h3&gt;
&lt;p&gt;Folding the timestamp into the signed data buys you replay protection for
free. If your server rejects any request whose &lt;code&gt;X-Timestamp&lt;/code&gt; is older than a
few minutes, then even if someone captures a genuine, correctly-signed
request, replaying it later will fail the freshness check.&lt;/p&gt;
&lt;p&gt;This is much simpler than tracking the &lt;code&gt;x-email-webhook-id&lt;/code&gt; of every request to avoid replay attacks and storing them in a database, and it works even if your endpoint is stateless.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://email-webhook.com/docs/authentication&quot;&gt;Read the full guide, including Node and Python examples of checking the HMAC signature, in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Rolling out safely&lt;/h3&gt;
&lt;p&gt;Signing is entirely &lt;strong&gt;opt-in&lt;/strong&gt;: existing webhooks are untouched until you add
a secret, so nothing changes for current integrations unless you choose it.
&lt;code&gt;GET&lt;/code&gt; webhooks aren&#39;t signed, since they carry their payload in the query
string and there&#39;s no body to sign.&lt;/p&gt;
&lt;p&gt;Encrypted inbound mail, by contrast, is on for everyone.&lt;/p&gt;
&lt;h2&gt;Overall&lt;/h2&gt;
&lt;p&gt;There&#39;s still more to do, of course. But these two changes are a big step forward for email-webhook.&lt;/p&gt;
&lt;p&gt;Email is a very private medium, and my goal is for email-webhook to be a safe and secure way to receive it in your own applications. These two changes make it safer than ever before.&lt;/p&gt;
&lt;p&gt;Encrypted in transit, end to end, from your mailbox to your endpoint.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>New in email-webhook: new design, logs and a status page!</title>
    <link href="https://codemade.net/blog/email-webhook-may-2026-updates/" />
    <updated>2026-05-11T00:00:00Z</updated>
    <id>https://codemade.net/blog/email-webhook-may-2026-updates/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://email-webhook.com&quot;&gt;Email-webhook&lt;/a&gt; the service I built to let you receive emails in your own applications, has been running smoothly for a while now.&lt;/p&gt;
&lt;p&gt;But, as with any service, there are always things that can be improved.&lt;/p&gt;
&lt;p&gt;The first thing you&#39;ll notice if you load the website is that it has a new design! I wanted to make it look more modern and clean, and I think I&#39;ve achieved that:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/email-webhook-2026.png&quot; alt=&quot;The new email-webhook homepage, with a clean and modern design&quot;&gt;&lt;/p&gt;
&lt;p&gt;But a fresh coat of paint is only the beginning!&lt;/p&gt;
&lt;h2&gt;Introducing opt-in logs&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/email-webhook-logs.png&quot; alt=&quot;The new logs page, showing a list of received emails and their status&quot;&gt;&lt;/p&gt;
&lt;p&gt;Imagine this: you&#39;re using email-webhook to receive emails in your application, and something goes wrong. Maybe the emails aren&#39;t being delivered, or maybe they&#39;re being delivered but your application isn&#39;t processing them correctly.&lt;/p&gt;
&lt;p&gt;In the past, you would have no way of knowing what was going on. You would have to guess, and maybe even contact support to get some answers.&lt;/p&gt;
&lt;p&gt;Now, with the new opt-in logs feature, you can see exactly what&#39;s going on with your email-webhook account. You can see when emails are being received, when they&#39;re being processed, and if there are any errors along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: logs are opt-in, so you have to enable them in your webhook settings to start seeing them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is because logs can contain sensitive information, such as the sender address, and the subject of the email (&lt;em&gt;we never log the body of the email or store attachments&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;By making them opt-in, you have full control over your data and your privacy.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://email-webhook.com/docs/message-logs&quot;&gt;You can read more about the logs feature in the documentation page&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;A status page to keep you informed&lt;/h2&gt;
&lt;p&gt;It&#39;s now easier than ever to keep track of the health of the email-webhook service. The new status page shows you the current status of the service, as well as any incidents or maintenance that might be happening.&lt;/p&gt;
&lt;p&gt;It&#39;s located at &lt;a href=&quot;https://status.email-webhook.com&quot;&gt;status.email-webhook.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;More to come!&lt;/h2&gt;
&lt;p&gt;This is just the beginning of a new era for email-webhook. I have many more features and improvements planned for the future, so stay tuned!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>It&#39;s getting hard to justify app stores</title>
    <link href="https://codemade.net/blog/appstores-are-in-trouble/" />
    <updated>2026-04-02T00:00:00Z</updated>
    <id>https://codemade.net/blog/appstores-are-in-trouble/</id>
    <content type="html">&lt;p&gt;Google&#39;s announcement of the &lt;a href=&quot;https://android-developers.googleblog.com/2026/03/android-developer-verification-rolling-out-to-all-developers.html&quot;&gt;new developer verification process for android developers&lt;/a&gt; and especially the incredibly cumbersome &amp;quot;&lt;a href=&quot;https://android-developers.googleblog.com/2026/03/android-developer-verification.html&quot;&gt;advanced flow&lt;/a&gt;&amp;quot; required to sideload unverified apps left a bad taste in my mouth.&lt;/p&gt;
&lt;p&gt;Here&#39;s why.&lt;/p&gt;
&lt;h3&gt;App stores are yucky&lt;/h3&gt;
&lt;p&gt;When was the last time you thought you&#39;d like to open the app store and see what&#39;s new? Me neither! The only reason to do so is because you already know what you&#39;re looking for, and even then you&#39;ll spend some quality time trying to find it among the sea of adware and sketchy knockoffs: because of the economics of developing apps,  most apps are filled with ads, trackers, and upsells.&lt;/p&gt;
&lt;p&gt;It&#39;s a race to the bottom, and the only way to win is to enshittify your app as much as possible.&lt;/p&gt;
&lt;h2&gt;And then AI coding came along&lt;/h2&gt;
&lt;p&gt;I built two small apps during the past weekend that made me reconsider the whole idea of needing app stores.&lt;/p&gt;
&lt;h3&gt;Hacker news reader&lt;/h3&gt;
&lt;p&gt;My favourite hacker news reader app, &lt;a href=&quot;https://github.com/hidroh/materialistic&quot;&gt;Materialistic&lt;/a&gt; hasn&#39;t been updated in ~ 3 years. It&#39;s so old that &lt;a href=&quot;https://github.com/hidroh/materialistic/issues/1464&quot;&gt;the Play Store doesn&#39;t even have it anymore&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I read HN in a specific way: I want to see the top posts of the day, sorted by number of comments, and I want to read the comments. Sometimes I&#39;ll navigate to the actual article. That&#39;s it.&lt;/p&gt;
&lt;p&gt;I asked Claude to build me a very simple HN reader that does just that, and in a few minutes it materialized on my screen.&lt;/p&gt;
&lt;p&gt;It&#39;s a PWA so it doesn&#39;t even need to be sideloaded, I just have to open the URL and add it to my home screen. It has no ads, no trackers, and it does exactly what I want it to do. It caches requests so it works offline, and it&#39;s blazing fast on cellular networks.&lt;/p&gt;
&lt;p&gt;It&#39;s hosted at &lt;a href=&quot;https://hnws.app&quot;&gt;https://hnws.app&lt;/a&gt; if you want to check it out.&lt;/p&gt;
&lt;p&gt;(I won&#39;t know, because I didn&#39;t add any analytics to it!)&lt;/p&gt;
&lt;h3&gt;Background switcher&lt;/h3&gt;
&lt;p&gt;There&#39;s another app that I&#39;ve been using for years, &lt;a href=&quot;https://github.com/muzei/muzei/&quot;&gt;Muzei&lt;/a&gt;. It&#39;s by far the best wallpaper switcher app.
The killer feature is that it will blur and dim the wallpaper so you can see the icons clearly, and you can reveal the unblurred version with a double tap.&lt;/p&gt;
&lt;p&gt;The only problem I have with it is that most of my wallpapers are landscape, and so they are always centered and cropped, which means that the most interesting part of the image is sometimes cut off.&lt;/p&gt;
&lt;p&gt;Again, my use for it is very specific: I only use local images, and I want the app to automatically switch to the next image in the folder every day. That&#39;s it.&lt;/p&gt;
&lt;p&gt;Claude Code built me &lt;a href=&quot;https://github.com/LBognanni/Localzei/&quot;&gt;LocalZei&lt;/a&gt;, a full fat Android app that does just that, and again, it has no ads, no trackers, and it does exactly what I want it to do.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It took an evening.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;An evening!! While I was watching TV and eating dinner!! Yes it only does a subset of what Muzei does, but it&#39;s &lt;em&gt;my&lt;/em&gt; subset. And it can crop to the most interesting part of the image 😄&lt;/p&gt;
&lt;h2&gt;The age of personal apps is here&lt;/h2&gt;
&lt;p&gt;The friction of building personal apps has always been so high that as users we accepted the ads, the trackers, the dark patterns, and the constant upsells.&lt;/p&gt;
&lt;p&gt;The friction is just not there anymore. You can build a personal app that has the exact subset of features you want, that is clean, fast, and respects your privacy, in a matter of hours.&lt;/p&gt;
&lt;h2&gt;But the enshittifiers are fighting back&lt;/h2&gt;
&lt;p&gt;And this is exactly what Google&#39;s new verification process is designed to prevent: they are desperately trying to protect their cash cow, in the name of &amp;quot;security&amp;quot; and &amp;quot;user experience&amp;quot;.&lt;/p&gt;
&lt;p&gt;Just as we&#39;re so close to our phones being the personal computing devices we always wanted.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Rumors of the death of SaaS have been greatly exaggerated</title>
    <link href="https://codemade.net/blog/death-of-saas/" />
    <updated>2026-02-18T00:00:00Z</updated>
    <id>https://codemade.net/blog/death-of-saas/</id>
    <content type="html">&lt;p&gt;There&#39;s a meme going around in tech and finance circles that goes something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Woah I just got Claude Code to build me a custom (insert niche use case here) that works exactly the way I want it! I won&#39;t need to pay for (insert SaaS product here)&#39;s monthly fees ever again! The end of SaaS is near!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&#39;s why I think this is an extrapolation that makes no sense, especially when thinking of B2B SaaS.&lt;/p&gt;
&lt;p&gt;I&#39;m going to take the weakest position possible here: &lt;strong&gt;vibe coding is not going to replace tasking and planning software like Jira, Asana, Linear etc&lt;/strong&gt;. I&#39;m choosing this specific niche because on paper it&#39;s the most AI-disruptable: it&#39;s software that&#39;s conceptually simple, there are a million TODO list implementations freely sitting on github that have been used as training, and they often need to be customized to a crazy degree to fit the needs of different teams and projects.&lt;/p&gt;
&lt;p&gt;I am confident the points below can apply even more strongly to more complex and specific SaaS products (especially in highly regulated industries like finance, healthcare, legal etc).&lt;/p&gt;
&lt;h2&gt;Nobody wants to maintain your crappy version of Trello&lt;/h2&gt;
&lt;p&gt;You vibe coded a replacement for Trello so that your company can drop the $10/user/month subscription. Congrats! Let&#39;s say you have 100 users (I like round numbers). That&#39;s a saving of $1000/month, or $12,000/year. Seems great, right?&lt;/p&gt;
&lt;p&gt;Except that... how much are &lt;em&gt;you&lt;/em&gt; paid? Let&#39;s say that you&#39;re a software engineer making between $60k and $120k a year. Even if you only spend 10% of your time maintaining this thing, that&#39;s between $6,000 and $12,000 a year of your time spent on it. And remember, that&#39;s time that you&#39;re not spending making money for the company. I won&#39;t even go into the token costs of running an AI agent, as they can range from insignificant to very expensive depending on the complexity of the software you&#39;re building.&lt;/p&gt;
&lt;p&gt;There&#39;s also the &amp;quot;lottery&amp;quot; factor: let&#39;s say you&#39;re offered an exciting position at a new company. Who&#39;s going to maintain the Trello replacement? Your company now has to find someone else to do it, or contract an external agency, or... switch back to Trello and pay the subscription fees again.&lt;/p&gt;
&lt;h2&gt;Free alternatives already exist&lt;/h2&gt;
&lt;p&gt;If your company wanted to replace Trello with a self-hosted alternative, there are already &lt;a href=&quot;https://opensource.com/alternatives/trello&quot;&gt;plenty of free and open source options available&lt;/a&gt;. These products are already maintained by a community of volunteers, and you can even contribute to them if you want to add features or fix bugs.&lt;/p&gt;
&lt;p&gt;So then why don&#39;t all companies just switch to these free alternatives? Because they don&#39;t want to. They don&#39;t want to spend time and resources on maintaining software that is not core to their business. Any time you spend configuring the EC2 instance that runs your self-hosted Trello is time that you&#39;re not spending on the actual product you&#39;re selling to customers. And of course, if something breaks, you&#39;re on the hook for fixing it.&lt;/p&gt;
&lt;h2&gt;Software is a liability&lt;/h2&gt;
&lt;p&gt;We&#39;re assuming that your version of Trello is actually working and doesn&#39;t have any security issues. The worst case scenario would be that &lt;a href=&quot;https://thecyberexpress.com/moltbook-platform-exposes-1-5-mn-api-keys/&quot;&gt;your Firebase database is actually freely available to anyone on the Internet, and all your company&#39;s data is leaked&lt;/a&gt;. And now you&#39;re on the hook for that too. Hope you like HR meetings!&lt;/p&gt;
&lt;p&gt;Had this been a SaaS product, your company could have done one or all of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sued the vendor for negligence&lt;/li&gt;
&lt;li&gt;Asked for compensation&lt;/li&gt;
&lt;li&gt;Switched to a different vendor&lt;/li&gt;
&lt;li&gt;Ask for a detailed post-mortem and action plan to prevent this from happening again&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Having a &amp;quot;throat to choke&amp;quot; is something that companies &lt;em&gt;really&lt;/em&gt; like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;SaaS developers can use AI too&lt;/h2&gt;
&lt;p&gt;This should be obvious, but there&#39;s nothing stopping developers at SaaS companies from using AI tools to improve their services. In fact, they&#39;re likely encouraged to do so!&lt;/p&gt;
&lt;p&gt;Whether AI is a net positive or a net negative for working within large, established codebases is still up for debate, but all successful software becomes &amp;quot;legacy&amp;quot; given enough time - even the shiny new vibecoded competitor.&lt;/p&gt;
&lt;p&gt;Some think that the SaaS space will become incredibly crowded with options now that building software is easier than ever. But I expect this will translate into higher expectations for what software should be able to do. In essence, if creating a TODO list app is a one-shot with an LLM, then the expectation for what a task management software should be able to do will be much higher than it is now, and the bar for entry will be raised.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI coding raises &lt;em&gt;the floor&lt;/em&gt; for what is considered a &amp;quot;viable&amp;quot; software as a service&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also expect that some newer, nimbler companies will eventually compete with the established players, but that&#39;s long been the story of software: a great example is Linear coming in and disrupting the Jira monolith by focussing on a better user experience, with what I assume is a tiny fraction of the team at Atlassian.&lt;/p&gt;
&lt;h2&gt;Counterpoint: SaaS companies are overvalued on the stock market&lt;/h2&gt;
&lt;p&gt;This is where the real crux of the issue lies: the stock market has been overvaluing software companies for years now. Given a rational market, the price of a stock should reflect the expected future cash flows of the company. But with software companies, the bet is often that they&#39;ll become unicorns, take over a whole market category, put the competition out of business, and raise prices (AKA the &lt;a href=&quot;https://en.wikipedia.org/wiki/Enshittification&quot;&gt;enshittification&lt;/a&gt; playbook).&lt;/p&gt;
&lt;p&gt;The recent hype about AI being the &amp;quot;next big thing&amp;quot; means that funds are being reallocated away from regular SaaS and into AI companies. This also means that investors &lt;em&gt;need&lt;/em&gt; to push the message that &amp;quot;AI is going to kill SaaS&amp;quot; in order to justify their increasingly outsized bets.&lt;/p&gt;
&lt;p&gt;In that sense yes, AI &lt;em&gt;is&lt;/em&gt; going to kill SaaS. Unprofitable companies that rely on a constant stream of funding are going to have a bad time, and some of them will go out of business. But for companies that are providing actual value to their customers, while staying profitable, I don&#39;t see any reason why they wouldn&#39;t be able to continue doing so, and even thrive in the new AI-powered world.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;AI isn&#39;t going to kill SaaS just yet. But it &lt;em&gt;is&lt;/em&gt; going to make bad SaaS harder to justify.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Building for an audience of one: starting and finishing side projects with AI</title>
    <link href="https://codemade.net/blog/building-for-one/" />
    <updated>2026-02-16T00:00:00Z</updated>
    <id>https://codemade.net/blog/building-for-one/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://github.com/LBognanni/fasttab&quot;&gt;⚡️ FastTab&lt;/a&gt; solves a very specific problem: the &amp;quot;Gallery&amp;quot; view of the built-in task switcher in the Plasma desktop environment is slightly too slow for my liking on X11. Sometimes it will take up to a second to open, which is way too long for a feature that I use constantly.&lt;/p&gt;
&lt;p&gt;FastTab is a custom task switcher that is built in Zig, uses OpenGL for rendering, and is designed to run as a daemon so that it can respond to keyboard shortcuts instantly.&lt;/p&gt;
&lt;h2&gt;I would have never built this without AI&lt;/h2&gt;
&lt;p&gt;It&#39;s very likely that this is a problem with an audience of one: users who are still on X11, like the gallery switcher, have many windows open, and care about performance.&lt;/p&gt;
&lt;p&gt;In the past I would have probably just accepted the performance tradeoff and moved on, but we&#39;re living in &lt;em&gt;the future&lt;/em&gt; now! Why not just ask Claude to build it for me? As long as the vision is good, and the code doesn&#39;t suck, it should be good enough, right?&lt;/p&gt;
&lt;p&gt;And so I did: with zero experience with Zig or X11 internals, I got a working prototype in a few days, and then iterated on it until I was happy with the results.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI tools enable you to build things that you would have never built otherwise. When you just want &amp;quot;the thing&amp;quot; rather than the process of building the thing, coding agents can be a game changer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;It all starts with a conversation&lt;/h2&gt;
&lt;p&gt;I was just irritated enough by the slight delay of the task switcher that I asked Claude for tips on how to make it faster. Sadly, there was no quick fix, I had already zeroed out all the animations and delays in the configuration, and the only alternative would have been to switch to the &amp;quot;list&amp;quot; or &amp;quot;icons-only&amp;quot; view, which I just don&#39;t like (looking at a window preview is much more efficient than reading titles for my brain).&lt;/p&gt;
&lt;p&gt;Then I just asked: &amp;quot;How difficult would it be to build a task switcher that is specifically for X11 and KDE?&amp;quot; and the conversation pivoted into planning. We went through a few iterations where I steered Claude towards a design that seemed promising.&lt;/p&gt;
&lt;p&gt;The result was a very &lt;strong&gt;detailed specification&lt;/strong&gt; of the application, with a clear vision of how it should work and look like, and a plan for how to implement it, split in &lt;strong&gt;several milestones&lt;/strong&gt; so that I could start with a simple prototype and then evolve it into a full-fledged application.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Start with a conversation, and explore the problem space with the LLM. The idea here is to gather options and ideas. Once you have a clear vision of what you want to build, ask for a detailed specification. Iterate on the spec until you &lt;em&gt;understand it fully&lt;/em&gt; and are happy with it.&lt;/p&gt;
&lt;p&gt;Then break down the spec in a set of milestones.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on writing good specs:&lt;/strong&gt; Claude especially will try to shove as many code snippets as possible into the spec. This is not only a waste of tokens, but it will also make it harder for you (&lt;em&gt;and the coding agent!&lt;/em&gt;) to implement the spec. Pseudocode is fine, and at this stage is actually preferable, since it allows you to focus on the overall architecture and design of the application, without getting bogged down in the details of the implementation.&lt;/p&gt;
&lt;p&gt;Mermaid diagrams are also your friend here, they not only look nicer, but fit in fewer tokens than ASCII diagrams.&lt;/p&gt;
&lt;p&gt;With a solid spec in hand, I was ready to start building. The next question was how to let an AI agent loose on my filesystem without risking my machine.&lt;/p&gt;
&lt;h2&gt;The perfect balance between safe and YOLO&lt;/h2&gt;
&lt;p&gt;Every developer should know that &lt;code&gt;git&lt;/code&gt; is your friend. You can build incrementally, commit when you have something working, and if the LLM messes up, you can always revert to the last working state.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I use the staging area a lot when I have something that is kinda working but not quite there yet, so I can decide whether the next iteration is good enough to make it into a commit, it&#39;s an improvement that gets staged, or it&#39;s garbage to be reverted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Checking the git diff before committing is also a great way to understand what the LLM is doing, and to catch any potential mistakes before they become a problem.&lt;/p&gt;
&lt;p&gt;Running Claude Code and approving... each... individual... command... is... exhausting. But at the same time, running &lt;code&gt;claude --dangerously-skip-permissions&lt;/code&gt; is gambling with your filesystem.&lt;/p&gt;
&lt;p&gt;The solution is 🐋 containers! I used a heavily customized version of &lt;a href=&quot;https://github.com/frequenz-floss/contai/&quot;&gt;contai&lt;/a&gt;, which is a handy wrapper around Docker that allows you to easily spin up a locked down container that has access to your code folder only. This way if the LLM accidentally runs &lt;code&gt;rm -rf /&lt;/code&gt; or something like that, it will only delete the container&#39;s filesystem, which is ephemeral, and not your actual files. (should it delete the code, you are using git, remember?)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I had to tell the LLM that it&#39;s running inside a container, and it doesn&#39;t have an X11 display or the ability to install system packages, otherwise it would spend a lot of tokens trying to do the impossible&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It worked so well that all my new projects are now developed in a &lt;a href=&quot;https://codemade.net/blog/devcontainers&quot;&gt;dev container&lt;/a&gt; that borrows heavily from the &lt;code&gt;contai&lt;/code&gt; configuration.&lt;/p&gt;
&lt;h2&gt;Trying different tools, and running out of tokens&lt;/h2&gt;
&lt;p&gt;I initially started working on FastTab with &lt;a href=&quot;https://opencode.ai/&quot;&gt;OpenCode&lt;/a&gt; and &lt;a href=&quot;https://github.com/code-yeongyu/oh-my-opencode&quot;&gt;oh-my-opencode&lt;/a&gt;. I really enjoyed OpenCode in other projects and, while skeptical, I hoped that oh-my-opencode would be the powerup that would make the whole experience 100 times better.&lt;/p&gt;
&lt;p&gt;In practice, I found that the multi-agent system was exceptional at consuming tokens, while producing the same results that I could have achieved with a single agent. At around the same time, Anthropic decided that using the Pro plan on OpenCode was a breach of their TOS, so I had to switch back to Claude Code.&lt;/p&gt;
&lt;p&gt;The token limit however remained a real problem: I assume that writing Zig is more taxing to LLMs than writing Python or JavaScript, probably because of the lower level nature of the language and the fact that less training data is available for it. I routinely ran out of juice, and had to either wait for the reset, or switch to the Gemini CLI. Alternating between the two was a bit of a hassle, but overall both Opus 4.5 and Gemini 3 were able to get the job done.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sidenote: I wonder what&#39;s going to happen when the crazy money runs out and Anthropic, OpenAI &amp;amp; co have to start charging for more than it costs them to run the models. Hopefully by then the open source models will have caught up?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But regardless of which model or tool I was using, the pattern was always the same: the LLM could get me 80% of the way there, and the last 20% was on me.&lt;/p&gt;
&lt;h2&gt;You still need to know how to code&lt;/h2&gt;
&lt;p&gt;In the case of FastTab, the first version that I got from the multi-agent system was actually working, which was way better than what I could have built in a couple of hours in a language I didn&#39;t know, using libraries I wasn&#39;t familiar with.&lt;/p&gt;
&lt;p&gt;It was also a single 1700 lines long file with most of the code in the &lt;code&gt;main&lt;/code&gt; function, no tests, lots of code duplication, and a lot of extra comments and logging. Refactoring it into a more modular and maintainable state was necessary not only so I could understand it, but so that the LLM could iterate on it without introducing new bugs or breaking existing functionality.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There&#39;s a reason why many developers call AI assisted coding &amp;quot;a slot machine&amp;quot;. You write your prompt, pull the lever, and get a surprise. Sometimes it&#39;s a fully working thing, sometimes it&#39;s 50% there, and sometimes it&#39;s completely off the mark. Better prompts will increase the chances of getting a good result, but there&#39;s always an element of randomness that you have to be comfortable with.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Asking the right questions also requires coding knowledge. For example, in an early iteration we were shuffling image bytes from BGRA to RGBA. The code worked, but the performance was not great. I asked Claude to use SIMD instructions and the speedup was significant, enough so that we could capture the content of a youtube window playing a video at ~10fps without incinerating my CPU. But &lt;strong&gt;I had to know that SIMD was a thing&lt;/strong&gt;, and that it could be used in this context.&lt;/p&gt;
&lt;p&gt;In the end we ended up scrapping much of that code in favour of &amp;quot;borrowing&amp;quot; the actual texture data from X11, which is much faster, but the point stands: you need to know what questions to ask in order to get the best results.&lt;/p&gt;
&lt;h2&gt;The age of finishing side projects is here?&lt;/h2&gt;
&lt;p&gt;I think I&#39;m finally turning the corner from the doom and gloom of &amp;quot;AI will take our jobs.&amp;quot; AI-assisted coding is still coding: without the taste and knowledge that come with experience, you&#39;ll invariably end up with a half working prototype that breaks in new and exciting ways every time you ask to &amp;quot;pls fix&amp;quot;.&lt;/p&gt;
&lt;p&gt;For my day job, where the codebase dwarfs any LLM&#39;s context window and the stakes are high, getting a coding agent to generate all or even most of the code for a feature is still not something I would subject my users to.&lt;/p&gt;
&lt;p&gt;But side projects have different goals: scratch an itch, maybe learn a thing or two, and get something working before I get bored or realize it&#39;s too much effort.&lt;/p&gt;
&lt;p&gt;I can spitball with Claude about my problem, get a spec, and have a working prototype in a few hours. If I like it enough, I can spend some time polishing it until it&#39;s something I&#39;m happy to share.&lt;/p&gt;
&lt;p&gt;The whole process is short enough that I might actually finish the thing, for a change!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to use Deno&#39;s own SQLite module on Alpine Linux</title>
    <link href="https://codemade.net/blog/deno-sqlite-on-alpine/" />
    <updated>2026-01-18T00:00:00Z</updated>
    <id>https://codemade.net/blog/deno-sqlite-on-alpine/</id>
    <content type="html">&lt;p&gt;Deno&#39;s built-in SQLite module is a fantastic way to integrate a lightweight database into your Deno applications.
However, to work its magic, it relies on a native SQLite library that needs to be present in your environment.&lt;/p&gt;
&lt;p&gt;If you&#39;re running one of Ubuntu, MacOS, or Windows, you&#39;re in luck: Deno has you covered with &lt;a href=&quot;https://github.com/denodrivers/sqlite3/releases/&quot;&gt;precompiled binaries for these platforms&lt;/a&gt;. But if you&#39;re using Alpine Linux, things get a bit trickier since Deno doesn&#39;t provide a precompiled SQLite library for this distribution.&lt;/p&gt;
&lt;p&gt;Try to run a Deno application that uses SQLite on Alpine Linux without any additional setup, and you&#39;ll get an error like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3.013 Downloading https://github.com/denodrivers/sqlite3/releases/download/0.13.0/libsqlite3.so
4.094 error: Uncaught (in promise) Error: Failed to load SQLite3 Dynamic Library
4.094   throw new Error(&amp;quot;Failed to load SQLite3 Dynamic Library&amp;quot;, { cause: e });
4.094         ^
4.094     at https://jsr.io/@db/sqlite/0.13.0/src/ffi.ts:642:9
4.094 Caused by: Error: Could not open library: Could not open library: /usr/local/lib/libm.so.6: version `GLIBC_2.38&#39; not found (required by /deno-dir/plug/https/github.com/2d624d1cb94dc52a04dcb942d98592ad72012273e34867346f5dde5d01035183.so)
4.094     at new DynamicLibrary (ext:deno_ffi/00_ffi.js:457:42)
4.094     at Object.dlopen (ext:deno_ffi/00_ffi.js:563:10)
4.094     at dlopen (https://jsr.io/@denosaurs/plug/1.1.0/mod.ts:158:15)
4.094     at eventLoopTick (ext:core/01_core.js:187:7)
4.094     at async https://jsr.io/@db/sqlite/0.13.0/src/ffi.ts:625:7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this tells us is that Deno attempted to download a precompiled SQLite library that depends on &lt;code&gt;glibc&lt;/code&gt;, the GNU C Library. However, Alpine Linux uses &lt;code&gt;musl&lt;/code&gt; as its standard C library, which is not compatible with the prebuilt sqlite binary.&lt;/p&gt;
&lt;p&gt;Luckily, Deno can use any precompiled SQLite library that matches its expected version.
For Alpine Linux, you can leverage the &lt;code&gt;libsqlite3&lt;/code&gt; package available in the Alpine repositories.&lt;/p&gt;
&lt;p&gt;You can install it using the &lt;code&gt;apk&lt;/code&gt; command:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;apk &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; libsqlite3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command installs the necessary SQLite library on your Alpine system, however in order for Deno to find and use it, you will need to set the &lt;code&gt;DENO_SQLITE_PATH&lt;/code&gt; environment variable to point to the location of the installed library.&lt;/p&gt;
&lt;p&gt;You can find the library at &lt;code&gt;/usr/lib/libsqlite3.so.0&lt;/code&gt; after installation.
Set the environment variable like this:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;DENO_SQLITE_PATH&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/lib/libsqlite3.so.0&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;That&#39;s nice and all but I use Docker?&lt;/h2&gt;
&lt;p&gt;Easy peasy! Here&#39;s a simple Dockerfile that sets up a Deno environment on Alpine Linux with SQLite support:&lt;/p&gt;
&lt;pre class=&quot;language-Dockerfile&quot;&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;# Use the official Deno Alpine image as the base
FROM denoland/deno:alpine-2.6.5

# Expose port 8000 for the application
EXPOSE 8000

# Install system SQLite library
RUN apk add --no-cache sqlite-libs

# Set working directory and permissions
WORKDIR /app
RUN mkdir /app/data &amp;&amp; chown -R deno:deno /app
USER deno

# Copy deno.json and deno.lock files
COPY deno.* .

# Set the DENO_SQLITE_PATH environment variable to point to the installed SQLite library
ENV DENO_SQLITE_PATH=/usr/lib/libsqlite3.so.0

# Pre-install app dependencies
RUN deno install --frozen=true
# Force Deno to load the SQLite module and download any necessary files, ensuring everything is set up correctly
RUN deno eval &quot;import &#39;jsr:@db/sqlite&#39;;&quot;

# Copy application source code
COPY src/ ./src/
# Install any other dependencies
RUN deno install --entrypoint src/main.ts

# Command to run the application (assumes you have a `prod` task in your deno.json)
CMD [&quot;deno&quot;, &quot;run&quot;, &quot;prod&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Hope this was helpful to you, random internet stranger! It certainly took me a bit of digging to figure this out, so hopefully this saves you some time! 🙃&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Announcing email-webhook</title>
    <link href="https://codemade.net/blog/announcing-email-webhook/" />
    <updated>2025-03-22T00:00:00Z</updated>
    <id>https://codemade.net/blog/announcing-email-webhook/</id>
    <content type="html">&lt;div class=&quot;callout&quot;&gt;
  &lt;p&gt;📨&lt;/p&gt;
  &lt;p&gt;
    &lt;b&gt;TLDR:&lt;/b&gt; 
    I&#39;m excited to announce the launch of &lt;a href=&quot;https://email-webhook.com&quot;&gt;email-webhook&lt;/a&gt;, a service that transforms emails into API calls. Use it to write email automations, process incoming emails, and build email-based workflows.
  &lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;What is email-webhook?&lt;/h2&gt;
&lt;p&gt;As the name suggests, email-webhook is a service for developers that transforms emails into REST requests to your API. It makes integrating an email based workflow into your applications and services a breeze.&lt;/p&gt;
&lt;p&gt;As opposed to traditional email polling techniques, email-webhook sends a request to your endpoint as soon as an email is received. This way, you can process emails in real-time, without the need for complex polling logic.&lt;/p&gt;
&lt;h3&gt;Here&#39;s how it works:&lt;/h3&gt;
&lt;p&gt;Once you&#39;re signed up, you&#39;ll receive a special web address like &lt;code&gt;you@email-webhook.com&lt;/code&gt;. Any time an email is sent there, email-webhook will send a request to your endpoint.&lt;/p&gt;
&lt;p&gt;You can specify the request method, custom headers, and even sender-based rules. This way, you can easily integrate email processing into your existing systems.&lt;/p&gt;
&lt;h2&gt;Why email-webhook?&lt;/h2&gt;
&lt;p&gt;This started a while ago as I was looking for a way to improve the efficiency of my lovely partner&#39;s business, using AI to automate her most time consuming tasks.&lt;/p&gt;
&lt;p&gt;One such task was improving her response time to various requests coming from her clients. She was receiving a lot of emails and she was spending &lt;strong&gt;a lot&lt;/strong&gt; of time reading and responding to them. Her biggest issue was the &amp;quot;blank screen&amp;quot; problem: she would open an email, read it, and then spend a lot of time thinking about what to do with it.&lt;/p&gt;
&lt;p&gt;I thought that I could help her by building a system that would intelligently propose a draft response based on the content of the email plus some external data sources such as her calendar and her CRM.&lt;/p&gt;
&lt;p&gt;One of the first steps was to find a way to process emails as they arrived. As a web developer, I&#39;m used to working with APIs, so I thought: &amp;quot;Why not build an API that receives emails as input?&amp;quot;&lt;/p&gt;
&lt;p&gt;Of course, the next step was to find a way to get emails into the API! I wanted to avoid polling, so I thought about using webhooks. I started looking for a service that would allow me to receive emails as webhooks, but I couldn&#39;t find anything that suited my needs. Every other option was either too complex, too expensive, or too limited.&lt;/p&gt;
&lt;p&gt;So I decided to build my own service: &lt;a href=&quot;https://email-webhook.com&quot;&gt;email-webhook&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Looking for feedback&lt;/h2&gt;
&lt;p&gt;Email-webhook is still in its early stages: it&#39;s sleek and functional (IMO), but there&#39;s still room for improvement.&lt;/p&gt;
&lt;p&gt;This is why I&#39;m offering it for the low low price of &lt;em&gt;FREE&lt;/em&gt; while I gather feedback and improve the service.&lt;/p&gt;
&lt;p&gt;I don&#39;t ever plan for email-webhook to be an expensive service, but I would like to offer a paid tier in the future with some advanced features to help me cover the costs of running it.&lt;/p&gt;
&lt;h2&gt;On a personal note&lt;/h2&gt;
&lt;p&gt;I had a lot of fun building email-webhook. After so many years of having random ideas and never acting on them, it feels great to finally have something concrete to show for it.&lt;/p&gt;
&lt;p&gt;It was a great chance to learn more about the SMTP protocol, and if you&#39;ve been following my blog, you will probably guess that &lt;a href=&quot;https://codemade.net/blog/a-journey-through-deno-ssr.md&quot;&gt;I used Deno&lt;/a&gt; to build the service 😄.&lt;/p&gt;
&lt;p&gt;I&#39;m excited to see how people will use email-webhook. I&#39;ve already received some great feedback from early users, and I can&#39;t wait to see what they&#39;ll build with it.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How I deploy my web apps in 2025</title>
    <link href="https://codemade.net/blog/my-docker-setup-in-2025/" />
    <updated>2025-03-15T00:00:00Z</updated>
    <id>https://codemade.net/blog/my-docker-setup-in-2025/</id>
    <content type="html">&lt;p&gt;The beauty of simplicity has become my north star for hosting web applications. Despite working with complex cloud architectures professionally, I&#39;ve chosen a straightforward Docker stack for my personal projects. As a solo maintainer, I need something manageable without breaking the bank.&lt;/p&gt;
&lt;p&gt;Docker hits that sweet spot between complexity and simplicity, providing enough structure without the overhead of enterprise cloud solutions. My personal projects don&#39;t need Kubernetes clusters or managed databases - just reliable, affordable infrastructure.&lt;/p&gt;
&lt;h2&gt;The Old Ways: Docker Compose for the Basics&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
web(Web Request)
subgraph **VPS**
  subgraph docker_compose.yml
    rp[Reverse Proxy]
    codemade_app[codemade.net]
    service_x[Wordpress 1]
    service_y[Postgres]
    service_z[Email forwarder]
  end
end
web -- web --&gt; rp --&gt; codemade_app
rp --&gt;service_x --&gt; service_y 
web -- email --&gt; service_z
&lt;/pre&gt;
&lt;p&gt;Initially, my hosting needs were modest - a mail forwarder and a couple of WordPress sites. For this small collection, a simple docker compose setup was adequate. I&#39;d SSH into my server, run commands, and everything would be up and running with minimal fuss.&lt;/p&gt;
&lt;p&gt;This hands-on approach worked perfectly - make a change, connect to the server, pull the latest version, and restart containers. Since updates were infrequent and downtime wasn&#39;t critical, this manual method suited my needs. My docker compose file was straightforward, defining just the necessary services with basic volume mounts and simple network configurations.&lt;/p&gt;
&lt;p&gt;The only &amp;quot;fancy&amp;quot; element was this website, which I&#39;ve since &lt;a href=&quot;https://github.com/LBognanni/codemade-site/blob/master/.github/workflows/deploy.yml&quot;&gt;moved to 11ty and GitHub Pages&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;And then I built a web app&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
web(Web Request)
web --&gt; caddy[/Reverse Proxy/]
app
redis
subgraph **VPS**
  caddy
  subgraph Stats Stack
    app[Stats App]
    redis[(Stats DB)]
  end
end
caddy -.-&gt; app
app &lt;-.-&gt; redis
&lt;/pre&gt;
&lt;p&gt;Recently, I built a web application that required a more robust solution. It was time to graduate from docker compose to something more sophisticated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Swarm&lt;/strong&gt; was the perfect choice as a step up from docker compose without going full Kubernetes-level complexity.&lt;/p&gt;
&lt;p&gt;I first heard about this approach from the &lt;a href=&quot;https://www.youtube.com/watch?v=fuZoxuBiL9o&quot;&gt;DreamsOfCode YouTube channel&lt;/a&gt;, where they called it &lt;code&gt;docker stack&lt;/code&gt;. It&#39;s technically Docker Swarm with one node, offering a pragmatic middle ground between my simple setup and complex orchestration tools.&lt;/p&gt;
&lt;p&gt;The stack deployment files look almost identical to docker compose files, with just a few additional orchestration-specific options. I took my existing compose file, added deployment constraints and update policies, and it was ready - no steep learning curve required.&lt;/p&gt;
&lt;p&gt;I host it on a single but capable &lt;strong&gt;VPS&lt;/strong&gt; on &lt;strong&gt;DigitalOcean&lt;/strong&gt;, which is more than enough for my needs.&lt;/p&gt;
&lt;h3&gt;The Automated Pipeline&lt;/h3&gt;
&lt;p&gt;Here is my current workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Push code changes to GitHub&lt;/li&gt;
&lt;li&gt;GitHub Actions builds a container image and publishes it to GitHub Container Registry&lt;/li&gt;
&lt;li&gt;A deployment action (&lt;a href=&quot;https://github.com/marketplace/actions/docker-stack-swarm-deploy-action&quot;&gt;&lt;code&gt;shockhs/docker-stack-deploy@v1.2&lt;/code&gt;&lt;/a&gt;) automatically updates my Swarm stack&lt;/li&gt;
&lt;li&gt;The new version rolls out with zero downtime&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can take a look at a simplified version of my &lt;a href=&quot;https://gist.github.com/LBognanni/484f420eb7dbc5679f071329a952831b&quot;&gt;GitHub Actions workflow&lt;/a&gt; if you&#39;re curious.&lt;/p&gt;
&lt;h3&gt;Handling SSL with Caddy&lt;/h3&gt;
&lt;p&gt;For SSL and routing, I use the excellent &lt;a href=&quot;https://caddyserver.com&quot;&gt;Caddy&lt;/a&gt; as a reverse proxy. It automatically handles certificate generation and renewal from Let&#39;s Encrypt, which is a huge time-saver.&lt;/p&gt;
&lt;p&gt;Caddy runs as a separate standalone container outside the Swarm cluster.
This was a deliberate architectural decision - running the reverse proxy inside the Swarm would mean &lt;a href=&quot;https://github.com/moby/moby/issues/25526&quot;&gt;losing the originating IP addresses of incoming requests&lt;/a&gt; due to how networking works in Swarm mode.&lt;/p&gt;
&lt;p&gt;By keeping Caddy separate, I maintain visibility of client IPs for proper logging and security monitoring.
It also means my reverse proxy isn&#39;t tied to the lifecycle of my applications. It gets its own repo, and its own deployment pipeline.&lt;/p&gt;
&lt;h3&gt;The Results&lt;/h3&gt;
&lt;p&gt;This setup strikes the perfect balance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automated&lt;/strong&gt;: No manual SSH sessions for routine deployments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliable&lt;/strong&gt;: Rolling updates ensure zero-downtime deployments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintainable&lt;/strong&gt;: Everything is defined in code and version controlled&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Affordable&lt;/strong&gt;: More cost-effective than equivalent managed cloud services&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;And then there were more&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
web(Web Request)
web --&gt; caddy[/Reverse Proxy/]
app
redis
subgraph **VPS**
  caddy
  subgraph Stats Stack
    app[Stats App]
    redis[(Stats DB)]
  end
  subgraph Games Stack
    gapp[Games App]
    gdb[(Games DB)]
  end
end
caddy -.-&gt; app
app &lt;-.-&gt; redis
caddy -.-&gt; gapp
gapp &lt;-.-&gt; gdb
&lt;/pre&gt;
&lt;p&gt;Docker Swarm excels at scaling to multiple applications. Adding a new web app was straightforward - I created a new stack definition file and deployed it to the same swarm.&lt;/p&gt;
&lt;p&gt;This provides independent deployment lifecycles for each application. I can update, restart, or delete one app without affecting others. Each stack gets its own isolated network by default, though they can communicate with each other if needed through overlay networks.&lt;/p&gt;
&lt;h2&gt;Looking forward&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
web(Web Request)
web --&gt; caddy[/Reverse Proxy/]
subgraph VPS4
  subgraph Stats Node / 3
    app4[Stats App]
    redis4[(Stats DB)]
  end
end
subgraph VPS3
  subgraph Stats Node / 2
    app3[Stats App]
    redis3[(Stats DB)]
  end
end
subgraph VPS2
  subgraph Stats Node / 1
    app2[Stats App]
    app21[Stats App]
    app22[Stats App]
    redis2[(Stats DB)]
  end
end
subgraph **VPS 1**
  caddy --&gt; other[other serivces]
end
caddy -.-&gt; app2
app2 -.-&gt; redis2
app21 -.-&gt; redis2
app22 -.-&gt; redis2
caddy -.-&gt; app3
app3 -.-&gt; redis3
caddy -.-&gt; app4
app4 -.-&gt; redis4
style other stroke-dasharray: 5, 5;
style VPS2 fill:#fff4,stroke-dasharray: 5, 5;
style VPS3 fill:#fff4,stroke-dasharray: 5, 5;
style VPS4 fill:#fff4,stroke-dasharray: 5, 5;
&lt;/pre&gt;
&lt;p&gt;Should one of my services become a runaway success, I can move it to its own VPS and scale independently, either by increasing VPS size or adding replicas to the swarm stack. For further growth, adding more VPSs to the same swarm would be the next step.&lt;/p&gt;
&lt;p&gt;But that&#39;s a problem for another day! 😎&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Running parameterized tests in Deno</title>
    <link href="https://codemade.net/blog/parameterized-testing-with-deno/" />
    <updated>2025-03-09T00:00:00Z</updated>
    <id>https://codemade.net/blog/parameterized-testing-with-deno/</id>
    <content type="html">&lt;p&gt;When writing tests, it&#39;s common to want to run the same test with different inputs. This is called parameterized testing, and it&#39;s a great way to ensure that your code behaves correctly under different conditions.&lt;/p&gt;
&lt;p&gt;This is usually done by writing a single test function that takes parameters, and then calling that function with different inputs. Here is an example in C# using NUnit:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;TestAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    Assert&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;AreEqual&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;expected&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing in Deno&lt;/h2&gt;
&lt;p&gt;In Deno, we can take advantage of the integrated testing library by using &lt;code&gt;Deno.test()&lt;/code&gt; to define our tests. We can further split our tests into separate steps, like so:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;Deno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;When activating the flux capacitor&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; capacitor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;activateFluxCapacitor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;It should generate 1.21 gigawatts of power&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; power &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; capacitor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generatePower&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;power&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.21&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;It should travel through time&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; time &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; capacitor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;travelThroughTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;time&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1985&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sometimes however we want to test a function with multiple inputs. An easy way to do so would be to iterate over an array of inputs and call the test function for each input. This can be a bit cumbersome, especially if we have a lot of inputs:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;Deno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;TestAdd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; testCases &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; input &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; testCases&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A disadvantage of this approach is that if any of the test cases fail, the test will stop running and we won&#39;t know the results of the remaining test cases. The test runner will also simply mark the whole test as failed, without providing any information about which test case failed!&lt;/p&gt;
&lt;h2&gt;How to run parameterized tests in Deno&lt;/h2&gt;
&lt;p&gt;After searching online for a bit and finding nothing helpful, I decided to take a look at how Deno&#39;s developers write tests.
A quick github search took me to &lt;a href=&quot;https://github.com/denoland/deno/blob/0ef3f6ba887d7aed2d94c8b622563d13bfecda2c/tests/unit/serve_test.ts#L2441&quot;&gt;the tests for Deno&#39;s &lt;code&gt;serve&lt;/code&gt; function&lt;/a&gt;. Here we can see that they use a helper function to generate multiple test cases.&lt;/p&gt;
&lt;p&gt;The trick is that they call &lt;code&gt;Deno.test()&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; the helper function!&lt;/p&gt;
&lt;p&gt;We can adapt this trick in our tests like so:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;testAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Deno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;TestAdd(&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;b&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;testAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;testAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;testAdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another approach is to use an array and a for loop:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; testCases &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;expected&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; testCases&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Deno&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;TestAdd(&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;b&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expected&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way, if any of the test cases fail, the test runner will still run the remaining test cases and provide information about which test case failed.&lt;/p&gt;
&lt;p&gt;Additionally, by specifying unique test names in each Deno.test() call, we can easily identify which test case failed!&lt;/p&gt;
&lt;h3&gt;A note on the Visual Studio Code Deno extension&lt;/h3&gt;
&lt;p&gt;At the time of writing, you won&#39;t be able to run individual tests in Visual Studio Code from the test file itself (ie you won&#39;t see a &amp;quot;Run Test&amp;quot; button next to each test).&lt;/p&gt;
&lt;p&gt;You can still run the whole test file by right-clicking on the file and selecting &amp;quot;Run Test&amp;quot; from the context menu.&lt;/p&gt;
&lt;p&gt;Once you&#39;ve done that, the tests will also be visible in the &amp;quot;Testing&amp;quot; tab, where you can see the results of each test case.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Test files in Deno are just regular TypeScript files, so you can use any TypeScript feature you like to write your tests. This includes using helper functions to generate multiple test cases, as we&#39;ve seen in this article.&lt;/p&gt;
&lt;p&gt;The key takeaway is that you can use &lt;code&gt;Deno.test()&lt;/code&gt; inside a helper function or a for loop to generate multiple test cases. This allows you to run parameterized tests in Deno, and get detailed information about which test cases failed.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>A journey through Server Side Rendering in Deno</title>
    <link href="https://codemade.net/blog/a-journey-through-deno-ssr/" />
    <updated>2025-02-11T00:00:00Z</updated>
    <id>https://codemade.net/blog/a-journey-through-deno-ssr/</id>
    <content type="html">&lt;p&gt;As a longtime .net developer, I&#39;ve always preferred languages with strong typing. I find that I can reason about the code much better if I understand what data structures are required by a function call, and what it will return.&lt;/p&gt;
&lt;p&gt;Naturally, this means that when doing any frontend work, I tend to reach for TypeScript. I find it&#39;s a welcome improvement over vanilla JavaScript, especially when using typed third-party libraries.&lt;/p&gt;
&lt;p&gt;There is only one catch: setting up typescript, esm modules, and bundling can be really painful when you&#39;re just trying to get a simple project off the ground. Soon you&#39;re 2 hours in, and you&#39;re still trying to figure out why your imports aren&#39;t working.&lt;/p&gt;
&lt;p&gt;This is where Deno comes in! Built by the creators of Node.js, deno sets out to undo the mistakes of their previous project. It has built-in support for TypeScript, esm modules, and even has a built-in bundler. This means that I can write all my code in TypeScript, and run it without any additional setup. It also does away with the &lt;code&gt;node_modules&lt;/code&gt; folder, which makes my hard drive very happy.&lt;/p&gt;
&lt;p&gt;So, when starting a new project recently, I decided to give Deno a try. The project is a simple server-side rendered web application, with a bit of interactivity sprinkled in.&lt;/p&gt;
&lt;p&gt;It&#39;s now been a few weeks, and I&#39;ve been through several iterations. Here is &lt;em&gt;my&lt;/em&gt; journey through server-side rendering in Deno.&lt;/p&gt;
&lt;h2&gt;Deno has a built-in web server??&lt;/h2&gt;
&lt;p&gt;Yes, yes it does. Using the builtin &lt;code&gt;Deno.Serve()&lt;/code&gt; function, you can create a simple web server &lt;a href=&quot;https://docs.deno.com/api/deno/~/Deno.serve&quot;&gt;in just a few lines of code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The only downside of this approach is that you&#39;ll have to handle absolutely everything yourself. This means parsing the request, handling the response, and even serving static files. This is easy enough for a simple project, but it can quickly become unwieldy as your project grows.&lt;/p&gt;
&lt;h2&gt;Shopping for a framework&lt;/h2&gt;
&lt;p&gt;After deciding that I didn&#39;t want to write my own web server, I started looking for a lightweight framework that would handle the heavy lifting for me. I quickly found &lt;a href=&quot;https://oakserver.org/&quot;&gt;Oak&lt;/a&gt;, a simple framework for Deno that is very similar to Express.js.&lt;/p&gt;
&lt;p&gt;Oak has built-in support for things like middleware and routing. It seemed like a great choice for a simple project, and it&#39;s easy to get started with.&lt;/p&gt;
&lt;p&gt;The next step was to figure out a way to go beyond serving API endpoints and start rendering HTML on the server. This is where things started to get interesting.&lt;/p&gt;
&lt;h2&gt;Shopping for a templating engine&lt;/h2&gt;
&lt;h3&gt;Handlebars&lt;/h3&gt;
&lt;p&gt;When thinking of templating engines, my first idea was to use good old &lt;a href=&quot;https://handlebarsjs.com/&quot;&gt;Handlebars&lt;/a&gt;. I had never used it before, but I remember it being all the rage a few years ago, so I figured it must be good. Right? &lt;em&gt;Right??&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It was... below my expectations. And by &amp;quot;below my expectations&amp;quot; I mean &lt;em&gt;&amp;quot;It makes me want to stab myself&amp;quot;&lt;/em&gt;. The syntax, oh the syntax!! I have nothing but compassion for anyone who has to coexist with it.&lt;/p&gt;
&lt;p&gt;The last straw was when I had to declare a helper function to compare two values. I had to write a helper function to compare two values. In a templating engine.&lt;/p&gt;
&lt;h3&gt;ejs&lt;/h3&gt;
&lt;p&gt;After a good old web search for &amp;quot;handlebars alternatives&amp;quot;, I stumbled upon &lt;a href=&quot;https://ejs.co/&quot;&gt;ejs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As a .net developer, I was immediately reminded of Razor Pages / MVC. Simple, clean, and easy to use. I was sold.&lt;/p&gt;
&lt;p&gt;...it did however come with its own set of quirks. For example, it doesn&#39;t have the equivalent of &amp;quot;Layouts&amp;quot; in Razor. This means that you have to manually include the header and footer in every page. It also has even weirder syntax than Razor, with various combinations of percent signs and angle brackets.&lt;/p&gt;
&lt;p&gt;But mainly, the thing I was really missing until now was being able to &lt;strong&gt;unit test my UI&lt;/strong&gt;. Not knowing if my partials were rendering correctly until I manually tested them in the browser was a pain.&lt;/p&gt;
&lt;p&gt;In the end, I found that while the syntax was cozy and familiar, the web has moved on. I wanted something more modern, more... reactive. I wanted, in fact, React.&lt;/p&gt;
&lt;h3&gt;Detour to the promised land of Deno Fresh&lt;/h3&gt;
&lt;p&gt;Before I could start using React, I had to figure out how to get it to work with Deno. This is where I found &lt;a href=&quot;https://fresh.deno.dev/&quot;&gt;🍋 Deno Fresh&lt;/a&gt;. Fresh is Deno’s answer to Next.js. With built-in support for JSX and an &amp;quot;islands&amp;quot; architecture that prioritizes minimal client-side JavaScript, it looked very promising.&lt;/p&gt;
&lt;p&gt;I quickly cloned the example repo on my machine to try it out. It worked great. Sure, it&#39;s extremely opinionated, but I can live with that.&lt;/p&gt;
&lt;p&gt;I decided to try converting one of my ejs partials into a jsx component right inside the example project. Easy. Done. I was sold. Writing JSX instead of ejs felt natural, the way it was supposed to be all along.&lt;/p&gt;
&lt;p&gt;At some point, however, things started to fall apart.&lt;/p&gt;
&lt;p&gt;Testing components, for example, is not really supported out of the box. There&#39;s &lt;a href=&quot;https://github.com/denoland/fresh/issues/427&quot;&gt;an open issue on github&lt;/a&gt; and the consensus seems to be... let&#39;s wait for Fresh 2.0.&lt;/p&gt;
&lt;p&gt;As far as I understand it, the wind behind Fresh&#39;s sails is now more like a gentle breeze.
It seems that the team has been busy working on Deno itself, and Fresh has been left behind.&lt;/p&gt;
&lt;p&gt;I notice as I write this that the &lt;a href=&quot;https://deno.com/blog/fresh-1.6&quot;&gt;last official blog about it&lt;/a&gt; dates December 1, 2023, which is about 10 years in trendy javascript framework time.&lt;/p&gt;
&lt;h3&gt;But I like react! Maybe I can salvage this?&lt;/h3&gt;
&lt;p&gt;So it turns out that Deno Fresh uses &lt;code&gt;preact-render-to-string&lt;/code&gt; under the hood to transform JSX components into HTML.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With some clever refactoring, I&#39;m sure I can just replace my ejs Oak middleware with a React one!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;-famous last words&lt;/p&gt;
&lt;p&gt;So I did! I created a new middleware that would render my React components server-side, and it worked like a charm. If you&#39;re curious to see what it looks like, &lt;a href=&quot;https://gist.github.com/LBognanni/4ce0247276b7cd77e72a9cc014cd6e12&quot;&gt;here is a github gist with the code&lt;/a&gt; 🙂&lt;/p&gt;
&lt;p&gt;I now had a working server-side rendered web app in Deno under my belt. I was happy with the result, even if it felt a bit of a Frankenstein&#39;s monster.&lt;/p&gt;
&lt;h2&gt;And then I built a second project&lt;/h2&gt;
&lt;p&gt;Some time later, I wanted to start a new project. Similar requirements, so I figured I could just copy-paste the code from the first project and be done with it.&lt;/p&gt;
&lt;p&gt;But then I thought,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If I need this, maybe others do too? Maybe I should make a library out of it? Maybe I should make my own framework? One framework to rule them all! 😈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yeah, no. No thanks Satan. I&#39;m good.&lt;/p&gt;
&lt;p&gt;An important requirement of my second project was user management, authentication, and authorization.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://better-auth.vercel.app/&quot;&gt;Better-Auth&lt;/a&gt; looked exactly like the kind of thing I needed. Fully featured, popular, good documentation. I had absolutely no intention of trying to bend my own little framework to work with it. I just wanted to get my project done.&lt;/p&gt;
&lt;p&gt;I also had the nagging feeling in the back of my head that I was just &lt;em&gt;writing too much code&lt;/em&gt; for something that should be simple.&lt;/p&gt;
&lt;h2&gt;Enter Hono&lt;/h2&gt;
&lt;p&gt;The first backend framework that is mentioned in Better-Auth&#39;s documentation is &lt;a href=&quot;https://hono.dev/&quot;&gt;Hono&lt;/a&gt;. I decided to take a look.&lt;/p&gt;
&lt;p&gt;Hono does everything I was trying to build manually—but better. It has &lt;a href=&quot;https://hono.dev/docs/guides/jsx#jsx&quot;&gt;built-in JSX support&lt;/a&gt;, a router, and great documentation. The best thing is that I don&#39;t have to support any of it myself!&lt;/p&gt;
&lt;p&gt;It also looks almost exactly like Oak, so that was already a plus.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, building my second project was a much smoother experience.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This was a long journey, but a rewarding one. The nice thing was that at every point during my first project I ended up having something that was working, and that I could build upon.&lt;/p&gt;
&lt;p&gt;Refactoring my code to use the next thing was luckily always pretty easy, and I was able to learn a lot along the way.&lt;/p&gt;
&lt;p&gt;One thing I learned is that it&#39;s easy to fall into the trap of doing everything &amp;quot;the Deno way&amp;quot;, and assuming that a library or framework that&#39;s written for Deno is the best choice.&lt;/p&gt;
&lt;p&gt;In reality, it&#39;s important to evaluate each library or framework on its own merits, and choose the one that best fits your needs.&lt;/p&gt;
&lt;p&gt;Frameworks like Hono that are built for multiple runtimes are obviously going to be more popular and better maintained than something that&#39;s built for Deno only.&lt;/p&gt;
&lt;p&gt;The great thing about Deno is that you can reference npm modules directly (without worrying about terabytes of &lt;code&gt;node_modules&lt;/code&gt;), so you really have the whole JavaScript ecosystem at your disposal.&lt;/p&gt;
&lt;h3&gt;🔑🥡 Key Takeaways:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Don’t assume that a library made for Deno is always the best choice—evaluate based on actual needs.&lt;/li&gt;
&lt;li&gt;Multi-runtime frameworks like Hono tend to be better maintained and more flexible.&lt;/li&gt;
&lt;li&gt;Deno’s ability to use npm modules expands its ecosystem significantly.&lt;/li&gt;
&lt;li&gt;Don’t be afraid to refactor and try new things — each iteration can teach you something new!&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Running Docker in Windows without virtualization</title>
    <link href="https://codemade.net/blog/docker-desktop-with-remote-server/" />
    <updated>2025-01-21T00:00:00Z</updated>
    <id>https://codemade.net/blog/docker-desktop-with-remote-server/</id>
    <content type="html">&lt;p&gt;Docker is an essential tool for developers. It allows you to run applications in isolated containers, making it easy to manage dependencies and run the same code in different environments.&lt;/p&gt;
&lt;p&gt;However, on Windows Docker requires virtualization support, which may not always be enabled or available. Even when using WSL2 as an alternative to Hyper-V, your machine must still support and enable Hyper-V.&lt;/p&gt;
&lt;p&gt;Recently I needed to run docker on a Windows virtual machine, running inside VirtualBox on a Linux host.&lt;/p&gt;
&lt;p&gt;Contrary to KVM/QEMU, VirtualBox does not support nested virtualization, &lt;em&gt;unless&lt;/em&gt; you&#39;re using VirtualBox inside the VM too.&lt;/p&gt;
&lt;p&gt;What VirtualBox offers, however, is much better (read: acceptable) video hardware acceleration compared to QEMU - and for someone like me who spends more time attending remote meetings than running containers, this is a good tradeoff.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;do&lt;/em&gt; however need to run containers from time to time, and having to switch to a different machine just for that is a bit of a hassle.&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;⚠️&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;Important!&lt;/strong&gt; We&#39;re going to install Docker Desktop in this guide &amp;mdash; however, without Hyper-V &lt;strong&gt;you won&#39;t be able&lt;/strong&gt; to run the docker engine or &lt;strong&gt;do anything in the GUI&lt;/strong&gt;.
&lt;/p&gt;
&lt;p&gt;
  Instead, we&#39;re relying on Docker desktop to provide the &lt;code&gt;docker&lt;/code&gt; command line tool, which we&#39;ll be using to connect to a Linux server.
&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;Step 1: Install Docker Desktop&lt;/h2&gt;
&lt;p&gt;Most online resources will point you to Docker Toolbox, which as of 2025 is a legacy solution that is no longer maintained. While you &lt;em&gt;can&lt;/em&gt; still download it and install it, it&#39;s stuck on a very old version of Docker and is missing some crucial components like &lt;code&gt;buildx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is the link to the &lt;a href=&quot;https://docs.docker.com/desktop/setup/install/windows-install/&quot;&gt;Docker Desktop download page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Docker Desktop should install just fine, but of course it will complain if you try to run it.&lt;/p&gt;
&lt;h2&gt;Step 2: Install Docker on the remote server&lt;/h2&gt;
&lt;p&gt;If you haven&#39;t already done so, you&#39;ll need to install Docker on the remote server. There are plenty of resources online on how to do this, so I won&#39;t go into details here. The official docker docs have plenty of information on how to install Docker on different platforms.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot;&gt;Here&#39;s the link to the instructions for Ubuntu&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Be sure to add your user to the &lt;code&gt;docker&lt;/code&gt; group so you can run docker commands without &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Step 2: Enable ssh on the remote server&lt;/h2&gt;
&lt;p&gt;You&#39;ll need to be able to connect to the remote server via ssh. If you&#39;re using a cloud provider, you&#39;ll likely already have this set up. If you&#39;re using a local server, or Desktop Ubuntu, you&#39;ll need to install an ssh server.&lt;/p&gt;
&lt;p&gt;This is simply done by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; openssh-server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll likely also need to open the ssh port on your firewall.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Configure Docker Desktop&lt;/h2&gt;
&lt;p&gt;Easy peasy. You just need to set an environment variable to tell Docker Desktop to connect to the remote server.&lt;/p&gt;
&lt;p&gt;Try it out in a terminal window. First, set the variable:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:DOCKER_HOST&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ssh://your-username@your-server:22&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run a docker command:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should get a prompt asking you to accept the server&#39;s fingerprint, then another prompt asking for your password. If everything goes well, you should see the list of containers running on the remote server. (none if you just installed Docker)&lt;/p&gt;
&lt;h2&gt;Step 4: Configure a Persistent Environment Variable&lt;/h2&gt;
&lt;p&gt;You don&#39;t want to have to set the environment variable every time you open a terminal window. Instead, we can set the environment variable for all of Windows.&lt;/p&gt;
&lt;p&gt;Right-click on the start button and select &amp;quot;System&amp;quot;. Then click on &amp;quot;Advanced system settings&amp;quot; and then &amp;quot;Environment Variables&amp;quot;.&lt;/p&gt;
&lt;p&gt;Add a new system variable with the name &lt;code&gt;DOCKER_HOST&lt;/code&gt; and the value &lt;code&gt;ssh://your-username@your-server&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Any new terminal window you open will now be using the environment variable, and docker commands will connected to the remote server.&lt;/p&gt;
&lt;h2&gt;Step 5: Usability improvements&lt;/h2&gt;
&lt;p&gt;As it is, Docker will ask for your password every time you run a command. This can be a very annoying, especially if you&#39;re running a lot of commands. Some commands, like &lt;code&gt;docker buildx build&lt;/code&gt; implicitly run multiple docker commands, so you&#39;ll be asked for your password multiple times.&lt;/p&gt;
&lt;p&gt;Instead, we can use ssh keys to authenticate.&lt;/p&gt;
&lt;p&gt;On your Windows machine, generate a new ssh key pair:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;ssh-keygen&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll be asked where to store the key. Be sure to enter this path: &lt;code&gt;C:&#92;Users&#92;your-username&#92;.ssh&#92;id_rsa&lt;/code&gt;. This is the default path where Docker will look for the key. (Some guides will tell you to just accept the default path, but doing so made the ssh-keygen command hang indefinitely in my case.)&lt;/p&gt;
&lt;p&gt;You&#39;ll be then asked for a passphrase. I recommend leaving this empty so you don&#39;t have to enter it every time you run a docker command.&lt;/p&gt;
&lt;p&gt;The next step is to copy the public key to the remote server. You can do this with the &lt;code&gt;ssh-copy-id&lt;/code&gt; command, but this command is not available on Windows.&lt;/p&gt;
&lt;p&gt;Instead, we can copy the key manually.&lt;/p&gt;
&lt;p&gt;First, print the public key:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; ~&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;.ssh&lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;id_rsa.pub&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the whole line that is printed.
Next, connect to the remote server and add the key at the end of the &lt;code&gt;authorized_keys&lt;/code&gt; file, using your favorite text editor (&lt;code&gt;nano&lt;/code&gt; in this case):&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; your-username@your-server
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; ~/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is it! You should now be able to run docker commands without being asked for your password. Try by running &lt;code&gt;docker ps&lt;/code&gt; again in a new terminal window.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While this article focussed on connecting docker to a local server, you can also follow the same steps to connect to a server in the cloud.&lt;/p&gt;
&lt;p&gt;This can be useful if you want to run docker commands on a more powerful machine, or if you want to run docker commands on a machine that is serving web traffic to the internet!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Announcing CodeMade Games!</title>
    <link href="https://codemade.net/blog/announcing-codemade-games/" />
    <updated>2025-01-07T00:00:00Z</updated>
    <id>https://codemade.net/blog/announcing-codemade-games/</id>
    <content type="html">&lt;div class=&quot;callout&quot;&gt;
  &lt;p&gt;🕹️&lt;/p&gt;
  &lt;p&gt;
    &lt;b&gt;TLDR&lt;/b&gt; 
    I&#39;m excited to announce the launch of &lt;a href=&quot;https://games.codemade.net&quot;&gt;CodeMade Games&lt;/a&gt;, a new website dedicated to fun, casual games that you can play in your browser 🎉
  &lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;My game development journey&lt;/h3&gt;
&lt;p&gt;Since I started playing videogames in my childhood, I&#39;ve always been fascinated by the idea of creating my own games. Building games takes a lot of time and effort, but it&#39;s also incredibly rewarding.&lt;/p&gt;
&lt;p&gt;I had somewhat of a failed start many, many years ago when I tried to build &lt;a href=&quot;https://www.gljakal.com/blog/2010/08/16/screenshot-update/&quot;&gt;a platformer game&lt;/a&gt; in C++. While I did get some traction initially, with a nice prototype and some basic game mechanics, I eventually lost interest and moved on to other projects. One of the main reasons for this was that I have no artistic skills whatsoever, and the little art I could produce would take many long hours to create.&lt;/p&gt;
&lt;h3&gt;A new beginning&lt;/h3&gt;
&lt;p&gt;Fast forward to a few weeks ago, when I wanted a break from my usual backend development work and decided to play around with the &lt;code&gt;:has&lt;/code&gt; CSS selector.
I ended up building &lt;a href=&quot;https://games.codemade.net/swep/index.html&quot;&gt;a simple minesweeper clone&lt;/a&gt; - and had a lot of fun doing it!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://games.codemade.net/sweep.png&quot; alt=&quot;A screenshot of Sweep, my minesweeper clone&quot;&gt;&lt;/p&gt;
&lt;p&gt;After comprehensively testing the game on my couch, on my way to work, and during my lunch breaks, I decided that it was time to release it to the world.
And so, &lt;a href=&quot;https://games.codemade.net&quot;&gt;CodeMade Games&lt;/a&gt; was born!&lt;/p&gt;
&lt;h3&gt;My core game development principles&lt;/h3&gt;
&lt;p&gt;It&#39;s been almost 15 years since I last worked on a game, and I&#39;ve learned a lot since then. Here are some of the core principles that I&#39;m following with CodeMade Games:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Do less&lt;/strong&gt;: Like, way less. No fancy graphics, no complex mechanics. Art was always my biggest blocker, so I&#39;m focusing on simple, available assets. Emojis are a great way to add some personality to the games without spending hours on art! 🔥&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build for the web&lt;/strong&gt;: I want my games to be accessible to everyone, so I&#39;m building them to be played in the browser. Mobile is also a priority, mostly because I want to be able to play them on my phone!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respect the user&lt;/strong&gt;: Games should be fun, not annoying. With most game companies trying to &lt;a href=&quot;https://www.howtogeek.com/fortnite-was-busted-for-using-dark-patterns-heres-what-that-means/&quot;&gt;manipulate their users&lt;/a&gt; into spending more time and money, I want to take a different approach. No ads, no invasive tracking, no dark patterns. Have your 10 minutes of fun and then go back to your life!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn something&lt;/strong&gt;: Writing games is my way to relax and have fun, but I also want to learn new things. Whether it&#39;s a new CSS trick, a new JavaScript feature, or a new game design pattern, I want to keep pushing myself to learn and improve.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What&#39;s next?&lt;/h3&gt;
&lt;p&gt;I&#39;m working on a few new games that I&#39;m excited to share with you soon.&lt;/p&gt;
&lt;p&gt;I&#39;m also planning to write more about my game development journey, the tools I&#39;m using, and the lessons I&#39;m learning along the way.&lt;/p&gt;
&lt;p&gt;Be sure to subscribe to the &lt;a href=&quot;https://codemade.net/atom.xml&quot;&gt;RSS feed&lt;/a&gt; or follow me on &lt;a href=&quot;https://bsky.app/profile/loris.codes&quot;&gt;BlueSky&lt;/a&gt; to stay updated!&lt;/p&gt;
&lt;h4&gt;And of course, see you on &lt;a href=&quot;https://games.codemade.net&quot;&gt;CodeMade Games&lt;/a&gt; 🎮&lt;/h4&gt;
</content>
  </entry>
  <entry>
    <title>Modern CSS is awesome!</title>
    <link href="https://codemade.net/blog/modern-css-is-awesome/" />
    <updated>2024-11-25T00:00:00Z</updated>
    <id>https://codemade.net/blog/modern-css-is-awesome/</id>
    <content type="html">&lt;p&gt;The last time I seriously worked with CSS was back when Firefox was still a relevant browser (😢), and Internet Explorer 11 was &lt;em&gt;just&lt;/em&gt; on its way out.&lt;/p&gt;
&lt;p&gt;Flexbox had just become a popular way to create layouts, and CSS Grid was new and exciting, but not yet widely supported.
Not that I could use either, since IE&#39;s implementation of flexbox was so riddled with bugs that it was practically unusable.&lt;/p&gt;
&lt;p&gt;But now. We&#39;re in 2024, and things have changed. A lot.&lt;/p&gt;
&lt;p&gt;Here are a few of my favourite features of modern CSS, in no particular order:&lt;/p&gt;
&lt;h3&gt;CSS Variables&lt;/h3&gt;
&lt;p&gt;CSS variables were once only possible with a preprocessor like SASS or LESS, but now they&#39;re a native feature of CSS.&lt;/p&gt;
&lt;p&gt;Here is how to define a variable:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--primary-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff0000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specifying the variable at the root level makes it available to the entire document.&lt;/p&gt;
&lt;p&gt;The variable can then be used with the &lt;code&gt;var()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--primary-color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The neat thing is that variables follow the cascading logic of CSS, so you can update their value at any level of the document, and all elements that use that variable will automatically update.&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;300&quot; data-default-tab=&quot;css,result&quot; data-slug-hash=&quot;XJrJvom&quot; data-pen-title=&quot;CSS Variables&quot; data-user=&quot;codemade&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/codemade/pen/XJrJvom&quot;&gt;
  CSS Variables&lt;/a&gt; by Loris Bognanni (&lt;a href=&quot;https://codepen.io/codemade&quot;&gt;@codemade&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;h3&gt;Nesting in CSS&lt;/h3&gt;
&lt;p&gt;This is again one of those things that you would have needed a CSS preprocessor for in the past, but is now a native feature of CSS. Useful for keeping your code concise and readable.&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.parent &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.child &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I especially find this useful when styling pseudo-elements and states:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;button &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token parent important&quot;&gt;&amp;amp;&lt;/span&gt;:hover &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token parent important&quot;&gt;&amp;amp;&lt;/span&gt;:after &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;🚀&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Centering a &lt;code&gt;div&lt;/code&gt; is now very easy&lt;/h3&gt;
&lt;p&gt;Centerig a &lt;code&gt;div&lt;/code&gt; has long been a recurring joke amongst web developers, but thanks to CSS Grid, it&#39;s now a two-liner:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;div.parent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;place-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Viewport units&lt;/h3&gt;
&lt;p&gt;Viewport units are a way to size elements based on the size of the viewport. They are particularly useful for creating responsive designs.&lt;/p&gt;
&lt;p&gt;Viewport units include &lt;code&gt;vw&lt;/code&gt; (viewport width), &lt;code&gt;vh&lt;/code&gt; (viewport height), &lt;code&gt;vmin&lt;/code&gt; (the smaller of the two), and &lt;code&gt;vmax&lt;/code&gt; (the larger of the two). So for example, if you want an element to be half as wide as the viewport, you can do this:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.my-element&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an example that combines viewport units and CSS Grid to center a div in the middle of the screen:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;300&quot; data-default-tab=&quot;css,result&quot; data-slug-hash=&quot;JoPogQW&quot; data-pen-title=&quot;centering&quot; data-user=&quot;codemade&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/codemade/pen/JoPogQW&quot;&gt;
  centering&lt;/a&gt; by Loris Bognanni (&lt;a href=&quot;https://codepen.io/codemade&quot;&gt;@codemade&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;h3&gt;Math in CSS&lt;/h3&gt;
&lt;p&gt;CSS has a &lt;code&gt;calc()&lt;/code&gt; function that allows you to perform calculations right in your stylesheets. This can be useful for things like calculating widths, margins, and padding.&lt;/p&gt;
&lt;p&gt;The exciting thing about &lt;code&gt;calc()&lt;/code&gt; is that it can combine different units of measurement, so you can do things like this:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.my-element&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;50% - 20px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can of course use &lt;code&gt;calc()&lt;/code&gt; with variables as well:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;article &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--margin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSS Containers, container queries, and container measurements&lt;/h3&gt;
&lt;p&gt;By telling CSS that a certain element is a &lt;code&gt;container&lt;/code&gt;, you can now do things like size its children based on the container&#39;s size, or apply styles based on the container&#39;s size.&lt;/p&gt;
&lt;p&gt;Similar to viewport units, container units are based on the size of the container element. They include &lt;code&gt;cqw&lt;/code&gt;, &lt;code&gt;cqh&lt;/code&gt;, &lt;code&gt;cqmin&lt;/code&gt;, and &lt;code&gt;cqmax&lt;/code&gt;.&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;300&quot; data-default-tab=&quot;css,result&quot; data-slug-hash=&quot;wBwawvN&quot; data-pen-title=&quot;Untitled&quot; data-user=&quot;codemade&quot; style=&quot;height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/codemade/pen/wBwawvN&quot;&gt;
  Untitled&lt;/a&gt; by Loris Bognanni (&lt;a href=&quot;https://codepen.io/codemade&quot;&gt;@codemade&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries&quot;&gt;Container queries&lt;/a&gt; take this a step further by allowing you to apply styles based on the size of the container. This is particularly useful for creating responsive designs where the layout changes based on the size of the container, rather than the viewport.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;aspect-ratio&lt;/code&gt; property&lt;/h3&gt;
&lt;p&gt;If you ever wanted to embed a video in your responsive page in the old days, you probably had to use some javascript to calculate the correct height based on the width of the video.&lt;/p&gt;
&lt;p&gt;But now, you can use the &lt;code&gt;aspect-ratio&lt;/code&gt; property:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.video &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;aspect-ratio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 16 &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; 9&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;:has()&lt;/code&gt; selector&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;:has()&lt;/code&gt; selector allows you to select an element based on its descendants. This can be useful for styling elements based on their content, or for selecting elements that contain a specific child element.&lt;/p&gt;
&lt;p&gt;While before &lt;code&gt;:has&lt;/code&gt; you would have to use javascript or some sort of preprocessing to achieve the same effect, now you can do it with a single line of CSS.&lt;/p&gt;
&lt;p&gt;For example, here is a simple way to style a &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; element when its child &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; is checked:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;200&quot; data-default-tab=&quot;css,result&quot; data-slug-hash=&quot;gbYpbgG&quot; data-pen-title=&quot;:has demo&quot; data-user=&quot;codemade&quot; style=&quot;height: 200px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/codemade/pen/gbYpbgG&quot;&gt;
  :has demo&lt;/a&gt; by Loris Bognanni (&lt;a href=&quot;https://codepen.io/codemade&quot;&gt;@codemade&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;script async=&quot;&quot; src=&quot;https://cpwebassets.codepen.io/assets/embed/ei.js&quot;&gt;&lt;/script&gt;</content>
  </entry>
  <entry>
    <title>The case of the unhappy CloudFront distribution</title>
    <link href="https://codemade.net/blog/the-case-of-the-unhappy-cloudfront/" />
    <updated>2024-06-29T00:00:00Z</updated>
    <id>https://codemade.net/blog/the-case-of-the-unhappy-cloudfront/</id>
    <content type="html">&lt;p&gt;For fun, and as a learning experience, I&#39;m building a very simple comment system for this blog.&lt;/p&gt;
&lt;p&gt;Currently codemade.net is a simple static website built by Jekyll everytime I push to the &lt;code&gt;main&lt;/code&gt; branch on its &lt;a href=&quot;https://github.com/LBognanni/codemade-site&quot;&gt;GitHub repo&lt;/a&gt;.
This is a relatively common setup for smaller websites, and it has the benefit of being extremely fast and cheap to host.&lt;/p&gt;
&lt;p&gt;There are a number of third party comment systems floating around the internet, but they all have some drawbacks. Some are just too expensive for a small website like mine, others are advertising platforms in disguise, and others still are just too complicated to set up. Perhaps this is why a lot of people rely on social media for comments.&lt;/p&gt;
&lt;p&gt;I wanted to build something that was simple, cheap and cheeful. I also wanted to get hands on with tools I rarely use, like ASP.net Core on Lambda, HTMX, terraform, CloudFront, etc.&lt;/p&gt;
&lt;p&gt;It follows a relatively common pattern: all the static content is stored in S3, while the dynamic APIs are served by Lambda functions. An HTTP API gateway is used to route requests to the correct Lambda function, and CloudFront sits in front of everything to provide caching, routing, and SSL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/basic-website.png&quot; alt=&quot;The architecture of the comment system&quot;&gt;&lt;/p&gt;
&lt;p&gt;I built and deployed this in pieces, starting with the main Lambda function. I set it up to serve both the frontend (a simple HTML page), and the backend: a GET endpoint to return the comments for a post, a PUT endpoint to post a new comment. I tested it locally, and it worked fine.&lt;/p&gt;
&lt;p&gt;I deployed it to AWS, behind an API Gateway, and surprisingly, it worked!&lt;/p&gt;
&lt;p&gt;I then decided to add CloudFront in front of the API Gateway, and store the static content in an S3 bucket.&lt;/p&gt;
&lt;p&gt;When configuring CloudFront, one must set up one or more &amp;quot;Origins&amp;quot; (the places where CloudFront will fetch the content from). I set up two origins: the default one being S3 where &lt;code&gt;index.html&lt;/code&gt; is stored, and the API Gateway, only for the &lt;code&gt;/comments/*&lt;/code&gt; path.&lt;/p&gt;
&lt;p&gt;Deploy the distribution, and yes! The index page loads, and navigating to a comments page brings up a list of comments.&lt;/p&gt;
&lt;p&gt;Try posting a comment however, and... this happens:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/cloudfront-is-not-happy.png&quot; alt=&quot;CloudFront returns HTTP 403&quot;&gt;&lt;/p&gt;
&lt;p&gt;Soo, somehow GETting the comments works, but PUTing a comment doesn&#39;t.&lt;/p&gt;
&lt;p&gt;I started debugging by looking at each piece of the puzzle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does the lambda work?&lt;/strong&gt; Yes, testing the lambda in isolation yields the expected result (a comment is posted)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does the API Gateway work?&lt;/strong&gt; Yes, invoking the API gateway URL directly calls the lambda and posts a comment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did I set up the CloudFront distribution correctly?&lt;/strong&gt; I think so, but let&#39;s check the settings. I double-checked the origins, and they look fine. I also checked the behaviors, and indeed requests to &lt;code&gt;/comments/*&lt;/code&gt; are routed to the API Gateway &lt;em&gt;(of course they are! GET requests work!)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Maybe it&#39;s a caching issue? I tried invalidating the cache, but it didn&#39;t help.&lt;/li&gt;
&lt;li&gt;Maybe it&#39;s a CORS issue? I tested by directly invoking the PUT endpoint via Postman, and it worked. So probably it&#39;s not that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hmm. Perhaps the CloudFront logs could help me? I enabled logging, and tried to post a comment. I then waited for a few minutes, and checked the logs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/cf-logs-1.png&quot; alt=&quot;CloudFront logs&quot;&gt;&lt;/p&gt;
&lt;p&gt;(shoutout to the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=mechatroner.rainbow-csv&quot;&gt;Rainbow CSV&lt;/a&gt; Vs Code extension for making this somewhat readable!)&lt;/p&gt;
&lt;p&gt;It&#39;s a bit hard to follow, but we can see 3 requests: the first two are GET requests to the &lt;code&gt;favicon.ico&lt;/code&gt; resource (presumably served from S3), and to the a comments page (served by the API Gateway). The third request is a PUT request to the same comments page, and it returns a 403 status code.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;x-edge-result-type&lt;/code&gt; field simply says &lt;code&gt;Miss&lt;/code&gt; for the GET requests (ie they were not in CloudFront&#39;s cache) and &lt;code&gt;Error&lt;/code&gt; for the PUT request. Not very helpful.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;x-edge-detailed-result-type&lt;/code&gt; field looks interesting. It says &lt;code&gt;InvalidRequestMethod&lt;/code&gt; for the PUT request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Could it be that I didn&#39;t allow PUT requests on the Api Gateway origin?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I checked my terraform code, and nope, all HTTP methods are allowed for that origin 🤔&lt;/p&gt;
&lt;p&gt;So I started googling (well &lt;em&gt;duckduckgoing&lt;/em&gt;) for every combination of &amp;quot;CloudFront&amp;quot;, &amp;quot;InvalidRequestMethod&amp;quot;, &amp;quot;403&amp;quot;, &amp;quot;PUT&amp;quot;, &amp;quot;API Gateway&amp;quot; I could think of.&lt;/p&gt;
&lt;p&gt;I found some StackOverflow posts where folks were having similar issues, but they were all related to not having configured one of the three components (CloudFront, API Gateway, S3) correctly. I double, triple, quadruple checked my configuration, and it all looked fine.&lt;/p&gt;
&lt;p&gt;Out of desperation, I tried mixing things up a bit. I changed the CloudFront distribution to have only one origin: the API Gateway. I then tried posting a comment, and... it worked! But why?&lt;/p&gt;
&lt;p&gt;I was really tempted to just leave it at that, but the nagging voice at the back of my head, complaining that I would have to invoke the lambda for each. single. static. asset. was too loud.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Obviously&lt;/em&gt; the moral imperative is to have the static content served from S3 (it&#39;s cheaper, faster, and more reliable). So I re-enabled the S3 origin, and tried posting a comment again. And... back to not working, of course.&lt;/p&gt;
&lt;p&gt;Okay. Time to take a step back. Go for a walk. Clear my head.&lt;/p&gt;
&lt;p&gt;It&#39;s the next day, and I&#39;m back at it. Time to try and simplify things a bit.&lt;/p&gt;
&lt;p&gt;Perhaps my terraform code was too complex, or it was missing something? I decided to try and set up the CloudFront distribution manually, using the AWS console. Manually add both origin, set up the behaviors, and deploy.&lt;/p&gt;
&lt;p&gt;Nope, same issue.&lt;/p&gt;
&lt;p&gt;There must be a reason.&lt;/p&gt;
&lt;p&gt;Maybe I can look at the CloudFront logs again? There are so many fields. Maybe I missed something? Maybe if I cross my eyes just right it will dawn on me?&lt;/p&gt;
&lt;p&gt;And oh yes, dawn on me it did. The stupid, obvious little thing that I missed. Let me highlight it for you:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/cf-logs-2.png&quot; alt=&quot;CloudFront Logs showing a capitalization error on my PUT requests&quot;&gt;&lt;/p&gt;
&lt;p&gt;Do you see it? The GET request is being sent to &lt;code&gt;/comments/foo/bar&lt;/code&gt;, but the PUT request is being sent to &lt;code&gt;/Comments/foo/bar&lt;/code&gt;. Capital C. FML 😩&lt;/p&gt;
&lt;p&gt;So CloudFront was directing our PUT request to the S3 origin, which of course didn&#39;t have the PUT method enabled, and returned a 403.&lt;/p&gt;
&lt;p&gt;Fun fact, because my API gateway was set to proxy all traffic to my lambda, it didn&#39;t care about the capitalization. But CloudFront does.&lt;/p&gt;
&lt;p&gt;AWS explicitly call this out in their &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesPathPattern&quot;&gt;documentation&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/cf-path-patterns.png&quot; alt=&quot;A screenshot of the link above&quot;&gt;&lt;/p&gt;
&lt;p&gt;And the best part? Not only I &lt;em&gt;knew&lt;/em&gt; about this, I must have read that page at least 15 times while debugging this.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Anyway, to make a long story short, I fixed my frontend code to PUT to the right path, everything started working as expected 🎉&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I hope you enjoyed this little story. I certainly learned a lot about CloudFront, API Gateway, and Lambda while debugging this. My desk has a fresh head-shaped indentation, but &lt;em&gt;c&#39;est la vie&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Oh, and the comment system is not ready yet 😅, so feel free to discuss this on HN, or Reddit 😅&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The case of the mysteriously disappearing window</title>
    <link href="https://codemade.net/blog/the-case-of-the-disappearing-window/" />
    <updated>2023-12-16T00:00:00Z</updated>
    <id>https://codemade.net/blog/the-case-of-the-disappearing-window/</id>
    <content type="html">&lt;p&gt;One small annoyance I contend with everyday is links opening in the &amp;quot;wrong&amp;quot; browser. See, I use different browsers for different purposes.&lt;/p&gt;
&lt;p&gt;Chrome is the browser I use for all my work stuff, partially because I prefer its Dev tools and also because most SaaS apps I use for work tend to work better with it. Firefox however is my preferred browser and has the advantage of not being built by an advertisement company, so I tend to do all my personal browsing there. I also use Edge for Netflix because why not.&lt;/p&gt;
&lt;p&gt;So I spent a few hours hacking together &lt;a href=&quot;https://codemade.net/roundabout&quot;&gt;Roundabout&lt;/a&gt;, a simple app that registers itself as a web browser and allows you to pick the browser you&#39;d like to open your link in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/hero-roundabout.png&quot; alt=&quot;Roundabout screenshot&quot;&gt;&lt;/p&gt;
&lt;p&gt;Upon testing, everything looked good: I could click a link in a non-browser application like Telegram Desktop or WhatsApp desktop, and Roundabout would pop up asking me if I&#39;d prefer Chrome or Firefox.&lt;/p&gt;
&lt;h3&gt;...except for Slack&lt;/h3&gt;
&lt;p&gt;As it turned out, clicking a link from Slack would... do nothing?&lt;/p&gt;
&lt;p&gt;I started with the usual debugging techniques: add some logs, test again. Build fails. Wait, why? A quick glance at &lt;a href=&quot;https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer&quot;&gt;Process Explorer&lt;/a&gt; revealed the issue: Roundabout was starting, but somehow it got stuck and wasn&#39;t showing the main window. After terminating all the stuck instances, I was able to test again, this time with logs.&lt;/p&gt;
&lt;p&gt;The logs looked OK, but just stopped after showing the main form 🤔&lt;/p&gt;
&lt;p&gt;Next it was time to investigate &lt;em&gt;how&lt;/em&gt; exactly Slack starts a web browser.
A few minutes of sleuthing with &lt;a href=&quot;https://learn.microsoft.com/en-us/sysinternals/downloads/procmon&quot;&gt;Process Monitor&lt;/a&gt; got me my answer: when opening links Slack runs a new command, in the form of &lt;code&gt;RunDll32.exe URL.dll,FileProtocolHandler {the URL}&lt;/code&gt;; interesting 🤔.&lt;/p&gt;
&lt;p&gt;If you never crossed paths with it, RunDll32 is perhaps one of the most interesting applications in Windows. It allows one to invoke functions that are defined in Dll files, simply by calling &lt;code&gt;rundll32 {the dll},{the function} {any parameters}&lt;/code&gt;. In this case, we&#39;re executing the &lt;code&gt;FileProtocolHandler&lt;/code&gt; function that is defined in &lt;code&gt;URL.dll&lt;/code&gt;, and passing our URL to it.&lt;/p&gt;
&lt;p&gt;So what happens if I just run that command from a command prompt?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/rundll32.png&quot; alt=&quot;A command prompt showing that we executed Rundll32, with a Roundabout window open in front of it&quot;&gt;&lt;br&gt;Hmmm, it works as expected...&lt;/p&gt;
&lt;p&gt;I began to suspect that there was some sort of problem actually showing the form, when starting from Slack. What would happen if I added a MessageBox just before showing the form? Interestingly, the message box showed just fine, and then the main form also showed up fine 🤔&lt;/p&gt;
&lt;p&gt;Perhaps the issue is just that Roundabout shows up too quickly? I added a 1 second pause just before starting and... nothing, the main window wouldn&#39;t show again. I&#39;m kinda relieved because I really didn&#39;t want to add a huge pause before something happens for the user 😌&lt;/p&gt;
&lt;p&gt;Okay, so it looks like Slack &amp;quot;swallows&amp;quot; the first proper window we create. I wonder what happens if we temporarily create an empty window and immediately close it before showing the main window?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/roundabout_slack_works.png&quot; alt=&quot;A Roundabout window, open in front of a Slack conversation&quot;&gt;&lt;br&gt;🎉It works!&lt;/p&gt;
&lt;p&gt;At this point, it&#39;s been a few hours debugging this on and off, and a hacky workaround sounds like the perfect solution...&lt;/p&gt;
&lt;p&gt;🧹 Time to clean up the code a bit, and hide this monstrosity behind a helper function.&lt;/p&gt;
&lt;p&gt;It would be also good to only do this when starting from Slack, otherwise users would see an empty window flash on screen. Unfortunately there&#39;s no good way to find out what is your &amp;quot;parent&amp;quot; program, but the user likely clicked on the link with their mouse, so finding the right window based on the pointer coordinates should suffice.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/LBognanni/Roundabout/blob/1b8c675c81628b163bb860fc651c19fb9289c04d/src/Roundabout/Program.cs#L68-L88&quot;&gt;Here is the admittedly horrible code that does this&lt;/a&gt;!&lt;/p&gt;
&lt;h3&gt;Fin.&lt;/h3&gt;
&lt;p&gt;In unraveling the mystery of Slack links not cooperating with Roundabout, we&#39;ve delved into unexpected corners of Windows application development. With the code tweaks and insights gained, Roundabout now seamlessly handles links from various applications, including Slack.&lt;/p&gt;
&lt;p&gt;As with any solution, there&#39;s always room for improvement. Considerations for future versions include refining the code further, and perhaps adpoting this workaround for other apps I missed this time.&lt;/p&gt;
&lt;p&gt;If you do find one such case of an app &amp;quot;swallowing&amp;quot; Roundabout, please do &lt;a href=&quot;https://github.com/LBognanni/Roundabout/issues&quot;&gt;open an issue on Github&lt;/a&gt;!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to draw a conic gradient with System.Drawing in gdiPlus</title>
    <link href="https://codemade.net/blog/conic-gradient/" />
    <updated>2021-05-24T00:00:00Z</updated>
    <id>https://codemade.net/blog/conic-gradient/</id>
    <content type="html">&lt;p&gt;Recently, I have been working on adding support for conic gradient (or conical gradients, as some say) to &lt;a href=&quot;https://codemade.net/clock&quot;&gt;Clock&lt;/a&gt;. Now, Clock uses GdiPlus, or the &lt;code&gt;System.Drawing&lt;/code&gt; namespace in C# to do all of its drawing. My aim was to replicate the &lt;code&gt;conic-gradient()&lt;/code&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/conic-gradient()&quot;&gt;CSS function&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Imagine my surprise when searching online for &amp;quot;gdiplus conic gradient&amp;quot; yielded few and very old results. Ok, maybe you&#39;re not very surprised because all the cool kids are now writing Electron apps and mid-2000s &lt;code&gt;System.Drawing&lt;/code&gt; has been relegated to history 😄&lt;/p&gt;
&lt;p&gt;Like any other methods of filling a shape, we&#39;re going to need a type of &lt;code&gt;Brush&lt;/code&gt; to do it. The one we&#39;re going to use for conic gradients is the &lt;code&gt;PathGradientBrush&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What&#39;s interesting about &lt;code&gt;PathGradientBrush&lt;/code&gt; is that it can be used in many ways. &lt;a href=&quot;https://www.c-sharpcorner.com/uploadfile/puranindia/783/&quot;&gt;This article from C# Corner&lt;/a&gt; goes over a few, but crucially the one that&#39;s missing is a proper conic gradient.
After a lot of searching, I finally stumbled on &lt;a href=&quot;https://web.archive.org/web/20140906083853/http://bobpowell.net/pgb.aspx&quot;&gt;this article&lt;/a&gt;(the original website is now offline!) and noticed this image and code snippet:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://web.archive.org/web/20140906083853im_/http://bobpowell.net/images/pgb.ht2.jpg&quot; alt=&quot;&amp;quot;Many vertices, few colors&amp;quot;&quot;&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;  &lt;span class=&quot;token class-name&quot;&gt;GraphicsPath&lt;/span&gt; pth&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;GraphicsPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  pth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;AddEllipse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ClientRectangle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  &lt;span class=&quot;token class-name&quot;&gt;PathGradientBrush&lt;/span&gt; pgb&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;PathGradientBrush&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  pgb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SurroundColors&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Color&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Red&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Orange&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Yellow&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Green&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Blue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Indigo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Violet &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  pgb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CenterColor&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Gray&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Graphics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;FillRectangle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pgb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ClientRectangle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s not a &amp;quot;real&amp;quot; conic gradient, but close. What&#39;s interesting in that example is that it&#39;s setting a &lt;code&gt;SurroundColors&lt;/code&gt; property, but, because a circle in gdi+ has many vertices, only a tiny portion of the whole is occupied by a gradient. Of course the next step to try was using a polygon with the same number of vertices as the number of colors and check the result:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; pth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;GraphicsPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; colors &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Color&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Red&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Orange&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Yellow&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Green&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Blue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Indigo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Violet &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; rect &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Rectangle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sz&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sz&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; polyPoints &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;List&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Point&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;double&lt;/span&gt;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  polyPoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Point&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sz &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Cos&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PI &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;pts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sz&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sz &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Sin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PI &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;pts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sz&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
pth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;AddPolygon&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;polyPoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;PathGradientBrush&lt;/span&gt; pgb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;PathGradientBrush&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

pgb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SurroundColors &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; colors&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

g&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;FillRectangle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pgb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rect&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/poly-gradient-white.png&quot; alt=&quot;close but no dice&quot;&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s close, but where does the white come from? Well if you look closely at the circle example from the article above, there&#39;s a &lt;code&gt;CenterColor&lt;/code&gt; property that is set to Gray there. When you don&#39;t specify it, instead of there being no center color, a white shade is used instead. Setting CenterColor to transparent renders the filling itself transparent so that&#39;s no good either.&lt;/p&gt;
&lt;p&gt;But! &lt;code&gt;PathGradientBrush&lt;/code&gt; has a &lt;code&gt;Blend&lt;/code&gt; property that allows us to control the &lt;code&gt;CenterColor&lt;/code&gt;&#39;s opacity from the center to the border of the shape. We can just set it to fully opaque at both ends to get our gradient:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;pgb&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Blend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Blend&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Factors &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Positions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1f&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/poly-gradient-ok.png&quot; alt=&quot;success!&quot;&gt;&lt;/p&gt;
&lt;p&gt;Isn&#39;t that something! Now to make it really look like the kind of conic gradient we get in css, we have to make it so the last and the first color don&#39;t blend into each other. We can do this by creating a polygon that has one less vertex, where the last Point is repeated. We also need to specify the &lt;code&gt;CenterPoint&lt;/code&gt; property, otherwise Gdi+ will auto-calculate it as being slightly off-center:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/poly-gradient-conic.png&quot; alt=&quot;fully conic&quot;&gt;&lt;/p&gt;
&lt;p&gt;One last thing! You might have noticed that, while we&#39;re using &lt;code&gt;FillRectangle&lt;/code&gt; to paint our gradient, we&#39;re not really filling &lt;em&gt;all of it&lt;/em&gt;, but we&#39;re drawing a polygon. This is easily fixed by using a suitably large radius for our polygon.&lt;/p&gt;
&lt;p&gt;If you&#39;re interested to see what I ended up with, the &lt;a href=&quot;https://github.com/LBognanni/CodeMadeClock/blob/master/src/CodeMade.ScriptedGraphics/ConicGradient.cs&quot;&gt;full source code&lt;/a&gt; is open source as part of Clock 😊&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Fix Windows restarting after sleep or hybernate: the complete 2020 guide</title>
    <link href="https://codemade.net/blog/windows-restarts-itself-guide/" />
    <updated>2020-10-25T00:00:00Z</updated>
    <id>https://codemade.net/blog/windows-restarts-itself-guide/</id>
    <content type="html">&lt;p&gt;So your computer starts on its own after you put it to sleep or even after you turn it off. Maybe it does so immediately or maybe it waits for a few minutes or hours. But you feel like you can&#39;t trust it anymore: there&#39;s few things more unsettling than being woken up by the bright light of your screen in the middle of the night. Well here is my comprehensive list of things to check (and to turn off!) to prevent this from happening again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First off, open an admin Powershell terminal.&lt;/strong&gt; You can do so by right-clicking the start menu button and selecting &lt;code&gt;Windows PowerShell (Admin)&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;1: Check why the system restarted&lt;/h2&gt;
&lt;p&gt;The first thing to do when diagnosing the problem is to use the &lt;code&gt;powercfg /lastwake&lt;/code&gt; command to find out what prompted the system to restart.
If a hardware device was responsible for restarting windows, you should see it here.&lt;/p&gt;
&lt;p&gt;Sometimes, however, things are more nebulous and you get something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ powercfg /lastwake
Wake History Count - 1
Wake History [0]
  Wake Source Count - 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, follow the next steps to make double sure that nothing can wake up your pc.&lt;/p&gt;
&lt;h2&gt;2: Disable wake timers&lt;/h2&gt;
&lt;p&gt;This is fairly straightforward and should really be all you need.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open the Start menu and type &lt;code&gt;Edit power plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;Change advanced power settings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Navigate to &lt;code&gt;Sleep&lt;/code&gt; -&amp;gt; &lt;code&gt;Allow wake timers&lt;/code&gt; and select &lt;code&gt;Disable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Press OK and close&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/poweroptions.png&quot; alt=&quot;Power options dialog, disable windows wake timers&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In theory&lt;/em&gt;, the above steps should be enough to stop any device from waking up your computer. In practice, you&#39;ll probably have to explicitly disable more stuff:&lt;/p&gt;
&lt;h2&gt;3: Disable any peripherals that can wake up your computer&lt;/h2&gt;
&lt;p&gt;The main peripherals to take into account here are keyboards, mice and network adapters. The network adapter can be set to wake up your computer whenever it receives any data (a very bad idea!) or when it receives a special &amp;quot;wake up&amp;quot; packet.&lt;/p&gt;
&lt;p&gt;You can check which devices can wake up your pc with the command: &lt;code&gt;powercfg /devicequery wake_armed&lt;/code&gt;. You&#39;ll see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ powercfg /devicequery wake_armed
HID-compliant mouse (006)
HID Keyboard Device (006)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now comes the fun part. Open the Device manager by right-clicking the Windows button and selecting &lt;code&gt;Device Manager&lt;/code&gt; and, one by one, open the property pages for the devices listed above. In the &lt;code&gt;Power management&lt;/code&gt; tab, you&#39;ll find a flag that you want to disable: &lt;code&gt;Allow this device to wake the computer&lt;/code&gt;. Do so for each device. For your network card, you can optionally set it so that only a &amp;quot;magic packet&amp;quot; can wake up the computer. If you don&#39;t know what a magic packet is, it&#39;s safe to disable it 😅&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://codemade.net/images/badkeyboard.png&quot; alt=&quot;Power options for a keyboard in Windows device manager&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4: Find out if any scheduled tasks are waking up your computer&lt;/h2&gt;
&lt;p&gt;If the above steps didn&#39;t work, it&#39;s time to bring out the big guns. And by big guns I mean this powershell command: &lt;code&gt;Get-ScheduledTask | where {$_.settings.waketorun}&lt;/code&gt;. It will list all the scheduled tasks that are allowed to wake up your computer in order to perform some maintenance, or more likely to install updates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ Get-ScheduledTask | where {$_.settings.waketorun}

TaskPath                                       TaskName                          State
--------                                       --------                          -----
&#92;Microsoft&#92;Windows&#92;.NET Framework&#92;             .NET Framework NGEN v4.0.30319... Disabled
&#92;Microsoft&#92;Windows&#92;.NET Framework&#92;             .NET Framework NGEN v4.0.30319... Disabled
&#92;Microsoft&#92;Windows&#92;InstallService&#92;             WakeUpAndContinueUpdates          Disabled
&#92;Microsoft&#92;Windows&#92;InstallService&#92;             WakeUpAndScanForUpdates           Disabled
&#92;Microsoft&#92;Windows&#92;SharedPC&#92;                   Account Cleanup                   Disabled
&#92;Microsoft&#92;Windows&#92;UpdateAssistant&#92;            UpdateAssistantWakeupRun          Disabled
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Backup Scan                       Ready
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Reboot                            Ready
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Reboot_AC                         Disabled
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Universal Orchestrator Start      Ready
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my case, the &amp;quot;Backup Scan&amp;quot; task above was the culprit, so I disabled it. You could disable it by opening the Windows task scheduler, but sometimes the UI won&#39;t allow you to disable one of the &amp;quot;system&amp;quot; scheduled tasks like the ones related to Windows Update. But a simple powershell command can take care of it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ Disable-ScheduledTask  &amp;quot;&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;Backup Scan&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check again with the previous command, and it will be disabled:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ Get-ScheduledTask | where {$_.settings.waketorun}

TaskPath                                       TaskName                          State
--------                                       --------                          -----
&#92;Microsoft&#92;Windows&#92;.NET Framework&#92;             .NET Framework NGEN v4.0.30319... Disabled
&#92;Microsoft&#92;Windows&#92;.NET Framework&#92;             .NET Framework NGEN v4.0.30319... Disabled
&#92;Microsoft&#92;Windows&#92;InstallService&#92;             WakeUpAndContinueUpdates          Disabled
&#92;Microsoft&#92;Windows&#92;InstallService&#92;             WakeUpAndScanForUpdates           Disabled
&#92;Microsoft&#92;Windows&#92;SharedPC&#92;                   Account Cleanup                   Disabled
&#92;Microsoft&#92;Windows&#92;UpdateAssistant&#92;            UpdateAssistantWakeupRun          Disabled
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Backup Scan                       Disabled
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Reboot                            Ready
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Reboot_AC                         Disabled
&#92;Microsoft&#92;Windows&#92;UpdateOrchestrator&#92;         Universal Orchestrator Start      Ready
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now hopefully you can set your device to sleep mode and have some well deserved, uninterrupted sleep yourself! 😁&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;small&gt;&lt;em&gt;The feature image above was taken by &lt;a href=&quot;https://unsplash.com/@cbpsc1&quot;&gt;Clint Patterson&lt;/a&gt;&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to combine Docker and VSCode to reach development nirvana</title>
    <link href="https://codemade.net/blog/devcontainers/" />
    <updated>2020-10-10T00:00:00Z</updated>
    <id>https://codemade.net/blog/devcontainers/</id>
    <content type="html">&lt;p&gt;One of the best features in VS Code, and my personal favourite is called &lt;a href=&quot;https://code.visualstudio.com/docs/remote/containers&quot;&gt;Remote Containers&lt;/a&gt;.
What it does is open the folder you&#39;re working on &lt;em&gt;inside&lt;/em&gt; a container and then connect to it, so that you can, for example, run your jekyll site on windows without having to install ruby system-wide. Because all the development is done inside the container, you don&#39;t have to worry about having multiple versions of node or python cluttering your machine.&lt;/p&gt;
&lt;p&gt;This is especially useful if your main OS is Windows, as getting set up with some languages is notoriously tougher there.&lt;/p&gt;
&lt;p&gt;Working in a devcontainer has also the advantage that all the settings can be committed into source control, so you can share the exact same development environment with the rest of your team. No more 15-step procedure to follow to set up the local dev environment!&lt;/p&gt;
&lt;p&gt;Getting started is really simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the &lt;code&gt;Remote - Containers&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;Press F1 to open the command palette&lt;/li&gt;
&lt;li&gt;Run &amp;quot;Open folder in container&amp;quot; command&lt;/li&gt;
&lt;li&gt;Select &amp;quot;From a predefined container definition&amp;quot;&lt;/li&gt;
&lt;li&gt;Select the most appropriate container image.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will create:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;.devcontainer&lt;/code&gt; folder in your project, containing:&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;dockerfile&lt;/code&gt; for the development container&lt;/li&gt;
&lt;li&gt;A file named &lt;code&gt;devcontainer.json&lt;/code&gt; that is mostly used to define any ports that should be forwarded to the host&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can now open the integrated terminal and type commands directly in your container!&lt;/p&gt;
&lt;h2&gt;An example: this website&lt;/h2&gt;
&lt;p&gt;I love the simplicity that comes with static site generators, and &lt;a href=&quot;https://jekyllrb.com&quot;&gt;jekyll&lt;/a&gt; is by far the most popular choice. Using a development container setup allows me to edit and test any change on all my computers, without having to install ruby, bundler or any of jekyll&#39;s dependencies.&lt;/p&gt;
&lt;p&gt;This is the dockerfile I use for my jekyll devcontainer. It&#39;s a slightly modified version based on the official Microsoft one from some time ago; I had to add a line to install &lt;code&gt;zlib1g-dev&lt;/code&gt; otherwise it wouldn&#39;t work 😅.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/LBognanni/ed91a3b29089b85a23e67bbdafaa10c2&quot;&gt;See the dockerfile on github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, when I open the repository in Visual studio code, I just have to run &lt;code&gt;bundle exec jekyll serve&lt;/code&gt; in the integrated terminal to run it. The container exposes port 4000 so I can just open http://localhost:4000 in my browser and preview the changes.
What&#39;s more, if port 4000 is already used by another program, when clicking on the localhost link in the terminal, VS Code will transparently open the site on a different port!&lt;/p&gt;
&lt;p&gt;Found this useful? Want to add something? &lt;a href=&quot;https://twitter.com/lorisdev&quot;&gt;Reach out on twitter&lt;/a&gt;!&lt;/p&gt;
</content>
  </entry>
</feed>