<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feed.xslt" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Andri Óskarsson | andri.dk</title><description>I make websites, create apps, manage infrastructure, develop products and more.</description><link>https://andri.dk/</link><atom:link rel="self" type="application/rss+xml" href="https://andri.dk/rss.xml"/><atom:link rel="alternate" type="text/html" href="https://andri.dk/blog/"/><language>en-us</language><item><title>Announcing EventPuffin</title><link>https://andri.dk/blog/2026/eventpuffin/</link><guid isPermaLink="true">https://andri.dk/blog/2026/eventpuffin/</guid><pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://eventpuffin.com&quot;&gt;EventPuffin&lt;/a&gt; is an event-planning platform that makes it easy to coordinate with multiple people and invite guests.&lt;/p&gt;
&lt;h2&gt;Problem domain&lt;/h2&gt;
&lt;p&gt;Occasionally, I have to plan birthdays, trips or other social events with friends, family and sometimes work. The current landscape of planning events involves horrible platforms that lock you in, spam you and/or fill your screen with ads.&lt;/p&gt;
&lt;p&gt;Sometimes it&apos;s simple and you can decide yourself, but sometimes you have to find a date that fits most people, or settle on a film most people want to see, etc. That&apos;s where people used to reach for Doodle - except you&apos;d have to create multiple Doodles for multiple polls.&lt;/p&gt;
&lt;p&gt;Then you need to know who&apos;s coming. Most people might have Facebook, so you have to create the event there. But, then there are people who are not on Facebook and don&apos;t want an account. Then you&apos;d have to text or email them individually.&lt;/p&gt;
&lt;h2&gt;Vision&lt;/h2&gt;
&lt;p&gt;I want everyone to be able to participate in my events, regardless of what platforms they live on. It should be easy and self-explanatory to help decide, or RSVP. Grandma should be able to do it!&lt;/p&gt;
&lt;p&gt;It&apos;s built for planning birthdays, trips to the cinema, a D&amp;amp;D campaign at a sommerhouse or your next meetup.&lt;/p&gt;
&lt;h2&gt;Walkthrough&lt;/h2&gt;
&lt;h3&gt;Create an event&lt;/h3&gt;
&lt;p&gt;With EventPuffin everyone can create an event. You don&apos;t need an account - although if you&apos;d like to log in on other devices, it&apos;s recommended.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ep-create-an-event.png&quot; alt=&quot;create an event&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Make a decision, or loop in others&lt;/h3&gt;
&lt;p&gt;The most common details like &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;when&lt;/em&gt; are already suggested. But you can ask whatever you want, like which pizza toppings people would like or what board games to play in the summer house.&lt;/p&gt;
&lt;p&gt;You decide who gets a vote. One link for &lt;em&gt;deciders&lt;/em&gt; (votes, rsvp) and another link for &lt;em&gt;guests&lt;/em&gt; (only rsvp).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ep-make-a-decision.png&quot; alt=&quot;make a decision&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Settle the poll, or roll the dice&lt;/h3&gt;
&lt;p&gt;After a while, it&apos;s time to make a decision. Then you can look at the answers, pick the obvious winner, decide yourself or roll the dice. You can also set a deadline for voting so that the poll closes itself automatically.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ep-roll-the-dice.png&quot; alt=&quot;roll the dice&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Invite guests&lt;/h3&gt;
&lt;p&gt;You can invite guests at any time. Sometimes it&apos;s good to send invites early, to reserve date and figure out the details later. You can control which details the guests can see - but you can always start with the RSVP.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ep-og-invite.png&quot; alt=&quot;invite guests&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Stay up to date, without spam&lt;/h3&gt;
&lt;p&gt;You and your people can follow along with live calendar links, RSS feeds or web notifications.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ep-stay-up-to-date.png&quot; alt=&quot;stay up to date&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Questions&lt;/h2&gt;
&lt;h3&gt;Why is it not a native app?&lt;/h3&gt;
&lt;p&gt;It&apos;s about barrier of entry. Both for us, but also for planners, deciders and guests. The app is made responsive, so it should work on any device.&lt;/p&gt;
&lt;h3&gt;Why log in with atproto?&lt;/h3&gt;
&lt;p&gt;Where other platforms are hungry for your private data, we&apos;d like to avoid storing it. Even an email address is personally identifiable and subject to privacy laws. We&apos;d like to help you do what you want, and move on with your life.&lt;/p&gt;
&lt;p&gt;There&apos;s also another reason for using &lt;a href=&quot;https://atproto.com&quot;&gt;atproto&lt;/a&gt;. If you have an account and  &lt;em&gt;publish&lt;/em&gt; your event. We&apos;ll create a special calendar record on your atproto account which makes it visible on all supported platforms like &lt;a href=&quot;https://atmo.rsvp&quot;&gt;atmo.rsvp&lt;/a&gt;. If you choose to &lt;em&gt;unpublish&lt;/em&gt; your event, we&apos;ll delete the record for you again.&lt;/p&gt;
&lt;h2&gt;Special thanks&lt;/h2&gt;
&lt;p&gt;To my wife talented wife &lt;em&gt;Louise&lt;/em&gt; for designing the logo, lending me her good taste and for putting up with me for so long. &lt;em&gt;Mads&lt;/em&gt;, &lt;em&gt;Jens&lt;/em&gt;, &lt;em&gt;Ryan&lt;/em&gt;, &lt;em&gt;Stefan&lt;/em&gt; and &lt;em&gt;Ulrik&lt;/em&gt; who kept asking about the project until one of the prototypes made it into production.&lt;/p&gt;
</content:encoded></item><item><title>Over-engineering our vacation in 2026</title><link>https://andri.dk/blog/2026/vacation-planner/</link><guid isPermaLink="true">https://andri.dk/blog/2026/vacation-planner/</guid><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I am of two brains. I need structure, but I hate planning. We had decided to stay in Iceland for a few weeks while visiting family and attending family gatherings. But, that left a lot of empty columns in the calendar, and people asking me if we have any plans. I shrugged and asked if they had any ideas.&lt;/p&gt;
&lt;p&gt;As time kept creeping in on me, I eventually decided that I should try to put &lt;em&gt;some&lt;/em&gt; plan together. But, that sounded very boring - so I decided to create software to help me (reads yak-shaving).&lt;/p&gt;
&lt;h2&gt;The itch worth scratching&lt;/h2&gt;
&lt;p&gt;Every time we go on vacation there are a few dominating things that shape the rest of the trip. Our method of &lt;strong&gt;transportation&lt;/strong&gt; and where we are &lt;strong&gt;sleeping&lt;/strong&gt;. Then there are things we&apos;d like to do, and the rest &lt;strong&gt;must&lt;/strong&gt; be flexible for any chance of relaxation.&lt;/p&gt;
&lt;h2&gt;Itinerary&lt;/h2&gt;
&lt;p&gt;The pillars of our plan; accomidation and sleeping. Easily set from the first page. Both can span one or more days.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-itinerary.png&quot; alt=&quot;itinerary&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Places and Activities&lt;/h2&gt;
&lt;p&gt;It was important that this was structured, but not so structured that we&apos;d hate using it. An &lt;em&gt;activity&lt;/em&gt; can be anything (eating, partying, driving, flying) and each activity needs a &lt;em&gt;location&lt;/em&gt;. Locations can be added via address, or pinning it on a map.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-driving.png&quot; alt=&quot;driving&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Here is where I should&apos;ve stopped&lt;/h2&gt;
&lt;p&gt;For all intents and purposes, the tool was now complete. We could look at the itinerary and plan our vacation accordingly.&lt;/p&gt;
&lt;p&gt;But, I kept going....&lt;/p&gt;
&lt;h3&gt;Syncing with our calendar&lt;/h3&gt;
&lt;p&gt;The tool generates a webcal subscription that can be copied into Google/Apple/etc calendars and it automatically updates as you iterate on your plan.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-ical.png&quot; alt=&quot;ical&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Read-only share link&lt;/h3&gt;
&lt;p&gt;I wanted to be able to show my family &lt;em&gt;the plan&lt;/em&gt; without teaching them about my over-engineered tool, so it&apos;s possible to copy/paste a secret link and send it to them so they can see what the plan is.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-share-link.png&quot; alt=&quot;share-link&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Day-view (Today)&lt;/h3&gt;
&lt;p&gt;I wanted an overview that could tell we what we&apos;re doing today, tomorrow and not stress me out on any future details.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-today-view.png&quot; alt=&quot;vp-today-view&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Map for all days, each day&lt;/h3&gt;
&lt;p&gt;Sometimes it&apos;s fun to visualize where you&apos;re going and see it on a map.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-map-trip.png&quot; alt=&quot;map-trip&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can also zoom in on each day.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-map-day.png&quot; alt=&quot;map-day&quot;&gt;&lt;/p&gt;
&lt;h3&gt;We&apos;re flying. Why not track flights?&lt;/h3&gt;
&lt;p&gt;I&apos;d share a screenshot here, but I&apos;ve already stretched the limits of my free &lt;a href=&quot;https://aviationstack.com&quot;&gt;AviationStack&lt;/a&gt; account. Their free tier has 100 lookups per month. Plenty for a single-user trip-platform, right?&lt;/p&gt;
&lt;p&gt;In a &amp;quot;flying&amp;quot; activity, you can supply an optional IATA flight number and it will pull flight data 30m before departure and arrival. Then it will update the page with flight information (gate, delays, etc) and your calendar too!&lt;/p&gt;
&lt;p&gt;Below is a screenshot of a test-flight from yesterday&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-flight-data.png&quot; alt=&quot;flight-data&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Single-user? Why not multiple users&lt;/h3&gt;
&lt;p&gt;Today, I decided to add &lt;em&gt;users&lt;/em&gt; to the platform. If you&apos;d like to play with my little tool you can find it at &lt;a href=&quot;https://trip.andri.dk&quot;&gt;trip.andri.dk&lt;/a&gt;. You&apos;ll need a passkey to create an account. Passwords are so 2025!&lt;/p&gt;
&lt;h2&gt;That&apos;s all&lt;/h2&gt;
&lt;p&gt;This was a surprisingly fun project. But, I still haven&apos;t planned our summer vacation yet. So, I should probably go do that now. Bye!&lt;/p&gt;
&lt;h2&gt;Still here?&lt;/h2&gt;
&lt;p&gt;I might was well put a few words on how this was made. With a scope like this, it should be pretty obvious that this was either done over multiple weeks/months, or assisted by AI.&lt;/p&gt;
&lt;h3&gt;What separates the vibe-coders from the agentic engineers?&lt;/h3&gt;
&lt;p&gt;Planning! Not the boring kind, like vacations. Code planning. I spent over half a day iterating on requirements, user-stories and implementation details before allowing allowing the AI to write any code.&lt;/p&gt;
&lt;h3&gt;Tech Stack&lt;/h3&gt;
&lt;p&gt;I went with a server-rendered Go application using &lt;a href=&quot;https://gastro.andri.dk&quot;&gt;Gastro&lt;/a&gt;. If you don&apos;t use your own framework, then who will? It also has &lt;a href=&quot;https://data-star.dev&quot;&gt;Datastar&lt;/a&gt;, web-components and some minor JavaScript.&lt;/p&gt;
&lt;p&gt;All behaviours have test-cases, and are usually implemented as http tests. I had the AI use a headless browser while implementing to test interactivity.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vp-techstack.png&quot; alt=&quot;tech stack&quot;&gt;&lt;/p&gt;
&lt;p&gt;Database is &lt;a href=&quot;https://sqlite.org&quot;&gt;SQLite&lt;/a&gt; and I&apos;m using &lt;a href=&quot;https://sqlc.dev&quot;&gt;sqlc&lt;/a&gt; to generate type-safe ORM-like functions from the schema. Migrations are managed by &lt;a href=&quot;https://github.com/pressly/goose&quot;&gt;Goose&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I&apos;m pretty happy with how this turned out, and the code is surprisingly readable. Most importantly, there is absolutely no browser state/side-effects to spoil my day. Yet the application feels quite dynamic.&lt;/p&gt;
&lt;p&gt;I would like to lean more into Datastar SSE patching and signals at some point. But, for now it&apos;s mostly just templates and form submittions.&lt;/p&gt;
&lt;p&gt;Could I have written this by hand? Sure! But, it would&apos;ve taken much longer and had less coverage. Besides, this is a low-stakes personal project that scratches an itch. I also used the frictions from this project, to improve Gastro.&lt;/p&gt;
</content:encoded></item><item><title>Old dog, new tricks</title><link>https://andri.dk/blog/2026/old-dog-new-tricks/</link><guid isPermaLink="true">https://andri.dk/blog/2026/old-dog-new-tricks/</guid><pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As an old school programmer, I thought I&apos;d write down my journey into agentic AI and reflect on it.&lt;/p&gt;
&lt;h3&gt;Why I love programming&lt;/h3&gt;
&lt;p&gt;I&apos;ve been programming for as long as I remember. My parents bought an Amstrad CPC 128k (yes, that&apos;s the RAM size) and I was immediatly hooked, at the age of 8. After getting bored of the computer games it came with I turned my attention to the manual. It, being in a foreign language didn&apos;t stop me either.&lt;/p&gt;
&lt;p&gt;Solving non-trivial problems is my passion and I&apos;ve been using the computer to do that for a long time. I&apos;d spend my entire workday programming, and then go home and work on side-projects.&lt;/p&gt;
&lt;h3&gt;Books as a reference material&lt;/h3&gt;
&lt;p&gt;Back when I was learning programming we had to rely on programming books to learn the syntax. There was no Stack Overflow, ChatGPT or even Google.&lt;/p&gt;
&lt;p&gt;As the web matured, I hardly ever look at programming books. The Internet has a more up-to-date reference than paper could ever keep up with.&lt;/p&gt;
&lt;h3&gt;Era of AI auto-complete&lt;/h3&gt;
&lt;p&gt;When these large-language-models (LLM) started drawing attention I thought I&apos;d try them out. My first impression was that they were a good way to look up documentation, code references and large amounts of text, as long as you were critical of the output.&lt;/p&gt;
&lt;p&gt;Copilot, Cursor and code-completion were all the rage; but I found them more &lt;em&gt;distracting&lt;/em&gt; than helpful. At this point I&apos;m was using ChatGPT daily to look things up, but I didn&apos;t want these things touching my code or break my rhythm.&lt;/p&gt;
&lt;h3&gt;Coding Agents. Are they good now?&lt;/h3&gt;
&lt;p&gt;I was late to the party, but in 2026 I decided to give coding-agents a shot. I didn&apos;t really know what to expect, since the quality of what AI delivered was so bad. I had a tedious task of updating popular library that was interconnected all over the place. I could&apos;ve done it by hand; but it probably would&apos;ve taken a week.&lt;/p&gt;
&lt;p&gt;Then the agent and I started planning. This was fundamentally different from telling it to do stuff and be disappointed. I was using the AI to explore and brainstorm a potential solution and the output wasn&apos;t code. The output was a markdown plan file. Only when I thought the plan was worth working on, would I let it start editing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I was blown a way&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If the plan was good, it would produce mostly good code. Not hand-crafted, beautifully abstracted masterpiece of code. But, working code. I&apos;ve worked for companies that used outsourcing, and I dare say that agentic-coding is far superior to handing over a project to an outsourcing company, only to throw it away 6 months later.&lt;/p&gt;
&lt;p&gt;Not because the code is better, mind you. But, because of a much faster feedback loop.&lt;/p&gt;
&lt;h3&gt;Beyond tedious tasks&lt;/h3&gt;
&lt;p&gt;After offloading boring, time consuming tasks to AI, I was curious how it would fair at building stuff. But, I wasn&apos;t ready to let AI loose at the production codebase at work. Enter my graveyard of side-projects!&lt;/p&gt;
&lt;p&gt;I get many ideas. And for years, I&apos;ve been writing the idea, with a rough solution down in my notes and forget all about them. I&apos;ve been doing that for years. Sometimes I&apos;d do a prototype, buy a domain-name and &lt;em&gt;then&lt;/em&gt; get bored of it.&lt;/p&gt;
&lt;p&gt;First project was something I&apos;ve wanted to do for a long time. Block ads via HTTP proxy instead of relying on browser extensions and after brainstorming on a plan for over an hour the agent asked me: &amp;quot;Ready to implement?&amp;quot;. &amp;quot;Ehm, sure!&amp;quot;. From my earlier experience I was skeptical of just letting it rip. So, I decided to force it to do Test Driven Development (TDD) in Go. After 2 evenings of hand-holding I had a working proxy server. This would&apos;ve taken me months to do by myself.&lt;/p&gt;
&lt;p&gt;Second project was a frustration with web templating in Go. I was surprised and annoyed by a lack of typings between Go code, and the standard templates. After a night of brainstorming with the agent it also ended with &amp;quot;Ready to implement?&amp;quot;.&lt;/p&gt;
&lt;p&gt;Both projects are working and relatively stable.&lt;/p&gt;
&lt;h3&gt;The mental trade-off&lt;/h3&gt;
&lt;p&gt;As many software developers are now learning, streaming text into your brain all day is exhausting. A normal day of a software development might consist of investigating an issue, talking to stake holders, talking with UX, drawing up a rough solution and iterate on the code.&lt;/p&gt;
&lt;p&gt;All of these steps include a break, personal contact and often blockers. With agents, you are suddenly the bottleneck between the backlog and production. That&apos;s stressful.&lt;/p&gt;
&lt;p&gt;Humans are also lazy by nature. What happens to a product when the feature is written by AI, the UX is done by AI, the programming and reviewing is done by AI? Who is steering the ship?!&lt;/p&gt;
&lt;h3&gt;Code Craftsmanship&lt;/h3&gt;
&lt;p&gt;As someone who likes to discuss design-patterns, security, testability, data modelling and other weird topics I&apos;m concerned. Not because these things are less important now - but because it&apos;s &lt;em&gt;too easy&lt;/em&gt; to offload these tasks to AI.&lt;/p&gt;
&lt;p&gt;Writing code was never the hard problem. Production software isn&apos;t just some functional requirements in JIRA, mashed together with ones and zeroes. There&apos;s a business domain and a very long list of past decisions that might or might not be documented somewhere.&lt;/p&gt;
&lt;p&gt;As Software Developers, we need to pick our battles. We probably won&apos;t be deciding where that curly-bracket is sitting or the name of each variable, and that&apos;s fine. But, we definitely should be thinking more about software architecture, design patterns and &lt;strong&gt;not&lt;/strong&gt; delegate that to the AI.&lt;/p&gt;
&lt;h3&gt;Security. What security?&lt;/h3&gt;
&lt;p&gt;I remember someone giving me the sage advise of &amp;quot;Always validate your inputs&amp;quot;. Yet, letting Claude access all your online services, your computer and run terminal commands on your computer is normal now. Where does Claude get its&apos; input from? Well, &lt;em&gt;everywhere&lt;/em&gt;. Everything it reads from your files, your skills and online resources can affect its&apos; behavior.&lt;/p&gt;
&lt;p&gt;What makes it worse is that we can&apos;t validate agent communication like we&apos;d do with web-forms. Anthropic is famously using regex in their harness and hard-coding dangerous commands. The best solution I can think of is putting everything in a sandbox and regulate the sandbox according to the purpose of the specialised agent.&lt;/p&gt;
&lt;p&gt;There are going to be some &lt;em&gt;terrible&lt;/em&gt; casualties on the stock-market when prompt-injection really takes off.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;As you may have guessed by reading this far, I&apos;m conflicted!&lt;/p&gt;
&lt;p&gt;What it eventually comes down to, is money. Is the potential &lt;em&gt;reward&lt;/em&gt; worth the &lt;em&gt;risk&lt;/em&gt; of burned out developers and lack of security? Maybe.&lt;/p&gt;
&lt;p&gt;My personal take. If we can get the security part to a reasonable level, then I think that humans should focus on strategy and let the machines do tactics. I will keep using AI for investigating code-bases, summarising large amounts of information and code tasks. But, I will write my own emails, articles and abstractions and force myself to take breaks during the day.&lt;/p&gt;
&lt;p&gt;Let&apos;s not outsource our thinking. We might forget how.&lt;/p&gt;
</content:encoded></item><item><title>Gastro: Server side web framework for Go</title><link>https://andri.dk/blog/2026/announcing-gastro/</link><guid isPermaLink="true">https://andri.dk/blog/2026/announcing-gastro/</guid><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Writing web applications in 2026&lt;/h3&gt;
&lt;p&gt;I&apos;ve been creating web pages for a long time. I started with Microsoft Frontpage, graduated to plain HTML (think table layouts, font color, limited css) and then when PHP v3 dropped I started building web applications.&lt;/p&gt;
&lt;p&gt;These applications were all server rendered, and when Google released Gmail/Calendar and everyone was like &amp;quot;The browser can do what?!&amp;quot;. Then the user-experience we were offering with our template languages wasn&apos;t up to the task anymore. So, we adjusted and added jQuery to our applications with a splash of Bootstrap and it was OK for a while.&lt;/p&gt;
&lt;p&gt;Then as the projects grew, these jQuery scripts grew from helpful to unmaintainable and we were starting to build business logic in two places. At the time, I championed for React (Grunt/Gulp, React Classes) to save our sanity and became a spokesperson for React for everyone who asked, and lots who didn&apos;t. The promise of composability was too strong to ignore!&lt;/p&gt;
&lt;p&gt;Fast forward to 2026, and everything is React. We no longer use classes, but have sprinkled useEffects, dependency arrays and state all over the place. This has brought some serious complexity to web development to the point that people complain about JS fatigue (well before AI fatigue). Newly scaffolded projects have somewhere between 500 and 1000 dependencies and updating those isn&apos;t trivial either.&lt;/p&gt;
&lt;p&gt;For client-heavy applications (that need to be), I still enjoy using React (Native); but for most of my work related tasks of CRUD pages, tables and forms I&apos;ve been feeling regret for some time now. Did we pick the right tool for the job, or did we just pick what was closest?&lt;/p&gt;
&lt;h3&gt;Web development in Go&lt;/h3&gt;
&lt;p&gt;As I dealt with more client complexity at work, I found myself leaning more towards backend and specifically Go at home. Normal people have normal hobbies. I typically compensate for the lack of learning at work with a side-project or three.&lt;/p&gt;
&lt;p&gt;But, writing web apps in Go isn&apos;t as nice as I thought. The built-in template engine is quite capable and widely used. But, even simple server rendered HTML pages require you to write markup in editors where you can either have partial template support, or partial HTML support. Then you have to write handlers, compile the templates, embed them and hope they run as you expected.&lt;/p&gt;
&lt;h3&gt;Existing solutions&lt;/h3&gt;
&lt;p&gt;The closest solution I could find at the time was Hugo; but I don&apos;t want to generate static HTML. I want to write web applications, like I did with PHP when I was young.&lt;/p&gt;
&lt;p&gt;So, I started searching; but all of the frameworks were focused on routing and ORM-like libraries and very focused on being the fastest.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://templ.guide&quot;&gt;Templ&lt;/a&gt; was the first Go template language where my IDE was actually helpful. It&apos;s a custom DSL that with Go syntax, that generates a &lt;code&gt;.go&lt;/code&gt; file for every &lt;code&gt;.templ&lt;/code&gt; file you write. I really wanted to love Templ. But, the fact that I&apos;m writing a Go project that populates my projects with generated Go files at every turn, was annoying me more than it should have.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.gomponents.com/&quot;&gt;Gomponents&lt;/a&gt; was also interesting to me. It&apos;s sort of like React, for Go. So every HTML-element has a generated function that you can use to write composable HTML with. It works, but I could never get used to writing HTML that way.&lt;/p&gt;
&lt;h3&gt;An inspiration from Astro&lt;/h3&gt;
&lt;p&gt;I was talking to my CTO the other day how I love &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt; and how their server-side components are far superior to approaches by Next.js and others for a single reason. There is never any ambiguity about what runs on the server, and what runs on the client.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// Frontmatter always runs on the server.
interface Props {
    name string;
}

