<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>rockyourcode</title><link>https://www.rockyourcode.com/</link><description>Recent content on rockyourcode</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>CC-BY 4.0 {year} [Sophia Brandt](https://www.sophiabrandt.com)</copyright><lastBuildDate>Sat, 23 Aug 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://www.rockyourcode.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Use Anubis to Firewall AI Scraping</title><link>https://www.rockyourcode.com/use-anubis-to-firewall-ai-scraping/</link><pubDate>2025-08-23</pubDate><guid>https://www.rockyourcode.com/use-anubis-to-firewall-ai-scraping/</guid><description>&lt;p>I have been using a self-hosted &lt;a href="https://github.com/redlib-org/redlib">Redlib&lt;/a> as an alternative front-end for Reddit. I like Redlib better than the original UI, as it is quite simple and doesn&amp;rsquo;t bother me with any popups.&lt;/p>
&lt;p>My instance is not private, so in theory, everyone can use it. As I do not advertise my instance, the traffic on my server was negligible.&lt;/p>
&lt;p>However, lately AI scrapers seem to have discovered my instance. That has led to my instance being &lt;a href="https://github.com/redlib-org/redlib/issues/446">rate-limited&lt;/a>.&lt;/p>
&lt;p>One of the suggested solutions to this problem is using &lt;strong>&lt;a href="https://anubis.techaro.lol/">Anubis&lt;/a>&lt;/strong> as a firewall to discourage scrapers from abusing your website.&lt;/p>
&lt;blockquote>
&lt;p>Anubis is a Web AI Firewall Utility that weighs the soul of your connection using one or more challenges in order to protect upstream resources from scraper bots.
This program is designed to help protect the small internet from the endless storm of requests that flood in from AI companies. Anubis is as lightweight as possible to ensure that everyone can afford to protect the communities closest to them.&lt;/p>&lt;/blockquote>
&lt;h2 id="setup-anubis-with-docker-compose">Setup Anubis with Docker Compose&lt;/h2>
&lt;p>I use &lt;a href="https://www.rockyourcode.com/traefik-2-docker-swarm-setup-with-docker-socket-proxy-and-more/">Traefik with Docker Swarm&lt;/a> to deploy to a VPS.&lt;/p>
&lt;p>Adding Anubis was straightforward and very similar to the &lt;a href="https://anubis.techaro.lol/docs/admin/environments/docker-compose">docs&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">version&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#309;font-weight:bold">services&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">anubis&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">image&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>ghcr.io/techarohq/anubis:latest&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">environment&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">BIND&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;:8080&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">DIFFICULTY&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;5&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">METRICS_BIND&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;:9090&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">SERVE_ROBOTS_TXT&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">TARGET&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;http://redlib:8080&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">POLICY_FNAME&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;/data/cfg/botPolicy.yaml&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">OG_PASSTHROUGH&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">OG_EXPIRY_TIME&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#34;24h&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">volumes&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#c30">&amp;#34;/home/$SERVER_USER/anubis/botPolicy.yaml:/data/cfg/botPolicy.yaml:ro&amp;#34;&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">deploy&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">labels&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- traefik.enable=true&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- traefik.constraint-label=public&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- traefik.http.routers.anubis.entrypoints=websecure&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- traefik.http.routers.anubis.rule=&amp;lt;your URL&amp;gt;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- traefik.http.services.anubis.loadbalancer.server.port=8080&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#09f;font-style:italic"># other labels, e.g. tls options, rate-limiting, etc.&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">networks&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- public&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">depends_on&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- redlib&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">redlib&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">container_name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>redlib&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">image&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>quay.io/redlib/redlib&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">environment&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">REDLIB_ROBOTS_DISABLE_INDEXING&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">on&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#09f;font-style:italic"># No Traefik labels - accessed internally through Anubis only&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">networks&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- public&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#309;font-weight:bold">networks&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">public&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">external&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">true&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now Redlib is not accessible publicly, because &lt;a href="https://traefik.io/traefik">Traefik&lt;/a> will route the URL to Anubis instead of Redlib.&lt;/p>
&lt;h2 id="configuration-via-botpolicyyaml">Configuration via botPolicy.yaml&lt;/h2>
&lt;p>The &lt;a href="https://anubis.techaro.lol/docs/admin/policies/">documentation&lt;/a> has a minimal configuration example that you can use:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">bots&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#309;font-weight:bold">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>cloudflare-workers&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">headers_regex&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">CF-Worker&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>.*&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">action&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>DENY&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#309;font-weight:bold">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>well-known&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">path_regex&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>^/.well-known/.*$&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">action&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>ALLOW&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#309;font-weight:bold">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>favicon&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">path_regex&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>^/favicon.ico$&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">action&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>ALLOW&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#309;font-weight:bold">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>robots-txt&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">path_regex&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>^/robots.txt$&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">action&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>ALLOW&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#309;font-weight:bold">name&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>generic-browser&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">user_agent_regex&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>Mozilla&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">action&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>CHALLENGE&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alternatively, you can dowload the &lt;a href="https://github.com/TecharoHQ/anubis/blob/main/data/botPolicies.yaml">default policy&lt;/a> and comment/uncomment what&amp;rsquo;s needed.&lt;br>
I found the configuration a bit confusing, to be honest.&lt;/p>
&lt;p>If you use docker compose/Docker Swarm, make sure to copy the file to your VPS and use volume binding to make the yaml file accessible to your container.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://littlecxm.me/posts/202506/config-anubis-with-nginx/">Config Anubis With Nginx&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.rockyourcode.com/traefik-2-docker-swarm-setup-with-docker-socket-proxy-and-more/">Traefik 2 Docker Swarm Setup With Docker Socket Proxy and More&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://traefik.io/traefik">Official Traefik website&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://anubis.techaro.lol/">Anubis&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Use Local LLMs With Zed Dev Editor</title><link>https://www.rockyourcode.com/use-local-llms-with-zed-dev-editor/</link><pubDate>2025-08-15</pubDate><guid>https://www.rockyourcode.com/use-local-llms-with-zed-dev-editor/</guid><description>&lt;p>Lately, I wanted to see if I can run a local LLM on my Macbook Air M2 and to use it as a provider for the &lt;a href="https://zed.dev">Zed editor&lt;/a>.&lt;/p>
&lt;p>tldr; it works but is slow. You probably need a more powerful machine.&lt;br>
(I have since moved to &lt;a href="https://openrouter.ai/">OpenRouter&lt;/a> as my provider, however that is not a local option.)&lt;/p>
&lt;h2 id="run-a-local-llm">Run a Local LLM&lt;/h2>
&lt;p>I use &lt;a href="https://lmstudio.ai/">LM Studio&lt;/a>. Make sure to install the binary into the standard &lt;code>/Applications/&lt;/code>-folder if you&amp;rsquo;re using a Mac. In that case, the installer will also install the necessary CLI &lt;code>lms&lt;/code>.&lt;/p>
&lt;p>Download a model. You can do that via the GUI, see the &lt;a href="https://lmstudio.ai/docs/app/basics/download-model">docs of LM Studio&lt;/a>.&lt;/p>
&lt;p>I have tried:&lt;/p>
&lt;ul>
&lt;li>deepseek/deepseek-r1-0528-qwen3-8b&lt;/li>
&lt;li>all-hands_openhands-lm-7b-v0.1&lt;/li>
&lt;/ul>
&lt;p>Others didn&amp;rsquo;t work at all on my Macbook Air. Even those two are sluggish.&lt;/p>
&lt;p>Now make sure that the desired model is loaded. The CLI is the easiest way to make sure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>lms load &amp;lt;model of choice&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># e.g., lms load deepseek/deepseek-r1-0528-qwen3-8b&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Don&amp;rsquo;t forget to start the server! I was missing this step originally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>lms server start
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="zed-editor">Zed Editor&lt;/h2>
&lt;p>This is an example of how you could configure zed.dev (in &lt;code>settings.json&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;agent&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;default_profile&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;ask&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;default_model&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;provider&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;lmstudio&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;model&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;deepseek/deepseek-r1-0528-qwen3-8b&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;inline_assistant_model&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;provider&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;lmstudio&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;model&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;all-hands_openhands-lm-7b-v0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;commit_message_model&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;provider&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;lmstudio&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;model&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;all-hands_openhands-lm-7b-v0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;thread_summary_model&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;provider&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;lmstudio&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;model&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;all-hands_openhands-lm-7b-v0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The above example shows &lt;a href="https://zed.dev/docs/ai/agent-settings">feature-specific models for the different AI-powered features in the Zed editor&lt;/a>.&lt;/p></description></item><item><title>Testcontainers With Orbstack</title><link>https://www.rockyourcode.com/testcontainers-with-orbstack/</link><pubDate>2024-09-05</pubDate><guid>https://www.rockyourcode.com/testcontainers-with-orbstack/</guid><description>&lt;p>Lately, I needed to run &lt;a href="https://www.testcontainers.com/">testcontainers&lt;/a> with &lt;a href="https://orbstack.dev">Orbstack&lt;/a>.&lt;/p>
&lt;p>Unfortunately, running my integration tests with &lt;code>dotnet test&lt;/code> yielded the following error:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>System.AggregateException
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>One or more errors occurred. &lt;span style="color:#555">(&lt;/span>Docker is either not running or misconfigured.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Please ensure that Docker is running and that the endpoint is properly configured.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>You can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. For more information, visit:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the end, I used the same solution for Orbstack as I did for &lt;a href="https://www.rockyourcode.com/testcontainers-with-colima/">using testcontainer with colima&lt;/a>. I overrode the docker host detection by setting environment variables in my (Fish) shell:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">set&lt;/span> -x TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE /var/run/docker.sock
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">set&lt;/span> -x DOCKER_HOST unix://&lt;span style="color:#555">{&lt;/span>&lt;span style="color:#033">$HOME&lt;/span>&lt;span style="color:#555">}&lt;/span>/.orbstack/run/docker.sock
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Unfortunately, this does not seem to work within Rider. That&amp;rsquo;s why I am running those tests in my shell right now instead of my IDE.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.rockyourcode.com/testcontainers-with-colima/">Testcontainers With Colima&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>I Use Devbox and Direnv for Local Development Now</title><link>https://www.rockyourcode.com/i-use-devbox-and-direnv-for-local-development-now/</link><pubDate>2024-07-15</pubDate><guid>https://www.rockyourcode.com/i-use-devbox-and-direnv-for-local-development-now/</guid><description>&lt;p>I am currently &lt;a href="https://lets-go-further.alexedwards.net/">developing a Go REST API&lt;/a>. I need an environment variable in my shell for the database connection string. I also need to install a Golang package for SQL migrations.&lt;/p>
&lt;p>However, I do not want to pollute my machine. I hate to install the package globally just for this one use case.&lt;br>
Do I want to set my env variable globally? No!&lt;br>
But if I only set it temporarily, I need to repeat that every time.&lt;br>
What a hassle!&lt;/p>
&lt;p>Is there a way to isolate my local dev environment?&lt;/p>
&lt;p>There is!&lt;/p>
&lt;p>&lt;a href="https://www.jetify.com/blog/automated-dev-envs-with-devbox-and-direnv/">Enter devbox and direnv&lt;/a>!&lt;/p>
&lt;blockquote>
&lt;p>We built Devbox to make isolated shell environments easy to learn, use, and configure. The combination of easy shell environments with Devbox, combined with convenient environment switching with Direnv makes it simple to manage multiple projects without having their setups interfere or leak into one another.&lt;/p>&lt;/blockquote>
&lt;p>After installing both packages for MacOS, it was easy to create my environment.&lt;/p>
&lt;ol>
&lt;li>Run &lt;code>devbox init&lt;/code> in my shell (&lt;a href="https://fishshell.com">fish shell&lt;/a> works!)&lt;/li>
&lt;li>Add dependencies: &lt;code>devbox add go@latest golangci-lint@latest go-migrate@latest&lt;/code>&lt;/li>
&lt;li>Create an &lt;code>.env&lt;/code> file with my environment variiables.&lt;/li>
&lt;li>Run &lt;code>devbox generate direnv&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>The last command creates a pre-filled &lt;code>.envrc&lt;/code> file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># Automatically sets up your devbox environment whenever you cd into this&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># directory via our direnv integration:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">eval&lt;/span> &lt;span style="color:#c30">&amp;#34;&lt;/span>&lt;span style="color:#069;font-weight:bold">$(&lt;/span>devbox generate direnv --print-envrc&lt;span style="color:#069;font-weight:bold">)&lt;/span>&lt;span style="color:#c30">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># for more details&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add the following to the command: &lt;code>--env-file .env&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-diff" data-lang="diff">&lt;span style="display:flex;">&lt;span># Automatically sets up your devbox environment whenever you cd into this
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># directory via our direnv integration:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+ eval &amp;#34;$(devbox generate direnv --print-envrc --env-file .env)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># for more details
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now &lt;code>direnv&lt;/code> will also read from the environment file.&lt;/p>
&lt;p>I can also use the &lt;code>.env&lt;/code> file in my Go program via &lt;a href="https://github.com/joho/godotenv">GoDotEnv&lt;/a>.&lt;/p>
&lt;p>Now I can use my database connection string both in my shell as well as in my Go project.&lt;/p>
&lt;h2 id="further-reading">Further Reading&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.jetify.com/blog/automated-dev-envs-with-devbox-and-direnv/">Automate Project Environments with Devbox and Direnv&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Scheduled Deploys for Netlify Builds</title><link>https://www.rockyourcode.com/scheduled-deploys-for-netlify-builds/</link><pubDate>2024-03-18</pubDate><guid>https://www.rockyourcode.com/scheduled-deploys-for-netlify-builds/</guid><description>&lt;p>I want to trigger a re-deploy of my &lt;a href="https://gohugo.io">Hugo&lt;/a> blog which is hosted on &lt;a href="https://netlify.com">Netlify&lt;/a>.&lt;/p>
&lt;p>In the past, I&amp;rsquo;ve successfully used &lt;a href="https://www.11ty.dev/docs/quicktips/netlify-ifttt/">ifttt&lt;/a> with a webhook.&lt;br>
Unfortunately, they&amp;rsquo;ve stopped supporting the webhook feature on the free tier.&lt;/p>
&lt;p>What&amp;rsquo;s the solution?&lt;/p>
&lt;p>Netlify has a &lt;a href="https://www.netlify.com/blog/how-to-schedule-deploys-with-netlify/">tutorial&lt;/a> on how to use serverless functions for automatic deploys.&lt;/p>
&lt;p>Here is the concise tutorial with a few steps added which weren&amp;rsquo;t clear to me.&lt;/p>
&lt;h2 id="how-to-schedule-deploys-on-netlify-using-serverless-functions">How to schedule deploys on Netlify using serverless functions&lt;/h2>
&lt;p>You&amp;rsquo;ll need &lt;a href="https://nodejs.org">Node.js&lt;/a> on your machine.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Create a new &lt;code>npm&lt;/code> project in your current directory (Hugo folder) by running &lt;code>npm init -y&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Install the dependencies by running the following command in your terminal:&lt;/p>
&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npm install @netlify/functions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>npm install node-fetch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>Add the required configuration in &lt;code>netlify.toml&lt;/code>:&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>[functions]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node_bundler = &lt;span style="color:#c30">&amp;#34;esbuild&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="4">
&lt;li>Get your &lt;a href="https://docs.netlify.com/configure-builds/build-hooks/">build hook URL&lt;/a>.&lt;/li>
&lt;/ol>
&lt;p>The name doesn&amp;rsquo;t matter much. I&amp;rsquo;ve called mine &amp;ldquo;netlify serverless function&amp;rdquo;.&lt;/p>
&lt;ol start="5">
&lt;li>Set the URL as environment variable (replace with yours, below is a placeholder example):&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx netlify-cli env:set BUILD_HOOK_URI https://api.netlify.com/build_hooks/&amp;lt;xxxxxxxx&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="6">
&lt;li>Create a file &lt;code>/netlify/functions/scheduled-deploy.mjs&lt;/code>. It&amp;rsquo;s important that you choose the correct path (inside the &lt;code>/netlify/functions&lt;/code> directory).&lt;/li>
&lt;/ol>
&lt;p>(You can choose a different location, but in that case you&amp;rsquo;ll need to configure it in &lt;code>netlify.toml&lt;/code>. I didn&amp;rsquo;t want to bother with that.)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> fetch from &lt;span style="color:#c30">&amp;#39;node-fetch&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">try&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> BUILD_HOOK &lt;span style="color:#555">=&lt;/span> Netlify.env.get(&lt;span style="color:#c30">&amp;#39;BUILD_HOOK_URI&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> response &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> fetch(BUILD_HOOK, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;POST&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> Response(&lt;span style="color:#c30">&amp;#39;Build triggered&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#069;font-weight:bold">catch&lt;/span> (error) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> console.error(&lt;span style="color:#c30">&amp;#39;Error triggering build hook:&amp;#39;&lt;/span>, error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> Response(&lt;span style="color:#c30">&amp;#39;Error triggering build hook&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> status&lt;span style="color:#555">:&lt;/span> error.response&lt;span style="color:#555">?&lt;/span>.status &lt;span style="color:#555">?&lt;/span> error.response.status &lt;span style="color:#555">:&lt;/span> &lt;span style="color:#f60">500&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> statusText&lt;span style="color:#555">:&lt;/span> error.response&lt;span style="color:#555">?&lt;/span>.message &lt;span style="color:#555">??&lt;/span> &lt;span style="color:#c30">&amp;#34;Uh oh, this didn&amp;#39;t work&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Schedules the handler function to run every day at 19h UTC
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> config &lt;span style="color:#555">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> schedule&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;0 19 * * *&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Check &lt;a href="https://crontab.guru">crontab.guru&lt;/a> for a different schedule.&lt;/p>
&lt;p>If you want to know more about &lt;a href="https://docs.netlify.com/functions/scheduled-functions/">scheduled functions, you can check the docs&lt;/a>.&lt;/p>
&lt;ol start="7">
&lt;li>Test locally&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx netlify-cli dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The command will start a new local server. Open your webbrowser on &lt;code>http://localhost:8888/.netlify/functions/serverless-deploy&lt;/code>.&lt;/p>
&lt;p>This will trigger the function and you should see that you&amp;rsquo;ve started a new deploy successfully in your Netlify dashboard.&lt;/p>
&lt;ol start="8">
&lt;li>Source control and commit&lt;/li>
&lt;/ol>
&lt;p>I use git and GitLab/GitHub for my blog, so I&amp;rsquo;ve commited my changes and pushed them to my repository. The remote repo is connected to Netlify. Netlify should pick up the changes automatically.&lt;/p>
&lt;h2 id="further-reading">Further Reading&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.netlify.com/blog/how-to-schedule-deploys-with-netlify/">How to Schedule Deploys with Netlify&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>How to Set a Vite Proxy to Your Backend</title><link>https://www.rockyourcode.com/how-to-set-a-vite-proxy-to-your-backend/</link><pubDate>2024-03-03</pubDate><guid>https://www.rockyourcode.com/how-to-set-a-vite-proxy-to-your-backend/</guid><description>&lt;p>I&amp;rsquo;m currently working on a Vite React application with a Nest.js backend.&lt;/p>
&lt;p>The React app runs on &lt;code>localhost:3000&lt;/code> and the backend on &lt;code>localhost:4000&lt;/code>.&lt;br>
When I query the backend from React, I get a CORS error.&lt;/p>
&lt;p>After unsucessfully trying to enable cors for local development, I&amp;rsquo;ve gone back to using a &lt;a href="https://vitejs.dev/config/server-options.html#server-proxy">Proxy setting in my vite config&lt;/a>.&lt;br>
This behavior is similar to &lt;a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/">Create React App&amp;rsquo;s proxy setting&lt;/a>.&lt;/p>
&lt;p>In the &lt;code>viteconfig.ts&lt;/code> file, add the following to your configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// https://vitejs.dev/config/
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> defineConfig({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> port: &lt;span style="color:#078;font-weight:bold">3000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proxy&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;/api&amp;#39;&lt;/span>&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> target&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;http://localhost:4000&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rewrite&lt;span style="color:#555">:&lt;/span> (path) &lt;span style="color:#555">=&amp;gt;&lt;/span> path.replace(&lt;span style="color:#3aa">/^\/api/&lt;/span>, &lt;span style="color:#c30">&amp;#39;&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The setting re-routes all traffic on &lt;code>localhost:3000/api&lt;/code> to the backend url &lt;code>localhost:4000&lt;/code> and then removes the &lt;code>/api&lt;/code> suffix.&lt;/p>
&lt;p>In your application, target your backend API via &lt;code>localhost:3000/api&lt;/code> instead of the &amp;ldquo;real&amp;rdquo; URL &lt;code>localhost:4000&lt;/code>.&lt;/p>
&lt;p>For example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { ApolloClient, InMemoryCache } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;@apollo/client&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { API_URL } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;./urls&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> client &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> ApolloClient({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uri&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">`localhost:3000/api/graphql`&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;include&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#078;font-weight:bold">new&lt;/span> InMemoryCache(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Underneath the hood, the proxy will route to &lt;code>localhost:4000/graphql&lt;/code>. And you won&amp;rsquo;t have CORS errors.&lt;br>
Neat!&lt;/p>
&lt;h2 id="further-reading">Further Reading&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://vitejs.dev/config/server-options.html#server-proxy">Configuring Vite&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>I Want to Like Helix, But...</title><link>https://www.rockyourcode.com/i-want-to-like-helix-but/</link><pubDate>2023-10-27</pubDate><guid>https://www.rockyourcode.com/i-want-to-like-helix-but/</guid><description>&lt;p>I&amp;rsquo;ve been using the &lt;a href="https://helix-editor.com/">Helix editor&lt;/a> for a while now.&lt;/p>
&lt;p>At first I was confused by how the word motions worked but overall it was a good experience.&lt;/p>
&lt;p>Helix&amp;rsquo;s biggest selling point is that it works right away. Setting up LSP support is very easy since no plugins are needed.&lt;/p>
&lt;p>Helix also has multiple cursors which I&amp;rsquo;ve come to like for editing multiple lines comfortably. In Vim &lt;a href="https://medium.com/@schtoeffel/you-don-t-need-more-than-one-cursor-in-vim-2c44117d51db">you can work around it&lt;/a> but I prefer how Helix makes it easy.&lt;/p>
&lt;p>However&amp;hellip;&lt;/p>
&lt;p>Helix can&amp;rsquo;t beat Vim &lt;em>for me&lt;/em>.&lt;/p>
&lt;p>There are many things I love about Vim like &lt;a href="https://nikodoko.com/posts/vim-ranges/">ranges&lt;/a>, inserting the result of a command as text and other small things.&lt;/p>
&lt;p>Vim has thousands of little tricks that make it really powerful. While the learning curve is steep, once you master it you have a powerful tool at your hand.&lt;/p>
&lt;p>I haven&amp;rsquo;t reached that level with Helix and don&amp;rsquo;t think I will.&lt;/p>
&lt;p>That&amp;rsquo;s not to say Helix is a bad editor. It just isn&amp;rsquo;t right for my needs.&lt;/p></description></item><item><title>Working With C# on Macos M1 (Helix, Rider)</title><link>https://www.rockyourcode.com/working-with-c-sharp-on-macos-m1-helix-rider/</link><pubDate>2023-08-09</pubDate><guid>https://www.rockyourcode.com/working-with-c-sharp-on-macos-m1-helix-rider/</guid><description>&lt;p>I want to get into C# development, because my future project will likely be in C#.&lt;/p>
&lt;p>I wanted to use &lt;a href="https://www.jetbrains.com">Rider&lt;/a>, a JetBrains product. I am using IntelliJ (the flagship JetBrains IDE) at my day job, and I find it more powerful than VS Code.&lt;br>
I had a bit of difficulties to get Rider working on macOs.&lt;/p>
&lt;p>My alternative editor right now is &lt;a href="https://helix-editor.com">Helix&lt;/a>, a terminal editor.&lt;/p>
&lt;p>There were also some small hiccups with setting up a language server support for Helix.&lt;/p>
&lt;p>So here&amp;rsquo;s how I solved my problems with C# on macOs.&lt;/p>
&lt;h2 id="install-dotnet--mono">Install Dot.Net &amp;amp; Mono&lt;/h2>
&lt;p>I normally use &lt;a href="https://brew.sh">homebrew&lt;/a> to install my dependencies. Unfortunately, this doesn&amp;rsquo;t seem to play well with Rider.&lt;/p>
&lt;p>So, instead, I manually downloaded the official installers for both &lt;a href="https://dotnet.microsoft.com/en-us/download">Dot.net&lt;/a> and &lt;a href="https://www.mono-project.com/download/stable/">Mono&lt;/a>.&lt;/p>
&lt;p>Don&amp;rsquo;t forget to add Mono to your &lt;code>$PATH&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">export&lt;/span> &lt;span style="color:#033">PATH&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#033">$PATH&lt;/span>:/Library/Frameworks/Mono.framework/Versions/Current/bin/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you know how to get Rider working with homebrew, please &lt;a href="https://rockyourcode.com/contact">let me know&lt;/a>.&lt;/p>
&lt;h2 id="rider">Rider&lt;/h2>
&lt;p>&lt;a href="https://tarkan.dev/blog/mono-project-mac-jetbrains-rider">Follow this guide&lt;/a> to set your toolchain.&lt;/p>
&lt;p>Rider seems to have problems finding the installation folder, so head over the Rider settings under &lt;a href="https://www.jetbrains.com/help/rider/Settings_Toolset_and_Build.html">&amp;ldquo;Build, Execution, Deployment &amp;gt; Toolset and Build&amp;rdquo;&lt;/a>.&lt;/p>
&lt;p>You can find out your installation directory by running the following commands in your terminal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>which dotnet
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&amp;amp;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>which mono
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="helix">Helix&lt;/h2>
&lt;p>The &lt;a href="https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers#macos">documentation has a guide on how to add OmniSharp as the language server fo C#&lt;/a>.&lt;/p>
&lt;p>But first we need to install OmniSharp.&lt;/p>
&lt;p>Find the newest &lt;a href="https://github.com/OmniSharp/omnisharp-roslyn/releases">release on the official website&lt;/a>.&lt;/p>
&lt;p>I install my user packages into &lt;code>~/.local/bin&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># download &amp;amp; extract archive&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -sSL https://github.com/OmniSharp/omnisharp-roslyn/releases/download/v1.39.8/omnisharp-linux-arm64.tar.gz | tar xvzf - -C ~/.local/bin/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># make sure that we have the correct permissions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chmod &lt;span style="color:#f60">744&lt;/span> ~/.local/bin/omnisharp/*
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, add the language server in &lt;code>~/.config/helix/languages.toml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>[[language]]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>name = &lt;span style="color:#c30">&amp;#34;c-sharp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>language-server = { command = &lt;span style="color:#c30">&amp;#34;dotnet&amp;#34;&lt;/span>, args = [ &lt;span style="color:#c30">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;/Users/me/.local/bin/omnisharp/Omnisharp.dll&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;--languageserver&amp;#34;&lt;/span> ] }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You need to use the absolute path as &lt;a href="https://github.com/helix-editor/helix/issues/5538">Helix can&amp;rsquo;t yet expand &lt;code>~&lt;/code> to your home folder&lt;/a>.&lt;/p>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;p>The biggest hurdle was the Rider setup as I&amp;rsquo;ve been sifting through the support forum for JetBrains to find out why Rider doesn&amp;rsquo;t find mono or dotnet.&lt;br>
There were some issues for Linux users, but nothing about homebrew.&lt;br>
I think the underlying issue is that Rider just doesn&amp;rsquo;t work well without any folders that are not &amp;ldquo;standard&amp;rdquo;.&lt;br>
And it seems not to recognize the packages installed via homebrew into &lt;code>/opt/homebrew/bin&lt;/code>.&lt;/p></description></item><item><title>Nextjs 13 With Prisma (MySQL) Planetscale and Vercel</title><link>https://www.rockyourcode.com/nextjs-13-with-prisma-mysql-planetscale-and-vercel/</link><pubDate>2023-08-07</pubDate><guid>https://www.rockyourcode.com/nextjs-13-with-prisma-mysql-planetscale-and-vercel/</guid><description>&lt;p>In the last two weeks I&amp;rsquo;ve been hacking away on a Next.js 13 application to learn more about React server components.&lt;/p>
&lt;p>I have used Prisma with a local Postgres database for development. I normally spin up my databases via docker.&lt;/p>
&lt;p>When it came to deployment, it was easy enough to throw my app on Vercel, but I have been trouble finding a good free offering for Postgres.&lt;/p>
&lt;p>In the past, I have used Heroku for such toy projects, but they&amp;rsquo;ve stopped their free tier. There have been new companies stepping up, for example, Railway, Render.com, Fly.io or Supabase.&lt;/p>
&lt;p>Unfortunately, most of them have also placed limits on their free usages. Take Supabase: their Postgres instance only lives for 90 days, then it will be paused if the database doesn&amp;rsquo;t receive any requests. For a hobby project, that was not a good option for me. I don&amp;rsquo;t want to resume the Postgres database manually via the Supabase web interface.&lt;/p>
&lt;p>I had high hopes for Fly.io. They offer a usage-based plan with a generous free threshold.&lt;br>
Turns out that you need a &lt;a href="https://community.fly.io/t/connect-external-vercel-nextjs-13-prisma-application-to-fly-io-postgres-with-ipv6/">dedicated ipv4 address for the database for it to play nicely with Vercel&amp;rsquo;s edge functions&lt;/a>.&lt;br>
This costs USD $2/month, which I don&amp;rsquo;t want to pay for a pet project. Sorry, folks.&lt;/p>
&lt;p>In the end, I&amp;rsquo;ve switched my database to MySQL and used &lt;a href="https://planetscale.com/">Planetscale&lt;/a> as my production database. The experience was seemless.&lt;/p>
&lt;p>In the following article I&amp;rsquo;ll explain how I would set up a new project with Next.js and MySQL.&lt;/p>
&lt;p>This guide assumes that you have Node.js, Docker and docker compose installed and working.&lt;/p>
&lt;h2 id="local-development">Local Development&lt;/h2>
&lt;ol>
&lt;li>Create a new Next.js application&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx create-next-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add prisma:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npm i -D prisma
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>Setup MySQL via docker compose &amp;amp; Docker&lt;/li>
&lt;/ol>
&lt;p>Create a folder for the database:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>mkdir db
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add Dockerfile to it (&lt;code>db/Dockerfile&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-docker" data-lang="docker">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">FROM&lt;/span>&lt;span style="color:#c30"> bitnami/mysql:8.0.34&lt;/span>&lt;span style="color:#a00;background-color:#faa">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a00;background-color:#faa">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a00;background-color:#faa">&lt;/span>&lt;span style="color:#069;font-weight:bold">ADD&lt;/span> local-init.sql /docker-entrypoint-initdb.d&lt;span style="color:#a00;background-color:#faa">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We need an initialization script: &lt;code>db/local-init.sql&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">CREATE&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">USER&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#39;user&amp;#39;&lt;/span>&lt;span style="color:#555">@&lt;/span>&lt;span style="color:#c30">&amp;#39;%&amp;#39;&lt;/span>&lt;span style="color:#bbb"> &lt;/span>IDENTIFIED&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">BY&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#39;password&amp;#39;&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#069;font-weight:bold">GRANT&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">ALL&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">PRIVILEGES&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">ON&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#555">*&lt;/span>&lt;span style="color:#bbb"> &lt;/span>.&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#555">*&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#069;font-weight:bold">TO&lt;/span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#c30">&amp;#39;user&amp;#39;&lt;/span>&lt;span style="color:#555">@&lt;/span>&lt;span style="color:#c30">&amp;#39;%&amp;#39;&lt;/span>;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why?&lt;/p>
&lt;p>We want to reach the database on &lt;code>localhost&lt;/code> on our machine. By default, MySQL and MariaDB restrict connections other than to the local machine. The Docker container runs on a separate network. To connect from your local machine, you’ll need to use the % wildcard as the host.&lt;/p>
&lt;p>Now we need a &lt;code>docker-compose.yml&lt;/code> in the root folder of our project:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">services&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">db&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">build&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">context&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>./db&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">dockerfile&lt;/span>:&lt;span style="color:#bbb"> &lt;/span>Dockerfile&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">ports&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- &lt;span style="color:#f60">33306&lt;/span>:&lt;span style="color:#f60">3306&lt;/span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">environment&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- MYSQL_DATABASE=&amp;lt;database name&amp;gt;&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">volumes&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>- db-data:/var/lib/mysql:delegated&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb">&lt;/span>&lt;span style="color:#309;font-weight:bold">volumes&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#bbb"> &lt;/span>&lt;span style="color:#309;font-weight:bold">db-data&lt;/span>:&lt;span style="color:#bbb">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace the &lt;code>&amp;lt;database name&amp;gt;&lt;/code> with the name you want for your project.&lt;/p>
&lt;p>Now you should be able to use the database for local development via Docker and docker compose:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The connection string for MysQL would be:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>DATABASE_URL=&amp;#34;mysql://user:password@localhost:33306/&amp;lt;database-name&amp;gt;?schema=public&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(Replace the database name.)&lt;/p>
&lt;h3 id="prisma-schema">Prisma Schema&lt;/h3>
&lt;p>Create a &lt;a href="https://www.prisma.io/docs/concepts/components/prisma-schema">database schema&lt;/a>.&lt;/p>
&lt;p>Planetscale has some &lt;a href="https://www.prisma.io/docs/guides/database/planetscale#differences-to-consider">differences to other databases&lt;/a> which we need to take care of. For instance, you cannot use foreign key constraints, but need to &lt;a href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations/relation-mode#emulate-relations-in-prisma-with-the-prisma-relation-mode">emulate relations&lt;/a>.&lt;/p>
&lt;p>Here is how an example database schema would look like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>datasource db {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> provider &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#34;mysql&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url &lt;span style="color:#555">=&lt;/span> env(&lt;span style="color:#c30">&amp;#34;DATABASE_URL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> relationMode &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#34;prisma&amp;#34;&lt;/span> &lt;span style="color:#09f;font-style:italic">// use this to emulate relations
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>generator client {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> provider &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#34;prisma-client-js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>model User {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id Int &lt;span style="color:#069;font-weight:bold">@id&lt;/span> &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(autoincrement())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> createdAt DateTime &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(now())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> email &lt;span style="color:#366">String&lt;/span> &lt;span style="color:#069;font-weight:bold">@unique&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#366">String&lt;/span>&lt;span style="color:#555">?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> role Role &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(USER)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> posts Post[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>model Post {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id Int &lt;span style="color:#069;font-weight:bold">@id&lt;/span> &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(autoincrement())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> createdAt DateTime &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(now())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updatedAt DateTime &lt;span style="color:#069;font-weight:bold">@updatedAt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> published &lt;span style="color:#366">Boolean&lt;/span> &lt;span style="color:#069;font-weight:bold">@default&lt;/span>(&lt;span style="color:#069;font-weight:bold">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> title &lt;span style="color:#366">String&lt;/span> &lt;span style="color:#069;font-weight:bold">@db&lt;/span>.VarChar(&lt;span style="color:#f60">255&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> author User &lt;span style="color:#069;font-weight:bold">@relation&lt;/span>(fields&lt;span style="color:#555">:&lt;/span> [authorId], references&lt;span style="color:#555">:&lt;/span> [id])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authorId Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a00;background-color:#faa">@&lt;/span>&lt;span style="color:#069;font-weight:bold">@unique&lt;/span>([authorId, title]) &lt;span style="color:#09f;font-style:italic">// important
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#a00;background-color:#faa">@&lt;/span>&lt;span style="color:#069;font-weight:bold">@index&lt;/span>([authorId, author]) &lt;span style="color:#09f;font-style:italic">// important
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">enum&lt;/span> Role {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> USER
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADMIN
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It&amp;rsquo;s important to &lt;a href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations/relation-mode#indexes">use indexes&lt;/a> because Prisma won&amp;rsquo;t do it for you implicitly when using the relation mode.&lt;/p>
&lt;h2 id="deployment">Deployment&lt;/h2>
&lt;h3 id="planetscale">Planetscale&lt;/h3>
&lt;p>Create new database and &lt;a href="https://planetscale.com/docs/concepts/connection-strings">click on the button &amp;ldquo;Get connection strings&amp;rdquo;&lt;/a>.&lt;/p>
&lt;p>There&amp;rsquo;s an option for Prisma which you can use.&lt;/p>
&lt;p>You need one admin account to run the migrations and one read-write one for the actual application. I have simply used the &amp;ldquo;main&amp;rdquo; branch for my production database.&lt;/p>
&lt;p>Planetscale offers branching strategies (like git) for your database. I didn&amp;rsquo;t need those, as my development only happens locally, and I need Planetscale only for the final production deployment.&lt;/p>
&lt;p>For that, I use a hacky technique.&lt;/p>
&lt;p>I temporarily change my connection string in my local &lt;code>.env&lt;/code> file to the admin connection string of my Planetscale DB:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>DATABASE_URL=&amp;#34;mysql://xxxxx:xxxxxx@aws.connect.psdb.cloud/&amp;lt;database-name&amp;gt;?schema=public&amp;amp;sslaccept=strict&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Please replace with your &lt;em>admin&lt;/em> connection string. &lt;strong>Important&lt;/strong>: add &lt;code>sslaccept=strict&lt;/code>.&lt;/p>
&lt;p>Then run migrations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx prisma migrate deploy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For an alternative solution, you can read &lt;a href="https://shadcn.com/next-prisma-planetscale-vercel">the blog post by shadcn&lt;/a>.&lt;/p>
&lt;h3 id="vercel">Vercel&lt;/h3>
&lt;p>If you use GitHub or GitLab, it&amp;rsquo;s easy to deploy your Next.js application to Vercel.&lt;/p>
&lt;p>You can import the project and are good to go.&lt;/p>
&lt;p>As we&amp;rsquo;re using Prisma, there&amp;rsquo;s a &lt;a href="https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/vercel-caching-issue">little workaround needed for caching the Prisma client&lt;/a>. Add the following postinstall script to &lt;code>package.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;scripts&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;postinstall&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;prisma generate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add the database connection string for your normal read/write account to &lt;a href="https://vercel.com/docs/concepts/projects/environment-variables">the Vercel environment variables&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>DATABASE_URL=&amp;#34;mysql://xxxxx:xxxxxx@aws.connect.psdb.cloud/&amp;lt;database-name&amp;gt;?sslaccept=strict&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deploy and done.&lt;/p>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;p>Using Planetscale with Vercel is a dream combo. The integration is very easy. I was pleasantly surprised by Planetscale.&lt;br>
Especially in comparison to Fly.io it was very straightforward to spin up a database and connect it to external services (local machine and Vercel). They don&amp;rsquo;t even require a credit card for their hobby plan.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shadcn.com/next-prisma-planetscale-vercel">Next.js, Prisma, PlanetScale and Vercel&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Update Gulp to Use ESM</title><link>https://www.rockyourcode.com/update-gulp-to-use-esm/</link><pubDate>2023-08-04</pubDate><guid>https://www.rockyourcode.com/update-gulp-to-use-esm/</guid><description>&lt;p>I built my &lt;a href="https://sophiabrandt.com">personal website&lt;/a> in &lt;a href="https://11ty.dev">Eleventy&lt;/a> a few years ago.&lt;/p>
&lt;p>For this, I followed a course called &lt;a href="https://github.com/Andy-set-studio/learneleventyfromscratch.com">Learn Eleventy From Scratch&lt;/a>. A couple of years ago, Eleventy made a big splash as a simple JavaScript framework to build static websites.&lt;br>
Nowadays, &lt;a href="https://astro.build">Astro&lt;/a> is probably more popular, but I have been happy with Eleventy so far.&lt;/p>
&lt;p>My Eleventy site has a &lt;a href="https://learneleventyfromscratch.com/lesson/18.html">Gulp pipeline&lt;/a> to optimize images, get fonts, and transform sass.&lt;/p>
&lt;p>I used Common JS syntax:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> { src, dest } &lt;span style="color:#555">=&lt;/span> require(&lt;span style="color:#c30">&amp;#39;gulp&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> images &lt;span style="color:#555">=&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// more code
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>module.exports &lt;span style="color:#555">=&lt;/span> images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One of the packages in the pipeline is &lt;a href="https://github.com/sindresorhus/gulp-imagemin">gulp-imagemin&lt;/a>, which helps to minify PNG, JPG and other images.&lt;/p>
&lt;p>The author has made the package &lt;a href="https://github.com/sindresorhus/gulp-imagemin/releases/tag/v8.0.0">pure ESM&lt;/a> in its latest version, thus you cannot use it in a gulp pipeline that uses CommonJS.&lt;/p>
&lt;p>In the following blog post, I&amp;rsquo;ll write down how to transfer the gulp pipeline to ESM.&lt;/p>
&lt;p>This article follows the excellent guide &lt;a href="https://gist.github.com/noraj/007a943dc781dc8dd3198a29205bae04">Moving gulpfile from CommonJS (CJS) to ECMAScript Modules(ESM)&lt;/a>.&lt;/p>
&lt;h2 id="how-i-updated-my-gulp-pipeline-to-esm">How I updated my gulp pipeline to ESM&lt;/h2>
&lt;h3 id="1-update-packages">1. Update packages&lt;/h3>
&lt;p>I updated my packages to the latest versions. Here is the excerpt from &lt;code>package.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;devDependencies&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;get-google-fonts&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^1.2.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;gulp&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^4.0.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;gulp-clean-css&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^4.3.0&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;gulp-imagemin&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^8.0.0&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;gulp-sass&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^5.1.0&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;sass&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;^1.64.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-update-gulp-to-use-esm">2. Update gulp to use ESM&lt;/h3>
&lt;p>I changed the file endings of all gulp files to &lt;code>.mjs&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>gulp-tasks/fonts.mjs&lt;/li>
&lt;li>gulp-tasks/images.mjs&lt;/li>
&lt;li>gulp-tasks/sass.mjs&lt;/li>
&lt;li>gulpfile.mjs&lt;/li>
&lt;/ul>
&lt;h3 id="3-change-syntax">3. Change syntax&lt;/h3>
&lt;p>Now comes the tedious part where you have to adjust how to import and export your modules correctly.&lt;/p>
&lt;p>&lt;code>gulpfile.mjs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> gulp from &lt;span style="color:#c30">&amp;#39;gulp&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> { parallel, watch&lt;span style="color:#555">:&lt;/span> gulpWatch } &lt;span style="color:#555">=&lt;/span> gulp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Pull in each task
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> fonts from &lt;span style="color:#c30">&amp;#39;./gulp-tasks/fonts.mjs&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> images from &lt;span style="color:#c30">&amp;#39;./gulp-tasks/images.mjs&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> sass from &lt;span style="color:#c30">&amp;#39;./gulp-tasks/sass.mjs&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Set each directory and contents that we want to watch and
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// assign the relevant task. `ignoreInitial` set to true will
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// prevent the task being run when we run `gulp watch`, but it
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// will run when a file changes.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> watcher &lt;span style="color:#555">=&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> gulpWatch(&lt;span style="color:#c30">&amp;#39;./src/images/**/*&amp;#39;&lt;/span>, { ignoreInitial&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span> }, images)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> gulpWatch(&lt;span style="color:#c30">&amp;#39;./src/scss/**/*.scss&amp;#39;&lt;/span>, { ignoreInitial&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span> }, sass)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// The default (if someone just runs `gulp`) is to run each task in parallel
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> parallel(fonts, images, sass)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// This is our watcher task that instructs gulp to watch directories and
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// act accordingly
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> watch &lt;span style="color:#555">=&lt;/span> watcher
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>gulp-tasks/fonts.mjs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> GetGoogleFonts from &lt;span style="color:#c30">&amp;#39;get-google-fonts&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> fonts &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Setup of the library instance by setting where we want
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// the output to go. CSS is relative to output font directory
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> instance &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> GetGoogleFonts({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> outputDir&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;./dist/fonts&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cssFile&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;./fonts.css&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Grabs fonts and CSS from google and puts in the dist folder
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> result &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> instance.download(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;https://fonts.googleapis.com/css2?family=Literata:ital,wght@0,400;0,700;1,400&amp;amp;family=Red+Hat+Display:wght@400;900&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> result
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> fonts
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>gulp-tasks/images.mjs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> gulp from &lt;span style="color:#c30">&amp;#39;gulp&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> imagemin, { mozjpeg, optipng } from &lt;span style="color:#c30">&amp;#39;gulp-imagemin&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Grabs all images, runs them through imagemin
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// and plops them in the dist folder
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> images &lt;span style="color:#555">=&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// We have specific configs for jpeg and png files to try
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// to really pull down asset sizes
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> gulp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .src(&lt;span style="color:#c30">&amp;#39;./src/assets/images/**/*&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .pipe(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imagemin(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mozjpeg({ quality&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#f60">60&lt;/span>, progressive&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span> }),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> optipng({ optimizationLevel&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#f60">5&lt;/span>, interlaced&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#069;font-weight:bold">null&lt;/span> }),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> silent&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .pipe(gulp.dest(&lt;span style="color:#c30">&amp;#39;./dist/assets/images&amp;#39;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> images
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>gulp-tasks/sass.mjs&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> gulp from &lt;span style="color:#c30">&amp;#39;gulp&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> cleanCSS from &lt;span style="color:#c30">&amp;#39;gulp-clean-css&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> &lt;span style="color:#555">*&lt;/span> as dartSass from &lt;span style="color:#c30">&amp;#39;sass&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> gulpSass from &lt;span style="color:#c30">&amp;#39;gulp-sass&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> sassProcessor &lt;span style="color:#555">=&lt;/span> gulpSass(dartSass)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Flags whether we compress the output etc
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> __prod__ &lt;span style="color:#555">=&lt;/span> process.env.NODE_ENV &lt;span style="color:#555">===&lt;/span> &lt;span style="color:#c30">&amp;#39;production&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// An array of outputs that should be sent over to includes
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> criticalStyles &lt;span style="color:#555">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;critical.scss&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;home.scss&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;page.scss&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">&amp;#39;project.scss&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Takes the arguments passed by `dest` and determines where the output file goes
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> calculateOutput &lt;span style="color:#555">=&lt;/span> ({ history }) =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// By default, we want a CSS file in our dist directory, so the
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// HTML can grab it with a &amp;lt;link /&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">let&lt;/span> response &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#39;./dist/css&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Get everything after the last slash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> sourceFileName &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#3aa">/[^/]*$/&lt;/span>.exec(history[&lt;span style="color:#f60">0&lt;/span>])[&lt;span style="color:#f60">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// If this is critical CSS though, we want it to go
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// to the css directory, so nunjucks can include it
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// directly in a &amp;lt;style&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">if&lt;/span> (criticalStyles.includes(sourceFileName)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#39;./src/css&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// The main Sass method grabs all root Sass files,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// processes them, then sends them to the output calculator
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> sass &lt;span style="color:#555">=&lt;/span> () =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> gulp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .src(&lt;span style="color:#c30">&amp;#39;./src/scss/*.scss&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .pipe(sassProcessor().on(&lt;span style="color:#c30">&amp;#39;error&amp;#39;&lt;/span>, sassProcessor.logError))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .pipe(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cleanCSS(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __prod__
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#555">?&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> level&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#f60">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#555">:&lt;/span> {}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .pipe(gulp.dest(calculateOutput, { sourceMaps&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#555">!&lt;/span>__prod__ }))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> sass
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="recap">Recap&lt;/h2>
&lt;p>While a boring task, it&amp;rsquo;s actually pretty straightforward to support ESM for gulp and it works fine with Eleventy as a build pipeline.&lt;/p>
&lt;p>Check out the links below for more info:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://11ty.dev">11ty&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Andy-set-studio/learneleventyfromscratch.com">Learn Eleventy From Scratch&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://astro.build">Astro&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gist.github.com/noraj/007a943dc781dc8dd3198a29205bae04">Moving gulpfile from CommonJS (CJS) to ECMAScript Modules(ESM)&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>TIL: Insert Macros for the Helix Editor</title><link>https://www.rockyourcode.com/til-insert-macros-for-the-helix-editor/</link><pubDate>2023-07-25</pubDate><guid>https://www.rockyourcode.com/til-insert-macros-for-the-helix-editor/</guid><description>&lt;p>I&amp;rsquo;ve been using the &lt;strong>&lt;a href="https://helix-editor.com/">Helix editor&lt;/a>&lt;/strong> for two weeks now as my daily driver in my personal coding projects.&lt;br>
Helix is a modern terminal text editor that&amp;rsquo;s quite capable as a tool for writing code.&lt;/p>
&lt;blockquote>
&lt;p>A Kakoune / Neovim inspired editor, written in Rust.&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>Features&lt;/p>
&lt;ul>
&lt;li>Vim-like modal editing&lt;/li>
&lt;li>Multiple selections&lt;/li>
&lt;li>Built-in language server support&lt;/li>
&lt;li>Smart, incremental syntax highlighting and code editing via tree-sitter&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>After some initial bumps in the road due to my Vim muscle memory, I have grown quite fond of Helix. For my pet projects (TypeScript) it works well.&lt;/p>
&lt;p>The language server support is great. It offers me the convenience I am used to from other editors (IntelliJ) - auto-complete functionality, hover information and so forth.&lt;br>
Helix is not a fully-fledged IDE, but it doesn&amp;rsquo;t aim to be one. It is supposed to be an alternative to Kakoune or Vim/NeoVim.&lt;/p>
&lt;h2 id="insert-macros">Insert Macros&lt;/h2>
&lt;p>My NeoVim config sports an &amp;ldquo;insert macro&amp;rdquo; for the fat arrow (=&amp;gt;). When I type &amp;lsquo;hsr&amp;rsquo; in insert mode, the editor automatically replaces these three characters with a fat arrow (hashrocket).&lt;/p>
&lt;p>Here is how the key mapping looks in Vim:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span># custom/keymappings.vim
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>inoremap hsr =&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the same config in lua (NeoVim):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lua" data-lang="lua">&lt;span style="display:flex;">&lt;span>vim.api.nvim_set_keymap(&lt;span style="color:#c30">&amp;#39;i&amp;#39;&lt;/span>, &lt;span style="color:#c30">&amp;#39;hsr&amp;#39;&lt;/span>, &lt;span style="color:#c30">&amp;#39;=&amp;gt;&amp;#39;&lt;/span>, { silent &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span>, noremap &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">true&lt;/span> }),
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This can also be achieved in Helix:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span># ~/.config/helix/config.toml
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>h = { s = { r = [&amp;#34;normal_mode&amp;#34;, &amp;#34;:insert-output echo &amp;#39;=&amp;gt;&amp;#39;&amp;#34;, &amp;#34;collapse_selection&amp;#34;, &amp;#34;insert_at_line_end&amp;#34;] } }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://helix-editor.com/">Helix&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.sidebits.tech/vim-insert-mode-tips-tricks/">Vim insert mode tips &amp;amp; tricks&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Kickstart Your Neovim Journey With kickstart.nvim</title><link>https://www.rockyourcode.com/kickstart-your-neovim-journey-with-kickstart-nvim/</link><pubDate>2023-07-06</pubDate><guid>https://www.rockyourcode.com/kickstart-your-neovim-journey-with-kickstart-nvim/</guid><description>&lt;p>I&amp;rsquo;ve been curating my &lt;a href="https://github.com/sophiabrandt/dotfiles/tree/main/vim">Vim config&lt;/a> for several years and it has blown up to several files and a lot of obscure settings.&lt;br>
My Vim config is &lt;em>my own&lt;/em>, a very individual piece of software that fits my preferences when it comes to text editing.&lt;/p>
&lt;p>Since NeoVim 0.8, the Vim fork has introduced new groundbreaking features that allow for smarter autocompletion and language server support.&lt;/p>
&lt;p>Lua has also become the preferred way of configuring NeoVim, letting VimScript fall out of favor.&lt;/p>
&lt;p>I&amp;rsquo;ve been grudglingly spending some time to partly port my config to lua to take advantage of the new NeoVim features. But I wasn&amp;rsquo;t happy with my clobbled-together setup anymore.&lt;/p>
&lt;p>A friend on Discord suggested &lt;a href="https://www.lazyvim.org/">LazyVim&lt;/a> to me. LazyVim is a fully-fledged config that transforms the barebones NeoVim config into a powerful IDE.&lt;/p>
&lt;p>I tried it out a few weeks ago, but it was a little too much magic for me.&lt;br>
If you are looking for a &amp;ldquo;one-size-fits-all&amp;rdquo;-solution, look no further. If you are coming from a different editor like VS Code, LazyVim offers a nice experience out of the box.&lt;/p>
&lt;p>But for me, it felt as if I had to &amp;ldquo;learn a new editor&amp;rdquo; again. There are many plugins and configurations and you have to spend some time figuring out what&amp;rsquo;s going on.&lt;/p>
&lt;p>This weekend I discovered &lt;strong>&lt;a href="https://github.com/nvim-lua/kickstart.nvim">kickstart.nvim&lt;/a>&lt;/strong> which solves (almost) all my problems.&lt;/p>
&lt;p>kickstart.nvim is a minimal configuration that sets some defaults, installs the language server and a few other plugins, adds several key mappings. That&amp;rsquo;s it.&lt;br>
The configuration is much more digestable than the more fully-fledged LazyVim and I felt that I was able to understand easily what was going on.&lt;/p>
&lt;p>kickstart.nvim is meant as a starting point and it excells at that. Out of the box you get an agreeable NeoVim experience, but it doesn&amp;rsquo;t overwhelm you.&lt;br>
Adding your preferred plugins and keybindings is also quite easy.&lt;/p>
&lt;p>I&amp;rsquo;m pretty happy that I stumbled over this &lt;a href="https://github.com/nvim-lua/kickstart.nvim">GitHub repo&lt;/a>. Maybe it&amp;rsquo;s worth a look, if you&amp;rsquo;re interested in a minimal but sufficient NeoVim config.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://youtu.be/stqUbv-5u2s">YouTube&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/nvim-lua/kickstart.nvim">kickstart.nvim&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.lazyvim.org/">LazyVim&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>How to Download HTML Elements as PDF</title><link>https://www.rockyourcode.com/how-to-download-html-elements-as-pdf/</link><pubDate>2023-06-22</pubDate><guid>https://www.rockyourcode.com/how-to-download-html-elements-as-pdf/</guid><description>&lt;p>As software developers, we often come across situations where we need to provide users with the option to download HTML content as a PDF document. This capability can be especially valuable when we want to enable users to save web pages, reports, or other dynamically generated content for offline use or easy sharing. In this article, we will explore how to accomplish this using jsPDF, a widely used JavaScript library for generating PDF files, along with SVG.&lt;/p>
&lt;h2 id="example-in-angular">Example in Angular&lt;/h2>
&lt;p>Let&amp;rsquo;s consider a practical code example in Angular that demonstrates how to download an HTML element as a PDF with the help of jsPDF and svg rendering.&lt;/p>
&lt;p>However, the code can be used in other web frameworks like React.js, Svelte or vanilla JavaScript.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { elementToSVG } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;dom-to-svg&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { Canvg } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;canvg&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { jsPDF } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;jspdf&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// Angular component code
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">@Component&lt;/span>({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> selector&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;my-app&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> standalone: &lt;span style="color:#078;font-weight:bold">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imports&lt;span style="color:#555">:&lt;/span> [CommonModule],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> styles&lt;span style="color:#555">:&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#c30">`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> .save-as-pdf {
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> margin: 0;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> padding: 0;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> background-color: lightblue;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> `&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> template&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;button (click)=&amp;#34;onSaveAsPdf()&amp;#34;&amp;gt;Save as PDF&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;div class=&amp;#34;save-as-pdf&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;h1&amp;gt;Hello from {{ name }}!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;a target=&amp;#34;_blank&amp;#34; href=&amp;#34;https://angular.io/start&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;p&amp;gt;Learn more about Angular&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;p&amp;gt;Save HTML with jsPDF&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> `&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">class&lt;/span> App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#c30">&amp;#39;Angular&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> onSaveAsPdf() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Find the HTML element to convert to PDF
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> element &lt;span style="color:#555">=&lt;/span> &amp;lt;&lt;span style="color:#309;font-weight:bold">HTMLElement&lt;/span>&amp;gt;&lt;span style="color:#366">document&lt;/span>.querySelector(&lt;span style="color:#c30">&amp;#39;.save-as-pdf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> width &lt;span style="color:#555">=&lt;/span> element.clientWidth
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> height &lt;span style="color:#555">=&lt;/span> element.clientHeight
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Create SVG
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> svgDoc &lt;span style="color:#555">=&lt;/span> elementToSVG(element)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> svgString &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> XMLSerializer().serializeToString(svgDoc)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Create a canvas
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> canvas &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#366">document&lt;/span>.createElement(&lt;span style="color:#c30">&amp;#39;canvas&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> canvas.height &lt;span style="color:#555">=&lt;/span> height
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> canvas.width &lt;span style="color:#555">=&lt;/span> width
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> ctx &lt;span style="color:#555">=&lt;/span> canvas.getContext(&lt;span style="color:#c30">&amp;#39;2d&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">if&lt;/span> (&lt;span style="color:#555">!&lt;/span>ctx) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">throw&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> &lt;span style="color:#366">Error&lt;/span>(&lt;span style="color:#c30">&amp;#39;Error creating the HTML canvas with 2D context&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Render the SVG onto the canvas
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> canvg &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> Canvg.&lt;span style="color:#069;font-weight:bold">from&lt;/span>(ctx, svgString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> canvg.render()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Determine PDF orientation and create a PDF document
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> isLandscape &lt;span style="color:#555">=&lt;/span> width &lt;span style="color:#555">&amp;gt;&lt;/span> height
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> orientation &lt;span style="color:#555">=&lt;/span> isLandscape &lt;span style="color:#555">?&lt;/span> &lt;span style="color:#c30">&amp;#39;landscape&amp;#39;&lt;/span> &lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;portrait&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> doc &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> jsPDF(orientation, &lt;span style="color:#c30">&amp;#39;px&amp;#39;&lt;/span>, [width, height])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// Add the rendered canvas as a JPEG image to the PDF and save
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> doc.addImage(canvas, &lt;span style="color:#c30">&amp;#39;JPEG&amp;#39;&lt;/span>, &lt;span style="color:#f60">0&lt;/span>, &lt;span style="color:#f60">0&lt;/span>, width, height)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> doc.save(&lt;span style="color:#c30">&amp;#39;Hello from Angular.pdf&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To begin, we have an Angular component that includes a button labeled &amp;ldquo;Save as PDF&amp;rdquo; and an HTML element designated with the CSS class &lt;code>.save-as-pdf&lt;/code>. When users click the button, it triggers the &lt;code>onSaveAsPdf()&lt;/code> method.&lt;/p>
&lt;p>Inside the &lt;code>onSaveAsPdf()&lt;/code> method, the following steps are performed to download the HTML element as a PDF:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>The HTML element is identified using &lt;code>document.querySelector('.save-as-pdf')&lt;/code>. This allows us to target the specific element we want to convert to a PDF.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>We calculate the width and height of the element to determine the dimensions of the canvas and the resulting PDF.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The HTML element is transformed into an SVG document using the &lt;code>elementToSVG()&lt;/code> function. This step is essential.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The SVG document is serialized into a string using &lt;code>XMLSerializer()&lt;/code>. This conversion prepares the SVG for rendering onto a canvas.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>We create a canvas element with the calculated dimensions and obtain its 2D context. The canvas will serve as the rendering surface for the SVG.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Using the &lt;code>Canvg&lt;/code> library, we render the SVG onto the canvas. This process ensures that the visual representation of the HTML element is accurately captured.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Based on the dimensions of the element, we determine the orientation of the resulting PDF (landscape or portrait). Then, using jsPDF, we create a new PDF document with the appropriate orientation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The rendered canvas is added to the PDF as a JPEG image using the &lt;code>doc.addImage()&lt;/code> method. This step embeds the visual representation of the HTML element into the PDF.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Finally, we save the PDF with a specified filename using the &lt;code>doc.save()&lt;/code> method.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>You have seen a practical approach to enable users to download HTML content as a PDF document using jsPDF in conjunction with SVG rendering. The provided code example in Angular illustrates the step-by-step process of converting an HTML element into a PDF.&lt;/p>
&lt;p>By following the outlined steps, you can easily capture and save dynamic web content.&lt;/p>
&lt;h2 id="sourcelinks">Source/Links&lt;/h2>
&lt;p>Thanks go to Manuel Navarro for the &lt;a href="https://dev.to/_mnavarros/how-to-convert-html-to-pdf-using-angular-3jj8">original implementation using similar libraries&lt;/a>.&lt;/p>
&lt;p>If you want to see a live example of the article, you can check this &lt;a href="https://stackblitz.com/edit/stackblitz-starters-uxgpxd">Stackblitz&lt;/a>.&lt;/p></description></item><item><title>Use CSS Grid for Decorative Elements</title><link>https://www.rockyourcode.com/use-css-grid-for-decorative-elements/</link><pubDate>2023-03-09</pubDate><guid>https://www.rockyourcode.com/use-css-grid-for-decorative-elements/</guid><description>&lt;p>I learned this trick in &lt;a href="https://noti.st/rachelandrew/IQsXZC/css-layout-workshop">Rachel Andrew&amp;rsquo;s workshop&lt;/a>.&lt;/p>
&lt;p>HTML:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#309;font-weight:bold">h1&lt;/span>&amp;gt;My heading&amp;lt;/&lt;span style="color:#309;font-weight:bold">h1&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CSS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">h1&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">display&lt;/span>: &lt;span style="color:#069;font-weight:bold">grid&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">grid-template-columns&lt;/span>: &lt;span style="color:#f60">1&lt;/span>fr &lt;span style="color:#069;font-weight:bold">auto&lt;/span> &lt;span style="color:#f60">1&lt;/span>fr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">gap&lt;/span>: &lt;span style="color:#f60">1&lt;/span>&lt;span style="color:#078;font-weight:bold">em&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">h1&lt;/span>::&lt;span style="color:#99f">before&lt;/span>&lt;span style="color:#555">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#309;font-weight:bold">h1&lt;/span>::&lt;span style="color:#99f">after&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">content&lt;/span>: &lt;span style="color:#c30">&amp;#39;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">border-top&lt;/span>: &lt;span style="color:#f60">1&lt;/span>&lt;span style="color:#078;font-weight:bold">px&lt;/span> &lt;span style="color:#069;font-weight:bold">solid&lt;/span> &lt;span style="color:#069;font-weight:bold">black&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">align-self&lt;/span>: &lt;span style="color:#069;font-weight:bold">center&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the code example provided, we can see how CSS Grid is used to style a heading. The &amp;ldquo;h1&amp;rdquo; element is selected in the CSS, and then the &amp;ldquo;grid&amp;rdquo; property is applied. This tells the browser to use CSS Grid to create a layout for the heading.&lt;/p>
&lt;p>The &amp;ldquo;grid-template-columns&amp;rdquo; property specifies the number and width of the columns in the grid. In this case, there are three columns, with the center column set to &amp;ldquo;auto&amp;rdquo;. This means that the width of the center column will adjust automatically to fit the content within it.&lt;/p>
&lt;p>The &amp;ldquo;gap&amp;rdquo; property sets the spacing between the columns and rows in the grid. In this example, there is a 1em gap between each column.&lt;/p>
&lt;p>In addition to using CSS Grid to create the layout, the code also demonstrates how generated content can be used to add visual elements to the design. The &amp;ldquo;::before&amp;rdquo; and &amp;ldquo;::after&amp;rdquo; pseudo-elements are used to create horizontal lines above and below the heading. The &amp;ldquo;content&amp;rdquo; property is set to an empty string, so the pseudo-elements do not actually generate any content. Instead, they are used purely for styling purposes.&lt;/p>
&lt;p>&lt;a href="https://codepen.io/sophiabrandt/pen/YzOrRqB">Codepen Example&lt;/a>&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://noti.st/rachelandrew/IQsXZC/css-layout-workshop">CSS Layout Workshop&lt;/a> by Rachel Andrew&lt;/li>
&lt;li>&lt;a href="https://www.youtube.com/watch?v=yMEjLBKyvEg">YouTube&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Use @Hostbinding to Access CSS Variables in an Angular Component</title><link>https://www.rockyourcode.com/use-hostbinding-to-access-css-variables-in-an-angular-component/</link><pubDate>2023-03-07</pubDate><guid>https://www.rockyourcode.com/use-hostbinding-to-access-css-variables-in-an-angular-component/</guid><description>&lt;p>Last week, I needed to access a CSS variable in an Angular template to configure a &lt;a href="https://www.chartjs.org/">Chart.js&lt;/a> chart.&lt;/p>
&lt;p>You can use &lt;code>@HostBinding&lt;/code> to achieve this.&lt;/p>
&lt;p>What is &lt;code>@HostBinding&lt;/code>?&lt;/p>
&lt;p>&lt;code>@HostBinding&lt;/code> is an Angular decorator that allows developers to bind a directive property to a host element property.
It is used to set a property value on the host element of a component, in this case the component&amp;rsquo;s root element.
This decorator is used in conjunction with the host property of the &lt;code>@Directive&lt;/code> decorator to define a host element.&lt;/p>
&lt;p>Here&amp;rsquo;s an example of how to use @HostBinding to access a CSS variable in your Angular component.&lt;/p>
&lt;p>Define your CSS variable in your &lt;code>.css&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>:&lt;span style="color:#99f">root&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#033">--primary-color&lt;/span>: &lt;span style="color:#f60">#007bff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In your component, create a @HostBinding property to bind the value of your CSS variable to a CSS custom property:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">@HostBinding&lt;/span>(&lt;span style="color:#c30">&amp;#39;style.--primary-color&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>primaryColor &lt;span style="color:#555">=&lt;/span> getComputedStyle(&lt;span style="color:#366">document&lt;/span>.documentElement)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .getPropertyValue(&lt;span style="color:#c30">&amp;#39;--primary-color&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This code binds the value of the &lt;code>--primary-color&lt;/code> CSS custom property to the &lt;code>primaryColor&lt;/code> property of your component.&lt;/p>
&lt;p>Now you can access the variable in your Angular component to style your chart:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>datasets &lt;span style="color:#555">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> label&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;Dataset 1&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data: &lt;span style="color:#078;font-weight:bold">Utils.numbers&lt;/span>(NUMBER_CFG),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> borderColor: &lt;span style="color:#078;font-weight:bold">this.primaryColor&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// more datasets
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">//
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you change your styles in the &lt;code>.css&lt;/code> file, your Angular component will pick it up and use the correct color.&lt;/p>
&lt;h2 id="further-reading">Further Reading&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://netbasal.com/binding-css-variables-in-angular-69dfd4136e21">Binding CSS Variables in Angular&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://angular.io/api/core/HostBinding">&lt;code>@Hostbinding&lt;/code>&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Angular Workspaces as Alternative to Nx Monorepo</title><link>https://www.rockyourcode.com/angular-workspaces-as-alternative-to-nx-monorepo/</link><pubDate>2023-02-18</pubDate><guid>https://www.rockyourcode.com/angular-workspaces-as-alternative-to-nx-monorepo/</guid><description>&lt;p>Today I learned that you can create &lt;a href="https://angular.io/guide/file-structure">monorepo workspaces with Angular&lt;/a>.&lt;br>
I&amp;rsquo;ve always used &lt;a href="https://nx.dev">nx&lt;/a>, but if you don&amp;rsquo;t want to use a third-party tool, the built-in Angular capabilities might be enough.&lt;/p>
&lt;h2 id="angular-workspaces">Angular Workspaces&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ng new my-workspace --no-create-application
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">cd&lt;/span> my-workspace
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you use &lt;a href="https://pnpm.io">pnpm&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pnpm dlx @angular/cli new my-workspace --no-create-application --package-manager pnpm
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">cd&lt;/span> my-workspace
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This creates the following directory structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── README.md
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── angular.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── node_modules
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── package.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── pnpm-lock.yaml
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── tsconfig.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you can create new applications like so:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ng generate application my-first-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For pnpm:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pnpm dlx @angular/cli g application my-first-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or libraries:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pnpm dlx @angular/cli g lib my-first-lib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here is an example folder structure with a &lt;code>dashboard&lt;/code> app and a &lt;code>shared-ui&lt;/code> library:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── README.md
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── angular.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── package.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── pnpm-lock.yaml
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── projects
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── dashboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   ├── src
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   ├── app
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   ├── assets
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   ├── favicon.ico
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   ├── index.html
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   ├── main.ts
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   │   └── styles.scss
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   ├── tsconfig.app.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   └── tsconfig.spec.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   └── shared-ui
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── README.md
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── ng-package.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── package.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── src
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   ├── lib
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   │   └── public-api.ts
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── tsconfig.lib.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   ├── tsconfig.lib.prod.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│   └── tsconfig.spec.json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── tsconfig.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you can easily import components from the &lt;code>shared-ui&lt;/code> in your &lt;code>dashboard&lt;/code> app.&lt;/p>
&lt;p>Another option is to use npm workspaces or &lt;a href="https://pnpm.io/workspaces">pnpm workspaces&lt;/a>. I found a &lt;a href="https://12ft.io/blog.nrwl.io/setup-a-monorepo-with-pnpm-workspaces-and-speed-it-up-with-nx-bc5d97258a7e">good tutorial for creating a workspace with pnpm&lt;/a>, so I won&amp;rsquo;t rehash it here.&lt;/p>
&lt;p>If you use an npm/pnpm workspace &lt;em>together&lt;/em> with Angular workspaces, you should take care to let Angular handle the Angular parts and npm/pnpm to handle the parts.&lt;/p>
&lt;p>Why?&lt;/p>
&lt;p>npm expects that the output of the build folder is in the same folder as the library. But if you create an Angular library, the output of the build will be in the main dist folder. This confuses npm.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://angular.io/guide/file-structure">Angular Workspaces&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://12ft.io/blog.nrwl.io/setup-a-monorepo-with-pnpm-workspaces-and-speed-it-up-with-nx-bc5d97258a7e">Setup a Monorepo with PNPM workspaces and speed it up with Nx!&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Creating a Smooth Fade in Fade/Out Animation Effect With CSS</title><link>https://www.rockyourcode.com/creating-a-smooth-fade-in-fade-out-animation-effect-with-css/</link><pubDate>2023-02-16</pubDate><guid>https://www.rockyourcode.com/creating-a-smooth-fade-in-fade-out-animation-effect-with-css/</guid><description>&lt;p>Have you ever wanted to gradually float a toast message into your screen?&lt;/p>
&lt;p>One way to do this is by adding an animation effect, and a fade-in/fade-out transition is just the thing I needed.&lt;/p>
&lt;p>In this article, I want to show you how to create a smooth fade-in/fade-out effect using CSS, with a faster fade-in time and a slower fade-out time.&lt;/p>
&lt;p>To create this effect, we can use the following code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#0a8;font-weight:bold">fade-in-out&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">opacity&lt;/span>: &lt;span style="color:#f60">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">animation&lt;/span>: fade-in &lt;span style="color:#f60">1&lt;/span>&lt;span style="color:#078;font-weight:bold">s&lt;/span> &lt;span style="color:#069;font-weight:bold">ease-in&lt;/span> &lt;span style="color:#069;font-weight:bold">forwards&lt;/span>, fade-out &lt;span style="color:#f60">4&lt;/span>&lt;span style="color:#078;font-weight:bold">s&lt;/span> &lt;span style="color:#f60">1&lt;/span>&lt;span style="color:#078;font-weight:bold">s&lt;/span> &lt;span style="color:#069;font-weight:bold">ease-out&lt;/span> &lt;span style="color:#069;font-weight:bold">forwards&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@&lt;span style="color:#069;font-weight:bold">keyframes&lt;/span> &lt;span style="color:#309;font-weight:bold">fade-in&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">100&lt;/span>&lt;span style="color:#555">%&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">opacity&lt;/span>: &lt;span style="color:#f60">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@&lt;span style="color:#069;font-weight:bold">keyframes&lt;/span> &lt;span style="color:#309;font-weight:bold">fade-out&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">0&lt;/span>&lt;span style="color:#555">%&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">opacity&lt;/span>: &lt;span style="color:#f60">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">100&lt;/span>&lt;span style="color:#555">%&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">opacity&lt;/span>: &lt;span style="color:#f60">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s how it works: we start by defining a class called &amp;ldquo;fade-in-out&amp;rdquo; and apply it to the element we want to animate. We set the initial opacity to 0 to make the element invisible.&lt;/p>
&lt;p>Next, we define two keyframe animations: &amp;ldquo;fade-in&amp;rdquo; and &amp;ldquo;fade-out&amp;rdquo;. The &amp;ldquo;fade-in&amp;rdquo; animation gradually increases the opacity of the element from 0 to 1, while the &amp;ldquo;fade-out&amp;rdquo; animation gradually decreases the opacity from 1 to 0.&lt;/p>
&lt;p>The &amp;ldquo;fade-in&amp;rdquo; animation has a single keyframe at 100% that sets the opacity to 1. It runs for 1 second and uses the &amp;ldquo;ease-in&amp;rdquo; timing function to start slowly and speed up.&lt;/p>
&lt;p>The &amp;ldquo;fade-out&amp;rdquo; animation has two keyframes: one at 0% and one at 100%. The first keyframe sets the opacity to 1, so the element is fully visible at the start of the animation. The second keyframe sets the opacity to 0, so the element is fully invisible at the end. This animation runs for 4 seconds and uses the &amp;ldquo;ease-out&amp;rdquo; timing function, which means it starts quickly and slows down.&lt;/p>
&lt;p>Finally, we apply both animations to the &amp;ldquo;fade-in-out&amp;rdquo; class using the &amp;ldquo;animation&amp;rdquo; property. The &amp;ldquo;forwards&amp;rdquo; keyword means that the final state of the animation will be retained after it&amp;rsquo;s finished.&lt;/p>
&lt;p>To use this effect, simply apply the &amp;ldquo;fade-in-out&amp;rdquo; class to the element you want to animate. You can customize the duration and timing functions of the animations to create different effects.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition">MDN Docs: Transition&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Maximize Learning at Conferences and Meetups</title><link>https://www.rockyourcode.com/maximize-learning-at-conferences-and-meetups/</link><pubDate>2022-12-23</pubDate><guid>https://www.rockyourcode.com/maximize-learning-at-conferences-and-meetups/</guid><description>&lt;p>Here are some notes from the Youtube video &lt;a href="https://www.youtube.com/watch?v=RhaDn0ZcmOg">Maximize Learning At Conferences &amp;amp; Meetups with Joe Eames&lt;/a> with Brooke Avery and Joe Eames.&lt;/p>
&lt;h2 id="what-do-community-gatherings-offer">What do community gatherings offer?&lt;/h2>
&lt;p>The atmosphere creates a zeal that cannot be replicated from home.&lt;br>
If you want to get better at something, get involved in the community.&lt;br>
This creates opportunity.&lt;/p>
&lt;p>Some Tips:&lt;/p>
&lt;ul>
&lt;li>leave work at home&lt;/li>
&lt;li>stay and engage until they kick you out&lt;/li>
&lt;li>be involved&lt;/li>
&lt;li>showing up is 90% of the work&lt;/li>
&lt;/ul>
&lt;h2 id="what-should-you-do-if-youre-an-introvert">What should you do if you&amp;rsquo;re an introvert?&lt;/h2>
&lt;ul>
&lt;li>try 1-on-1&lt;/li>
&lt;li>participate in the activity (which can act as a frame for the social interaction)&lt;/li>
&lt;li>put down your phone&lt;/li>
&lt;li>be available&lt;/li>
&lt;li>other people will approach you, if you show up&lt;/li>
&lt;li>&amp;ldquo;think and share&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;h2 id="note-taking">Note Taking&lt;/h2>
&lt;ul>
&lt;li>take analog notes of keywords, so you can look it up later&lt;/li>
&lt;li>don&amp;rsquo;t use digital tools (to avoid distractions)&lt;/li>
&lt;li>take home new ideas&lt;/li>
&lt;/ul>
&lt;h2 id="learn-topics">Learn Topics&lt;/h2>
&lt;ul>
&lt;li>talk about what you want to learn, so others can help you&lt;/li>
&lt;/ul>
&lt;h2 id="meetups">Meetups&lt;/h2>
&lt;ul>
&lt;li>if you can, attend in person (offline)&lt;/li>
&lt;li>a good place to &amp;ldquo;learn to teach&amp;rdquo;, as the barrier is low&lt;/li>
&lt;/ul>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.youtube.com/watch?v=RhaDn0ZcmOg">Maximize Learning At Conferences &amp;amp; Meetups with Joe Eames | Roadmap to Learning Angular E5 | ng-conf&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Verify Your Online Accounts With Keyoxide</title><link>https://www.rockyourcode.com/verify-your-online-accounts-with-keyoxide/</link><pubDate>2022-11-16</pubDate><guid>https://www.rockyourcode.com/verify-your-online-accounts-with-keyoxide/</guid><description>&lt;blockquote>
&lt;p>&lt;a href="https://keyoxide.org/">Keyoxide&lt;/a> is a privacy-friendly t1. ool to create and verify decentralized online identities.&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>Just like passports for real life identities, Keyoxide can be used to verify the online identity of people to make sure one is interacting with whom they are supposed to be and not imposters. Unlike real life passports, Keyoxide works with online identities or &amp;ldquo;personas&amp;rdquo;, meaning these identities can be anonymous and one can have multiple separate personas to protect their privacy, both online and in real life.&lt;/p>&lt;/blockquote>
&lt;p>The tool helps you to verify your online profiles like GitHub, Mastodon, dev.to and others.&lt;/p>
&lt;p>Here is my &lt;a href="https://keyoxide.org/4C38CC1AF264C3DE8D6D97283B31E6D41B517DA5">profile&lt;/a>.&lt;/p>
&lt;p>&lt;em>Acknowledgements&lt;/em>:&lt;br>
I used &lt;a href="https://code.rawlinson.us/2022/11/setup-keyoxide-account-on-mac.html">Bill Rawlinson&amp;rsquo;s guide&lt;/a> as a reference.&lt;br>
During setup, I encountered some pitfalls. This article is an attempt to clarify and document the process by rewriting the original source.&lt;/p>
&lt;p>In this article you&amp;rsquo;ll learn:&lt;/p>
&lt;ul>
&lt;li>how to setup a GPG key and what to do to use it with keyoxide&lt;/li>
&lt;li>how to verify your Mastodon, dev.to &amp;amp; GitHub account&lt;/li>
&lt;li>how to setup a keyoxide account&lt;/li>
&lt;/ul>
&lt;h2 id="what-do-i-need">What Do I Need?&lt;/h2>
&lt;p>Keyoxide is a weird beast.&lt;br>
It wasn&amp;rsquo;t clear to me how to get a keyoxide account.&lt;br>
Do I need to sign up somewhere?&lt;/p>
&lt;p>The answer is: No, you don&amp;rsquo;t need to sign up for keyoxide.&lt;/p>
&lt;p>But you need to create a &lt;a href="https://wiki.archlinux.org/title/GnuPG">GPG key pair&lt;/a> and you need to upload it to &lt;a href="https://keys.openpgp.org/upload">keys.openpgp.org&lt;/a>.&lt;/p>
&lt;p>You&amp;rsquo;ll also need a &lt;em>valid&lt;/em> email address.&lt;br>
&lt;em>This email address will be public on the Keyoxide website.&lt;/em>&lt;/p>
&lt;p>The GPG key pair needs a secure passphrase, so a password manager is recommended.&lt;/p>
&lt;h3 id="gpg-setup">GPG Setup&lt;/h3>
&lt;p>First, we need &lt;a href="https://wiki.archlinux.org/title/GnuPG">GnuPGP&lt;/a> on our local machine.&lt;/p>
&lt;p>On macOs, you can install it via &lt;a href="https://formulae.brew.sh/formula/gnupg#default">homebrew&lt;/a>.&lt;/p>
&lt;p>In your terminal, type:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>brew install gnupg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Arch Linux (with &lt;a href="https://wiki.archlinux.org/title/AUR_helpers">yay&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>yay -S gnupg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="create-a-key-pair">Create a Key Pair&lt;/h4>
&lt;p>Again, you need to use the terminal:&lt;/p>
&lt;p>macOs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --full-generate-key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>(Arch) Linux:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --full-gen-key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>Choose &lt;code>RSA and RSA&lt;/code> (option 1).&lt;/li>
&lt;li>Keysize: 4096&lt;/li>
&lt;li>Expiration date: 2y (2 years, you can extend the expiration)&lt;/li>
&lt;li>Real name: you don&amp;rsquo;t need to use your real name, but this is the handle which will appear on your keyoxide side&lt;/li>
&lt;li>email: use an email address &lt;em>that works and that you have access to&lt;/em> (you can also add more email addresses later)&lt;/li>
&lt;li>optional comment: leave blank&lt;/li>
&lt;li>secure passphrase: use your password manager to create a password (and save it in your password manager together with the email address!)&lt;/li>
&lt;/ul>
&lt;p>You&amp;rsquo;ll be asked to generate some randomness, so you can move your cursor to help GPG to create your key.&lt;/p>
&lt;h3 id="get-your-fingerprint">Get Your Fingerprint&lt;/h3>
&lt;p>In your terminal, run the following command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg -k
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The answer will look like similar to this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pub rsa4096 2020-07-01 &lt;span style="color:#555">[&lt;/span>SC&lt;span style="color:#555">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;HERE IS YOUR FINGERPRINT&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>uid &lt;span style="color:#555">[&lt;/span>ultimate&lt;span style="color:#555">]&lt;/span> My name &amp;lt;valid@email.address&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Your keyoxide URL will be &lt;code>https://keyoxide.org/FINGERPRINT&lt;/code>. It will not work right now, but we&amp;rsquo;ll come back to it later.&lt;/p>
&lt;p>I know, it is confusing.&lt;/p>
&lt;p>&lt;a href="https://docs.keyoxide.org/using-cryptography/openpgp-gnupg/#Obtaining_the_fingerprint">More info on the Keyoxide website&lt;/a>.&lt;/p>
&lt;h2 id="add-your-accounts">Add Your Accounts&lt;/h2>
&lt;h3 id="mastodon">Mastodon&lt;/h3>
&lt;p>For Mastodon, you&amp;rsquo;ll need to set &lt;a href="https://docs.joinmastodon.org/user/profile/#fields">profile metadata&lt;/a>.&lt;/p>
&lt;ol>
&lt;li>go to your profile in Mastodon (&lt;code>https://&amp;lt;your instance url&amp;gt;/profile/settings/profile&lt;/code>)&lt;/li>
&lt;li>edit your profile&lt;/li>
&lt;li>scroll down to &amp;ldquo;Profile metadata&amp;rdquo;&lt;/li>
&lt;li>add a label &amp;ldquo;keyoxide&amp;rdquo;&lt;/li>
&lt;li>as content add your keyoxide URL (&lt;code>https://keyoxide.org/FINGERPRINT&lt;/code>)&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://docs.keyoxide.org/service-providers/mastodon/">Read more about Mastodon on the keyoxide docs&lt;/a>.&lt;/p>
&lt;h3 id="github">GitHub&lt;/h3>
&lt;p>&lt;a href="https://gist.github.com/new">Create a new &lt;strong>public&lt;/strong> GitHub gist&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Important&lt;/strong>: The file name &lt;em>must be&lt;/em> &lt;code>openpgp.md&lt;/code>!&lt;/p>
&lt;p>The description for the file can be whatever you like.&lt;/p>
&lt;p>As content for the file, add the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-md" data-lang="md">&lt;span style="display:flex;">&lt;span>[Verifying my cryptographic key:openpgp4fpr:FINGERPRINT]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gist.github.com/sophiabrandt/5351d90f64a3e2c28f62621d8def13b6">Here is my example Gist.&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://docs.keyoxide.org/service-providers/github/">Read more about GitHub on the keyoxide docs&lt;/a>.&lt;/p>
&lt;h3 id="devto">dev.to&lt;/h3>
&lt;p>Make a new blog post. The title does not matter, I chose &amp;ldquo;Keyoxide Proof&amp;rdquo; (see &lt;a href="https://dev.to/sophiabrandt/keyoxide-proof-42o6">example&lt;/a>).&lt;/p>
&lt;p>The content of the post is the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>[Verifying my keyoxide cryptographic key: https://keyoxide.org/FINGERPRINT]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Yes, it&amp;rsquo;s a public post which will look strange.&lt;/p>
&lt;h2 id="add-all-your-proofs-to-your-gpg-key">Add All Your Proofs to Your Gpg Key&lt;/h2>
&lt;p>Now we need to go back to the command-line.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --edit-key YOUR_EMAIL_ADDRESS
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --edit-key FINGERPRINT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace with your email address.&lt;/p>
&lt;p>This will open a command prompt.&lt;/p>
&lt;ol>
&lt;li>type: &lt;code>uid 1&lt;/code> (to select your user ID)&lt;/li>
&lt;li>type: &lt;code>notation&lt;/code>&lt;/li>
&lt;li>enter the notation: &lt;code>proof@ariadne.id=https://URL_TO_YOUR_GIST&lt;/code> (replace with your Gist URL)&lt;/li>
&lt;li>you will be asked for your passphrase, enter it now (you used a password manager, right?)&lt;/li>
&lt;li>repeat the process for your other accounts, type &lt;code>notation&lt;/code> again&lt;/li>
&lt;li>enter the notation: &lt;code>proof@ariadne.id=https://dev.to/YOUR_USERNAME/BLOG_POST_URL&lt;/code> (replace with your dev.to blog post URL)&lt;/li>
&lt;li>same again for all other accounts&lt;/li>
&lt;/ol>
&lt;p>For example, the notation for Mastodon is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>proof@ariadne.id&lt;span style="color:#555">=&lt;/span>https://YOUR_MASTODON_INSTANCE/@YOUR_USERNAME
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>proof@ariadne.id&lt;span style="color:#555">=&lt;/span>https://hachyderm.io/@sbr
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="how-to-show-your-notations-for-a-key">How to Show Your Notations for a Key?&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --edit-key FINGERPRINT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Show a list of user IDs to find the index, select it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>list
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>uid N &lt;span style="color:#09f;font-style:italic"># for example: uid 1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Show a list of notations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>showpref
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="how-to-delete-a-notation">How to Delete a Notation?&lt;/h2>
&lt;p>What happens if you made a mistake?&lt;/p>
&lt;p>To delete an existing notation, you need to add it again, but with a minus symbol:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>-key&lt;span style="color:#555">=&lt;/span>value
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="upload-your-gpg-key">Upload Your GPG Key&lt;/h2>
&lt;p>Finally, you need to &lt;a href="https://keys.openpgp.org/upload">upload your &lt;strong>public key&lt;/strong> to the OpenPGP.org&lt;/a>.&lt;/p>
&lt;p>First, we&amp;rsquo;ll need to find a way to export our &lt;strong>public key&lt;/strong> for upload. In your terminal, type the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>gpg --armor --export YOUR_EMAIL_ADRESS &amp;gt; pubkey.asc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace with your email address. Don&amp;rsquo;t forget the greater than sign (&lt;code>&amp;gt;&lt;/code>).&lt;/p>
&lt;p>Open &lt;a href="https://keys.openpgp.org/upload">the OpenPGP website and upload the &lt;code>pubkey.asc&lt;/code>&lt;/a>.&lt;/p>
&lt;p>Now you can go to your keyoxide URL and check if it works.&lt;br>
It might take a few minutes, but the process is reasonably fast.&lt;/p>
&lt;h2 id="what-happens-if-i-want-to-add-more-accounts">What Happens if I Want to Add More Accounts?&lt;/h2>
&lt;p>If you later want to add more accounts, you can go through the process again.&lt;/p>
&lt;p>First, find a way to way to &lt;a href="https://docs.keyoxide.org">create a proof&lt;/a>, then edit the GPG key.&lt;br>
&lt;a href="https://keys.openpgp.org/upload">Upload the key&lt;/a>.&lt;/p>
&lt;p>Keyoxide will pick up the changes.&lt;/p>
&lt;h2 id="how-can-i-export-my-key-pair">How Can I Export My Key Pair?&lt;/h2>
&lt;p>If you want to backup your key pair, you can &lt;a href="https://linuxhint.com/export-import-keys-with-gpg/">read this article on how to export both the public and private key&lt;/a>.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://code.rawlinson.us/2022/11/setup-keyoxide-account-on-mac.html">Setting Up Keyoxide Profile on a Mac!&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.joinmastodon.org/user/profile/#fields">Mastodon: Profile Metadata&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://wiki.archlinux.org/title/GnuPG">GnuPG&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://linuxhint.com/export-import-keys-with-gpg/">How to export and import keys with GPG&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Use Environment Variables With VuePress 2</title><link>https://www.rockyourcode.com/use-environment-variables-with-vuepress-2/</link><pubDate>2022-11-07</pubDate><guid>https://www.rockyourcode.com/use-environment-variables-with-vuepress-2/</guid><description>&lt;p>For &lt;code>$DAYJOB&lt;/code> I had to build a new feature for our internal &lt;strong>&lt;a href="https://v2.vuepress.vuejs.org/">VuePress 2&lt;/a>&lt;/strong> documentation.&lt;/p>
&lt;p>I needed to fetch data from an API that needs an API token. That&amp;rsquo;s why I wanted to use &lt;a href="https://www.freecodecamp.org/news/what-are-environment-variables-and-how-can-i-use-them-with-gatsby-and-netlify/">environment variables&lt;/a> to store the token, both locally as well in our &lt;a href="https://www.redhat.com/en/topics/devops/what-cicd-pipeline">CI pipeline&lt;/a>.&lt;/p>
&lt;p>Surprisingly this was hard to figure out. It didn&amp;rsquo;t help that I&amp;rsquo;ve never worked with &lt;a href="https://vuejs.org">Vue.js&lt;/a> or VuePress. The documentation was of limited help.&lt;/p>
&lt;p>For example, I stumbled upon the famous error when trying to use &lt;code>process.env&lt;/code> in a Vue component:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>process is not defined
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I&amp;rsquo;ll show you how you can use environment variables in VuePress in the following article.&lt;/p>
&lt;h2 id="goal">Goal&lt;/h2>
&lt;p>We want to be able to use a local file called &lt;code>.env&lt;/code> (or something similar like &lt;code>.env.local&lt;/code>) to store sensitive data.&lt;/p>
&lt;p>Example content of the &lt;code>.env&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-env" data-lang="env">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#033">GOOGLE_API_TOKEN&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#c30">&amp;#39;abcdefghi12345&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We want to be able to use this token in the &lt;a href="https://vuejs.org/guide/introduction.html#single-file-components">JavaScript part of Vue&lt;/a>.&lt;/p>
&lt;p>Example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-vue" data-lang="vue">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> fetch from &lt;span style="color:#c30">&amp;#39;cross-fetch&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>async &lt;span style="color:#069;font-weight:bold">function&lt;/span> fetchData() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> response &lt;span style="color:#555">=&lt;/span> await fetch(&lt;span style="color:#c30">&amp;#39;some-url&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;GET&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// here we need our token 👇
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> Authorization&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">`Token token=`&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> response.json()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;details>
&lt;summary>Note about cross-fetch&lt;/summary>
&lt;p>I've installed this library to be able to use the fetch API in Node.js during VuePress's generate step.&lt;/p>
&lt;p>In the generate phase Node.js builds all pages, so we don't have the fetch API (browser-only until Node 18) at our disposal.&lt;/p>
&lt;/details>
&lt;h2 id="problem">Problem&lt;/h2>
&lt;p>I couldn&amp;rsquo;t find a guide in the &lt;a href="https://v2.vuepress.vuejs.org/guide">VuePress documentation&lt;/a>, the &lt;a href="https://stackoverflow.com/questions/53669076/how-do-i-expose-node-environment-variables-via-configurewebpack-to-vuepress-comp">top StackOverflow question&lt;/a> seems outdated, and the &lt;a href="https://github.com/vuejs/vuepress/issues/214">GitHub issue&lt;/a> only got me 90% to the solution.&lt;/p>
&lt;h2 id="solution">Solution&lt;/h2>
&lt;p>There is one library we need to install:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>npm i dotenv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://www.npmjs.com/package/dotenv">&lt;code>dotenv&lt;/code>&lt;/a> is a popular JavaScript library which allows us to load environment variables from files. Exactly our use case!&lt;/p>
&lt;p>Now we need to adjust the configuration for VuePress. You can read more about the &lt;a href="https://v2.vuepress.vuejs.org/guide/configuration.html">Config file in the docs&lt;/a>.&lt;/p>
&lt;p>Add &lt;code>dotenv&lt;/code> to the Vue config file (&lt;code>config.js&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// all other imports, e.g.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">// import { registerComponentsPlugin } from &amp;#39;@vuepress/plugin-register-components&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> &lt;span style="color:#555">*&lt;/span> as dotenv from &lt;span style="color:#c30">&amp;#39;dotenv&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotenv.config()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The above code allows us to read our API token from the environment file. But how can we pass the variable to our frontend Vue component?&lt;/p>
&lt;p>You &lt;strong>cannot&lt;/strong> do this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-vue" data-lang="vue">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> TOKEN &lt;span style="color:#555">=&lt;/span> process.env.GOGGLE_API_TOKEN
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Vue component in VuePress &lt;em>can&lt;/em> be a client-side component. The browser can&amp;rsquo;t access &lt;code>process&lt;/code>, that&amp;rsquo;s Node.js-only.&lt;/p>
&lt;p>You&amp;rsquo;ll see this error:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>process is not defined
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://v2.vuepress.vuejs.org/advanced/cookbook/passing-data-to-client-code.html">VuePress has a hook to define global constants for the client code&lt;/a>.&lt;/p>
&lt;p>Add the following to your config object in &lt;code>config.js&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> defineUserConfig({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// previous configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// dest: &amp;#39;public&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> &lt;span style="color:#09f;font-style:italic">// lang: &amp;#39;de-DE&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> define&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __TOKEN__&lt;span style="color:#555">:&lt;/span> process.env.GOOGLE_API_TOKEN,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you can do the following in your Vue component:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-vue" data-lang="vue">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> fetch from &lt;span style="color:#c30">&amp;#39;cross-fetch&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>async &lt;span style="color:#069;font-weight:bold">function&lt;/span> fetchData() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> response &lt;span style="color:#555">=&lt;/span> await fetch(&lt;span style="color:#c30">&amp;#39;some-url&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;GET&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#555">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// here we can access the global constant 👇
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> Authorization&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">`Token token=&lt;/span>&lt;span style="color:#a00">${&lt;/span>__TOKEN__&lt;span style="color:#a00">}&lt;/span>&lt;span style="color:#c30">`&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> response.json()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#309;font-weight:bold">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>__TOKEN__&lt;/code> variable will &amp;ldquo;magically&amp;rdquo; work.&lt;/p>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>Here we have a working solution. Maybe there&amp;rsquo;s a better way.&lt;br>
I don&amp;rsquo;t like to use global constants. If you work with the Vue component it&amp;rsquo;s not clear where the variable comes from.&lt;/p>
&lt;p>But that&amp;rsquo;s at least a working solution.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://v2.vuepress.vuejs.org/advanced/cookbook/passing-data-to-client-code.html">Passing Data to Client Code&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/vuejs/vuepress/issues/214">GitHub: Are environment variables supported? #214&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Notes on “Building a Pragmatic Unit Test Suite”</title><link>https://www.rockyourcode.com/notes-on-building-a-pragmatic-unit-test-suite/</link><pubDate>2022-10-30</pubDate><guid>https://www.rockyourcode.com/notes-on-building-a-pragmatic-unit-test-suite/</guid><description>&lt;p>Here are some notes on the course “Building a Pragmatic Unit Test Suite” by Vladimir Khorikov.&lt;/p>
&lt;h2 id="goals-and-guidelines">Goals and Guidelines&lt;/h2>
&lt;p>Unit tests help with &lt;em>confidence&lt;/em>: you know that changes don&amp;rsquo;t break functionality.&lt;/p>
&lt;p>Not all unit tests are equal.&lt;/p>
&lt;p>Coverage metrics are problematic: you can work around them, for example, by writing assertion-free unit tests.&lt;br>
Coverage metrics are a good negative indicator, but 100% test coverage is impractical.&lt;/p>
&lt;p>Test are code, and you also have to pay a maintenance cost for your tests.&lt;/p>
&lt;p>What makes a unit test valuable?&lt;/p>
&lt;ul>
&lt;li>carefully choose code to test&lt;/li>
&lt;li>use the most valuable tests only&lt;/li>
&lt;/ul>
&lt;p>A good unit test:&lt;/p>
&lt;ul>
&lt;li>has a high chance of catching a regression error&lt;/li>
&lt;li>has a low chance of producing a false positive&lt;/li>
&lt;li>provides fast feedback&lt;/li>
&lt;li>has low maintenance cost&lt;/li>
&lt;/ul>
&lt;p>&lt;em>Testing trivial code is not worth the cost.&lt;/em>&lt;/p>
&lt;p>Decouple tests from implementation details as much as possible.&lt;/p>
&lt;p>Spend most of the time on testing business logic.&lt;/p>
&lt;h2 id="styles-of-unit-testing">Styles of Unit Testing&lt;/h2>
&lt;ul>
&lt;li>output-based verification (functional style)&lt;/li>
&lt;li>state verification&lt;/li>
&lt;li>collaboration verification (uses &lt;em>test doubles&lt;/em>)&lt;/li>
&lt;/ul>
&lt;h3 id="hexagonal-architecture">Hexagonal Architecture&lt;/h3>
&lt;p>&lt;img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Hexagonal_Architecture.svg/768px-Hexagonal_Architecture.svg.png" alt="hexagonal architecture">
_image from &lt;a href="https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)">Wikipedia&lt;/a>_&lt;/p>
&lt;h2 id="implementation-detail">Implementation Detail&lt;/h2>
&lt;p>Public API is the surface area that you can access from outside a class.&lt;/p>
&lt;p>What are the requirements?&lt;/p>
&lt;ul>
&lt;li>address an immediate goal of the client code&lt;/li>
&lt;li>address that goal completely&lt;/li>
&lt;/ul>
&lt;p>Look at the client code: if it uses more than 1 operation to achieve a single goal, the class is leaking implementation details.&lt;/p>
&lt;p>Note: Neighboring classes might be aware of implementation details.&lt;br>
Example: the Root Entity of an &lt;em>Aggregate&lt;/em> (Domain Driven Design) might know about implementation details of the Entities.&lt;/p>
&lt;p>Communication inside a hexagon is implementation detail.&lt;br>
Between hexagons a public API of the hexagon exist (contract).&lt;/p>
&lt;h3 id="styles">Styles&lt;/h3>
&lt;ul>
&lt;li>functional style: has no state, easy to maintain, offers the best protection against false positive&lt;/li>
&lt;li>state verification: should verify through public API, reasonable maintenance cost&lt;/li>
&lt;li>collaboration verification: within the hexagon lots of false positives; between hexagons more stable&lt;/li>
&lt;/ul>
&lt;h3 id="black-box-testing-vs-white-box-testing">Black-Box Testing Vs. White-Box Testing&lt;/h3>
&lt;ul>
&lt;li>black-box testing: testing without knowing the internal structure&lt;/li>
&lt;li>white-box testing: testing the internal structure&lt;/li>
&lt;/ul>
&lt;p>Adhere to black-box testing as much as possible.&lt;/p>
&lt;h3 id="business-requirements">Business Requirements&lt;/h3>
&lt;p>Does the test verify a business requirement?&lt;/p>
&lt;ul>
&lt;li>view your code from the end user&amp;rsquo;s perspective&lt;/li>
&lt;li>verify its observable behavior&lt;/li>
&lt;/ul>
&lt;h2 id="integration-tests">Integration Tests&lt;/h2>
&lt;ul>
&lt;li>test data cleanup: wipe out all data &lt;em>before&lt;/em> test execution&lt;/li>
&lt;/ul>
&lt;h2 id="unit-testing-anti-patterns">Unit Testing Anti-Patterns&lt;/h2>
&lt;ul>
&lt;li>private methods: if needed expose the hidden abstraction by extracting a new concept&lt;/li>
&lt;li>expose state getters: test the observable behavior only&lt;/li>
&lt;li>leaking domain knowledge to tests: use property-paced testing, or verify end result&lt;/li>
&lt;li>code pollution (introduce additional code just to enable unit testing)&lt;/li>
&lt;li>overriding methods in classes-dependencies: violates single-repository-principle, instead split functionality into different pieces&lt;/li>
&lt;li>non-determinism in tests: try to avoid testing async code (separate code into async/sync), use Tasks&lt;/li>
&lt;/ul>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://pluralsight.com/courses/pragmatic-unit-testing">Building a Pragmatic Unit Test Suite&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>My First T3 App</title><link>https://www.rockyourcode.com/my-first-t3-app/</link><pubDate>2022-10-25</pubDate><guid>https://www.rockyourcode.com/my-first-t3-app/</guid><description>&lt;p>&lt;a href="https://trpc.io">tRPC&lt;/a> is the hottest new thing in the TypeScript ecosystem: build end-to-end &lt;strong>type-safe&lt;/strong> APIs without the overhead of GraphQL.&lt;/p>
&lt;p>tRPC is a protocol to expose a function of your backend to your frontend using TypeScript type definitions.&lt;br>
No code generation required. You write both your backend and your frontend with TypeScript and share the types.&lt;/p>
&lt;p>tRPC is framework-agnostic.&lt;/p>
&lt;p>&lt;a href="https://create.t3.gg/">Create-t3-app&lt;/a> is build on top of tRPC. It offers an opinionated starter template that helps with building a complete web application with &lt;a href="https://nextjs.org">Next.js&lt;/a> and Prisma.&lt;/p>
&lt;p>This blog post chronicles my journey in creating my first T3 app. Let&amp;rsquo;s see how the T3 stack works!&lt;/p>
&lt;h2 id="create-application">Create Application&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pnpm dlx create-t3-app@latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The command guides you through the installation process and allows you to choose a few options (trpc, prisma, next-auth, tailwind).&lt;/p>
&lt;p>I am happy to see that the command also works with &lt;a href="https://pnpm.io">pnpm&lt;/a> out of the box.&lt;/p>
&lt;p>The command bootstraps the application. At the end of the process, there is a hint on what commands to run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#366">cd&lt;/span> my-t3-app
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pnpm install
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pnpm prisma db push
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pnpm dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The project also offers a &lt;code>README&lt;/code> file with minimal information to get you started.&lt;/p>
&lt;h2 id="prisma">Prisma&lt;/h2>
&lt;p>My application should show cat pictures because the internet loves cats.&lt;/p>
&lt;p>Let&amp;rsquo;s adjust the Prisma schema:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-diff" data-lang="diff">&lt;span style="display:flex;">&lt;span>generator client {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> provider = &amp;#34;prisma-client-js&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>datasource db {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> provider = &amp;#34;sqlite&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = env(&amp;#34;DATABASE_URL&amp;#34;)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+model Cat {
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+ id String @id @default(cuid())
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+ createdAt DateTime @default(now())
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+ updatedAt DateTime @updatedAt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+ imageUrl String
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="background-color:#cfc">+}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This looks like a minimal example for a first application. Run &lt;code>pnpm exec prisma migrate dev --name add_cat_model&lt;/code>.&lt;/p>
&lt;h2 id="trpc-router">tRPC Router&lt;/h2>
&lt;p>My next instinct is to hook up the trpc router. The project comes with an example router in &lt;code>src/server/router/example.ts&lt;/code>. I&amp;rsquo;ll adjust that to be a cat router.&lt;/p>
&lt;p>The router uses &lt;a href="https://github.com/colinhacks/zod">zod&lt;/a>, a schema-validation library, to build a router.&lt;/p>
&lt;p>The example query has an input parameter of the String type.&lt;br>
For my case, I want a random cat picture, so no input is needed. Can I just delete the input parameter and return a random cat?&lt;/p>
&lt;p>Before:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { createRouter } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;./context&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { z } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;zod&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> exampleRouter &lt;span style="color:#555">=&lt;/span> createRouter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .query(&lt;span style="color:#c30">&amp;#39;hello&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> input: &lt;span style="color:#078;font-weight:bold">z&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#078;font-weight:bold">object&lt;/span>({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text: &lt;span style="color:#078;font-weight:bold">z.string&lt;/span>().nullish(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .nullish(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resolve({ input }) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> greeting&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">`Hello &lt;/span>&lt;span style="color:#a00">${&lt;/span>input&lt;span style="color:#555">?&lt;/span>.text &lt;span style="color:#555">??&lt;/span> &lt;span style="color:#c30">&amp;#39;world&amp;#39;&lt;/span>&lt;span style="color:#a00">}&lt;/span>&lt;span style="color:#c30">`&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .query(&lt;span style="color:#c30">&amp;#39;getAll&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> resolve({ ctx }) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> ctx.prisma.example.findMany()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { createRouter } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;./context&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { Cat } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;@prisma/client&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> catRouter &lt;span style="color:#555">=&lt;/span> createRouter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .query(&lt;span style="color:#c30">&amp;#39;random&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> resolve({ ctx }) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> randomCats &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> ctx.prisma.$queryRaw&amp;lt;&lt;span style="color:#309;font-weight:bold">Cat&lt;/span>&lt;span style="color:#a00;background-color:#faa">[]&lt;/span>&amp;gt;&lt;span style="color:#c30">`SELECT id, imageUrl
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> FROM Cat
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> ORDER BY RANDOM()
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30"> LIMIT 1`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> randomCats[&lt;span style="color:#f60">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .query(&lt;span style="color:#c30">&amp;#39;getAll&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> resolve({ ctx }) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> ctx.prisma.cat.findMany()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I use a &lt;a href="https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access">raw SQL query&lt;/a> to retrieve a random cat from the database and add a typing for &lt;code>Cat[]&lt;/code>.&lt;br>
That&amp;rsquo;s not pretty and does not give me the advantage of using the schema validator, but Prisma doesn&amp;rsquo;t implement &lt;a href="https://github.com/prisma/prisma/issues/5894">getting a random record&lt;/a>. So raw SQL it is!&lt;/p>
&lt;p>The raw query returns an array in any case, so we select the first element and return it.&lt;/p>
&lt;h2 id="seed-script">Seed Script&lt;/h2>
&lt;p>Before I try to hook up the frontend, I remember that I don&amp;rsquo;t have any example data in my database.&lt;/p>
&lt;p>Luckily, the Prisma documentation can &lt;a href="https://www.prisma.io/docs/guides/database/seed-database">help me&lt;/a>.&lt;/p>
&lt;p>Add a new entry to &lt;code>package.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;prisma&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309;font-weight:bold">&amp;#34;seed&amp;#34;&lt;/span>: &lt;span style="color:#c30">&amp;#34;ts-node --compiler-options {\&amp;#34;module\&amp;#34;:\&amp;#34;CommonJS\&amp;#34;} prisma/seed.ts&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create a new seed script in the &lt;code>prisma&lt;/code> folder (&lt;code>prisma/seed.ts&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { PrismaClient } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;@prisma/client&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { fetch } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;next/dist/compiled/@edge-runtime/primitives/fetch&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> prisma &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#069;font-weight:bold">new&lt;/span> PrismaClient()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">async&lt;/span> &lt;span style="color:#069;font-weight:bold">function&lt;/span> main() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> requests &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#366">Array&lt;/span>(&lt;span style="color:#f60">10&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .fill(&lt;span style="color:#c30">&amp;#39;https://aws.random.cat/meow&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .map((url) &lt;span style="color:#555">=&amp;gt;&lt;/span> fetch(url))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Promise.all(requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// map array of responses into an array of response.json() to read their content
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> .then((responses) &lt;span style="color:#555">=&amp;gt;&lt;/span> Promise.all(responses.map((r) &lt;span style="color:#555">=&amp;gt;&lt;/span> r.json())))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#09f;font-style:italic">// insert all responses as imageUrl
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic">&lt;/span> .then((cats) &lt;span style="color:#555">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cats.forEach(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">async&lt;/span> (cat) &lt;span style="color:#555">=&amp;gt;&lt;/span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> prisma.cat.create({ data&lt;span style="color:#555">:&lt;/span> { imageUrl: &lt;span style="color:#078;font-weight:bold">cat.file&lt;/span> } })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>main()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .then(&lt;span style="color:#069;font-weight:bold">async&lt;/span> () &lt;span style="color:#555">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> prisma.$disconnect()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#069;font-weight:bold">catch&lt;/span>(&lt;span style="color:#069;font-weight:bold">async&lt;/span> (e) &lt;span style="color:#555">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> console.error(e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">await&lt;/span> prisma.$disconnect()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> process.exit(&lt;span style="color:#f60">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I fetch ten image URLs from an API that offers random cat images and insert them into the database. Quite ugly, but it works.&lt;/p>
&lt;p>In my terminal, I run type the following command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pnpm &lt;span style="color:#366">exec&lt;/span> prisma db seed
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Success!&lt;/p>
&lt;h2 id="hook-up-the-client">Hook Up the Client&lt;/h2>
&lt;p>Finally, we can try to show this data on the browser.&lt;/p>
&lt;p>After ripping out the example router and replacing it with my cat router, I check &lt;code>src/pages/index.tsx&lt;/code>.&lt;/p>
&lt;p>It has some boilerplate which I adjust to my needs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> &lt;span style="color:#069;font-weight:bold">type&lt;/span> { NextPage } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;next&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> Head &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;next/head&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> Image &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;next/image&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">import&lt;/span> { trpc } &lt;span style="color:#069;font-weight:bold">from&lt;/span> &lt;span style="color:#c30">&amp;#39;../utils/trpc&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">const&lt;/span> Home: &lt;span style="color:#078;font-weight:bold">NextPage&lt;/span> &lt;span style="color:#555">=&lt;/span> () &lt;span style="color:#555">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">const&lt;/span> { data: &lt;span style="color:#078;font-weight:bold">cat&lt;/span> } &lt;span style="color:#555">=&lt;/span> trpc.useQuery([&lt;span style="color:#c30">&amp;#39;cat.random&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#069;font-weight:bold">return&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">div&lt;/span> &lt;span style="color:#309">style&lt;/span>&lt;span style="color:#555">=&lt;/span>{{ display&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;grid&amp;#39;&lt;/span>, placeItems&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;center&amp;#39;&lt;/span> }}&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">Head&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">title&lt;/span>&amp;gt;T3 Cats&amp;lt;/&lt;span style="color:#309;font-weight:bold">title&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">meta&lt;/span> &lt;span style="color:#309">name&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#c30">&amp;#34;T3 cats&amp;#34;&lt;/span> &lt;span style="color:#309">content&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#c30">&amp;#34;Generated by create-t3-app&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">link&lt;/span> &lt;span style="color:#309">rel&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#c30">&amp;#34;icon&amp;#34;&lt;/span> &lt;span style="color:#309">href&lt;/span>&lt;span style="color:#555">=&lt;/span>&lt;span style="color:#c30">&amp;#34;/favicon.ico&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">Head&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">h1&lt;/span> &lt;span style="color:#309">style&lt;/span>&lt;span style="color:#555">=&lt;/span>{{ textAlign&lt;span style="color:#555">:&lt;/span> &lt;span style="color:#c30">&amp;#39;center&amp;#39;&lt;/span> }}&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Create &amp;lt;&lt;span style="color:#309;font-weight:bold">span&lt;/span>&amp;gt;T3&amp;lt;/&lt;span style="color:#309;font-weight:bold">span&lt;/span>&amp;gt; App
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">h1&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">section&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {cat &lt;span style="color:#555">?&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">Image&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309">src&lt;/span>&lt;span style="color:#555">=&lt;/span>{cat.imageUrl}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309">alt&lt;/span>&lt;span style="color:#555">=&lt;/span>{&lt;span style="color:#c30">`random cat &lt;/span>&lt;span style="color:#a00">${&lt;/span>cat.id&lt;span style="color:#a00">}&lt;/span>&lt;span style="color:#c30">`&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309">layout&lt;/span>&lt;span style="color:#555">=&lt;/span>{&lt;span style="color:#c30">&amp;#39;fixed&amp;#39;&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309">width&lt;/span>&lt;span style="color:#555">=&lt;/span>{&lt;span style="color:#f60">300&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#309">height&lt;/span>&lt;span style="color:#555">=&lt;/span>{&lt;span style="color:#f60">300&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#555">:&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#309;font-weight:bold">p&lt;/span>&amp;gt;Loading...&amp;lt;/&lt;span style="color:#309;font-weight:bold">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">section&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#309;font-weight:bold">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#069;font-weight:bold">export&lt;/span> &lt;span style="color:#069;font-weight:bold">default&lt;/span> Home
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That was surprisingly easy, especially if you are familiar with Prisma.&lt;/p>
&lt;h2 id="first-impressions">First Impressions&lt;/h2>
&lt;p>The starter template does a good job on guiding you through the process.&lt;/p>
&lt;p>The examples are enough to paint a broad picture on how trpc with Next.js works. Familiarity with prisma is assumed.&lt;/p>
&lt;p>You might need to consult the Prisma documentation, trpc is almost self-declaratory, Prisma is not.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://trpc.io">tRPC&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://create.t3.gg/">Create T3 App&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Write Better in Neovim With Languagetool</title><link>https://www.rockyourcode.com/write-better-in-neovim-with-languagetool/</link><pubDate>2022-10-02</pubDate><guid>https://www.rockyourcode.com/write-better-in-neovim-with-languagetool/</guid><description>&lt;p>&lt;strong>&lt;a href="https://languagetool.org/">LanguageTool&lt;/a>&lt;/strong> is a grammar tool and spell checker with an open-source core.&lt;/p>
&lt;p>I have used &lt;a href="https://www.grammarly.com/">grammarly&lt;/a> for a while, but the browser extension was crap.&lt;br>
A colleague recommended LanguageTool (LT), and I&amp;rsquo;ve been a happy user of the browser extension ever since.&lt;/p>
&lt;p>LT has superior support for other languages than English.
It also features decent suggestions for improving your grammar.
The free tier of LT has been a better experience for me than grammarly.&lt;/p>
&lt;p>Did you know that you can use LT as a language server for NeoVim and other editors?&lt;/p>
&lt;h2 id="install-ltex-ls">Install ltex-ls&lt;/h2>
&lt;p>LT has no direct support for the &lt;a href="https://microsoft.github.io/language-server-protocol/">language server protocol&lt;/a>, but fear not – a clever programmer has included the LT server in his LSP implementation.&lt;/p>
&lt;p>The tool is called &lt;strong>&lt;a href="https://valentjn.github.io/ltex/">ltex-ls&lt;/a>&lt;/strong>, and it&amp;rsquo;s free software.&lt;/p>
&lt;p>You can find the installation instructions on the &lt;a href="https://valentjn.github.io/ltex/ltex-ls/installation.html">ltex-ls website&lt;/a>.&lt;/p>
&lt;p>For macOS, you can use &lt;a href="https://brew.sh">homebrew&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>brew install ltex-ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The command installs the server as &lt;code>ltex-ls&lt;/code> in your standard homebrew location and thus adds it to your &lt;code>$PATH&lt;/code>.&lt;/p>
&lt;p>If you manually install the program, make sure to add the binary to your &lt;code>$PATH&lt;/code>, for example with &lt;a href="https://fishshell.com/docs/current/cmds/fish_add_path.html">&lt;code>fish_add_path&lt;/code>&lt;/a>.&lt;/p>
&lt;h2 id="integration-with-nvim-lsp">Integration With nvim-lsp&lt;/h2>
&lt;p>Check the &lt;a href="https://github.com/neovim/nvim-lspconfig#suggested-configuration">recommended configuration on the nvim-lsp GitHub&lt;/a> and add the following configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lua" data-lang="lua">&lt;span style="display:flex;">&lt;span>require(&lt;span style="color:#c30">&amp;#39;lspconfig&amp;#39;&lt;/span>)[&lt;span style="color:#c30">&amp;#39;ltex&amp;#39;&lt;/span>]({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> on_attach &lt;span style="color:#555">=&lt;/span> on_attach,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd &lt;span style="color:#555">=&lt;/span> { &lt;span style="color:#c30">&amp;#34;ltex-ls&amp;#34;&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> filetypes &lt;span style="color:#555">=&lt;/span> { &lt;span style="color:#c30">&amp;#34;markdown&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;text&amp;#34;&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> flags &lt;span style="color:#555">=&lt;/span> { debounce_text_changes &lt;span style="color:#555">=&lt;/span> &lt;span style="color:#f60">300&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should add the server command into the &lt;code>cmd&lt;/code> section. On macOS, &lt;code>ltex-ls&lt;/code> worked fine for me.&lt;/p>
&lt;p>Here you can see how the tool points out possible errors in my text:&lt;/p>
&lt;p>&lt;img src="https://www.rockyourcode.com/languagetool-neovim.png" alt="neovim languagetool example">&lt;/p>
&lt;h2 id="other-editors">Other Editors&lt;/h2>
&lt;p>I originally found the instructions for using ltex-lsp on a blog about the &lt;a href="https://helix-editor.com">Helix Editor&lt;/a>: &lt;a href="https://blog.getreu.net/20220828-tp-note-new8/">Note talking with Helix, Tp-Note and LanguageTool&lt;/a>.&lt;/p>
&lt;p>Here is the the content of &lt;code>~/.config/helix/languages.toml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>[[language]]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>file-types = [&lt;span style="color:#c30">&amp;#34;md&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;txt&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>indent = { tab-width = &lt;span style="color:#f60">2&lt;/span>, unit = &lt;span style="color:#c30">&amp;#34; &amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>injection-regex = &lt;span style="color:#c30">&amp;#34;md|markdown&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>language-server = { command = &lt;span style="color:#c30">&amp;#34;ltex-ls&amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>name = &lt;span style="color:#c30">&amp;#34;markdown&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>roots = [&lt;span style="color:#c30">&amp;#34;.git&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>scope = &lt;span style="color:#c30">&amp;#34;source.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you use &lt;a href="https://code.visualstudio.com/">VS Code&lt;/a>, you can use the extension &lt;a href="https://github.com/valentjn/vscode-ltex">vscode-ltex&lt;/a>.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://languagetool.org/">LanguageTool&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://valentjn.github.io/ltex/">ltex-ls&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.getreu.net/20220828-tp-note-new8/">Note talking with Helix, Tp-Note and LanguageTool&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>TIL About GNU Stow to Manage Dotfiles</title><link>https://www.rockyourcode.com/til-about-gnu-stow-to-manage-dotfiles/</link><pubDate>2022-09-26</pubDate><guid>https://www.rockyourcode.com/til-about-gnu-stow-to-manage-dotfiles/</guid><description>&lt;p>Today I learned about the tool &lt;a href="https://www.gnu.org/software/stow/">GNU Stow&lt;/a>, a free program that helps with managing symbolic links on your system.&lt;/p>
&lt;p>What does that even mean?&lt;/p>
&lt;p>I store my user configuration in a &lt;a href="https://dotfiles.github.io/">dotfile&lt;/a>.&lt;br>
Using Git, I can synchronize my settings across different machines.&lt;br>
Now I have a backup, even if I lose my computer, or it breaks.&lt;/p>
&lt;p>So far, I&amp;rsquo;ve used &lt;a href="https://www.freecodecamp.org/news/symlink-tutorial-in-linux-how-to-create-and-remove-a-symbolic-link/">symlinks&lt;/a> to create a link on my machine to the central git repository.&lt;/p>
&lt;p>But I learned about a great utility called &lt;strong>&lt;a href="https://www.gnu.org/software/stow/">GNU Stow&lt;/a>&lt;/strong> that helps with managing dotfiles. Stow makes neat symbolic links in the right location.&lt;/p>
&lt;p>You can easily pick and choose which configurations you want to sync.&lt;/p>
&lt;p>So, let&amp;rsquo;s say your dotfiles repository looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ tree -a ~/dotfiles
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ ├── .bashrc
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── .bash_profile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── kitty
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── .config
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── kitty
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── kitty.conf
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── vim
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └── .vimrc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then you can use stow like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>stow bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>stow kitty
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>stow vim
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="links">Links&lt;/h2>
&lt;p>You can find detailed explanations here:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://alexpearce.me/2016/02/managing-dotfiles-with-stow/">Managing dotfiles with GNU stow&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://brandon.invergo.net/news/2012-05-26-using-gnu-stow-to-manage-your-dotfiles.html">Using GNU Stow to manage your dotfiles&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Thanks to &lt;a href="http://www.sandra-parsick.de/">Sandra Parsick&lt;/a> and &lt;a href="https://georg.berky.dev/">Georg Berky&lt;/a> for introducing me to this tool.&lt;/p></description></item><item><title>Helix Editor – 90% of Neovim With Kakoune</title><link>https://www.rockyourcode.com/helix-editor-90-percent-of-neovim-with-kakoune/</link><pubDate>2022-09-22</pubDate><guid>https://www.rockyourcode.com/helix-editor-90-percent-of-neovim-with-kakoune/</guid><description>&lt;p>I&amp;rsquo;ve spend too many hours setting up the recent NeoVim features (since v0.5): &lt;a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter&lt;/a>, &lt;a href="https://github.com/neovim/nvim-lspconfig">nvim-lsp&lt;/a>, &lt;a href="https://github.com/hrsh7th/nvim-cmp">nvim-cmp&lt;/a>.&lt;/p>
&lt;p>Why?&lt;br>
NeoVim&amp;rsquo;s parser tool &lt;a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter&lt;/a> offers a better integration of language servers, syntax highlighting and auto-completion.&lt;/p>
&lt;h2 id="the-problem">The Problem&lt;/h2>
&lt;p>Vim and NeoVim are great.&lt;br>
However, I put a lot of effort into customizing these editors to my liking, so that I could comfortably use them for coding.&lt;/p>
&lt;p>In fact, my &lt;a href="https://github.com/sophiabrandt/dotfiles/tree/main/vim">configuration&lt;/a> has become more complicated over the years.&lt;/p>
&lt;p>Migrating my Vim configuration to take advantage of tree-sitter was an exercise in frustration.&lt;/p>
&lt;h2 id="better-than-neovim">Better Than (Neo)Vim?&lt;/h2>
&lt;p>By chance I stumbled upon a review of &lt;a href="https://matduggan.com/battle-of-the-text-editors/">Rust text editors&lt;/a> on &lt;a href="https://lobste.rs">lobste.rs&lt;/a>.&lt;/p>
&lt;p>The article favorably mentions &lt;strong>&lt;a href="https://helix-editor.com/">Helix&lt;/a>&lt;/strong>, a modal text editor inspired by Vim and &lt;a href="http://kakoune.org/">Kakoune&lt;/a>.&lt;br>
Other commentators also seemed taken with this new text editor.&lt;/p>
&lt;p>&lt;img src="https://github.com/helix-editor/helix/raw/master/screenshot.png" alt="helix text editor">&lt;/p>
&lt;p>I gave Helix a try and I am pleasantly surprised.&lt;/p>
&lt;p>Helix is a fully-fledged text editor that comes with wonderful capabilities out of the box.&lt;br>
For example, you get a fuzzy file finder, language server integration, a &lt;a href="https://github.com/tpope/vim-surround">vim-surround&lt;/a>-like plugin and great editor themes for free.&lt;/p>
&lt;p>In the end, Helix offers almost everything I need from a terminal-based text editor &lt;em>with zero config&lt;/em>.&lt;/p>
&lt;p>After wasting hours of my free time on tweaking NeoVim, Helix&amp;rsquo;s sane defaults and inbuilt features blew me out of the water.&lt;/p>
&lt;h2 id="kakoune--why">Kakoune – Why!?&lt;/h2>
&lt;p>Helix has one advantage over Vim/NeoVim - multiple cursors.
This features makes text editing a smoother experience.&lt;/p>
&lt;p>Multiple cursors come from &lt;a href="http://kakoune.org/">Kakoune&lt;/a>, a text editor I never heard of.&lt;/p>
&lt;p>Vim&amp;rsquo;s core editing model revolves around &lt;em>verbs&lt;/em> and &lt;em>(text) objects&lt;/em>.
For example, to delete a word, you type &lt;code>dw&lt;/code>, like in a natural language like English.&lt;/p>
&lt;p>Kakoune turns this model on its head: in Kakoune, you always select text objects first, then operate on them with words.&lt;/p>
&lt;p>Helix uses the same model as Kakoune.&lt;br>
The idea is that you always start by making a selection first (that way it&amp;rsquo;s interactive and you see that you selected the right thing), then you operate on it.&lt;/p>
&lt;p>I&amp;rsquo;m not sure if I can forego my muscle memory and retrain myself to use the &amp;ldquo;Kakoune&amp;rdquo; way.
For me, it feels incredibly awkward.&lt;/p>
&lt;p>I am also missing some commands in normal mode.&lt;br>
For example, I can easily move or copy lines to other locations in the file &lt;a href="https://www.rockyourcode.com/til-about-copying-a-range-in-vim/">without having to make a selection and without leaving normal mode&lt;/a>.&lt;br>
This is a clash with Kakoune&amp;rsquo;s/Helix&amp;rsquo;s philosophy.&lt;/p>
&lt;p>What I am missing most is &lt;code>ci&lt;/code> (for &lt;code>change inside&lt;/code>).&lt;br>
I often use this command to change text in brackets (&lt;code>ci{&lt;/code>), single quotes (&lt;code>ci'&lt;/code>) or other text objects.&lt;/p>
&lt;h2 id="now-what">Now What?&lt;/h2>
&lt;p>Helix is in active development.&lt;br>
But even so, the editor is already usable. Because it is written in Rust, it&amp;rsquo;s fast and stable.&lt;/p>
&lt;p>The maintainers plan a &lt;a href="https://github.com/helix-editor/helix/issues/122">plugin system with WebAssembly&lt;/a>, which would be a big milestone for the project.&lt;/p>
&lt;p>All in all, Helix looks like a valid alternative to Vim/NeoVim if you like modal editors.&lt;/p>
&lt;h1 id="my-configuration">My Configuration&lt;/h1>
&lt;p>Here is my complete configuration file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-toml" data-lang="toml">&lt;span style="display:flex;">&lt;span>theme = &lt;span style="color:#c30">&amp;#34;nord&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[editor.cursor-shape]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>insert = &lt;span style="color:#c30">&amp;#34;bar&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>normal = &lt;span style="color:#c30">&amp;#34;block&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>select = &lt;span style="color:#c30">&amp;#34;underline&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[editor.statusline]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>left = [&lt;span style="color:#c30">&amp;#34;mode&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;diagnostics&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>center = [&lt;span style="color:#c30">&amp;#34;file-name&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>right = [&lt;span style="color:#c30">&amp;#34;selections&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;file-type&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;file-encoding&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;position-percentage&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;position&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[keys.normal]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>g = { a = &lt;span style="color:#c30">&amp;#34;code_action&amp;#34;&lt;/span>, o = &lt;span style="color:#c30">&amp;#34;goto_last_accessed_file&amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#c30">&amp;#34;ret&amp;#34;&lt;/span> = [&lt;span style="color:#c30">&amp;#34;move_line_down&amp;#34;&lt;/span>, &lt;span style="color:#c30">&amp;#34;goto_first_nonwhitespace&amp;#34;&lt;/span>] &lt;span style="color:#09f;font-style:italic"># Maps the enter key to move to start of next line&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>X = &lt;span style="color:#c30">&amp;#34;extend_line_above&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>D = &lt;span style="color:#c30">&amp;#34;delete_char_backward&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[keys.insert]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>C-space = &lt;span style="color:#c30">&amp;#34;completion&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#09f;font-style:italic"># Move cursor in insert mode&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-h = &lt;span style="color:#c30">&amp;#34;move_char_left&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-j = &lt;span style="color:#c30">&amp;#34;move_line_down&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-k = &lt;span style="color:#c30">&amp;#34;move_line_up&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-l = &lt;span style="color:#c30">&amp;#34;move_char_right&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-o = &lt;span style="color:#c30">&amp;#34;open_below&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A-O = &lt;span style="color:#c30">&amp;#34;open_above&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It adds a default theme, a few convenience key mappings plus some customization around the status-line and the cursor.&lt;/p>
&lt;p>In comparison, here is &lt;a href="https://github.com/sophiabrandt/dotfiles/blob/main/vim/.vim/plugin/statusline.vim">my code for the status-line in Vim/NeoVim &lt;em>alone&lt;/em>&lt;/a> – which is (more or less) a three-liner in Helix.&lt;/p>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://helix-editor.com/">Helix&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://kakoune.org/">Kakoune&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://lobste.rs/s/xyexnb/reviewing_some_new_rust_text_editors">Reviewing some new Rust text editors&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>