const { name } = Astro.props
---
&amp;lt;!-- body is HMTL sent to the client --&amp;gt;
&amp;lt;p&amp;gt;Hello {name}!&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, Astro is great, and I almost left it at that. But, I want to write Go! I like that I can build applications with &lt;em&gt;zero&lt;/em&gt; dependencies outside of the standard library and not be horrified when the next npm vulnerability pops up.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://gastro.andri.dk&quot;&gt;Gastro&lt;/a&gt; - as in gastronomy&lt;/h3&gt;
&lt;p&gt;I love food, and I wanted to recognize the influence Astro components had on the project. The goal of Gastro is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make web-development fun again&lt;/li&gt;
&lt;li&gt;Learn from past mistakes.
&lt;ul&gt;
&lt;li&gt;PHP was too unstructured and eventually ended up in tears&lt;/li&gt;
&lt;li&gt;React was an overcorrection to a painful SSR experience with modern UX&lt;/li&gt;
&lt;li&gt;Static types and full IDE support for a great developer experience&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use AI to do the heavy lifting&lt;/li&gt;
&lt;li&gt;Server Side Rendering (SSR) only.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gastro dev&lt;/code&gt; will listen for changes and generate all the code in &lt;code&gt;.gastro/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gastro build&lt;/code&gt; should generated a binary, embedded with all assets and be ready to ship&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let me address the AI elephant in the room first. I enjoy writing code, and solving problems. But, as someone who has abandoned more pet-projects than most start in their lifetime; I like that I can outsource the tedious stuff, so I can focus on the actual problem solving.&lt;/p&gt;
&lt;h3&gt;A typical Gastro project&lt;/h3&gt;
&lt;p&gt;After installing Gastro on your machine, a typical workflow is like below. See &lt;a href=&quot;https://gastro.andri.dk&quot;&gt;Gastro&apos;s homepage&lt;/a&gt; for a better introduction.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;gastro new awesomesauce # scaffolds a new project
cd aweseomesauce
code . # launch vscode (search for Gastro in the extension marketplace)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will prompt you to install Go, gopls and Gastro (if you don&apos;t have it already). The project structure is opinionated, and simple.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/components&lt;/code&gt;: Put re-usable elements here and call them from pages. Components can call other components.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/pages&lt;/code&gt;: Any &lt;code&gt;.gastro&lt;/code&gt; component will be treated as a page and added to the generated routes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/static&lt;/code&gt;: Public assets are embedded and shipped. Accessible from &lt;code&gt;/static&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main.go&lt;/code&gt;: &lt;em&gt;Your&lt;/em&gt; main project file. Here you can setup http multiplexing, move domain-logic into packages or whatever floats your gravy-boat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pages&lt;/h3&gt;
&lt;p&gt;Here we import a component using a special &lt;code&gt;.gastro&lt;/code&gt; import statement. Title is exported (upper-case) to the template and validated in the template by our language server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import (
	Hello &amp;quot;components/hello.gastro&amp;quot; // Imports Hello component
)

Title := &amp;quot;Welcome to Gastro&amp;quot; // Uppercase makes it visible to the template below
---
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
		&amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
		&amp;lt;title&amp;gt;{{ .Title }}&amp;lt;/title&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;h1&amp;gt;{{.Title}}&amp;lt;/h1&amp;gt;
		{{Hello (dict &amp;quot;Name&amp;quot; &amp;quot;sup&amp;quot;)}}
		{{.Title}}
		&amp;lt;p&amp;gt;Edit &amp;lt;code&amp;gt;pages/index.gastro&amp;lt;/code&amp;gt; to get started.&amp;lt;/p&amp;gt;
	&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Components&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Props&lt;/code&gt; type defines what the props look like, and &lt;code&gt;gastro.Props()&lt;/code&gt; makes them available in the frontmatter in a type-safe way. The ensures the language-server can verify and autocomplete the template variables in the body.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
type Props struct {
	Name string
}

p := gastro.Props()
Name := p.Name
---

&amp;lt;div&amp;gt;
	&amp;lt;p&amp;gt;Hello {{.Name}}&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;So, we&apos;re back to 2002?&lt;/h3&gt;
&lt;p&gt;Not exactly. In 2002 we didn&apos;t have web-components, JS modules in the browsers or hypermedia libraries like &lt;a href=&quot;https://htmx.org/&quot;&gt;htmx&lt;/a&gt; and &lt;a href=&quot;https://data-star.dev&quot;&gt;datastar&lt;/a&gt;. Modern CSS can also do many things that we previously needed JavaScript for.&lt;/p&gt;
&lt;p&gt;I think we can have our server-rendered cake, and eat it too! I&apos;m pretty happy with how the design turned out, and I think this is a sweet spot between composable HTML and the complexity that usually follows.&lt;/p&gt;
&lt;h3&gt;OK, you convinced me. What&apos;s next?&lt;/h3&gt;
&lt;p&gt;Check out &lt;a href=&quot;https://gastro.andri.dk&quot;&gt;Gastro&apos;s webpage&lt;/a&gt;, &lt;a href=&quot;https://github.com/andrioid/gastro&quot;&gt;the GitHub repo&lt;/a&gt; and the &lt;a href=&quot;https://github.com/andrioid/gastro/tree/main/examples&quot;&gt;examples&lt;/a&gt;. The Gastro page is written in Gastro, compiles in a few seconds and deploys as a single binary.&lt;/p&gt;
&lt;p&gt;Gastro is still in early development, so there will be bugs and rough edges. I&apos;d appreciate any feedback, so we can make it better.&lt;/p&gt;
</content:encoded></item><item><title>Why your coding-agent should be writing Go</title><link>https://andri.dk/blog/2026/why-go-is-perfect-for-ai/</link><guid isPermaLink="true">https://andri.dk/blog/2026/why-go-is-perfect-for-ai/</guid><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Go (or Golang for some) may not be the best-in-class language for everything, but I think it&apos;s ideally suited for LLM coding.&lt;/p&gt;
&lt;p&gt;The community is special in a way that is a bit differrent than communities of other programming languages. There&apos;s a strong feeling of doing things &amp;quot;The right way&amp;quot;, &amp;quot;The Go way&amp;quot;, &amp;quot;Idiomatic&amp;quot; and &amp;quot;avoid external dependencies&amp;quot;. For this reason, I find it much easier to browse open-source projects written in Go, than say TypeScript. Solutions are similar, even if the programmers are not.&lt;/p&gt;
&lt;p&gt;Feedback is essential to any programmer and your performance is closely tied to how fast that feedback-loop is. Go has a number of features that help with returning feedback quickly to you, Claude, Codex or even Fred.&lt;/p&gt;
&lt;p&gt;When Google started development of Go, they were struggling with long build-times, so one of the core features of Go is that it compiles very quickly. Think seconds, not minutes. Since Go is a statically typed language it also will refuse to compile on invalid syntax or unused import.&lt;/p&gt;
&lt;p&gt;Testing is also built into the &lt;code&gt;go&lt;/code&gt; command so running tests is as simple as &lt;code&gt;go test&lt;/code&gt;. This makes it easy to write unit-tests and integration tests as you go. Race conditions can be surfaced by running &lt;code&gt;go test -race&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Where you place curly-brackets, line-lines or spaces is also handled by &lt;code&gt;go fmt&lt;/code&gt; so all code looks the same.&lt;/p&gt;
&lt;p&gt;Go also has a rich standard-library so it reduces the need to install 3rd party libraries.&lt;/p&gt;
&lt;h3&gt;Why Go for LLMs?&lt;/h3&gt;
&lt;p&gt;When talking to programmers, their biggest complaints are things that Go is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overly verbose&lt;/li&gt;
&lt;li&gt;Overly opinionated&lt;/li&gt;
&lt;li&gt;Hostile to patterns from classic OOP languages&lt;/li&gt;
&lt;li&gt;Lack of fancy language features&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I sympathise, I do. But, all of these &lt;em&gt;negative&lt;/em&gt; things are &lt;em&gt;positive&lt;/em&gt; things when it comes to letting the AI write all your code. You want your AI written to code to be flat, easy to reason about. You want it to look like other code, so that it&apos;s easy to review it.&lt;/p&gt;
&lt;p&gt;In a word. The code may be verbose, but it&apos;s more &lt;em&gt;simple&lt;/em&gt; than what many other languages produce.&lt;/p&gt;
&lt;h3&gt;My experience, so far&lt;/h3&gt;
&lt;p&gt;I started using coding agents at work, as our CTO actively encourages us to try it out. Cursor and Copilot in the IDE never really clicked for me. These tools disrupted my train-of-thought in such a way that I rarely used them.&lt;/p&gt;
&lt;p&gt;Then came Claude Code, OpenCode, Codex, etc. Changing the interface from micro-managing every line of code, to larger batches makes sense for me. I started with annoying, time-consuming tasks like cleaning up tech-debt and was pleasantly surprised. Then I connected the Sentry MCP and asked it to find root-causes of problems and that also worked brilliantly.&lt;/p&gt;
&lt;p&gt;Then I decided to try &amp;quot;vibe coding&amp;quot; and quickly created an Expo app w. TypeScript and Jazz Tools. Next project was &lt;a href=&quot;https://andrioid.github.io/ublproxy&quot;&gt;ublproxy&lt;/a&gt; in Go, where I was able to leverage red/green (test-first) to create an adblocking HTTP proxy in a few days.&lt;/p&gt;
&lt;p&gt;While I&apos;d call both projects a success, I notice that the agent struggles more with the TypeScript codebase. Each exploration run takes longer, it makes silly mistakes where it feels like for every 2 steps forward, we take 1 step back.&lt;/p&gt;
&lt;p&gt;The Go project doesn&apos;t suffer from this, and I challenge you to try using Go for your project.&lt;/p&gt;
&lt;h3&gt;Reflections on the age of AI coding&lt;/h3&gt;
&lt;p&gt;I&apos;ve been writing code since I discovered BASIC as an 8 year old playing with our first computer. I still enjoy writing it, but my true passion lies in &lt;em&gt;problem solving&lt;/em&gt;. Leading an AI isn&apos;t that different from leading a junior developer, or a team in India - the feedback loop is just faster.&lt;/p&gt;
&lt;p&gt;I was asked recently if I feel threatened by the AI gold-rush. I think we&apos;re going to see a big disruption in many jobs that have been considered AI-safe. However, I think that critical-thinking, domain-knowledge and the ability to understand problems has never been more important.&lt;/p&gt;
&lt;p&gt;Learning new things is fun, and AI is just one more tool for the old toolbox. But, I imagine that there are specialists out there that haven&apos;t kept up with the times, or had any reason to. They might have a rude awakening at some point; but should remember that their &lt;em&gt;real&lt;/em&gt; value is their domain knowledge and that&apos;s not going out of style!&lt;/p&gt;
&lt;p&gt;I haven&apos;t quite made up my mind on coding-agents yet. But, for now I can see the value in getting a crazy side-project idea, and flesh it out in a couple of days instead of buying a domain and quietly punish myself every time I have to renew it.&lt;/p&gt;
</content:encoded></item><item><title>Learning n8n and using it to promote my blog</title><link>https://andri.dk/blog/2025/learning-n8n/</link><guid isPermaLink="true">https://andri.dk/blog/2025/learning-n8n/</guid><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been having a bunch of fun learning n8n during the holiday vacation. I&apos;ve hooked it up so that when I write a blog post in my site, it&apos;s published to RSS and...&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checks if the URL is already processed, abort if it is&lt;/li&gt;
&lt;li&gt;Content is summarized by an LLM (I&apos;m lazy, sorry)&lt;/li&gt;
&lt;li&gt;Stored in a data-table&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then occasionally, it checks if the post has been shared on social-media:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If it&apos;s shared already, abort&lt;/li&gt;
&lt;li&gt;Create a post on Bluesky and store a reference to the at-protocol URL&lt;/li&gt;
&lt;li&gt;Create a post on LinkedIn and store a reference to the post&lt;/li&gt;
&lt;li&gt;Add Bluesky comments by fetching the atUri from an n8n web-hook&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a part of this process, I&apos;ve also had to&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remove all of the social-media posting stuff from my blog. It has always felt like a scope-creep to have it here, so now it&apos;s a part of my n8n setup and if that dies, that&apos;s not the end of the world.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://andri.dk/blog/2025/takomi-og-image/&quot;&gt;Redo how I&apos;m generating open-graph images&lt;/a&gt; because my earlier way of doing it didn&apos;t feel quite right.&lt;/li&gt;
&lt;li&gt;Setup hosting and automatic deployments on my own server via Dokploy&lt;/li&gt;
&lt;li&gt;Create an alternative open-graph image for code-snippet like posts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./n8n-example1.png&quot; alt=&quot;n8n workflow&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Did you do anything interesting with your Holiday Vacation?&lt;/h3&gt;
</content:encoded></item><item><title>MySQL/MariaDB cheatsheet</title><link>https://andri.dk/blog/2025/mysql-cheatsheet/</link><guid isPermaLink="true">https://andri.dk/blog/2025/mysql-cheatsheet/</guid><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few useful snippets while using MariaDB/MySQL. Copied from my notes. Get ready for some YELLING.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- show slave status without your eyes bleeding
SHOW SLAVE STATUS\G

-- show master status
SHOW MASTER STATUS\G

-- processes
SHOW PROCESSLIST;

-- show users
SELECT User, Host FROM mysql.user;

--- show permissions
SHOW GRANTS FOR &apos;username&apos;@&apos;host&apos;;

-- change password
ALTER USER &apos;repl&apos;@&apos;%&apos; IDENTIFIED BY &apos;newpassword&apos;;
FLUSH PRIVILEGES;

--- setup replica
STOP SLAVE;
SET GLOBAL gtid_slave_pos=&apos;xxx&apos;;
CHANGE MASTER TO MASTER_HOST=&apos;xxx&apos;, MASTER_PORT=3306, MASTER_USER=&apos;replica&apos;, MASTER_PASSWORD=&apos;xxx&apos;, MASTER_USE_GTID=slave_pos, MASTER_CONNECT_RETRY=3, MASTER_SSL_VERIFY_SERVER_CERT=0;
START SLAVE;
SHOW SLAVE STATUS;

CHANGE MASTER TO MASTER_HOST=&apos;xxx&apos;, MASTER_PORT=3306, MASTER_USER=&apos;replica&apos;, MASTER_PASSWORD=&apos;xxx&apos;, MASTER_USE_GTID=slave_pos, MASTER_CONNECT_RETRY=3, MASTER_SSL_VERIFY_SERVER_CERT=0;

SET GLOBAL gtid_slave_pos=&apos;xxx&apos;

--- show mysql version
SELECT @@version AS &apos;MySQL Version&apos;;

--- connection status
SHOW STATUS WHERE Variable_name IN (&apos;Threads_connected&apos;,&apos;Max_used_connections&apos;);
SHOW VARIABLES LIKE &apos;max_connections&apos;;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Social-media images directly into PNG from JSX with Takumi</title><link>https://andri.dk/blog/2025/takomi-og-image/</link><guid isPermaLink="true">https://andri.dk/blog/2025/takomi-og-image/</guid><pubDate>Mon, 29 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Improving engagement of your online content can come down to how it&apos;s previewed on social-media services. If you&apos;re like me, and not keen on creating these open-graph images by hand for every post then you can generate them programmatically.&lt;/p&gt;
&lt;h2&gt;Takumi&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://takumi.kane.tw/&quot;&gt;Takumi&lt;/a&gt; is a tool that receives JSX and outputs images. It has support for (some) CSS, Tailwind and HTML. We&apos;ll be using React&apos;s JSX in this example; but there&apos;s also a lower-level API you can use if you like. It can run on Node.js, or in the browser using WASM.&lt;/p&gt;
&lt;p&gt;If your template looks like this&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Hello() {
  return (
    &amp;lt;div tw=&amp;quot;bg-white w-full h-full flex items-center justify-center&amp;quot;&amp;gt;
      &amp;lt;h1 tw=&amp;quot;text-4xl font-bold&amp;quot;&amp;gt;Hello&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;ll receive something like this:
&lt;img src=&quot;./takomi-example1.png&quot; alt=&quot;Hello Example&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Astro SSR example&lt;/h3&gt;
&lt;p&gt;Most of the documentation is focused around Next.js. But, my framework of choice is &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Create a handler in &lt;code&gt;pages/blog/[...slug].og.ts&lt;/code&gt;. It will respond to anything at &lt;code&gt;/blog/2025/whatever/you/want.og&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { ImageResponse } from &amp;quot;@takumi-rs/image-response&amp;quot;;

export async function GET({ params }: { params: Params }) {
  const { slug } = params;
  const post: CollectionEntry&amp;lt;&amp;quot;blog&amp;quot;&amp;gt; = await getPost(slug);
  // TODO: You should handle errors here

  return new ImageResponse(
    // Using createElement instead of JSX because file is TS, not TSX
    // - Replace OgImage with your component
    createElement(OgImage, { post: post }),
    {
      width: 1200,
      height: 630,
      format: &amp;quot;png&amp;quot;, // webp, others supported
      headers: {
        &amp;quot;Cache-Control&amp;quot;: &amp;quot;public, max-age=3600&amp;quot;,
      },
    },
  );
}

// Simplified version of my getPost using a content-collection &amp;quot;blog&amp;quot;
async function getPost(slug: string): Promise&amp;lt;CollectionEntry&amp;lt;&amp;quot;blog&amp;quot;&amp;gt;&amp;gt; {
  const post = await getEntry(&amp;quot;blog&amp;quot;, slug);
  if (!post) throw new Error(&amp;quot;Post not found&amp;quot;);
  return post;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;My template&lt;/h2&gt;
&lt;p&gt;The template component looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./takomi-example2.webp&quot; alt=&quot;andri.dk og image example&quot;&gt;&lt;/p&gt;
&lt;p&gt;With the following template&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function OgImage({ post }: { post: CollectionEntry&amp;lt;&amp;quot;blog&amp;quot;&amp;gt; }) {
  return (
    &amp;lt;div
      tw=&amp;quot;bg-black w-full h-full flex flex-col justify-evenly text-white px-16 gap-6 py-8 &amp;quot;
      style={{
        backgroundImage: &amp;quot;linear-gradient(to bottom, #033359ff, black)&amp;quot;,
      }}
    &amp;gt;
      &amp;lt;h1 id=&amp;quot;title&amp;quot; tw=&amp;quot;text-6xl font-bold&amp;quot;&amp;gt;
        {post.data.title}
      &amp;lt;/h1&amp;gt;

      &amp;lt;div tw=&amp;quot;flex flex-col gap-8 text-3xl&amp;quot;&amp;gt;
        &amp;lt;div id=&amp;quot;post-meta&amp;quot; tw=&amp;quot;flex flex-row gap-6&amp;quot;&amp;gt;
          &amp;lt;img src=&amp;quot;avatar&amp;quot; alt=&amp;quot;avatar&amp;quot; tw=&amp;quot;h-20 w-20 rounded-full&amp;quot; /&amp;gt;
          &amp;lt;div tw=&amp;quot;h-full flex flex-col justify-center&amp;quot;&amp;gt;
            &amp;lt;span tw=&amp;quot;font-bold&amp;quot;&amp;gt;{site.author}&amp;lt;/span&amp;gt;
            &amp;lt;span tw=&amp;quot;text-gray-400&amp;quot;&amp;gt;{dateToIsoDate(post.data.date)}&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div id=&amp;quot;tags&amp;quot; tw=&amp;quot;flex flex-row gap-4 font-semibold&amp;quot;&amp;gt;
          {post.data.tags?.map((tag) =&amp;gt; (
            &amp;lt;span tw=&amp;quot;rounded-full bg-white text-black py-1.5 px-4&amp;quot;&amp;gt;
              #{tag}
            &amp;lt;/span&amp;gt;
          )) ?? null}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;m using an image in my template. We need fetch it and pass it to Takomi in the &lt;code&gt;persistedImages&lt;/code&gt; array of the response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const persistentImages: Array&amp;lt;PersistentImage&amp;gt; = [
  {
    src: &amp;quot;avatar&amp;quot;,
    data: readFileSync(path.join(process.cwd(), &amp;quot;public/img/coffee-art.jpg&amp;quot;)),
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the template above, we reference it with &lt;code&gt;&amp;lt;img src=&amp;quot;avatar&amp;quot;&amp;gt;&lt;/code&gt;. Note that we&apos;re doing this in the module-scope to cache the image in memory. If you&apos;re tight on memory, you could do this as a part of the request handler. But, then it will be slower.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Takomi is an amazing tool to create images from markup. It supports Tailwind out of the box and it&apos;s not as heavy handed as running, say a full Google Chrome to generate a single image.&lt;/p&gt;
&lt;p&gt;While I&apos;m using this for server-side-rendering, it should work fine in static-site-generation (SSG) scenarios as well.&lt;/p&gt;
&lt;p&gt;It&apos;s also not limited to Astro, Next.js or the framework of the week. It&apos;s written in Rust, and compiles to WASM.&lt;/p&gt;
&lt;p&gt;If you&apos;re still reading this, please check out &lt;a href=&quot;https://takumi.kane.tw/docs&quot;&gt;Takumi&apos;s documentation&lt;/a&gt; and make it your own.&lt;/p&gt;
</content:encoded></item><item><title>Replacing Makefile, direnv, asdf and more with Mise</title><link>https://andri.dk/blog/2025/mise-en-place/</link><guid isPermaLink="true">https://andri.dk/blog/2025/mise-en-place/</guid><pubDate>Sun, 31 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you ever joined a new project and then spend an entire day setting up your developer-environment? Worked on a large project where one of the many tech-debts are that nobody updated the runtime for 10 years, and now nobody dares touch it?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;What version of Java/Python/etc do I need to run the backend?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;How do I start a dev server?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Can you send me that ENV file we use for DEV?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;How do I run database migrations?&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;How do I update our SDK from the API spec?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;ll be covering the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;: How you can make sure that you and your co-workers are running the correct runtime, build-tool or CLI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ENV&lt;/strong&gt;: Set project defaults for environment variables without forcing people to rename a &lt;code&gt;.env.default&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasks&lt;/strong&gt;: Like &amp;quot;npm scripts&amp;quot;, but for any language, sub-project or monorepo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;: Leaner, cleaner Docker images&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub actions&lt;/strong&gt;: Making sure our pipelines are running the same version as you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: This guide may not work well on Windows and is aimed at Linux or Mac users.&lt;/p&gt;
&lt;h3&gt;Getting started&lt;/h3&gt;
&lt;p&gt;You&apos;ll need to install &lt;a href=&quot;https://mise.jdx.dev&quot;&gt;mise&lt;/a&gt;. There are multiple ways of doing that, but for simplicity&apos;s sake we&apos;ll do it with a curl-pipe.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl https://mise.run | sh
mise --version
# mise 2025.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let&apos;s create a directory called &amp;quot;mise/&amp;quot; in the root of your repository.&lt;/p&gt;
&lt;h3&gt;Tools&lt;/h3&gt;
&lt;p&gt;Mise supports a &lt;a href=&quot;https://mise.jdx.dev/dev-tools/&quot;&gt;varity of tools&lt;/a&gt;, and tool backends. For our purposes, we&apos;ll be asking it to install Node.js v20 and Go 1.24.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# mise.toml
[tools]
node = &amp;quot;v20&amp;quot;
go = &amp;quot;1.24&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we run &lt;code&gt;mise install&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# tells mise to trust the configuration in `mise/`
mise trust
mise install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mise will now install your requested tool versions. You might notice that if you go into the parent directory (above your project) and try running &lt;code&gt;go version&lt;/code&gt; or &lt;code&gt;node --version&lt;/code&gt; it might show different versions. That is because these tool versions are scoped to your project.&lt;/p&gt;
&lt;p&gt;You can also pop a configuration file into &lt;code&gt;~/.config/mise/config.toml&lt;/code&gt; and set defaults.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# ~/.config/mise/config.toml
[tools]
# Always have Node.js LTS installed
node = &amp;quot;lts&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, unless specified otherwise you&apos;ll have access to Node.js LTS in other projects. Running &lt;code&gt;mise install&lt;/code&gt; will update it to the latest Long-Term-Support if that has changed since last time.&lt;/p&gt;
&lt;p&gt;Another neat thing is that when working on larger projects, you can override sub-packages too&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- ~/.config/mise/config.toml # defaults for your machine
- ~/pied-piper/mise.toml # project defaults
- ~/pied-piper/packages/algorithm/mise.toml # uses an older version of python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;re not limited to &amp;quot;dev tools&amp;quot; either. You can install pretty much anything that has a GitHub release, or a release page with binary files using &lt;a href=&quot;https://mise.jdx.dev/dev-tools/backends/&quot;&gt;tool backends&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, I use the GitHub CLI to create releases on another project like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;gh release create &amp;quot;$VERSION&amp;quot; --target &amp;quot;$GITHUB_SHA&amp;quot; --title &amp;quot;md-social $VERSION&amp;quot; --generate-notes
gh release upload &amp;quot;$VERSION&amp;quot; dist/*.zip dist/*.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Normally, a script like that would have to check if &amp;quot;gh&amp;quot; is even installed. But, instead I define it as a tool and let Mise handle it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# mise.toml
[tools]
# other tools
&amp;quot;ubi:cli/cli&amp;quot; = { version = &amp;quot;latest&amp;quot;, exe = &amp;quot;gh&amp;quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Environment Variables&lt;/h3&gt;
&lt;p&gt;You can also use these configuration files to &lt;a href=&quot;https://mise.jdx.dev/environments/&quot;&gt;control your environment&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# mise.toml
# *snip* tool stuff *snip*

[env]
_.file = &apos;.env.defaults&apos; # Loads default ENV file
_.path = &apos;./bin&apos; # Adds your local bin/ directory to path
VERSION = &amp;quot;v0.0.1-dev&amp;quot; # custom variable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also support for templating, file loading, redactions, secrets and more.&lt;/p&gt;
&lt;h3&gt;Tasks&lt;/h3&gt;
&lt;p&gt;JavaScript developers might be familiar with &lt;a href=&quot;https://docs.npmjs.com/cli/v8/using-npm/scripts?v=true&quot;&gt;npm scripts&lt;/a&gt;. This like that, only you can use it with any programming language.&lt;/p&gt;
&lt;p&gt;Tasks can be defined &lt;a href=&quot;https://mise.jdx.dev/tasks/#tasks-in-mise-toml-files&quot;&gt;decleratively&lt;/a&gt; in TOML, or as scripts in any language. Those are great for npm-like scripts that depend on each other.&lt;/p&gt;
&lt;p&gt;Let&apos;s define our development environment in TOML below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mise.toml
[tools]
watchexec = latest # Listens for changes on the file-system

[tasks.dev]
description = &amp;quot;Development Environment&amp;quot;
run = &amp;quot;mise watch dev.*&amp;quot;

[tasks.&amp;quot;dev:go&amp;quot;]
# restart if go files change
sources = [&amp;quot;go.mod&amp;quot;, &amp;quot;**/*.go&amp;quot;]
description = &amp;quot;Go backend&amp;quot;
run = &amp;quot;go run .&amp;quot;

[tasks.&amp;quot;dev:js&amp;quot;]
# vite is handling the file watching here
run = &amp;quot;npm run dev&amp;quot;
description = &amp;quot;TypeScript frontend&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, if you run &lt;code&gt;mise run dev&lt;/code&gt; it will run both backend and frontend. Should any files change on the backend, it will automatically restart it for you.&lt;/p&gt;
&lt;p&gt;For more complicated tasks (e.g. linting across packages, migrations, publishing, etc), I like to keep &lt;a href=&quot;https://mise.jdx.dev/tasks/file-tasks.html&quot;&gt;file-tasks&lt;/a&gt; in either &lt;code&gt;scripts/&lt;/code&gt; or &lt;code&gt;tasks/&lt;/code&gt; folders. You can also split your TOML tasks into files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# mise.toml
# *snip* previous stuff *snip*
[task_config]
includes = [
    &amp;quot;tasks.toml&amp;quot;,
    &amp;quot;scripts&amp;quot;,
    &amp;quot;tasks&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating a file-based task is like creating a normal shell script.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash
#MISE description=&amp;quot;Deploy to Kubernetes&amp;quot;
echo kubectl stuff here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Place this in &lt;code&gt;scripts/deploy&lt;/code&gt; and run &lt;code&gt;chmod +x scripts/deploy&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mise tasks
# Name      Description
dev         your description here
dev:go
dev:js
deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &amp;quot;deploy&amp;quot; scenario, I might put a &lt;code&gt;#MISE depends=[&amp;quot;build&amp;quot;]&lt;/code&gt;, to make it trigger a build before running the deployment script.&lt;/p&gt;
&lt;p&gt;It&apos;s also worth mentioning, that you can have tasks for each of your repository projects. So if you only wanted to run the frontend, you could run &lt;code&gt;mise run dev&lt;/code&gt; from &amp;quot;frontend/&amp;quot; and have its&apos; mise.toml configure the &amp;quot;dev&amp;quot; task for something else.&lt;/p&gt;
&lt;h3&gt;Docker&lt;/h3&gt;
&lt;p&gt;If you&apos;ve read this far, you might be thinking to yourself: &amp;quot;Why would I spend so much time doing this, when I need to do this again for our Dockerfile and our pipeline too?!&amp;quot;&lt;/p&gt;
&lt;p&gt;You can also use Mise inside of Docker using their official image, or by running the curl command manually from a base-image of your choice.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM jdxcode/mise:latest AS tools
COPY mise.toml .
RUN mise trust
RUN mise install

# [DEPENDENCIES]
FROM tools AS dependencies

# Friends dont let friends run containers as root
RUN useradd -m -u 1000 app &amp;amp;&amp;amp; mkdir -p /app &amp;amp;&amp;amp; chown app:app /app
USER app
WORKDIR /app
COPY . .
RUN whatever you want
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have nested configuration files, you&apos;d have to adjust this to copy them as well in the first stage.&lt;/p&gt;
&lt;h3&gt;GitHub Actions&lt;/h3&gt;
&lt;p&gt;There&apos;s also an &lt;a href=&quot;https://github.com/jdx/mise-action&quot;&gt;GitHub Action for Mise&lt;/a&gt; from its&apos; creator.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Mise allows you to define how your development environment is set up, in one place and frees you up to focus on more important things. It can simplify containers and pipelines too.&lt;/p&gt;
&lt;p&gt;The only thing that could make this setup more magical, would be if you could setup multi-language git-hooks for your monorepo too. Funny enough, &lt;a href=&quot;https://github.com/jdx&quot;&gt;JDX&lt;/a&gt; has you covered with a sibling project, &lt;a href=&quot;https://github.com/jdx/hk&quot;&gt;hk&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you find this project valuable, consider &lt;a href=&quot;https://github.com/sponsors/jdx&quot;&gt;sponsoring him&lt;/a&gt; on GitHub.&lt;/p&gt;
</content:encoded></item><item><title>Technical Scrolls for Bluesky</title><link>https://andri.dk/blog/2024/bluesky-technical-feed/</link><guid isPermaLink="true">https://andri.dk/blog/2024/bluesky-technical-feed/</guid><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve always been interested in technology, and not much else. When social networks emerged I was excited for all the technical content I&apos;d have access to. Unfortunately big-tech has no interest in allowing us to control how we consume content. Facebook started out with allowing you to select all your interests so that they could better show you interesting things. Typing this out loud now, makes me think I was incredibly naive. Of course they did that so they could show me interesting &lt;em&gt;ads&lt;/em&gt;. Twitter somehow was different, because I could selectively follow people who posted things that were interesting to me.&lt;/p&gt;
&lt;p&gt;Try out &lt;a href=&quot;https://bsky.app/profile/technical.scrolls.org/feed/tech-following&quot;&gt;Following Tech&lt;/a&gt; or &lt;a href=&quot;https://bsky.app/profile/technical.scrolls.org/feed/tech-all&quot;&gt;Technical Scrolls&lt;/a&gt; and DM &lt;a href=&quot;https://bsky.app/profile/andri.dk&quot;&gt;me&lt;/a&gt; on Bluesky if you have any feedback.&lt;/p&gt;
&lt;h3&gt;Selectively following&lt;/h3&gt;
&lt;p&gt;For a while, I got my fix of tech news from Twitter from a carefully selected group of people who mostly posted technical stuff. The &amp;quot;problem&amp;quot; with that approach, is that people are human and post things that &lt;em&gt;they&lt;/em&gt; like. Google+ had an interesting concept of &amp;quot;circles&amp;quot; where you could post carefully curated content and people could only follow what they were interested in. Problem is, people don&apos;t like to do that and of all the people I had on G+, I was the only one using circles. People want to post whatever is on their mind, and that is perfectly understandable.&lt;/p&gt;
&lt;p&gt;Now, you might be reading this and thinking that I must really hate people and their people posts. Not really. It&apos;s a matter of context. I may enjoy reading about what my friend had for lunch or see a drawing that their kid made for them. But, that does not mean I&apos;m interested in kid-drawings or lunch photos for people I have no personal connection to.&lt;/p&gt;
&lt;p&gt;So the closer you are to me, the more interested I am in reading whatever it is you&apos;re thinking. So, I have been pruning the list of people to follow for years, and sometimes that means missing out on some great, interesting content and engaging with interesting people who just wanted to share stuff they like.&lt;/p&gt;
&lt;h2&gt;Vision&lt;/h2&gt;
&lt;p&gt;I want a place on the Internet where I can read interesting technical posts. Experiences of problem solving, new libraries, JS framework of the week, crazy experiments or anything like that. Without the noise.&lt;/p&gt;
&lt;h3&gt;Enter Blue Sky&lt;/h3&gt;
&lt;p&gt;I was more invested in Mastodon/ActivityPub and even attempted to host my own personal instance for a while. It had it&apos;s upsides, but the protocol requires you to store a bunch of stuff that really has nothing to do with you or your users.&lt;/p&gt;
&lt;p&gt;So when Bluesky started to get popular I had my doubts. Then Elon Musk and Donald Trump happened and Bluesky was flooded with millions of people and I saw a clear uptick in interesting technical content on the platform. But, that also came with what I would consider &amp;quot;noise&amp;quot;. I get my fair share of &amp;quot;current events&amp;quot; from other sources, so I absolutely don&apos;t want to read about that here.&lt;/p&gt;
&lt;h3&gt;How hard can it be?&lt;/h3&gt;
&lt;p&gt;Every engineer probably has mumbled those words. I had read that Bluesky had this concept of &amp;quot;label services&amp;quot; and I immediately wanted to see if I could build a labeler for &amp;quot;technical&amp;quot; / &amp;quot;non-technical&amp;quot;. But, after reading more about &lt;a href=&quot;https://atproto.com/&quot;&gt;atProto&lt;/a&gt; I came to the conclusion that labeling everything that &lt;em&gt;was not&lt;/em&gt; tech related would be an impossible task and add noise to an otherwise impressive protocol. Labels were meant as a form of moderation; to label what might be sensitive to others.&lt;/p&gt;
&lt;h3&gt;Feeds&lt;/h3&gt;
&lt;p&gt;If labels are an exclusion-list, then feeds are an inclusion-list. That sounded like the Internet I wanted, so during the Christmas holidays I started prototyping. I was put off by the amount of libraries and dependencies the examples were using; but eventually came across a few great examples of feed generators that were minimal in nature.&lt;/p&gt;
&lt;p&gt;Then I created a feed that always showed a single post, using a HTTP endpoint. The only library used was the &amp;quot;@atproto/api&amp;quot; for registering the feed. Initial prototypes used Bun and almost no external dependencies.&lt;/p&gt;
&lt;p&gt;OK, let&apos;s do this!&lt;/p&gt;
&lt;h2&gt;Tech Stack and challenges&lt;/h2&gt;
&lt;p&gt;I needed a web-server for &lt;code&gt;/xrpc/app.bsky.feed.getFeedSkeleton&lt;/code&gt; and &lt;code&gt;/.well-known/did.json&lt;/code&gt;. I needed somewhere to store the posts, their classification and I also wanted to build some moderation tools to make it easier for me to debug the classifiers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript and Bun&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt; for the web-application in SSR mode&lt;/li&gt;
&lt;li&gt;Postgres, Drizzle-ORM with postgres-js&lt;/li&gt;
&lt;li&gt;React (as Astro interactive islands) for the moderation tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Jetstream&lt;/h3&gt;
&lt;p&gt;The Bluesky firehose is massive and I&apos;m not at all interested in burning a hole in my pocket while trying to process all of the 30 million users worth of posts. So, I opted in for using the &lt;a href=&quot;https://github.com/bluesky-social/jetstream&quot;&gt;Jetstream&lt;/a&gt; service and only subscribe to posts followed by my users.&lt;/p&gt;
&lt;p&gt;I tried some libraries, but the most promising one had an issue with Bun and none of them supported compression. So like any overly optimistic programmer, I made my own. It&apos;s specifically designed to my use-case of requesting only the &lt;code&gt;DID&lt;/code&gt; (user identities) I want from the service instead of &amp;quot;all posts&amp;quot;. The Jetstream service supports up to 10000 accounts per socket and state that I can open as many sockets as I want. Challenge accepted!&lt;/p&gt;
&lt;p&gt;My initial attempts went well, but as soon as I approached 200 accounts the socket refused to connect. I suspect that my query-string was too long, but I was a big issue, since even with &lt;em&gt;one&lt;/em&gt; user (me) the service was opening 3 sockets.&lt;/p&gt;
&lt;p&gt;Reading through some of the other libraries, I came across an option of Jetstream to update it&apos;s configuration through the socket instead of requesting it through the URL. That worked wonders, and I could easily fit all the requested accounts in one socket.&lt;/p&gt;
&lt;p&gt;Looking at the posts quickly filling up my terminal&apos;s buffer I decided that I needed to get that stream compressed before I get a call from my ISP. Finding a zstd library was easy, but finding one that supported Bluesky&apos;s custom dictionary was harder. I finally found a great library called &lt;a href=&quot;https://www.npmjs.com/package/@bokuweb/zstd-wasm&quot;&gt;@bokuweb/zstd-wasm&lt;/a&gt; and quickly had the compressed stream working.&lt;/p&gt;
&lt;h3&gt;Classifier&lt;/h3&gt;
&lt;p&gt;You can&apos;t have a feed without knowing sort of what posts you want in your feed. I wanted something simple in the beginning so I&apos;d have some content while trying to figure out the best way to classify posts as tech/non-tech. The first few classifiers use regular expressions to mute or tag posts according sentences that I&apos;ve hard-coded. But, I designed the data-model so that any post can be tagged multiple times by different algorithms. That way, I could start simple and then bring GPT and friends if I came across a big pile of money.&lt;/p&gt;
&lt;p&gt;Next attempt was creating my own naive bayes classifier; but I ended up spending more time than I had fine-tuning the tokenizer function and still wasn&apos;t seeing the results I wanted. But, at this time I knew I needed a way to manually classify posts to train the models.&lt;/p&gt;
&lt;p&gt;I was desperate, so I reached out to ChatGPT. It was quite helpful, and suggested Tensorflow JS with a bit back and forth, ChatGPT and I were able to stitch together a promising tokenizer and bayes classifier. I then started to consider how I was going to store the model and it seemed that the Tensorflow either want you to use local-storage (browser) or raw files (node). But, I didn&apos;t want that... I wanted to store this in my trusty Postgres.&lt;/p&gt;
&lt;p&gt;So, I wrote a custom &lt;code&gt;IOHandler&lt;/code&gt; implementation, that was able to load and save models from the database with the dictionary.&lt;/p&gt;
&lt;h3&gt;Web application&lt;/h3&gt;
&lt;p&gt;What I want from the web application is to serve the feeds, be able to preview the feeds and most importantly, be able to classify manually. All of the API calls are implemented as Astro Actions (type-safe and zod validated).&lt;/p&gt;
&lt;p&gt;I work with React in my day-job, so it felt natural to grab it for my moderation tools. Being naturally curious, I decided to go for React 19, fresh off the shelf and try their new &lt;code&gt;use()&lt;/code&gt; function. Then my development environment started hammering the Bluesky API every few millisecond. Sorry Team Bluesky. After a few hours of scratching my head, I found out that the &lt;code&gt;use()&lt;/code&gt; function is intended for content from react-server-components and any promises created on the client is re-created on every render. If you ask me, that&apos;s an accident waiting to happen. We (React users) have been hoping for a simpler way to consume API&apos;s on the frontend and I really thought that &lt;code&gt;use()&lt;/code&gt; was it.&lt;/p&gt;
&lt;p&gt;To preview the posts I started out with using &lt;a href=&quot;https://github.com/hichemfantar/react-bluesky-embed&quot;&gt;react-embed-bluesky&lt;/a&gt;, which is excellent but intended for creating engagement on the platform. I wanted a simple preview, with moderator tools. But, it was a great inspiration - so thank you &lt;a href=&quot;https://github.com/hichemfantar&quot;&gt;Hichem&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Database&lt;/h3&gt;
&lt;p&gt;I can&apos;t end this post without saying a few words about the heart of the solution. I&apos;ve been programming professionally since the late 90&apos;s and was always a MySQL person. But, since I finally learned Postgres there really isn&apos;t any competition. Postgres is a work-horse and while it may not be best at everything; it&apos;s good enough at surprisingly everything.&lt;/p&gt;
&lt;p&gt;I&apos;m using Postgres to store posts, tags, classifier models and not to mention listen/notify that helps my processes communicate what posts need processing without me needing to invest in a full blown queue solution.&lt;/p&gt;
&lt;p&gt;I&apos;ve written harsh things about ORM (Object-Relational-Model) libraries, but &lt;a href=&quot;https://orm.drizzle.team/&quot;&gt;Drizzle-ORM&lt;/a&gt; fits a sweet spot for me. It&apos;s strongly typed and it makes querying and storing stuff incredibly easy - just like every ORM. But, Drizzle has this wonderful &amp;quot;Magical &lt;code&gt;sql&lt;/code&gt; operator&amp;quot; as they call it. As with any library, you will eventually find a use-case that it wasn&apos;t created for. But, with Drizzle you can use whatever SQL syntax your heart desires. So, I&apos;ve come to like Drizzle very much.&lt;/p&gt;
&lt;p&gt;The database is running on a small 512M node and it has performed admirably.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The service is running and mostly churning out technical content, but the holidays are soon over and I have other things to occupy my time.&lt;/p&gt;
&lt;h3&gt;What I&apos;d really like to work on&lt;/h3&gt;
&lt;p&gt;In no special order&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&apos;d love a &amp;quot;Programming memes&amp;quot; feed. It could be done by applying image classification models on. That would also mean more image content in the feed.&lt;/li&gt;
&lt;li&gt;The services are leaking memory, and I can&apos;t tell if it&apos;s my crappy programming or some edge cases in Bun. I&apos;d love to figure out why, and fix it.&lt;/li&gt;
&lt;li&gt;So far the focus of my classifiers has been &amp;quot;relevancy&amp;quot; or figuring out if something is &amp;quot;tech&amp;quot; or &amp;quot;not&amp;quot;. When the &lt;a href=&quot;https://bsky.app/profile/technical.scrolls.org/feed/tech-all&quot;&gt;Technical Scrolls&lt;/a&gt; feed is so busy that it&apos;s unusable, I hope to spend some time on figuring out what&apos;s &amp;quot;trending&amp;quot; and &amp;quot;good&amp;quot; somehow.&lt;/li&gt;
&lt;li&gt;If this gets popular I will need some help to train the models, and to do that I will have to do what I&apos;ve been dreading. Implement oauth login and sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This has been a really interesting project and I&apos;ve learned a lot. Specifically about how Bluesky and atProto work, machine-learning and how (not) to write long-running JS services.&lt;/p&gt;
&lt;p&gt;Still reading? Well, thank you so much. That means a lot!&lt;/p&gt;
</content:encoded></item><item><title>Tech Podcasts for 2024</title><link>https://andri.dk/blog/2024/podcast-recommendation/</link><guid isPermaLink="true">https://andri.dk/blog/2024/podcast-recommendation/</guid><pubDate>Wed, 14 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some of the podcasts I enjoy during my bike-trip to work.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://softskills.audio/&quot;&gt;Soft Skills Engineering&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Soft Skills are an underrated ability in engineers and Dave and Jamison deliver their advise in such a funny and helpful way that it&apos;s hard not to smile while listening to it.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://syntax.fm/&quot;&gt;Syntax.fm&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Scott and Wes are really in touch with what&apos;s trending in the tech world and usually have both great guests and interesting takes on a various range of subjects.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://changelog.com/gotime&quot;&gt;Go time&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A great Go(lang) specific show with deep technical topics and plenty of humor.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://changelog.com/podcast&quot;&gt;The Changelog&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Good all-around tech podcast.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.dotnetrocks.com/about&quot;&gt;.NET Rocks&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Even though I don&apos;t really program in .NET, I enjoy this show very much. Patterns and ways of working are universal and the hosts are very entertaining.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://darknetdiaries.com/&quot;&gt;Darknet Diaries&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Host interviews hackers and other cyber-criminals about their exploits and mistakes.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.devtools.fm/&quot;&gt;devtools.fm&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Recently started listening, but so far very happy with their content which focuses on developer tooling (duh).&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://hasspodcast.io/&quot;&gt;Home Assistant Podcast&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As someone who likes tinkering with smart-home stuff, this show is very inspiring to what you can do with a few lines of Python or YAML.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://changelog.com/jsparty&quot;&gt;JS Party&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In depth show about all things JavaScript/TypeScript.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://reactnativeradio.com/&quot;&gt;React Native Radio&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A show from Infinite Red about React Native, Expo and the art of making cross platform apps.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://selfhosted.show/&quot;&gt;Self Hosted&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;An inspiring show about software that you can host yourself without &amp;quot;the cloud&amp;quot;. I may not do a lot of self hosting, but I enjoy listening to others talk about it in detail.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://shoptalkshow.com/&quot;&gt;Shop Talk&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Somewhat Ruby specific, but a great all-around tech pod cast.&lt;/p&gt;
</content:encoded></item><item><title>Live reloading a Go web application</title><link>https://andri.dk/blog/2024/live-reload-golang-tailwind-templ-htmx/</link><guid isPermaLink="true">https://andri.dk/blog/2024/live-reload-golang-tailwind-templ-htmx/</guid><pubDate>Tue, 19 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import video from &amp;quot;./live-reloading-golang.mp4&amp;quot;;&lt;/p&gt;
&lt;p&gt;I was playing around with server-rendering, and couldn&apos;t really find a good workflow for restarting the Go server, generate templates and generate CSS on changes without manually reloading it.&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src={video} /&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not quite as instant as hot-module-reloading in JS, but it&apos;s better than managing it yourself.&lt;/p&gt;
&lt;h3&gt;NPM dependencies&lt;/h3&gt;
&lt;p&gt;We&apos;re not really using NPM for anything except developer tooling, but you need it all the same.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm init
npm install -D browser-sync concurrently nodemon tailwindcss
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go dependencies&lt;/h3&gt;
&lt;p&gt;If you&apos;re using &lt;a href=&quot;https://templ.guide&quot;&gt;Templ&lt;/a&gt;, then run...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;go install github.com/a-h/templ/cmd/templ@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;package.json&lt;/h3&gt;
&lt;p&gt;Then change the scripts portion of your &lt;code&gt;package.json&lt;/code&gt; look like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;web&amp;quot;,
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Note: Strictly used for dev tools. Not production&amp;quot;,
  &amp;quot;main&amp;quot;: &amp;quot;index.js&amp;quot;,
  &amp;quot;private&amp;quot;: true,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;watch:go&amp;quot;: &amp;quot;nodemon -e go,sql --exec &apos;go build . &amp;amp;&amp;amp; ./app&apos; --signal SIGTERM&amp;quot;,
    &amp;quot;watch:css&amp;quot;: &amp;quot;tailwindcss -i ./web/global.css -o ./web/public/generated.css --watch&amp;quot;,
    &amp;quot;watch:templ&amp;quot;: &amp;quot;templ generate --watch&amp;quot;,
    &amp;quot;watch:browser&amp;quot;: &amp;quot;browser-sync start --port 3001 --no-ui --proxy &apos;localhost:3000&apos; --files ./app&amp;quot;,
    &amp;quot;dev&amp;quot;: &amp;quot;concurrently npm:watch:* --kill-others&amp;quot;,
    &amp;quot;test&amp;quot;: &amp;quot;echo \&amp;quot;Error: no test specified\&amp;quot; &amp;amp;&amp;amp; exit 1&amp;quot;
  },
  &amp;quot;author&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;license&amp;quot;: &amp;quot;ISC&amp;quot;,
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;browser-sync&amp;quot;: &amp;quot;^2.29.3&amp;quot;,
    &amp;quot;concurrently&amp;quot;: &amp;quot;^8.2.2&amp;quot;,
    &amp;quot;nodemon&amp;quot;: &amp;quot;^3.0.2&amp;quot;,
    &amp;quot;tailwindcss&amp;quot;: &amp;quot;^3.3.6&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Honorable mention&lt;/h3&gt;
&lt;p&gt;I tried using &lt;a href=&quot;https://github.com/cosmtrek/air&quot;&gt;Air&lt;/a&gt;, but ended up dropping it for Nodemon. I already needed NPM for Tailwind (plugins), and it works great.&lt;/p&gt;
</content:encoded></item><item><title>Declarative Software Environments</title><link>https://andri.dk/blog/2022/declarative-software-environments/</link><guid isPermaLink="true">https://andri.dk/blog/2022/declarative-software-environments/</guid><description>How I envision we can bring as much value as possible out of a software team</description><pubDate>Thu, 14 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Someone smart, who works in infrastructure said to me recently (paraphrasing):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We can&apos;t have secure defaults on Kubernetes clusters, because the developers require full freedom to work on the cluster&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I used to work as Linux System Administrator for 6 years before focusing on Software Development (10+ years). I honestly think developers want &lt;strong&gt;NOTHING&lt;/strong&gt; to do with Kubernetes directly. That, however does not mean they don&apos;t want it running redundantly somewhere where it &amp;quot;&lt;em&gt;just works (tm)&lt;/em&gt;&amp;quot; though.&lt;/p&gt;
&lt;h3&gt;What I want for my developers&lt;/h3&gt;
&lt;p&gt;I want the ability of &lt;em&gt;continuous delivery&lt;/em&gt;. That the &lt;em&gt;feedback loop&lt;/em&gt; for a single code change is as short as possible, with as little fallout as possible. Every single developer on my team, should be able to make a change and see it through to production.&lt;/p&gt;
&lt;p&gt;Should we then teach every developer about DNS, clusters, schedulers, Kubernetes, Helm and the differences of each cloud provider? No! That is not the best use of their time and expertise.&lt;/p&gt;
&lt;h3&gt;Flow of a change&lt;/h3&gt;
&lt;p&gt;For a relatively standard stack, we have a database, some server code and some client code in a single repository.&lt;/p&gt;
&lt;p&gt;Should any of these steps fail, &lt;em&gt;abort&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Push to branch
&lt;ul&gt;
&lt;li&gt;Create a new infrastructure-environment to stage the change&lt;/li&gt;
&lt;li&gt;New environment should have an exact copy of the database (partial copy if huge)&lt;/li&gt;
&lt;li&gt;Migrations are run on the database copy&lt;/li&gt;
&lt;li&gt;All services (and sites) are deployed (staged)&lt;/li&gt;
&lt;li&gt;End-to-End tests run&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Only for main/trunk branch&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;Migrations are run on the actual database&lt;/li&gt;
&lt;li&gt;All services (and sites) are deployed&lt;/li&gt;
&lt;li&gt;Production is ready&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Clean-up: Delete servers, databases and everything used in the transitional phase.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What is a declarative infrastructure&lt;/h3&gt;
&lt;p&gt;We define &amp;quot;what we want&amp;quot;, but not the the &amp;quot;how we want it&amp;quot;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git commit: This is how your world looks like at this very point in time&lt;/li&gt;
&lt;li&gt;Infrastructure as Code (IaC): Describes what servers, services should look like&lt;/li&gt;
&lt;li&gt;Database migrations: Describe how the database structure should like like&lt;/li&gt;
&lt;li&gt;API schema: Describes the API (GraphQL, OpenAPI, gRPC, etc)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Reducing the barrier&lt;/h3&gt;
&lt;p&gt;I stated earlier, that I want every single developer to be able to follow through on a change to production. But, that doesn&apos;t mean that everyone needs to know about everything. They need to know &lt;em&gt;just enough&lt;/em&gt; to being able to do their work in the best way possible.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;Jim, our frontend developer added a field to our API specification and made a change to our app&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Tim, our backend developer saw a broken test due to an API change and fixed it on the backend&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Kim, our ops consultant changed the number of instances for our API server to 3&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Sim, our database admin increased the ram of our database and changed one of our INT fields to BIGINT&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In an infrastructure where this stuff is automated, they can comfortably change what they need without fearing that they have accidentally broken production.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The scenario I&apos;m describing is absolutely doable, as long as we have test-coverage (I&apos;m not talking percent&apos;s) of the stack as a whole so that anyone on the team can act in confidence. The only thing that I&apos;m uncertain about, is how a large production database can be cloned easily enough for this to be quick. I&apos;ve seen migrations pass in flying colours on test environments, only to fail in production. The data matters, but having pipelines finish quickly is important too.&lt;/p&gt;
&lt;p&gt;As I write this, this is wishful thinking on my part. Our automations aren&apos;t quite there yet, but I think this little rant of mine illustrates what we want for our software teams and hopefully a little insight into how to get there.&lt;/p&gt;
&lt;p&gt;Ideally, I&apos;d like something like this for pull-requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const environment = makeEnvironment(&amp;quot;feature/awesomesauce&amp;quot;, &amp;quot;main&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it would look for the declaration files (from earlier), make the base from &amp;quot;main&amp;quot;, then migrate it to &amp;quot;feature/awesomesauce&amp;quot; and deploy it, after all the tests pass.&lt;/p&gt;
&lt;p&gt;Do you know of anything that might fit the bill? Please &lt;a href=&quot;https://twitter.com/messages/compose?recipient_id=14566494&quot;&gt;reach out&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Upgrading the home-network</title><link>https://andri.dk/blog/2022/home-network/</link><guid isPermaLink="true">https://andri.dk/blog/2022/home-network/</guid><description>Or how my home probably has better network than the office.</description><pubDate>Sun, 03 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Our home-network has been running on an Edgerouter Lite (now discontinued) for 8 years or so and it worked just fine. But, during the summer the heat in our garage has been exceeding 30C/86F degrees and it gave up on me. Now it peeps like a little bird when I turn it on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;edge-router-lite-borked.png&quot; alt=&quot;edge-router-lite&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Finding a replacement&lt;/h3&gt;
&lt;p&gt;So, I used to install Cisco equipment in another lifetime so I have a deep hatred for routers that attempt to do both WiFi and wired Internet. My router should route stuff, act as a firewall and &lt;em&gt;maybe&lt;/em&gt; provide basic network services.&lt;/p&gt;
&lt;p&gt;Requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No WiFi&lt;/li&gt;
&lt;li&gt;Have at least 3 gigabit ethernet ports&lt;/li&gt;
&lt;li&gt;Run something that doesn&apos;t have a horrible &lt;a href=&quot;https://www.cvedetails.com/vulnerability-list.php?vendor_id=12508&amp;amp;product_id=23641&quot;&gt;security track record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Allow me to manually manage stuff like firewall rules/interface settings/routing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A friend from work recommended &lt;a href=&quot;https://mikrotik.com&quot;&gt;Mikrotik&lt;/a&gt; and while I wasn&apos;t terribly thrilled about the WiFi access-point I bought from them, I decided to give it a shot.&lt;/p&gt;
&lt;p&gt;I settled on the &lt;a href=&quot;https://mikrotik.com/product/RB750Gr3&quot;&gt;Mikrotik&lt;/a&gt;. They publish throughput test results on their page and I really like that. Usually vendors try to hide stuff like that. But, given that I have a 1 gigabit connection to my house and it can throughput around 1.972 Mbps it sounded perfect.&lt;/p&gt;
&lt;h3&gt;The plan&lt;/h3&gt;
&lt;p&gt;Now that I had my router (props to &lt;a href=&quot;https://www.proshop.dk&quot;&gt;Proshop&lt;/a&gt; for shipping it within 12 hours) I had an opportunity to plan the network a little better.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;network-architecture.png&quot; alt=&quot;network-diagram&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I&apos;m very happy with the whole setup, except the cable-mess I spawned in our technical cabinet&lt;/li&gt;
&lt;li&gt;Should the fiber fail, it will automatically route all the traffic through 4G (around 100mbit/s)&lt;/li&gt;
&lt;li&gt;Automatically assigns a hostname to the external addres (regardless of fiber/4g)&lt;/li&gt;
&lt;li&gt;VPN from my phone with Wireguard so I can access Home Assistant from wherever without exposing any services to the Internet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;speed-test.png&quot; alt=&quot;speed-test&quot;&gt;&lt;/p&gt;
&lt;p&gt;Tell me about your network setup?&lt;/p&gt;
</content:encoded></item><item><title>Migrating to Astro</title><link>https://andri.dk/blog/2022/migrating-to-astro/</link><guid isPermaLink="true">https://andri.dk/blog/2022/migrating-to-astro/</guid><description>How I built my new site with Astro</description><pubDate>Sat, 02 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This site was originally built with &lt;a href=&quot;https://www.gatsbyjs.com&quot;&gt;Gatsby&lt;/a&gt; site in 2019 and I used it to teach myself Gatsby, React, GraphQL and Tailwind. Gatsby was a natural fit, because I was excited about both GraphQL and React at the time.&lt;/p&gt;
&lt;p&gt;I&apos;ve been considering migrating the site to either Next.js or Remix, but after playing around for a while I settled on &lt;a href=&quot;www.astro.build&quot;&gt;Astro&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What the hell is Astro?&lt;/h2&gt;
&lt;p&gt;Astro is a static site generator (and SSR) that isn&apos;t stuck to a single framework. It supports most of them out of the box. The spoke to me, because I have a bunch of older React components that I wanted to reuse.&lt;/p&gt;
&lt;h2&gt;What&apos;s in the box?&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;p&gt;Since I first created the site, I&apos;ve learned TypeScript, so everything is now strictly typed.&lt;/p&gt;
&lt;h3&gt;Resumé&lt;/h3&gt;
&lt;p&gt;The page parses a &lt;a href=&quot;https://jsonresume.org&quot;&gt;JSON resume&lt;/a&gt; a creates the technology summary on the homepage, but also &lt;a href=&quot;/cv.pdf&quot;&gt;my cv&lt;/a&gt;. The enables the lazy dev that I am to update both my CV and homepage in one go.&lt;/p&gt;
&lt;p&gt;The CV is rendered as an Astro page, as part of the build process, instead of as a post-build hook on the old site.&lt;/p&gt;
&lt;h3&gt;Social Cards&lt;/h3&gt;
&lt;p&gt;A site in 2022 isn&apos;t complete without being able to see a pretty preview when sharing on the larger networks.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;social-card-example.png&quot; alt=&quot;social-card-example&quot;&gt;&lt;/p&gt;
&lt;p&gt;When I created &lt;a href=&quot;https://github.com/andrioid/gatsby-plugin-social-cards&quot;&gt;gatsby-plugin-social-cards&lt;/a&gt;, I used React to generate dynamic SVG files that &lt;a href=&quot;https://sharp.pixelplumbing.com&quot;&gt;sharp&lt;/a&gt; converted into png files. Sharp uses &amp;quot;fontconfig&amp;quot;, so the fonts available when converting the SVG file are dependant on the machine. That made me lose interest in developing my social card plugin further.&lt;/p&gt;
&lt;p&gt;Fast forward to 2022. I learned from a colleague that there is a Rust library called &lt;a href=&quot;https://github.com/RazrFalcon/resvg&quot;&gt;resvg&lt;/a&gt; that can convert to PNG and &lt;strong&gt;supports custom fonts&lt;/strong&gt;! It even has a &lt;a href=&quot;https://github.com/yisibl/resvg-js&quot;&gt;nice wrapper&lt;/a&gt; for JavaScript that I was able to use.&lt;/p&gt;
&lt;p&gt;Astro already has a very nice social-card plugin called &lt;a href=&quot;https://github.com/Princesseuh/astro-social-images#readme&quot;&gt;astro-social-images&lt;/a&gt; and much of the plumbing between my custom hack and Astro is based off of that. Right now, this works for my purposes, but I might open-source it later.&lt;/p&gt;
&lt;h3&gt;Storing the content&lt;/h3&gt;
&lt;p&gt;Here my story takes a sad turn. Clones of my site were popping up with the content in place, so I moved everything off Git and into Nextcloud. But dealing with images across different systems has become a real pain, so I&apos;ve moved the content back into Git.&lt;/p&gt;
&lt;p&gt;But until I come up with a better solution for keeping the content seperate from the source-code, my repository is now private. If you want to take a look, reach out and I will invite you until I find a better solution.&lt;/p&gt;
&lt;h3&gt;Other&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;React: Mostly for the resumé and a few minor components&lt;/li&gt;
&lt;li&gt;Tailwind for styling&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Final words&lt;/h3&gt;
&lt;p&gt;I already had a working Next.js branch, but a little Saturday project to see how troublesome it would be to move some of it to Astro turned into a full migration project.&lt;/p&gt;
&lt;p&gt;My old React components worked out of the box, but most of the newer components are built as Astro components. The syntax is pretty clean and reminds me of Svelte. What finally sold me, was being able to render the resumé PDF directly from Astro.&lt;/p&gt;
&lt;p&gt;My Gatsby site was always pretty fast, but I&apos;ve never seen a Lighthouse score this nice before.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;lighthouse-astro.png&quot; alt=&quot;lighthouse score&quot;&gt;&lt;/p&gt;
&lt;p&gt;Was this overengineering? Absolutely! But it was very fun.&lt;/p&gt;
</content:encoded></item><item><title>ENV files</title><link>https://andri.dk/blog/2019/2021/env-files/</link><guid isPermaLink="true">https://andri.dk/blog/2019/2021/env-files/</guid><pubDate>Sat, 18 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An environmental-variable is a way for the environment (your operating-system, or shell) to share information with future processes. Some ENV vars are global (and set by the OS), others are only useful in a certain context.&lt;/p&gt;
&lt;p&gt;You could be using a configuration file, but the world of hosting has very much adopted the &lt;a href=&quot;https://12factor.net&quot;&gt;12 Factor App&lt;/a&gt; way of configuring applications. For environments like CI/CD, Heroku and Kubernetes, this makes a lot of sense.&lt;/p&gt;
&lt;p&gt;In development though, this can be quite awkard to use.&lt;/p&gt;
&lt;h3&gt;Example: Backend API&lt;/h3&gt;
&lt;p&gt;You&apos;ve written a beautiful monolith, but it needs a couple of things to run properly. It uses a &lt;strong&gt;private key&lt;/strong&gt; to sign auth tokens, and it uses &lt;strong&gt;AWS credentials&lt;/strong&gt; for uploads to S3.&lt;/p&gt;
&lt;p&gt;You decide to use ENV variables and decide on &lt;code&gt;AUTH_KEY&lt;/code&gt; and &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;. Then you set up your hosting, and configure your app to abort if any of those are missing.&lt;/p&gt;
&lt;p&gt;In development though, instead of running &lt;code&gt;npm run dev&lt;/code&gt;, you now need to run &lt;code&gt;AUTH_KEY=xxx AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx npm run dev&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is rather annoying, and might prompt you to write a blog post about ENV variables...&lt;/p&gt;
&lt;h3&gt;Creating the .env file&lt;/h3&gt;
&lt;p&gt;Instead of defining the ENV variables every time, we can create a &lt;code&gt;.env&lt;/code&gt; file in our project workspace. It usually looks something like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# JWT encoding key
AUTH_KEY=youWillNeverGuessThisYouEvilHackers
# AWS Developer Access
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may be tempted to check this file into source control (e.g. Git) and store with your code. &lt;strong&gt;That would be a mistake&lt;/strong&gt;. Especially if you decide at a later point to open-source your project. Then you&apos;d also be giving everyone access to your AWS credentials. Even if you later delete the file from your repo.&lt;/p&gt;
&lt;p&gt;Instead, add it to your &lt;code&gt;.gitignore&lt;/code&gt; file to make sure that it will &lt;em&gt;never&lt;/em&gt; get accidentally pushed with your code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It may not be 100% secure, to store stuff like this on your developer laptop, but it&apos;s still better than storing secrets in your repository.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pro tip: New developers on your projects may not know anything about ENV variables or your local &lt;code&gt;.env&lt;/code&gt; file. So make sure you update your documentation, and provide useful error messages if those ENV variables are missing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never create &lt;code&gt;.env&lt;/code&gt; files for anything other than your development setup. It is very easy to accidentally expose secrets that way.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Reading the .env file&lt;/h3&gt;
&lt;p&gt;If we now run our backend, it will complain that our ENV variables are not set. We have to tell Node (or whatever) about those variables.&lt;/p&gt;
&lt;p&gt;On Linux/Mac this is quite easy.&lt;/p&gt;
&lt;h4&gt;Pass the env-vars to MYCOMMAND&lt;/h4&gt;
&lt;p&gt;In this case &lt;code&gt;npm run dev&lt;/code&gt; will have access to any ENV variables in the file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eval $(egrep -v &apos;^#&apos; .env | xargs) npm run dev

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Export the vars in .env into your shell&lt;/h4&gt;
&lt;p&gt;It&apos;s also possible to &amp;quot;export&amp;quot; the variables to your current session. That way, any command you run afterwards from the same shell will inherit it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export $(egrep -v &apos;^#&apos; .env | xargs)
npm run build
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js&lt;/h3&gt;
&lt;p&gt;It is also possible to read ENV files without doing shell-script dark arts.&lt;/p&gt;
&lt;h4&gt;dotenv&lt;/h4&gt;
&lt;p&gt;You can inject the ENV variables into your Node.js process like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your app.js&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;require(&amp;quot;dotenv&amp;quot;).config();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my experience, this is a horrible way of reading the ENV file. Remember, that this is a convenience for development. Why are we adding it to production code?&lt;/p&gt;
&lt;h4&gt;dotenv, without polluting your code&lt;/h4&gt;
&lt;p&gt;Add a file called &lt;code&gt;local-dev.js&lt;/code&gt; to your project workspace.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { execSync } = require(&amp;quot;child_process&amp;quot;);

// This reads the .env file into the environment
//  and shares it with any child process
require(&amp;quot;dotenv&amp;quot;).config();

const [argnode, argcmd, ...argrest] = process.argv;

// Run whatever follows `node env.js` as a child process and inherit stdin/stdout/etc
execSync(argrest.join(&amp;quot; &amp;quot;), {
  stdio: &amp;quot;inherit&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your package.json. In my example, I&apos;m loading &lt;code&gt;.env&lt;/code&gt; and then running the Remix dev-server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;your package&amp;quot;
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;node ./local-dev.js npm run dev:server&amp;quot;,
	&amp;quot;dev:server&amp;quot;: &amp;quot;remix dev&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are packages on NPM that do this for you, like &lt;a href=&quot;https://www.npmjs.com/package/dotenv-cli&quot;&gt;dotenv-cli&lt;/a&gt; or &lt;a href=&quot;https://www.npmjs.com/package/dotenv-run-script&quot;&gt;dotenv-run-script&lt;/a&gt;. But why install a dependency for 4 lines of code?&lt;/p&gt;
&lt;h3&gt;Notable mentions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://direnv.net&quot;&gt;direnv&lt;/a&gt; can automatically load variables into (and out of) the environment as you enter directories with &lt;code&gt;.envrc&lt;/code&gt; files.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Host Static Websites on a VPS with TLS</title><link>https://andri.dk/blog/2019/2021/host-static-websites-on-a-vps/</link><guid isPermaLink="true">https://andri.dk/blog/2019/2021/host-static-websites-on-a-vps/</guid><pubDate>Sun, 02 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hosting doesn&apos;t have to be complicated or expensive. I&apos;ll show you how to host a static site for $6/mo on DigitalOcean (&lt;a href=&quot;https://m.do.co/c/eb735821ebb8&quot;&gt;ref link&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Be warned, this is a bit technical and you will need to type some things into a terminal. I will explain what each line does on the way.&lt;/p&gt;
&lt;h2&gt;Creating the server&lt;/h2&gt;
&lt;p&gt;Log on to the DigitalOcean &lt;a href=&quot;https://cloud.digitalocean.com&quot;&gt;cloud console&lt;/a&gt; and create a new Droplet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Distribution&lt;/strong&gt;: Ubuntu LTS or Debian is fine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt;: Basic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Options&lt;/strong&gt;: AMD, $6/mo (1G ram, 20G SSD, 1TB bandwidth)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Datacenter&lt;/strong&gt;: Just pick the one closest to you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Pick a password or use a public SSH key (i use SSH keys).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hostname&lt;/strong&gt;: Something that helps you remember. Mine is called &amp;quot;hostingdemo&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DigitalOcean offers automatic backups for $9.60/mo and you can select that if you want, but in our case... we&apos;ll be building the site elsewhere and uploading it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting1.png&quot; alt=&quot;Digital Ocean 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;After a few seconds, you should see your server ready.&lt;/p&gt;
&lt;h2&gt;Logging on to the server&lt;/h2&gt;
&lt;p&gt;Note the &lt;strong&gt;IP address&lt;/strong&gt; field. Click it to copy the address. You need SSH. It should already be installed on your Linux, Mac or Windows machine.&lt;/p&gt;
&lt;p&gt;In your terminal shell (open terminal og cmd).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ssh root@your-droplet-ip-address
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you selected a password, you&apos;ll be prompted for that. If you selected an SSH key and that key is setup on your machine you&apos;re all logged in.&lt;/p&gt;
&lt;h2&gt;Installing what we need&lt;/h2&gt;
&lt;p&gt;We&apos;ll be using &lt;a href=&quot;https://caddyserver.com&quot;&gt;Caddy&lt;/a&gt; for our web-server to use its&apos; automatic TLS feature. Other options could be nginx or apache.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Install the web server
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&apos; | sudo apt-key add -
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&apos; | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy
# Update your server, highly recommended
apt dist-upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now open a browser and access &lt;em&gt;http://your-droplet-ip-address&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting2.png&quot; alt=&quot;DigitalOcean 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;That was easy, right?&lt;/p&gt;
&lt;h2&gt;Adding a web user to upload with&lt;/h2&gt;
&lt;p&gt;So far, we&apos;ve been using &amp;quot;root&amp;quot;, a system user that can do anything. We don&apos;t want to spread that around. Create a new user:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;useradd webuser --password somethingstronger -s /sbin/nologin -m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates &amp;quot;webuser&amp;quot; with the password &amp;quot;somethingstronger&amp;quot; (and I do mean, it... use your own strong password), disables the SSH login and creates a home directory.&lt;/p&gt;
&lt;h2&gt;Enabling SFTP&lt;/h2&gt;
&lt;p&gt;To be able to upload files from a non-privleged user we need to change the SSH configuration a bit. Open the SSH daemon config in &amp;quot;/etc/ssh/sshd_config&amp;quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure that the following lines look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Subsystem       sftp    internal-sftp
PasswordAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first one enables use of SFTP without having a login shell (like our limited webuser) and the second allows password logins for SSH.&lt;/p&gt;
&lt;p&gt;Restart the SSH server&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;service ssh restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test the login from your machine (this works on Mac and Linux, unsure about Windows)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sftp webuser@your-droplet-ip
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;webuser@your-droplet-ip&apos;s password:
Connected to your-droplet-ip.
sftp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tell Caddy to serve files from our webuser directory&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nano /etc/caddy/Caddyfile
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;:80

# Set this path to your site&apos;s directory.
root * /home/webuser

# Enable the static file server.
file_server

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Caddy to serve from the webuser home directory instead. But, we need to give it access.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;usermod -a -G webuser caddy
service caddy restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Test the web-server&lt;/h3&gt;
&lt;p&gt;Upload an &amp;quot;index.html&amp;quot; to your server. Use whatever SFTP client you like (like FileZilla or winscp). From my local machine, I create a simple HTML file on my computer called &amp;quot;index.html&amp;quot; with &amp;quot;hello world&amp;quot; in it. Then I uploaded it to our server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sftp sftp webuser@your-droplet-ip
put index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting3.png&quot; alt=&quot;hello world on caddy&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Adding a hostname or a domain&lt;/h2&gt;
&lt;p&gt;I added a DNS record to give my server a name. How exactly you add this depends on your DNS hosting provider. DigitalOcean has this under &amp;quot;Networking -&amp;gt; Domains&amp;quot;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hostname&lt;/strong&gt;: whateveryouwant (I used &amp;quot;hostingdemo&amp;quot;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record Type&lt;/strong&gt;: A&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Target&lt;/strong&gt;: your-droplet-ip-address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting4.png&quot; alt=&quot;DNS record&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, if I point a browser to http://hostingdemo.andri.dk...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting5.png&quot; alt=&quot;Digital Ocean 5&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Adding Encryption (TLS/SSL)&lt;/h2&gt;
&lt;p&gt;To do this, we open our Caddyfile again in &amp;quot;/etc/caddy/Caddyfile&amp;quot; and change it to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hostingdemo.andri.dk # replace this with your hostname

# Set this path to your site&apos;s directory.
root * /home/webuser

# Enable the static file server.
file_server

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload the configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;service caddy reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your browser change the URL from &amp;quot;http&amp;quot; to &amp;quot;https&amp;quot; and reload.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting6.png&quot; alt=&quot;Digital Ocean 6&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We created a new server on DigitalOcean&lt;/li&gt;
&lt;li&gt;We added a new non-privileged user &amp;quot;webuser&amp;quot; and configured it to upload the site&lt;/li&gt;
&lt;li&gt;We setup a DNS name for our new site&lt;/li&gt;
&lt;li&gt;We setup Caddy with automatic TLS using &lt;a href=&quot;https://letsencrypt.org&quot;&gt;Lets Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bonus&lt;/h2&gt;
&lt;h3&gt;Automatically upload your Jamstack site with Github Actions&lt;/h3&gt;
&lt;p&gt;This is covered in more details in my &lt;a href=&quot;https://andri.dk/blog/2021/deploy-static-websites-anywhere&quot;&gt;Deploy your Static Site Anywhere&lt;/a&gt; post, but copy paste the following into a &lt;code&gt;.github/workflows/deploy.yaml&lt;/code&gt; file in your repo on Github.&lt;/p&gt;
&lt;p&gt;You need the following secrets set up on your repo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SFTP_HOST&lt;/strong&gt;: host or ip of droplet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP_USER&lt;/strong&gt;: webuser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP_PASS&lt;/strong&gt;: yourverysecretpassword&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: buildpack
on: [push, pull_request]
jobs:
  remote-build:
    runs-on: ubuntu-latest
    container:
      image: docker:stable
      volumes:
        - /home/runner:/workspace
    env:
      IMG_NAME: ${{ github.workflow }}
      NODE_ENV: production # Makes this a little more clean on the Node side
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Downloads and caches the pack command (the code is open)
      - uses: andrioid/setup-pack@main
      # Attempt to fetch any previous image so that we may use it for caching
      - run: docker pull ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME} || true
      # Builds your site with buildpacks (heroku builder)
      - run: pack build ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME} --builder=heroku/buildpacks:20
      # Pushes your new image to Github&apos;s Container Registry
      - run: docker push ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME}
      # Copy the build assets from the container and cleanup
      - run: CONTAINER_ID=$(docker create ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME}:latest /bin/sh) &amp;amp;&amp;amp; docker cp ${CONTAINER_ID}:/workspace/public . &amp;amp;&amp;amp; docker rm ${CONTAINER_ID}
      - run: ls -alh public
      - uses: actions/upload-artifact@v2
        with:
          name: public
          path: public/

  deploy-via-sftp:
    needs: remote-build
    runs-on: ubuntu-latest
    env:
      RCLONE_CONFIG_VPS_TYPE: sftp
      RCLONE_CONFIG_VPS_HOST: ${{ secrets.SFTP_HOST }}
      RCLONE_CONFIG_VPS_USER: ${{ secrets.SFTP_USER }}
      RCLONE_CONFIG_VPS_PASS_PLAIN: ${{ secrets.SFTP_PASS }}
    steps:
      - uses: andrioid/setup-rclone-action@main
      - uses: actions/download-artifact@v2
        with:
          name: public
          path: public
      - run: echo &amp;quot;RCLONE_CONFIG_VPS_PASS=$(rclone obscure ${RCLONE_CONFIG_VPS_PASS_PLAIN})&amp;quot; &amp;gt;&amp;gt; $GITHUB_ENV
      - run: rclone -v copy public vps:.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires you to have a repository on Github and that your site can be built with Buildpacks. I have this running with Gatsby, but it should work with almost anything that builds into &lt;code&gt;public/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;do-hosting7.png&quot; alt=&quot;Deployed via Github Actions&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now my Jamstack Example site is deployed on our VPS!&lt;/p&gt;
</content:encoded></item><item><title>Deploy Static Websites Anywhere</title><link>https://andri.dk/blog/2019/2021/deploy-static-websites-anywhere/</link><guid isPermaLink="true">https://andri.dk/blog/2019/2021/deploy-static-websites-anywhere/</guid><pubDate>Sat, 01 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Before I started my carrier in Software Development, I was a System- and Network Administrator. I used to setup web servers and managed my own little server room. I truly, do not miss upgrading operating-systems on live services or dealing with hardware failure. However, hosting web-pages back then was much easier. You&apos;d get an FTP/SFTP access and you&apos;d upload your files. Then they got served statically...&lt;/p&gt;
&lt;p&gt;Today, everything is about &lt;strong&gt;scale&lt;/strong&gt; and maybe prematurely so. If you want to host a typical static site today, many hosting providers force you to add a Github (or similar) integration to your repository. To be fair; these providers offer automatic TLS, CDN, fail over and full scaling. But, what if I want to upload my own files? What if I already have a build-pipeline somewhere? Then this forced build-process is not convenient, but quite &lt;strong&gt;annoying&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;m going to show you how to break free and deploy your static site wherever you want, using nothing but Github Actions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;deploy-anywhere2.png&quot; alt=&quot;Deploy Anywhere&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Our toolbox for today&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;Github Actions&lt;/a&gt;&lt;/strong&gt;: Github&apos;s build pipeline. This can be done on any CI platform. I simply chose Github actions because almost everyone uses Github and it&apos;s free for open source projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://rclone.org&quot;&gt;rclone&lt;/a&gt;&lt;/strong&gt;: A wonderful file-syncing tool that supports a whole range of services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://buildpacks.io&quot;&gt;Buildpacks&lt;/a&gt;&lt;/strong&gt;: Build almost any project without writing a single Dockerfile. It detects your project, handles caching and has been used in production at Heroku for a very long time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You also need &lt;a href=&quot;https://nodejs.org&quot;&gt;node.js&lt;/a&gt; and &lt;a href=&quot;https://github.com/git-guides/install-git&quot;&gt;git&lt;/a&gt; installed before we proceed. This guide expects that you know how to work with Git and Github.&lt;/p&gt;
&lt;h2&gt;TLDR;&lt;/h2&gt;
&lt;p&gt;If you&apos;re anything like me, you just want to skip the bullshit and &lt;a href=&quot;https://github.com/andrioid/jamstack-example&quot;&gt;dive straight into the code&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Setting up our sample Gatsby site&lt;/h2&gt;
&lt;p&gt;This could be any type of static site. I decided to go with Gatsby, because that&apos;s what I&apos;m using. The only thing we assume here, is that &lt;code&gt;npm run build&lt;/code&gt; works and builds to &lt;code&gt;public/&lt;/code&gt;. Otherwise you have to adjust for it yourself.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you already have a site you want to deploy, you can skip this part.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm init gatsby
# follow the prompts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;gatsby-example-init.png&quot; alt=&quot;gatsby install prompt&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you enabled the feed or sitemap plugins you need to add a &lt;code&gt;metadata.siteUrl&lt;/code&gt; to your &lt;code&gt;gatsby-config.js&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd jamstack-example # or whatever you named yours
npm run develop # if you want to play with it
npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you have a site in &lt;code&gt;public/&lt;/code&gt; that is ready to be deployed. Let&apos;s try it out real quick before going on to the Github part.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run serve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then direct your browser to &lt;a href=&quot;http://localhost:9000&quot;&gt;http://localhost:9000&lt;/a&gt;. If everything went well, you should see this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gatsby-localhost.png&quot; alt=&quot;gatsby on localhost&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Building it with Actions&lt;/h2&gt;
&lt;p&gt;Log on to Github and create a new repo. On that repo, find the repository URL and copy it where it says &lt;code&gt;&amp;lt;repo&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git remote add origin &amp;lt;repo&amp;gt;
git branch -M main
git push -u origin main

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now then. Let&apos;s add some actions! Create a file with this name and add it to your project.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;.github/workflows/deploy-anywhere.yml&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: buildpack
on: [push, pull_request]
jobs:
  remote-build:
    runs-on: ubuntu-latest
    container:
      image: docker:stable
      volumes:
        - /home/runner:/workspace
    env:
      IMG_NAME: ${{ github.workflow }}
      NODE_ENV: production # Makes this a little more clean on the Node side
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Downloads and caches the pack command (the code is open)
      - uses: andrioid/setup-pack@main
      # Attempt to fetch any previous image so that we may use it for caching
      - run: docker pull ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME} || true
      # Builds your site with buildpacks (heroku builder)
      - run: pack build ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME} --builder=heroku/buildpacks:20
      # Pushes your new image to Github&apos;s Container Registry
      - run: docker push ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME}
      # Copy the build assets from the container and cleanup
      - run: CONTAINER_ID=$(docker create ghcr.io/${GITHUB_REPOSITORY}/${IMG_NAME}:latest /bin/sh) &amp;amp;&amp;amp; docker cp ${CONTAINER_ID}:/workspace/public . &amp;amp;&amp;amp; docker rm ${CONTAINER_ID}
      - run: ls -alh public
      - uses: actions/upload-artifact@v2
        with:
          name: public
          path: public/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It goes without saying that you need to add, commit and push this change afterwards.&lt;/p&gt;
&lt;h2&gt;Deployment&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#deploy-to-aws-s3&quot;&gt;Deploy to AWS S3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#deploy-to-github-pages&quot;&gt;Deploy to Github Pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#deploy-to-neocities&quot;&gt;Deploy to Neocities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#deploy-to-netlify&quot;&gt;Deploy to Netlify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#deploy-to-sftp&quot;&gt;Deploy to SFTP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Deploy to AWS S3&lt;/h3&gt;
&lt;p&gt;Amazon&apos;s S3 is one of the oldest managed cloud service and it shows why. It&apos;s capable to act as a bin for all your files, yet also supports things like static hosting and rewrites.&lt;/p&gt;
&lt;h4&gt;Create a bucket&lt;/h4&gt;
&lt;p&gt;Log on to AWS management console and find the S3 page. Create a bucket and note the ARN name.&lt;/p&gt;
&lt;h4&gt;IAM &amp;amp; access control&lt;/h4&gt;
&lt;p&gt;One of the most painful AWS experiences, is how we deal with permissions. I&apos;ve done this many times, but I have to look it up every time.&lt;/p&gt;
&lt;p&gt;Create a user with IAM and assign the following permissions with a new policy. Replace MYBUCKET with your ARN from earlier. Mine is &lt;em&gt;&amp;quot;arn:aws:s3:::jamstack-example.andri.dk/&amp;quot;&lt;/em&gt;.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;jamstack-policy&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;Version&amp;quot;: &amp;quot;2012-10-17&amp;quot;,
    &amp;quot;Statement&amp;quot;: [
        {
            &amp;quot;Sid&amp;quot;: &amp;quot;VisualEditor0&amp;quot;,
            &amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
            &amp;quot;Action&amp;quot;: [
                &amp;quot;s3:PutObject&amp;quot;,
                &amp;quot;s3:GetObject&amp;quot;,
                &amp;quot;s3:ListBucket&amp;quot;,
                &amp;quot;s3:DeleteObject&amp;quot;,
                &amp;quot;s3:PutObjectAcl&amp;quot;
            ],
            &amp;quot;Resource&amp;quot;: [
                &amp;quot;MYBUCKET&amp;quot;,
                &amp;quot;MYBUCKET/*&amp;quot;
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This policy allows your user to upload, download, list files, delete objects and upload with permissions (rclone uses this to apply your acl). The resources limit this permission to this single bucket (a good idea!).&lt;/p&gt;
&lt;h4&gt;TLS / SSL&lt;/h4&gt;
&lt;p&gt;Some services do this automatically, but AWS requires you to do this manually. I didn&apos;t add TLS to my example site, but you can take a look at this &lt;a href=&quot;https://towardsdatascience.com/static-hosting-with-ssl-on-s3-a4b66fb7cd00?gi=2d84a6cc94b4&quot;&gt;this blog post&lt;/a&gt; on how to add TLS to your S3 hosted site.&lt;/p&gt;
&lt;h4&gt;All done!&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;aws-deployment.png&quot; alt=&quot;Github Action results&quot;&gt;
The site is now deployed on AWS S3.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;aws-deployed.png&quot; alt=&quot;Screenshot of the deployed site&quot;&gt;&lt;/p&gt;
&lt;p&gt;Doing with any S3/SFTP any site isn&apos;t much different from the S3 example. In the appendix, I&apos;ll provide a few more examples.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy-aws-s3:
  needs: remote-build
  runs-on: ubuntu-latest
  env:
    RCLONE_CONFIG_AWSS3_TYPE: s3
    RCLONE_CONFIG_AWSS3_ACCESS_KEY_ID: ${{ secrets.S3_KEY_ID }}
    RCLONE_CONFIG_AWSS3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
    RCLONE_CONFIG_AWSS3_REGION: eu-central-1 # yours may differ
    RCLONE_CONFIG_AWSS3_ACL: &amp;quot;public-read&amp;quot;
    BUCKETNAME: jamstack-example.andri.dk
  steps:
    - uses: andrioid/setup-rclone-action@main
    - uses: actions/download-artifact@v2
      with:
        name: public
        path: public
    - run: ls -alh
    - run: rclone ls awss3:${BUCKETNAME}
    - run: rclone -v copy public awss3:${BUCKETNAME}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how my deploy-aws-s3 job looks in my workflow file.&lt;/p&gt;
&lt;p&gt;You can see the site in action &lt;a href=&quot;http://jamstack-example.andri.dk/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Deploy to Netlify&lt;/h3&gt;
&lt;p&gt;To be able to upload files to Netlify, you have to create a new site. To create a site without adding a Git repo, you can go to the &amp;quot;Sites&amp;quot; page and in the bottom there is a drag-drop area where you can drop your &amp;quot;public&amp;quot; directory to create a new site.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;netlify-new-site.png&quot; alt=&quot;creating a new site manually on Netlify&quot;&gt;&lt;/p&gt;
&lt;p&gt;When that is done, you can use the following job definition. Just grab the &lt;em&gt;Site API ID&lt;/em&gt; from the site-settings page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy-netlify:
  needs: remote-build
  runs-on: ubuntu-latest
  steps:
    - uses: actions/download-artifact@v2
      with:
        name: public
        path: public
    - uses: jsmrcaga/action-netlify-deploy@v1.1.0
      with:
        NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
        NETLIFY_DEPLOY_TO_PROD: true
        NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
        BUILD_DIRECTORY: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the site in action &lt;a href=&quot;https://jamstack-example-andri.netlify.app&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Deploy to Github Pages&lt;/h3&gt;
&lt;p&gt;Github Pages uses Git, and if you have a project that you want to use for Github Pages you can just create an empty &amp;quot;gh-pages&amp;quot; branch and use it to update your site.&lt;/p&gt;
&lt;p&gt;For this blog-post, I created &amp;quot;andrioid/jamstack-example&amp;quot; and I&apos;m using that for a demo deployment.&lt;/p&gt;
&lt;p&gt;To setup our pages branch, do the following. &lt;a href=&quot;https://stackoverflow.com/a/34100189/99209&quot;&gt;Credit&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git checkout --orphan gh-pages
git rm -rf .
git commit --allow-empty -m &amp;quot;root commit&amp;quot;
git push -u origin gh-pages

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a branch named &amp;quot;gh-pages&amp;quot; without any commit history. Then deletes all of the tracked files. Then we add an empty commit and push it. Github will automatically set Pages up when the branch is named &amp;quot;gh-pages&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ghpages.png&quot; alt=&quot;github pages setting&quot;&gt;&lt;/p&gt;
&lt;p&gt;The only remaining thing is to use our build-step to upload the site to this branch. Fortunately there is &lt;a href=&quot;https://github.com/JamesIves/github-pages-deploy-action&quot;&gt;an action&lt;/a&gt; that does just that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy-ghpages:
  needs: remote-build
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v2 # needed to setup git
    - uses: actions/download-artifact@v2
      with:
        name: public
        path: public
    - uses: JamesIves/github-pages-deploy-action@4.1.1
      with:
        branch: gh-pages
        folder: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this job to &lt;strong&gt;your deploy-anywhere.yaml&lt;/strong&gt; file.&lt;/p&gt;
&lt;p&gt;You can see the site in action &lt;a href=&quot;https://andrioid.github.io/jamstack-example&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Deploy to Neocities&lt;/h3&gt;
&lt;p&gt;Neocities offers free hosting and if you upgrade you can have TLS and a custom domain as well. They also support Webdav and that would allow us to upload with rclone (just like we did with S3), but I don&apos;t have a premium account.&lt;/p&gt;
&lt;p&gt;So, to get this running we grab the &lt;a href=&quot;https://github.com/marketplace/actions/deploy-to-neocities&quot;&gt;Deploy to Neocities action&lt;/a&gt;. You can find your API key in the Neocities Site Settings. Add it to your repo secrets as &amp;quot;NEOCITIES_API_KEY&amp;quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy-neocities:
  needs: remote-build
  runs-on: ubuntu-latest
  steps:
    - uses: actions/download-artifact@v2
      with:
        name: public
        path: public
    - name: Deploy to neocities
      uses: bcomnes/deploy-to-neocities@v1
      with:
        api_token: ${{ secrets.NEOCITIES_API_KEY }}
        cleanup: false
        dist_dir: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this job to &lt;strong&gt;your deploy-anywhere.yaml&lt;/strong&gt; file.&lt;/p&gt;
&lt;p&gt;You can see the site in action &lt;a href=&quot;https://jamstack-example.neocities.org/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Deploy to SFTP&lt;/h3&gt;
&lt;p&gt;I setup a small webserver with automatic TLS on DigitalOcean to demonstrate SFTP. You can read about it &lt;a href=&quot;https://andri.dk/blog/2021/host-static-websites-on-a-vps&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You need the following secrets set up on your repo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SFTP_HOST&lt;/strong&gt;: host or ip of sftp server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP_USER&lt;/strong&gt;: webuser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP_PASS&lt;/strong&gt;: yourverysecretpassword&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add this job to your &lt;strong&gt;deploy-anywhere.yaml&lt;/strong&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy-via-sftp:
  needs: remote-build
  runs-on: ubuntu-latest
  env:
    RCLONE_CONFIG_VPS_TYPE: sftp
    RCLONE_CONFIG_VPS_HOST: ${{ secrets.SFTP_HOST }}
    RCLONE_CONFIG_VPS_USER: ${{ secrets.SFTP_USER }}
    RCLONE_CONFIG_VPS_PASS_PLAIN: ${{ secrets.SFTP_PASS }}
  steps:
    - uses: andrioid/setup-rclone-action@main
    - uses: actions/download-artifact@v2
      with:
        name: public
        path: public
    - run: echo &amp;quot;RCLONE_CONFIG_VPS_PASS=$(rclone obscure ${RCLONE_CONFIG_VPS_PASS_PLAIN})&amp;quot; &amp;gt;&amp;gt; $GITHUB_ENV
    - run: rclone -v copy public vps:.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the site in action &lt;a href=&quot;https://hostingdemo.andri.dk&quot;&gt;here&lt;/a&gt;. This link may stop working when I decide that I don&apos;t want to pay $5/mo for it.&lt;/p&gt;
&lt;h3&gt;Additional Hosting Providers?&lt;/h3&gt;
&lt;p&gt;Please drop me a line on &lt;a href=&quot;https://twitter.com/andrioid&quot;&gt;Twitter&lt;/a&gt; if you want to add a hosting provider to this post. The only requirement is that it&apos;s possible to create the project without Git integration.&lt;/p&gt;
&lt;h4&gt;Deploy to Vercel 🚫️&lt;/h4&gt;
&lt;p&gt;Before Vercel was Zeit, their CLI &lt;code&gt;now&lt;/code&gt; was able to take any folder and make it a website. But, it doesn&apos;t look like Vercel allows any of that anymore. I also can&apos;t find a way to create a project without integrating with a Git hosting provider.&lt;/p&gt;
&lt;h4&gt;Deploy to DigitalOcean 🚫️&lt;/h4&gt;
&lt;p&gt;DigitalOcean Spaces was actually my first attempt, but only after getting almost everything working, I realized that &lt;a href=&quot;https://www.digitalocean.com/community/questions/spaces-set-index-html-as-default-landing-page&quot;&gt;they don&apos;t support indexes&lt;/a&gt;. So, I had everything up and running with their CDN, but I had to manually link to &amp;quot;index.html&amp;quot;.&lt;/p&gt;
&lt;p&gt;As far as I know, there is no way of manually uploading files to their &amp;quot;Static Apps&amp;quot; offering.&lt;/p&gt;
&lt;h2&gt;Final words&lt;/h2&gt;
&lt;p&gt;When I started writing this post, I was sure that most of the hosting providers &lt;em&gt;did&lt;/em&gt; in fact support open standards like SFTP, Webdav or even S3. But, that&apos;s not the case and that makes me sad.&lt;/p&gt;
&lt;p&gt;Why those providers have the need to dominate our workflows is beyond my understanding. After all, what we&apos;re uploading here isn&apos;t any different than classic websites. All we need is a web-server. Everything else is just gravy.&lt;/p&gt;
&lt;h2&gt;Appendix&lt;/h2&gt;
&lt;h3&gt;How to copy files from a Docker image&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker create $image  # returns container ID
docker cp $container_id:$source_path $destination_path
docker rm $container_id
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Blogging with Obsidian, Gatsby and Nextcloud</title><link>https://andri.dk/blog/2019/2021/blogging-with-obsidian-gatsby-and-nextcloud/</link><guid isPermaLink="true">https://andri.dk/blog/2019/2021/blogging-with-obsidian-gatsby-and-nextcloud/</guid><pubDate>Sat, 24 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Finding the motivation to blog has always been hard for me. Sometimes, because I have nothing to blog about, and that&apos;s fair. But sometimes it is because I&apos;m not on a computer with my Gatsby site setup, or I just want to write something down quick.&lt;/p&gt;
&lt;p&gt;Maybe unsurprising, I have the same problem with note-taking and I&apos;ve attempted and abandoned countless platforms. Recently I bought Nextcloud hosting for my photos and I can happily report that my photos and files are synchronized nicely. Then I discovered Obsidian and found that using Nextcloud to sync my notes allowed my to have them everywhere.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;obsidian-example.png&quot; alt=&quot;Obsidian Example&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://obsidian.md&quot;&gt;Obsidian&lt;/a&gt; is a markdown editor, with a twist. It offers zeitgeist or outliner features that otherwise were limited to expensive tools or org-mode in Emacs.&lt;/p&gt;
&lt;p&gt;I use it for work, for my daily notes, recipies and now recently, blogging. In case you&apos;re curious of the theme, it&apos;s &lt;a href=&quot;https://github.com/mgmeyers/obsidian-california-coast-theme&quot;&gt;California Coast&lt;/a&gt; and a few plugins. Send me a DM if you want the list.&lt;/p&gt;
&lt;h2&gt;What I wanted from the workflow&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Write stuff in Obsidian on &lt;strong&gt;any&lt;/strong&gt; of my computers.&lt;/li&gt;
&lt;li&gt;Add frontmatter tags to my blog-post.&lt;/li&gt;
&lt;li&gt;Go to bed and have my site automatically updated if there are any new posts.&lt;/li&gt;
&lt;li&gt;Wake up the next morning and read comments about my blog post.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re thinking...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;overengineering.png&quot; alt=&quot;Over-engineering&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then you&apos;re absolutely right. But if you can&apos;t overengineer your personal projects, then there is something wrong with the world. &lt;a href=&quot;https://bonkersworld.net/building-software&quot;&gt;Image credit&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Before we get started&lt;/h2&gt;
&lt;p&gt;You can skip this if you don&apos;t plan on repeating my experiment.&lt;/p&gt;
&lt;p&gt;I must add that this isn&apos;t exactly &lt;em&gt;production ready&lt;/em&gt;, since I had to modify some plugins to make this work. Proceed at your own risk.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextcloud.com/install/#instructions-server&quot;&gt;Install&lt;/a&gt; or &lt;a href=&quot;https://nextcloud.com/signup/&quot;&gt;sign up&lt;/a&gt; for Nextcloud&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://nextcloud.com/clients/&quot;&gt;Nextcloud client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://obsidian.md&quot;&gt;Obsidian&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gatsbyjs.com/get-started/&quot;&gt;Setup a new Gatsby site&lt;/a&gt; or use an existing one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting up your vault&lt;/h2&gt;
&lt;p&gt;I have a one big vault in Obsidian and source my blog posts from &lt;code&gt;blog/&lt;/code&gt;. So anything in there will get picked up by Gatsby (more on that later). Nextcloud will sync your files across all of your computers.&lt;/p&gt;
&lt;p&gt;Writing Markdown is easy, but if you want to know more, I recommend Github&apos;s &lt;a href=&quot;https://guides.github.com/features/mastering-markdown/&quot;&gt;Mastering Markdown&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Obsidian also supports drag-and-drop on images, so I just drop them into my editor and Obsidian copies the files.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; By default, Obsidian puts them in the root of your vault. I have &amp;quot;Files &amp;amp; Links &amp;gt; Default Location for New Attachments&amp;quot; set to &amp;quot;Same folder as current file&amp;quot;. Also be sure to set &amp;quot;Use [[Wikilinks]]&amp;quot; to &amp;quot;false&amp;quot;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Writing blog posts&lt;/h2&gt;
&lt;p&gt;At this point you have Obsidian setup and Nextcloud syncing your files to a server. Create a new file in &lt;code&gt;blog/2021/testing.md&lt;/code&gt; and add some frontmatter data to it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```md
---
title: Overriding blog title
tags: [&amp;quot;gatsby&amp;quot;, &amp;quot;nextcloud&amp;quot;, &amp;quot;obsidian&amp;quot;]
---

## I am a test blog post

Fear my grammar skills!

```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Syncing to Nextcloud&lt;/h2&gt;
&lt;p&gt;After you&apos;ve created your first blog-post, check your Nextcloud instance to verify the changes were synced.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;nextcloud-files.png&quot; alt=&quot;This blog post on my Nextcloud instance&quot;&gt;&lt;/p&gt;
&lt;p&gt;I use a seperate user that only has read-access to my blog directory for security reasons, but to keep things simple; I&apos;m going to skip that part.&lt;/p&gt;
&lt;h2&gt;Enter the Great Gatsby&lt;/h2&gt;
&lt;h3&gt;Installing the plugins&lt;/h3&gt;
&lt;p&gt;You need to install the following plugins for this to work.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/andrioid/gatsby-source-webdav&quot;&gt;@andrioid/gatsby-source-webdav&lt;/a&gt;: Allows us to fetch Gatsby nodes from webdav. This is basically &lt;a href=&quot;https://github.com/dmgarland/gatsby-source-webdav&quot;&gt;this&lt;/a&gt; with a newer version of webdav and support for remark nodes. I tried getting a PR merged, but received no response. I&apos;d be happy to try again if the author wishes me to.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/andrioid/andri.dk/tree/master/plugins/gatsby-remark-webdav&quot;&gt;gatsby-remark-webdav&lt;/a&gt;: Something i hacked together. Not ready to release on NPM yet. Only use this if you need image assets from webdav. It parses your markdown posts from webdav and searches for images. If then copies the images and makes relative links work during the build process.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;npm install --save @andrioid/gatsby-source-webdav
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can copy the other plugin into your &lt;code&gt;plugins/&lt;/code&gt; directory if you want to use it. I may release it on NPM later.&lt;/p&gt;
&lt;h3&gt;Configuring the plugins&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;gatsby-config.js&lt;/strong&gt; (in your plugins section)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[
  {
    resolve: &amp;quot;@andrioid/gatsby-source-webdav&amp;quot;
      options: {
        baseURL: `https://${process.env[&amp;quot;BLOGDATA_BASEURL&amp;quot;]}/remote.php/dav/files/${process.env[&amp;quot;BLOGDATA_USER&amp;quot;]}`,
        credentials: {
          username: process.env[&amp;quot;BLOGDATA_USER]&amp;quot;,
          password: process.env[&amp;quot;BLOGDATA_PASSWORD&amp;quot;], // Please don&apos;t hardcode this
		},
		recursive: true,
		glob: &amp;quot;**/*.{md,jpg,jpeg,png,gif}&amp;quot;,
		sharePath: &amp;quot;Documents/MyVault/Blog&amp;quot;, // yours will vary
      },
    },
    {
      resolve: &amp;quot;gatsby-transformer-remark&amp;quot;,
      options: {
        plugins: [
          &amp;quot;gatsby-remark-webdav&amp;quot;, // if you use it
		  &amp;quot;gatsby-remark-images&amp;quot;
        ],
      },
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you start your Gatsby dev-server, make sure to add the ENV variables you just configured, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BLOGDATA_BASEURL=https://mynextcloud.example.com \
BLOGDATA_USER=myuser \
BLOGDATA_PASSWORD=secret \
npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gatsby will now connect to your Nextcloud server and process all the files listed in &lt;code&gt;sharePath&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;blog-post-example.png&quot; alt=&quot;How this looks on my dev-server&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Node Creation&lt;/h2&gt;
&lt;p&gt;What &lt;code&gt;gatsby-source-webdav&lt;/code&gt; does, is creating Gatsby file nodes for the files it finds on the webdav endpoint. Then &lt;code&gt;gatsby-transform-remark&lt;/code&gt; reacts to those file-nodes and creates markdown nodes.&lt;/p&gt;
&lt;p&gt;If you&apos;re using my &lt;code&gt;gatsby-remark-webdav&lt;/code&gt; plugin, then it reacts to webdav backed Markdown nodes and then copies the files necessary to link image&lt;/p&gt;
&lt;p&gt;Those markdown nodes are then used to generate our blog posts or other markdown powered pages (and we won&apos;t be covering that).&lt;/p&gt;
&lt;p&gt;Every Gatsby site is a bit different in how it creates pages, but I like to add node fields so that my GraphQL queries on pages are as simple as possible.&lt;/p&gt;
&lt;p&gt;Here is my &lt;code&gt;onCreateNode&lt;/code&gt; handler, yours may differ. What is happening here, is that some of my content is sourced from files, but some of it is sourced from webdav. To unify the node-fields, I use frontmatter where I can, but otherwise I try to use sensible defaults (like filename on webdav).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Please don&apos;t copy this blindly. There are some things (such as the slug generation) that should be rewritten when I wrote this blog post. Hopefully I already did that at the time you read this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;gatsby-node.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;exports.onCreateNode = ({ node, getNode, actions, reporter }) =&amp;gt; {
  const isDev = process.env.NODE_ENV !== &amp;quot;production&amp;quot;;
  const { createNodeField } = actions;
  if (node.internal.type === `MarkdownRemark`) {
    if (!node.parent) {
      return;
    }
    const fileNode = getNode(node.parent);

    const { frontmatter } = node;
    let title = frontmatter.title;
    let slug = frontmatter &amp;amp;&amp;amp; frontmatter.path;
    let date = frontmatter &amp;amp;&amp;amp; frontmatter.date &amp;amp;&amp;amp; new Date(frontmatter.date);
    let tags = (frontmatter &amp;amp;&amp;amp; frontmatter.tags) || [];
    let draft = (frontmatter.draft === true &amp;amp;&amp;amp; !isDev) || false;

    if (fileNode.parent) {
      const davNode = getNode(fileNode.parent);
      if (davNode &amp;amp;&amp;amp; davNode.internal.type === &amp;quot;webdav&amp;quot;) {
        const namePath = path.parse(davNode.filename);
        if (!title) {
          title = namePath.name;
        }
        if (!slug) {
          slug = `${namePath.dir}/${namePath.name}`.toLowerCase();
        }
        if (!date) {
          date = new Date(davNode.lastmod);
        }
      }
    }

    if (!slug) {
      console.error(&amp;quot;no slug&amp;quot;, node);
      //reporter.panic(&amp;quot;No slug found for markdown node&amp;quot;);
    }

    createNodeField({
      node,
      name: `title`,
      value: title,
    });

    createNodeField({
      node,
      name: `slug`,
      value: slug,
    });

    createNodeField({
      node,
      name: `date`,
      value: date,
    });

    createNodeField({
      node,
      name: `tags`,
      value: tags,
    });

    createNodeField({
      node,
      name: `draft`,
      value: draft,
    });
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Automating the process&lt;/h2&gt;
&lt;p&gt;I did mention that I don&apos;t want to think about publishing. The only thing I need to change to publish my blog-posts is to set &lt;code&gt;draft: false&lt;/code&gt; in my frontmatter and it should just deploy.&lt;/p&gt;
&lt;p&gt;What I really wanted to do, was to get Nextcloud to notify DigitalOcean (where I host my blog) and initiate a deployment from there. But, I couldn&apos;t get Nextcloud&apos;s Flow to work. If you know how to do it, &lt;a href=&quot;https://twitter.com/andrioid&quot;&gt;please let me know&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Plan B&lt;/h3&gt;
&lt;p&gt;I settled on a less complicated solution. I use Github Actions to deploy my site every night.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Update. Github didn&apos;t like that much and changed how cron works for Github Actions..&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You need to add &lt;code&gt;DIGITALOCEAN_ACCESS_TOKEN&lt;/code&gt; and &lt;code&gt;DIGITALOCEAN_APP_ID&lt;/code&gt; to your project&apos;s secrets. To create the acceess-token, follow &lt;a href=&quot;https://docs.digitalocean.com/reference/api/create-personal-access-token/&quot;&gt;these instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;.github/workflows/daily-deploy.yml&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Daily Deploy
on:
  schedule:
    - cron: &amp;quot;0 3 * * *&amp;quot;
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
      # Force is used because we fetch from webdav
      - run: doctl apps create deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While I use DigitalOcean&apos;s App platform, there is nothing preventing this from working on other great hosting providers like Netlify or Vercel. But because we&apos;re sourcing our blog-posts from webdav, these Git based integrations have no way of knowing that our site now has new data.&lt;/p&gt;
&lt;p&gt;Remember to add your Nextcloud credentials to whatever build-platform you&apos;re using. Otherwise you&apos;re not going to get any data from the webdav server.&lt;/p&gt;
&lt;h2&gt;Room for improvement&lt;/h2&gt;
&lt;h3&gt;Slug generation&lt;/h3&gt;
&lt;p&gt;I really wanted to get this out there, so I haven&apos;t made the slug generation as pretty as I&apos;d like.&lt;/p&gt;
&lt;h3&gt;Mermaid&lt;/h3&gt;
&lt;p&gt;Obsidian supports &lt;a href=&quot;https://mermaid-js.github.io/mermaid/#/&quot;&gt;Mermaid&lt;/a&gt; diagrams, and I want my Gatsby site to render those as well. I tried using &lt;code&gt;gatsby-remark-mermaid&lt;/code&gt;, but it didn&apos;t work with DigitalOcean&apos;s buildpacks due to a missing Chrome(ium) installation. That&apos;s one heck of a dependency, if you ask me.&lt;/p&gt;
&lt;p&gt;I&apos;ll look into it later.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;gatsby-remark-webdav&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;As I noted earlier, I just threw something together that works for me and if I find the time and motivation; I&apos;d like to release it. Until then, grab it from &lt;a href=&quot;https://github.com/andrioid/andri.dk/tree/master/plugins/gatsby-remark-webdav&quot;&gt;here&lt;/a&gt; if you need it.&lt;/p&gt;
&lt;h2&gt;What&apos;s missing?&lt;/h2&gt;
&lt;p&gt;It&apos;s hard to compress my entire website into one article. I hope I covered what you need to set up something similar with your Gatsby site.&lt;/p&gt;
&lt;p&gt;My &lt;a href=&quot;https://github.com/andrioid/andri.dk&quot;&gt;site&apos;s source-code&lt;/a&gt; is on Github, so feel free to poke around there if something isn&apos;t working.&lt;/p&gt;
&lt;p&gt;Let me know via &lt;a href=&quot;https://twitter.com/andrioid&quot;&gt;DM&lt;/a&gt; if you think I should add something to this article.&lt;/p&gt;
&lt;p&gt;Oh, and in case you were wondering. This blog post was written in Obsidian and automatically deployed.&lt;/p&gt;
</content:encoded></item><item><title>Adding Progressive Web Apps (PWA) to the Linux Desktop (2025)</title><link>https://andri.dk/blog/2020/10/adding-web-apps-to-the-linux-desktop/</link><guid isPermaLink="true">https://andri.dk/blog/2020/10/adding-web-apps-to-the-linux-desktop/</guid><pubDate>Fri, 02 Oct 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At work, we use Microsoft Teams, and we used to (and sort of still) use Slack. I recently switched from a Macbook Pro to a Linux laptop (again), and what annoys me greatly is that when I &amp;quot;share screen/application&amp;quot; in the Slack or Teams app it offers me only to share my entire monitor.&lt;/p&gt;
&lt;h3&gt;Chrome can do it!&lt;/h3&gt;
&lt;p&gt;By accident, I stumbled upon that Slack running in Chrome on Linux can share windows just fine. The native Slack app does not. So I went on an adventure to figure out how to add web-apps to the Linux desktop and it&apos;s surprisingly hard to find (hence this post).&lt;/p&gt;
&lt;p&gt;As of 2025, this option is in &amp;quot;Menu -&amp;gt; Cast, Save &amp;amp; Share -&amp;gt; Install&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;create-shortcut-chrome.png&quot; alt=&quot;Create Shortcut in Chrome&quot;&gt;&lt;/p&gt;
&lt;p&gt;Just navigate to the site you want to open as an application, click &amp;quot;Create Shortcut&amp;quot; from the menu, check &amp;quot;Open as Window&amp;quot; and you&apos;re good.&lt;/p&gt;
&lt;p&gt;On Linux, this will create a &lt;code&gt;.desktop&lt;/code&gt; file in &lt;code&gt;~/.local/share/applications&lt;/code&gt; so that your desktop environment will pick it up. This guide should also work fine on Windows and Mac.&lt;/p&gt;
&lt;p&gt;Note, that if you have Chrome installed via Flatpak, &lt;a href=&quot;https://discourse.flathub.org/t/pwas-this-flatpak-does-not-have-write-access-to/3830/1&quot;&gt;it might not have permissions&lt;/a&gt; to save the &lt;code&gt;.desktop&lt;/code&gt; files. Use a tool like &lt;a href=&quot;https://flathub.org/en/apps/com.github.tchx84.Flatseal&quot;&gt;Flatseal&lt;/a&gt; to add &lt;code&gt;~/.local/share/applications&lt;/code&gt; and &lt;code&gt;~/.local/share/icons&lt;/code&gt; to filesystem permissions.&lt;/p&gt;
&lt;h3&gt;Plus, it&apos;s better!&lt;/h3&gt;
&lt;p&gt;Today, I&apos;m running Outlook, Slack and Teams as progressive-web-applications and the performance is much better, screen/window sharing works and it uses less CPU than the Electron apps.&lt;/p&gt;
&lt;h3&gt;Desktop files&lt;/h3&gt;
&lt;p&gt;The application definition file looks something like this. You can technically create those yourself. Just make sure that the IDs are fresh.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env xdg-open
[Desktop Entry]
Version=1.0
Terminal=false
Type=Application
Name=andri.dk | Andri Óskarsson
Exec=flatpak &apos;run&apos; &apos;--command=/app/bin/chrome&apos; &apos;com.google.Chrome&apos; &apos;--profile-directory=Default&apos; &apos;--app-id=mciljpmicooimcedlcgapdnklfobboeb&apos;
Icon=chrome-mciljpmicooimcedlcgapdnklfobboeb-Default
StartupWMClass=crx_mciljpmicooimcedlcgapdnklfobboeb
X-Flatpak-Part-Of=com.google.Chrome
TryExec=/var/lib/flatpak/exports/bin/com.google.Chrome
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Deploying a Full-Stack app on Google with Terraform</title><link>https://andri.dk/blog/2020/devops/app-stack-with-terraform-on-gcp/</link><guid isPermaLink="true">https://andri.dk/blog/2020/devops/app-stack-with-terraform-on-gcp/</guid><pubDate>Thu, 18 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this series, we are going to setup continuous deployment on Github Actions that will deploy a full environment in the cloud. Then we will setup a scheduled action to tear it all down, if we&apos;re not using it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Part 1: Covers the infrastructure setup and backend&lt;/li&gt;
&lt;li&gt;Part 2: Covers Github Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: While this is perfectly fine for development-environments, you should probably NOT do this in production.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;I didn&apos;t like paying for hosting on projects that I rarely have time to work on. So, I figured that if I could describe my environment decleratively, then I could just create automatically when I need it, and delete it automatically when I don&apos;t.&lt;/p&gt;
&lt;h2&gt;Before we begin&lt;/h2&gt;
&lt;p&gt;You will need a few things before we get started.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Google Cloud account&lt;/li&gt;
&lt;li&gt;A Google Cloud project&lt;/li&gt;
&lt;li&gt;Terraform installed&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://cloud.google.com/iam/docs/creating-managing-service-account-keys&quot;&gt;service-account&lt;/a&gt; with a JSON key file.&lt;/li&gt;
&lt;li&gt;The following roles on your service-account: Cloud Build Service Account, Cloud Build Editor, Cloud SQL Admin, Editor, Service Account User, Cloud, Run Admin, Storage Admin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you have the JSON key file, you can add it to an environmental variable while you work with Terraform. If you forget to add this to ENV, then Terraform won&apos;t be able to talk to GCP.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export GOOGLE_CREDENTIALS=$(cat ~/Downloads/path-to-my-secret-service-account.json)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: We are giving this service-account some pretty broad permissions on our project. So, be careful of not putting the file into Git or anything like that. I also recommend trimming these permissions manually.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Describing our environment&lt;/h2&gt;
&lt;p&gt;Hashicorp&apos;s Terraform is &amp;quot;infrastructure as code&amp;quot;, or as I would put it: &amp;quot;A declaritive way of describing your hosting environment&amp;quot;. Instead of telling Google or Amazon what to do, we tell Terraform how it should look like. Terraform then does the required API calls, and if everything is as it should be, it will do nothing at all.&lt;/p&gt;
&lt;p&gt;This may seem overly complicated, but by doing this, copying an environment is trivial. If there&apos;s a new person on your team that wants to know what environments are running, we can look in one place.&lt;/p&gt;
&lt;p&gt;We will be deploying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a Postgres database&lt;/li&gt;
&lt;li&gt;a storage bucket&lt;/li&gt;
&lt;li&gt;a managed container with a Go backend&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Initial Terraform configuration&lt;/h3&gt;
&lt;p&gt;Terraform configuration-files have a &lt;code&gt;.tf&lt;/code&gt; postfix. To keep things simple, we&apos;ll only use one file (&lt;code&gt;main.tf&lt;/code&gt;). The file is rather large, so we will start small and then add to it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;provider &amp;quot;google&amp;quot; {
  project = var.project
  region  = var.region
  version = &amp;quot;= 3.23&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This just tells Terraform that we&apos;ll be using the Google Cloud provider. We lock the version, so that our configuration file matches the provider API used.&lt;/p&gt;
&lt;h3&gt;Persisting our state&lt;/h3&gt;
&lt;p&gt;If you run Terraform locally, you might notice a &lt;code&gt;.tfstate&lt;/code&gt; file. It tells Terraform which services are already created, their last known configuration and so on. We will be using Terraform as a part of our builds, so we want this state to be persisted somewhere globally.&lt;/p&gt;
&lt;p&gt;To be able to do that, you need to manually create a bucket called &lt;code&gt;tfstate.internal.example.com&lt;/code&gt; (or whatever you want, just make it match the configuration below) in your Google Cloud project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;terraform {
  backend &amp;quot;gcs&amp;quot; {
    bucket = &amp;quot;tfstate.internal.example.com&amp;quot;
    prefix = &amp;quot;terraform/state&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The Terraform state bucket cannot be managed by Terraform. You must create it yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Variables&lt;/h3&gt;
&lt;p&gt;Using variables allow us to define stuff like &amp;quot;region&amp;quot; and &amp;quot;database_secret&amp;quot; in one place. Terraform variables can also be set using ENV, like &amp;quot;TF_VAR_REGION&amp;quot; will be matched to a &amp;quot;region&amp;quot; variable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;variable &amp;quot;region&amp;quot; {
  type        = string
  description = &amp;quot;GCP region&amp;quot;
  default     = &amp;quot;europe-west1&amp;quot;
}

variable &amp;quot;project&amp;quot; {
  type        = string
  description = &amp;quot;GCP project&amp;quot;
  default     = &amp;quot;example-project&amp;quot;
}

# Used by our non-root database user
variable &amp;quot;db_password&amp;quot; {
  type        = string
  description = &amp;quot;Database secret&amp;quot;
}

# Used by our deployment pipeline to set the new image.
variable &amp;quot;docker_tag&amp;quot; {
  type        = string
  description = &amp;quot;Tag for Docker Image&amp;quot;
  default     = &amp;quot;latest&amp;quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Adding the database&lt;/h3&gt;
&lt;p&gt;Since this is just a teeny tiny staging environment, we buy the cheapest database server around. We are using Postgres 11, there are also &lt;a href=&quot;https://www.terraform.io/docs/providers/google/r/sql_database_instance.html&quot;&gt;other database types and options&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: We will be destroying our database periodically, so if you need to store something permenently, you should do backups.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_sql_database_instance&amp;quot; &amp;quot;db&amp;quot; {
  name             = &amp;quot;db&amp;quot;
  region           = var.region
  database_version = &amp;quot;POSTGRES_11&amp;quot;
  settings {
    tier = &amp;quot;db-f1-micro&amp;quot;
  }
}

resource &amp;quot;google_sql_database&amp;quot; &amp;quot;dbname&amp;quot; {
  name     = &amp;quot;staging&amp;quot;
  instance = google_sql_database_instance.db.name
}

resource &amp;quot;google_sql_user&amp;quot; &amp;quot;dbuser&amp;quot; {
  name     = &amp;quot;pgadmin&amp;quot;
  instance = google_sql_database_instance.db.name
  password = var.db_password
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will notice that we are referencing our variables (like &lt;code&gt;var.db_password&lt;/code&gt;), but we&apos;re also referencing the resources controlled by Terraform. In this case, we fetch the instance-name from the newly created instance. In a normal setup, we&apos;d have to know this information to be able to proceed. By using Terraform, it manages all of these references for us.&lt;/p&gt;
&lt;h3&gt;Adding a storage bucket for assets&lt;/h3&gt;
&lt;p&gt;My application uses &lt;a href=&quot;https://gocloud.dev&quot;&gt;Go Cloud Development Kit&lt;/a&gt; to store assets. And on Google Cloud, that means adding a Cloud Storage bucket to the project. At the moment, this bucket is private. Check the &lt;a href=&quot;https://www.terraform.io/docs/providers/google/r/storage_bucket_access_control.html&quot;&gt;documentation&lt;/a&gt; if you want a public bucket.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_storage_bucket&amp;quot; &amp;quot;assets&amp;quot; {
  name     = &amp;quot;assets.example.com&amp;quot;
  location = &amp;quot;EU&amp;quot;
  #force_destroy = true # If you want this destroyed when we take our environment down, uncomment this line
  bucket_policy_only = true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Adding a cloud-run service&lt;/h3&gt;
&lt;p&gt;This guide is not going to cover building Docker images or creating Dockerfiles. In the second part of this guide, we will build a docker-image, push it to Container Registry and pass it on to Cloud Run.&lt;/p&gt;
&lt;p&gt;I was going to use Github Packages, but Cloud-Run only works from Google&apos;s Container Registry.&lt;/p&gt;
&lt;p&gt;For demonstration purposes, we are just using a sample image from Google, and the ENV variables will not be used. But I spent quite some time digging up how to make the SQL connection work, so I think leaving it in will be helpful The &lt;code&gt;metadata&lt;/code&gt; part tells the Google SQL Proxy how to connect to the database and how many instances we will allow Google to scale to.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &amp;quot;google_cloud_run_service&amp;quot; &amp;quot;backend&amp;quot; {
  name     = &amp;quot;backend&amp;quot;
  location = var.region

  template {
    spec {
      containers {
        image = &amp;quot;gcr.io/cloudrun/hello:${var.docker_tag}&amp;quot;
        # Demonstration purposes only
        env {
          name  = &amp;quot;PG_STRING&amp;quot;
          value = &amp;quot;host=/cloudsql/${google_sql_database_instance.db.connection_name} user=${google_sql_user.dbuser.name} password=${google_sql_user.dbuser.password} dbname=${google_sql_database.dbname.name} sslmode=require&amp;quot;
        }
        # Demonstration purposes only
        env {
          name  = &amp;quot;BUCKET&amp;quot;
          value = &amp;quot;gs://${google_storage_bucket.assets.name}&amp;quot;
        }
      }
    }

    metadata {
      annotations = {
        &amp;quot;autoscaling.knative.dev/maxScale&amp;quot;      = &amp;quot;3&amp;quot;
        &amp;quot;run.googleapis.com/cloudsql-instances&amp;quot; = &amp;quot;${var.project}:${var.region}:${google_sql_database_instance.db.name}&amp;quot;
        &amp;quot;run.googleapis.com/client-name&amp;quot;        = &amp;quot;terraform&amp;quot;
      }
    }
  }

  autogenerate_revision_name = true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom name for your Cloud Run instance&lt;/h3&gt;
&lt;p&gt;Google offers automatic TLS and load balancing with custom-names. First you have to validate your domain with Google, and then you have to &lt;a href=&quot;https://cloud.google.com/run/docs/mapping-custom-domains#adding_verified_domain_owners_to_other_users_or_service_accounts&quot;&gt;add your service-account to it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When that is done, you can use the following to add a custom dns name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# https://cloud.google.com/run/docs/mapping-custom-domains#adding_verified_domain_owners_to_other_users_or_service_accounts
resource &amp;quot;google_cloud_run_domain_mapping&amp;quot; &amp;quot;backend&amp;quot; {
  location = var.region
  name     = &amp;quot;api.example.com&amp;quot;

  metadata {
    namespace = var.project
  }

  spec {
    route_name = google_cloud_run_service.backend.name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Terraform Plan&lt;/h2&gt;
&lt;p&gt;Navigate to your directory. I keep mine in &lt;code&gt;devops/google/main.tf&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Terraform Apply&lt;/h2&gt;
&lt;p&gt;If you agree with the changes suggested by &lt;code&gt;terraform plan&lt;/code&gt; you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Terraform Destroy&lt;/h2&gt;
&lt;p&gt;To clean up all the resources we have created, we can run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: I don&apos;t take any responsibility for what you might leave on your account. So make sure that the cleanup worked as expected.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Part 2: Setting this up on Github Actions.&lt;/h2&gt;
&lt;p&gt;I&apos;ll add a link to the second part of this blog post as soon as it&apos;s ready.&lt;/p&gt;
</content:encoded></item><item><title>Meetings from Home: Tools</title><link>https://andri.dk/blog/2020/03/meetings-from-home/</link><guid isPermaLink="true">https://andri.dk/blog/2020/03/meetings-from-home/</guid><pubDate>Mon, 16 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Are you working from home? Here&apos;s a list of some great tools that you can use for free.&lt;/p&gt;
&lt;p&gt;If you have anything to add to the list, please give me a shout at &lt;a href=&quot;https://twitter.com/andrioid&quot;&gt;@andrioid&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Video Conferencing and chat&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://jitsi.org/jitsi-meet/&quot;&gt;Jitsi Meet&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Not quite as polished as some of the commercial options, but quite capable. Works best with Chrome, but seemed to work fine in Firefox too.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: ✅&lt;/li&gt;
&lt;li&gt;No organization required: ✅&lt;/li&gt;
&lt;li&gt;Open Source: ✅&lt;/li&gt;
&lt;li&gt;Self Hostable: ✅&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://team.video&quot;&gt;Team.video&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Does exactly what the names says. No account required, very simple to use.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: ✅&lt;/li&gt;
&lt;li&gt;No organization required: ✅&lt;/li&gt;
&lt;li&gt;Open Source: 🚫&lt;/li&gt;
&lt;li&gt;Self Hostable: 🚫&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://whereby.com&quot;&gt;Whereby&lt;/a&gt; (formerly Appear.in)&lt;/h3&gt;
&lt;p&gt;Used to be my favourite video-conferencing tool.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: 🚫&lt;/li&gt;
&lt;li&gt;No organization required: ✅&lt;/li&gt;
&lt;li&gt;Open Source: 🚫&lt;/li&gt;
&lt;li&gt;Self Hostable: 🚫&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://slack.com&quot;&gt;Slack&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Needs no introduction. A chat app that also has voice and video features.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: 🚫&lt;/li&gt;
&lt;li&gt;No organization required: 🚫&lt;/li&gt;
&lt;li&gt;Open Source: 🚫&lt;/li&gt;
&lt;li&gt;Self Hostable: 🚫&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://teams.microsoft.com/start&quot;&gt;Microsoft Teams&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s answer to Slack. Needs your organization to have a Microsoft account and the Teams feature activated.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: 🚫&lt;/li&gt;
&lt;li&gt;No organization required: 🚫&lt;/li&gt;
&lt;li&gt;Open Source: 🚫&lt;/li&gt;
&lt;li&gt;Self Hostable: 🚫&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://openvidu.io&quot;&gt;OpenVidu&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Free, but you have to host it yourself or pay for it. Has multiple types of rooms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: 🚫&lt;/li&gt;
&lt;li&gt;No organization required: 🚫&lt;/li&gt;
&lt;li&gt;Open Source: ✅&lt;/li&gt;
&lt;li&gt;Self Hostable: ✅&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://bigbluebutton.org/&quot;&gt;BigBlueButton&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;More focused on teaching than meetings. Integrates to learning-systems. You need to host it yourself or pay for the hosting.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No account required: 🚫&lt;/li&gt;
&lt;li&gt;No organization required: 🚫&lt;/li&gt;
&lt;li&gt;Open Source: ✅&lt;/li&gt;
&lt;li&gt;Self Hostable: ✅&lt;/li&gt;
&lt;li&gt;Screen Sharing: ✅&lt;/li&gt;
&lt;li&gt;Chat: ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Whiteboarding&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://excalidraw.com&quot;&gt;Excalidraw&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;An excellent tool that makes it quite easy to draw diagrams or simple drawings.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Diagrams: ✅&lt;/li&gt;
&lt;li&gt;Live collaboration: ✅&lt;/li&gt;
&lt;li&gt;Free form: 🚫&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>2019 in Tech</title><link>https://andri.dk/blog/2019/12/2019-in-tech/</link><guid isPermaLink="true">https://andri.dk/blog/2019/12/2019-in-tech/</guid><pubDate>Sun, 29 Dec 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every year, or that was my plan back in 2013; I do a review of what technology I&apos;ve been using during the year and reflect on my experiences.&lt;/p&gt;
&lt;p&gt;This year has been fun in many ways. I have been creating apps, websites, microservices and APIs.&lt;/p&gt;
&lt;h2&gt;React&lt;/h2&gt;
&lt;p&gt;When I started playing with React 4 years ago, Angular was insanely popular in Denmark and everyone was looking at me funny. There was no standard tool for anything and every week there were major version changes in the stack.&lt;/p&gt;
&lt;p&gt;I heard this somewhere:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;People use Angular because of Google. People use React even if Facebook made it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Today, I look back on React with fondness. I started making web-sites back when we had &lt;code&gt;.shtml&lt;/code&gt; files and &lt;code&gt;.php3&lt;/code&gt;. But then something changed. We as an industry started using the browser for more than rendering markup and then we had a few broken years.&lt;/p&gt;
&lt;p&gt;The core problem, as I see it; is that we no longer have enough information to fully generate web-pages on the server anymore. The users expect a certain amount of dynamic functionality and while it isn&apos;t hard to add small snippets, this quickly becomes unmaintainable.&lt;/p&gt;
&lt;p&gt;Some people realized this and tried to combine one templating language for both server and client. But as soon as the initial data started to change, there were issues.&lt;/p&gt;
&lt;p&gt;React changed all of this. We now have one language to describe UI as a function of props and state. I can&apos;t imagine doing UI any other way. It might be Svelte or Vue in the future; but components are here to stay.&lt;/p&gt;
&lt;h2&gt;Go, the programming language&lt;/h2&gt;
&lt;p&gt;I&apos;m sure some people consider me an early adopter, but I had my eye on Go for a very long time before I started using it professionally 3 years ago.&lt;/p&gt;
&lt;p&gt;It&apos;s funny how I can be excited for a language that has very few features, roughly 6 years after I first started playing around with it.&lt;/p&gt;
&lt;p&gt;It feels like being on the same team. We all use the same formatting, the standard library usually has the functionality we need and there is less friction when working with others. This contributes to what I consider to be a very productive language.&lt;/p&gt;
&lt;p&gt;In 2019 I have created GraphQL API&apos;s, microservices and small tools and Go is by far my favourite backend language.&lt;/p&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;In May, I joined a new company and they were doing all of their new code in TypeScript. The project I joined has &lt;code&gt;alwaysStrict=true&lt;/code&gt; and I hated it very much. Everything I was used to do in JavaScript was now something that the compiler refused to make work. I spent 2/3 of the time defining types for something I thought was obvious.&lt;/p&gt;
&lt;p&gt;Later, I moved to another project where we also did TypeScript with React, but without strict-mode this time. This time, it clicked for me. It helped that I had 6 months experience with TypeScript at the time, but being free to &lt;em&gt;use TypeScript&lt;/em&gt; without the feeling that I was being used by it.&lt;/p&gt;
&lt;p&gt;Bottom line. Would I use TypeScript again? Yes. Being able to define types for props and state really speeds up development. Strict-mode? It&apos;s too verbose and results in some very ugly code without any good reason.&lt;/p&gt;
&lt;h2&gt;Immer&lt;/h2&gt;
&lt;p&gt;I first heard about &lt;a href=&quot;https://github.com/immerjs/immer&quot;&gt;Immer&lt;/a&gt; over at React Europe 2018, in the context of being &amp;quot;better than Immutable.js&amp;quot;, but I had absolutely no intention of doing anything, that changed all of my state data to some weird types again. So I ignored it.&lt;/p&gt;
&lt;p&gt;Then, I was dealing with some internal state in React that included messing with items of an array. Arrays in React state is probably the most annoying part of React. It feels verbose and hard to read at the same time. This immutable dance with arrays is painful; even in modern JS or TS.&lt;/p&gt;
&lt;p&gt;Immer is lovely. It&apos;s a function that takes the current state, gives you a &amp;quot;draft-state&amp;quot; that you can mutate. Then it produces a new output and returns it. The best part&lt;/p&gt;
&lt;p&gt;So you can simply do&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import produce from &amp;quot;immer&amp;quot;;
const [animals, setAnimals] = useState([&amp;quot;Cock&amp;quot;, &amp;quot;Raven&amp;quot;, &amp;quot;Dog&amp;quot;, &amp;quot;Pig&amp;quot;]);
setAnimals(
  produce((draftAnimals) =&amp;gt; {
    draftAnimals[0] = &amp;quot;Rooster&amp;quot;;
  }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will produce a new array with the changes applied and React will render the changes as expected.&lt;/p&gt;
&lt;p&gt;Immer is a must-have tool in your React toolbox.&lt;/p&gt;
&lt;h2&gt;React Native&lt;/h2&gt;
&lt;p&gt;I used to work at a company that does mobile-payments for parking. They had an app that was once a nice Xamarin application that worked on iOS and Android. But after 2 years of out-sourcing the app was unstable and unmaintainable (we spent 4 months on just making it stop crashing, without luck). I was responsible for the project and I had a feeling in my stomach that this was probably never going to be ok unless we&apos;d invest an entire team into this.&lt;/p&gt;
&lt;p&gt;I had tried playing with React Native when 2 years earlier, when they released initial support for Android, but it didn&apos;t feel mature at all. So, I decided to give it another shot and spent a few evenings and a weekend making a simple proof-of-concept in React Native. Long story short: I pitched it to my boss and we created a new app in React Native and Expo.&lt;/p&gt;
&lt;p&gt;Expo was a really positive experience for me. I didn&apos;t have a big team, so being able to skip the native part of React Native was a clear win for me. All of the libraries we needed were already in Expo, so it was a no-brainer.&lt;/p&gt;
&lt;p&gt;So when I started on a new React Native project this year, where we had to do all of the native parts ourselves, I was a bit scared. To my surprise, it wasn&apos;t as hard as I thought. I managed to do some minor changes to a Swift library without knowing anything about Swift or Xcode with help from my co-workers. And making the native bits talk to the JS bits was quite simple too.&lt;/p&gt;
&lt;p&gt;All in all, a positive experience. Would do again.&lt;/p&gt;
&lt;h2&gt;For 2020?&lt;/h2&gt;
&lt;p&gt;I&apos;m starting the year with by joining an existing React Native team and I&apos;m looking forward to help bring the project forward.&lt;/p&gt;
&lt;p&gt;So here&apos;s hoping for another year filled with good technology. I wish you a happy new year!&lt;/p&gt;
</content:encoded></item><item><title>Announcing gatsby-plugin-social-cards</title><link>https://andri.dk/blog/2019/09/announcing-gatsby-plugin-social-cards/</link><guid isPermaLink="true">https://andri.dk/blog/2019/09/announcing-gatsby-plugin-social-cards/</guid><pubDate>Sun, 15 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ever seen those images shown next to posts on Twitter, Slack or Facebook? Ever wonder where they come from? Me too!&lt;/p&gt;
&lt;p&gt;This plugin will create automatic cards for your (ReMark) at build-time.&lt;/p&gt;
&lt;h2&gt;Announcing&lt;/h2&gt;
&lt;p&gt;I am ready to announce my new Gatsby plugin, gatsby-plugin-social-cards.&lt;/p&gt;
&lt;p&gt;There are a few alternatives for Gatsby social-cards, but I wanted to try making these cards with SVG, React and the Sharp library that most Gatsby sites already depend on.&lt;/p&gt;
&lt;p&gt;Currently this plugin resides within my site, but if there is interest in it, I&apos;ll move it into its own repo. Contact me on Twitter if you have any issues.&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;h3&gt;Backgrounds&lt;/h3&gt;
&lt;p&gt;You can put a cover frontmatter on your post, and we&apos;ll use that. Otherwise, we&apos;ll use a default-background that you can specify or if that fails, we&apos;ll use a fallback one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;default-design.jpg&quot; alt=&quot;default card design&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Designs&lt;/h3&gt;
&lt;p&gt;There are two design available now, &amp;quot;card&amp;quot; and &amp;quot;default&amp;quot;. But we can expand that later.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;card-design.jpg&quot; alt=&quot;default card design&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Author image&lt;/h3&gt;
&lt;p&gt;If specified, an author image is shown on the image. That is also configurable.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cover-custom-author.jpg&quot; alt=&quot;default card design&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Custom author-image&lt;/h3&gt;
&lt;p&gt;The author image can be ommitted if it&apos;s not wanted, or customized in the configuration.&lt;/p&gt;
&lt;h2&gt;Install&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yarn add gatsby-plugin-social-cards
# or npm install --save gatsby-plugin-social-cards
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How to use&lt;/h2&gt;
&lt;p&gt;Please see the &lt;a href=&quot;https://www.npmjs.com/package/gatsby-plugin-social-cards&quot;&gt;README&lt;/a&gt; for how to use.&lt;/p&gt;
</content:encoded></item><item><title>Slim Docker images for your Go application</title><link>https://andri.dk/blog/2019/09/slim-docker-images-for-golang/</link><guid isPermaLink="true">https://andri.dk/blog/2019/09/slim-docker-images-for-golang/</guid><pubDate>Fri, 06 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How to build a slim Docker container for your Go application using multi-stage images.&lt;/p&gt;
&lt;p&gt;I&apos;m using Go 1.13 and the community module proxy to build the binary and Alpine as a base image. It adds a user and group instead of running as root.&lt;/p&gt;
&lt;h3&gt;Dockerfile&lt;/h3&gt;
&lt;p&gt;Be sure to replace &amp;quot;cmd/server/server.go&amp;quot; with your main file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-docker&quot;&gt;FROM golang:1.13 as builder

WORKDIR /app
COPY . /app
RUN CGO_ENABLED=0 GOOS=linux GOPROXY=https://proxy.golang.org go build -o app cmd/server/server.go

FROM alpine:latest
# mailcap adds mime detection and ca-certificates help with TLS (basic stuff)
RUN apk --no-cache add ca-certificates mailcap &amp;amp;&amp;amp; addgroup -S app &amp;amp;&amp;amp; adduser -S app -G app
USER app
WORKDIR /app
COPY --from=builder /app/app .
ENTRYPOINT [ &amp;quot;./app&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope this is helpful. I mostly blogged this to document it for myself.&lt;/p&gt;
</content:encoded></item><item><title>Making a Gatsby site from Meetup event data</title><link>https://andri.dk/blog/gatsby-meetup-site/</link><guid isPermaLink="true">https://andri.dk/blog/gatsby-meetup-site/</guid><pubDate>Sun, 25 Aug 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As part of my talk preperation for Aalborg Frontend Meetup, I was going to do a walkthrough of a new Gatsby site and use &lt;a href=&quot;https://www.gatsbyjs.org/packages/gatsby-source-meetup/?=meetup&quot;&gt;gatsby-source-meetup&lt;/a&gt; to import the upcoming events.&lt;/p&gt;
&lt;p&gt;Imagine my surprise, when I discovered that Meetup had stopped issuing API keys.&lt;/p&gt;
&lt;h2&gt;gatsby-source-ical&lt;/h2&gt;
&lt;p&gt;Gatsby has a wonderful collection of source plugins, and one of them happens to be &lt;a href=&quot;https://www.gatsbyjs.org/packages/gatsby-source-ical/?=ical&quot;&gt;gatsby-source-ical&lt;/a&gt;. I&apos;ll hand it to Meetup support; they did send me a useful link that indicated they I may not need an API, since they have a calendar feed available.&lt;/p&gt;
&lt;h2&gt;Finding the calendar export&lt;/h2&gt;
&lt;p&gt;First, find your group on &lt;a href=&quot;https://www.meetup.com&quot;&gt;Meetup&lt;/a&gt;, click &amp;quot;Events&amp;quot;, &amp;quot;Calendar&amp;quot; and at the bottom there are feed links (if you&apos;re logged in).&lt;/p&gt;
&lt;p&gt;For our group, the URL is: &amp;quot;https://www.meetup.com/Aalborg-Frontend/events/ical/&amp;quot;&lt;/p&gt;
&lt;h2&gt;Add the plugin&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yarn add gatsby-source-ical
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;gatsby-config.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// In your gatsby-config.js
module.exports = {
  plugins: [
    // You can have multiple instances of this plugin
    // to read source nodes from different remote files
    {
      resolve: `gatsby-source-ical`,
      options: {
        name: `events`,
        url: `https://web-standards.ru/calendar.ics`,
      },
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query the data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
	allIcal {
		edges {
			node {
				start
				end
				summary
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why?&lt;/h2&gt;
&lt;p&gt;I put this together in case someone else is creating a meetup website with Gatsby and got frustrated with the API change.&lt;/p&gt;
</content:encoded></item><item><title>Choosing a programming language</title><link>https://andri.dk/blog/2019/08/choosing-a-programming-language/</link><guid isPermaLink="true">https://andri.dk/blog/2019/08/choosing-a-programming-language/</guid><pubDate>Sun, 04 Aug 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Imagine that you&apos;re a CTO, a Tech Lead or someone that has to make an early technical decision that will effect the company for years to come. What programming language do you choose?&lt;/p&gt;
&lt;h2&gt;The cost of abstraction&lt;/h2&gt;
&lt;p&gt;Systems tend to have two types of complexity. Complexity from the business-domain, and complexity from the technical-domain. In my experience, the technical-complexity takes up more room than it should.&lt;/p&gt;
&lt;p&gt;There are, of course brilliant programmers out there, who manage complexity really well, regardless of what language they code in. But people have different skill-sets, and a great team has many different types of members.&lt;/p&gt;
&lt;h2&gt;Readability&lt;/h2&gt;
&lt;p&gt;When I took my first programming job, I was looking forward to &amp;quot;writing some awesome code&amp;quot;. But, writing code wasn&apos;t my primary job. Reading it was.&lt;/p&gt;
&lt;p&gt;There are some tools that help manage this. Coding-standards, linters, code-formatters all try to make reading each other&apos;s code easier.&lt;/p&gt;
&lt;p&gt;But what if every feature of our programming language, comes with a cost? If you give 10 programmers the same task, how similar would the implementations be? I argue that if you get many different types of implementations, then your programming language is too complex.&lt;/p&gt;
&lt;p&gt;To be productive at writing code, we also need to be productive at reading it.&lt;/p&gt;
&lt;h2&gt;Dependencies&lt;/h2&gt;
&lt;p&gt;At a certain point we need something from our programming language that isn&apos;t a language-feature, but a library feature. We need to open a file, parse HTTP headers, decode JSON, or talk to databases.&lt;/p&gt;
&lt;p&gt;Every dependency we add to our programs comes with a price, even if the code is free. What if the maintainer get&apos;s hit by a bus? What if the maintainer becomes malicious? What if there&apos;s a critical security bug?&lt;/p&gt;
&lt;p&gt;Why did PHP win over Perl for web-development in the late 90&apos;s? I&apos;m willing to bet it was because PHP&apos;s standard-library was so good, and well documented.&lt;/p&gt;
&lt;p&gt;Another approach is Javascript&apos;s NPM where there is no standard library. Every little thing has it&apos;s own package. The advantage has been that the community of JS really stepped up and you can find a package for any thing you can imagine. The disadvantage is that with nested dependencies, we have no idea on what code we&apos;re actually depending on. There hasn&apos;t been a NPM apocalypse yet, just the story of left-pad and an occasional bitcoin miner.&lt;/p&gt;
&lt;p&gt;I think this a no-brainer. If we&apos;re building something that needs to be reliable for years to come, then we need a standard library that has security updates and advisories when shit happens. And remember, that we don&apos;t include dependencies, we adopt them.&lt;/p&gt;
&lt;h2&gt;Hiring people&lt;/h2&gt;
&lt;p&gt;At some point, we all need help. And that help usually comes to us by means of hiring people. If it wasn&apos;t for this point, I might be programming in Erlang, Elixir and Elm right now. I think these languages are extremely interesting, and not just because they all start with the letter E.&lt;/p&gt;
&lt;p&gt;But reality sets in, and we need a pool of people to hire from, that either know your tech-stack or will have an easy time learning it.&lt;/p&gt;
&lt;p&gt;The exception is when an organization has decided to invest in something, has a resources to teach newcomers and is willing to pay premium for talent.&lt;/p&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;p&gt;Performance is important. But, aim for &amp;quot;good enough&amp;quot; instead of &amp;quot;perfect&amp;quot;. &amp;quot;Perfect&amp;quot; will almost certainily have a downside in other parts of this post.&lt;/p&gt;
&lt;p&gt;Does your project require some hard-core performance? Systems that have a &amp;quot;hard&amp;quot; real-time requirement might limit your choices. &amp;quot;Soft&amp;quot; real-time is still doable in almost any language.&lt;/p&gt;
&lt;p&gt;We once programmed in a world of a single CPU. That world is no more. Please pick a langauge that handles multiple executions well.&lt;/p&gt;
&lt;p&gt;A practical example would be a checkout-API call on a webshop. The customer has submitted a payment token, an order, and an email we need to verify. None of these things depend on each other, but all three things need to complete to succeed.&lt;/p&gt;
&lt;h2&gt;Developer Experience&lt;/h2&gt;
&lt;p&gt;To me, a good developer experience consists of the following things.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The code is easy to read and understand.&lt;/li&gt;
&lt;li&gt;The language is supported in my editor (or IDE) of choice.&lt;/li&gt;
&lt;li&gt;There is a good, &lt;em&gt;reliable&lt;/em&gt; debugger available.&lt;/li&gt;
&lt;li&gt;Easy to write and run tests, both locally and in build-systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final words&lt;/h2&gt;
&lt;p&gt;We try to plan for the future, but nobody really knows it will bring. So, aim for the next 5 years and then time will tell.&lt;/p&gt;
&lt;p&gt;What language would you pick, and why?&lt;/p&gt;
</content:encoded></item><item><title>Creating a CV in React</title><link>https://andri.dk/blog/2019-07/creating-a-cv-in-react/</link><guid isPermaLink="true">https://andri.dk/blog/2019-07/creating-a-cv-in-react/</guid><pubDate>Mon, 22 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TLDR;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/cv.pdf&quot;&gt;See my CV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/andrioid/andri.dk/tree/master/src/cv&quot;&gt;See the code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;But why?&lt;/h2&gt;
&lt;p&gt;I never much cared for pixel-pushing on screen. It has always been a necessary evil. But, print? Love that shit. I did my university reports in LaTeX, even the graphics and even though the errors were HORRIBLE, it remained a loyal TeX fan.&lt;/p&gt;
&lt;p&gt;So when I received a task at work to evaluate &lt;a href=&quot;https://react-pdf.org/&quot;&gt;react-pdf&lt;/a&gt; vs CSS-printing I knew I had something special to play with.&lt;/p&gt;
&lt;p&gt;I wanted the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;a href=&quot;https://jsonresume.org/schema/&quot;&gt;JSON resume&lt;/a&gt; for the CV data&lt;/li&gt;
&lt;li&gt;Components for for work-experience, education and sections&lt;/li&gt;
&lt;li&gt;Built automatically with my Gatsby site into a PDF file&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A good starting point&lt;/h2&gt;
&lt;p&gt;There is a an &lt;a href=&quot;https://github.com/diegomura/react-pdf/tree/master/examples/resume&quot;&gt;example&lt;/a&gt; in the react-pdf repo that has much prettier code then mine. So, if you want to make your own, I suggest you start there.&lt;/p&gt;
&lt;h2&gt;Using JSON resume, well mostly&lt;/h2&gt;
&lt;p&gt;If you&apos;re anything like me, you don&apos;t like updating your CV, or portfolio. Or you just forget. We can use &lt;a href=&quot;https://github.com/andrioid/andri.dk/blob/master/src/cv/resume.json&quot;&gt;one JSON file&lt;/a&gt; for all those things and be done with it.&lt;/p&gt;
&lt;p&gt;The spec is good, but I made some minor changes to mine. I added a &amp;quot;skills&amp;quot; array to work-item and &amp;quot;color&amp;quot; string to skill-items.&lt;/p&gt;
&lt;p&gt;Visit &lt;a href=&quot;https://jsonresume.org&quot;&gt;jsonresume.org&lt;/a&gt; and make your own &lt;em&gt;resume.json&lt;/em&gt; file.&lt;/p&gt;
&lt;p&gt;They even offer free hosting and rendering of your résumé, and if you&apos;re feeling lazy, then just do that instead.&lt;/p&gt;
&lt;h2&gt;Components&lt;/h2&gt;
&lt;p&gt;I&apos;ve pasted some code in here, so that you can get a little feeling for how this is built. But, keep in mind that the code might change, and refer to the repo for code-examples.&lt;/p&gt;
&lt;h3&gt;Box&lt;/h3&gt;
&lt;p&gt;A simple box, with an headline.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;box-operations.png&quot; alt=&quot;box with content&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export const Box = ({ children, title, color, style = {} }) =&amp;gt; (
  &amp;lt;View wrap={false} style={{ marginBottom: 20 }}&amp;gt;
    &amp;lt;SectionHeader color={color}&amp;gt;{title}&amp;lt;/SectionHeader&amp;gt;
    &amp;lt;View style={{ ...style }}&amp;gt;
      {children &amp;amp;&amp;amp; typeof children === &amp;quot;string&amp;quot; ? (
        &amp;lt;Text&amp;gt;{children}&amp;lt;/Text&amp;gt;
      ) : (
        children
      )}
    &amp;lt;/View&amp;gt;
  &amp;lt;/View&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Work Item&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;experience-item.png&quot; alt=&quot;box with content&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export const TimelineItem = ({
  title,
  period,
  children,
  employer,
  tags = [],
  location,
}) =&amp;gt; {
  tags = tags.sort();
  return (
    &amp;lt;View wrap={false} style={{ marginBottom: 10 }}&amp;gt;
      &amp;lt;View
        style={{
          flexDirection: &amp;quot;row&amp;quot;,
          justifyContent: &amp;quot;space-between&amp;quot;,
          marginBottom: 2.5,
          flexWrap: &amp;quot;wrap&amp;quot;,
        }}
      &amp;gt;
        &amp;lt;Text style={{ fontWeight: &amp;quot;bold&amp;quot; }}&amp;gt;
          {title}, &amp;lt;Text style={{ fontWeight: &amp;quot;normal&amp;quot; }}&amp;gt;{employer}&amp;lt;/Text&amp;gt;
        &amp;lt;/Text&amp;gt;
        &amp;lt;Text&amp;gt;{period}&amp;lt;/Text&amp;gt;
      &amp;lt;/View&amp;gt;

      {children &amp;amp;&amp;amp; &amp;lt;Text style={{ marginBottom: 2.5 }}&amp;gt;{children}&amp;lt;/Text&amp;gt;}
      {tags &amp;amp;&amp;amp; (
        &amp;lt;View style={{ flexDirection: &amp;quot;row&amp;quot; }}&amp;gt;
          {tags &amp;amp;&amp;amp;
            tags.map((m) =&amp;gt; (
              &amp;lt;Tag key={m} color={tagColors[m.toLowerCase()]}&amp;gt;
                {m}
              &amp;lt;/Tag&amp;gt;
            ))}
        &amp;lt;/View&amp;gt;
      )}
    &amp;lt;/View&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build with Gatsby&lt;/h2&gt;
&lt;p&gt;Originally, I wanted Gatsby to render my CV as a page, using react-dom on the client and pdf on the server. That turned out to be very hard to do, with little gain. So now we just generate the PDF file seperately.&lt;/p&gt;
&lt;p&gt;In retrospect, I should probably move this into pkg/cv instead of src/cv.&lt;/p&gt;
&lt;h3&gt;package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;	&amp;quot;scripts&amp;quot;: {
		&amp;quot;build-cv&amp;quot;: &amp;quot;cd src/cv &amp;amp;&amp;amp; babel-node build.js&amp;quot;,
		&amp;quot;watch-cv&amp;quot;: &amp;quot;cd src/cv &amp;amp;&amp;amp; nodemon --exec babel-node build.js&amp;quot;
	},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;gatsby-config.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;exports.onPostBuild = () =&amp;gt; {
  const cp = require(&amp;quot;child_process&amp;quot;);
  cp.execSync(&amp;quot;yarn run build-cv&amp;quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;src/cv/.babelrc&lt;/h3&gt;
&lt;p&gt;From the react-pdf repo. I also tried to adapt Gatsby&apos;s babel configuration here, but without luck.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{
	&amp;quot;presets&amp;quot;: [
		[
			&amp;quot;@babel/preset-env&amp;quot;,
			{
				&amp;quot;loose&amp;quot;: true,
				&amp;quot;targets&amp;quot;: {
					&amp;quot;node&amp;quot;: &amp;quot;current&amp;quot;
				}
			}
		],
		&amp;quot;@babel/preset-react&amp;quot;
	],
	&amp;quot;plugins&amp;quot;: [
		&amp;quot;@babel/plugin-transform-runtime&amp;quot;,
		&amp;quot;@babel/plugin-proposal-class-properties&amp;quot;
	]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Workflow&lt;/h3&gt;
&lt;p&gt;Then just run &lt;code&gt;yarn run watch-cv&lt;/code&gt; while developing it. I use evince on Linux as my PDF viewer, because it automatically reloads the file on-write. So almost like hot-reloading.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This was a fun project for me. I&apos;m not seeking employment, so I&apos;m not motivated to polish it further at this time. I hope this gave a few bread-crumbs, if you&apos;re considering something similar.&lt;/p&gt;
</content:encoded></item><item><title>Breaking the silence</title><link>https://andri.dk/blog/2019/07/breaking-the-silence/</link><guid isPermaLink="true">https://andri.dk/blog/2019/07/breaking-the-silence/</guid><pubDate>Sat, 06 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As you can see from my frontpage, I haven&apos;t blogged since 2014. Sorry about that.&lt;/p&gt;
&lt;p&gt;I&apos;m learning new things while building this site, again again. It&apos;s currently running on Gatsby with Tailwind-CSS, GraphQL and a few plugins.&lt;/p&gt;
&lt;p&gt;I&apos;m also playing around with react-pdf and JSON-Resume. My CV is currently generated with the site and I plan to make the site, and the CV fetch data from my resume.json.&lt;/p&gt;
&lt;p&gt;More on that later.&lt;/p&gt;
</content:encoded></item><item><title>2013 Tech Stack</title><link>https://andri.dk/blog/2013-tech-stack/</link><guid isPermaLink="true">https://andri.dk/blog/2013-tech-stack/</guid><pubDate>Fri, 03 Jan 2014 08:19:54 GMT</pubDate><content:encoded>&lt;p&gt;As I do pretty much, every year. 2013 was spent learning new technologies. Here&apos;s a short list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go: I taught myself a little bit of Golang and made a few small prototypes in it. I love the language so far, and while it is a bit restrictive, it&apos;s for my own good, as the code is easier to handle.&lt;/li&gt;
&lt;li&gt;PostgreSQL: While a pain in the ass to setup, pgsql is an awesome database and with HSTORE and JSON data types, it can also be used as a key/value storage. I am mostly using it to replace MySQL in some pet projects.&lt;/li&gt;
&lt;li&gt;KnockoutJS: A declarative JS library that makes it possible to create rich applications without dealing too much with markup. I have been using it for a small project that deals with nesting templates in a tree.&lt;/li&gt;
&lt;li&gt;Redis: A fast, distributed key/value database that I was using for a small prototype dashboard project. Especially the queue features are nice, although I wouldn&apos;t use this as a persistent storage.&lt;/li&gt;
&lt;li&gt;D3js: Create SVG diagrams from data. I was also experimenting with D3 for my dashboard project and while I can&apos;t say that I am 100% comfortable with it, it is extremely powerful.&lt;/li&gt;
&lt;li&gt;Java: Most people learn Java early, but not me. I spent 3 months at a new job programming Java and I must say that I like the language, although I hate the toolkit.&lt;/li&gt;
&lt;li&gt;Docker: Absolutely a game-changer in 2013. Docker allows you to create containers for your applications that you can use for development, testing and eventually production without encumbering you with virtual machines. I&apos;ve been using it for development and hope to do more soon.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am probably forgetting something, but these are the highlights of 2013. I look forward to playing with even more new stuff in 2014.&lt;/p&gt;
</content:encoded></item></channel></rss>