<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://stefanzweifel.dev">
    <title>stefanzweifel.dev</title>
    <subtitle>RSS Feed of Stefan Zweifel&#039;s personal blog</subtitle>
    <link href="https://stefanzweifel.dev/public/rss.xml" rel="self" />
    <link href="https://stefanzweifel.dev" />
    <updated>
        2026-04-04T21:00:00Z
    </updated>
    <id>https://stefanzweifel.dev/rss.xml</id>
    <author>
        <name>Stefan Zweifel</name>
        <email>stefan@stefanzweifel.dev</email>
    </author>
    <entry>
        <title>Show Remaining Characters in a Filament Text Input</title>
        <link href="https://stefanzweifel.dev/posts/2026/04/04/show-remaining-characters-in-a-filament-text-input/" />
        <updated>2026-04-04T21:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2026/04/04/show-remaining-characters-in-a-filament-text-input/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;&lt;em&gt;At work&lt;/em&gt; I’ve recently brought up the idea of showing a “remaining characters indicator” to our Filament form components. A small circle that appears when you soon reach the maximum character limit and that turns red once you’re over that limit.&lt;/p&gt;
&lt;p&gt;Here is a quick video on how this feature would look like:&lt;/p&gt;
&lt;video muted=&quot;&quot; loop=&quot;&quot; playsinline=&quot;&quot; lazyload=&quot;&quot; controls=&quot;&quot;&gt;
    &lt;source src=&quot;https://stefanzweifel.dev/assets/images/posts/20260404-remaining-characters/demo-without-max-length.mov&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;p&gt;After internal discussions, we’ve decided to scrap the idea again, but I thought why not share my hacky solution with the rest of the world?&lt;/p&gt;
&lt;h2 id=&quot;adding-the-indicator&quot; tabindex=&quot;-1&quot;&gt;Adding the Indicator &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/04/04/show-remaining-characters-in-a-filament-text-input/#adding-the-indicator&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’m in a Laravel 12 and Filament 5 project here and abused the &lt;code&gt;suffix&lt;/code&gt; and &lt;code&gt;extraAttributes&lt;/code&gt; methods on a &lt;code&gt;TextInput&lt;/code&gt;-component to make this work.&lt;/p&gt;
&lt;p&gt;This is how my form field declaration looks like.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;TextInput&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;make&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;name&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    -&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;Name&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    -&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    -&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;maxLength&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    -&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;suffix&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; HtmlString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;Blade&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;render&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;&amp;#x3C;x-remaining-characters /&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    -&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;extraAttributes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;([&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;class&#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;relative&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;x-data&#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            maxChars: 50,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get charCount() { return &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;state ? &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;state.length : 0 },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get remaining() { return this.maxChars - this.charCount },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get percentage() { return (this.charCount / this.maxChars) * 100 },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get isOverLimit() { return this.charCount &gt; this.maxChars },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get showCount() { return this.remaining &amp;#x3C;= 40 || this.isOverLimit },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            get strokeColor() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                if (this.charCount === 0) return &#39;#e5e7eb&#39;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                if (this.isOverLimit) return &#39;#ef4444&#39;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                if (this.remaining &amp;#x3C;= 40) return &#39;#f59e0b&#39;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                return &#39;#3b82f6&#39;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                get circumference() { return 2 * Math.PI * 10 },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                get strokeDashoffset() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                    const progress = Math.min(this.percentage, 100);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                    return this.circumference - (progress / 100) * this.circumference;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            }&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        ]),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important pieces here are the values passed to &lt;code&gt;maxLength&lt;/code&gt; and &lt;code&gt;maxChars&lt;/code&gt; which dictate the maximum allowed length of the input. Then there is a condition to check, if the user is &lt;em&gt;near&lt;/em&gt; the maximum length (&lt;code&gt;this.remaining &amp;lt;= 40&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The rest of the Alpine.js code is there to calculate the right text color and stroke offset of the little circle we display in our Blade component.&lt;/p&gt;
&lt;p&gt;And the code below is the content of the &lt;code&gt;&amp;lt;x-remaining-characters /&amp;gt;&lt;/code&gt; Blade component located in &lt;code&gt;resources/components/remaining-characters.blade.php&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt; data-class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;absolute bottom-1 right-1 select-none&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;relative w-6 h-6 flex items-center justify-center&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;        &amp;#x3C;!-- Character count in center --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;span&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            x-show&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;showCount&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            x-cloak&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;text-[10px] font-bold absolute z-10 leading-none&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            :class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                &#39;text-red-600&#39;: isOverLimit,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                &#39;text-amber-600&#39;: !isOverLimit &amp;#x26;&amp;#x26; remaining &lt;/span&gt;&lt;span style=&quot;color:#FDAEB7;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;= 40,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;                &#39;text-blue-600&#39;: !isOverLimit &amp;#x26;&amp;#x26; remaining &gt; 40&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;            }&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            x-text&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;remaining&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        &gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;span&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;        &amp;#x3C;!-- SVG Circle (24px) --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;svg&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;transform -rotate-90 absolute&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            width&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;24&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            height&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;24&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;            viewBox&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;0 0 24 24&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        &gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;circle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                cx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;12&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                cy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;12&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                r&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;10&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                fill&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;#e5e7eb&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                stroke-width&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;circle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                x-show&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;charCount &gt; 0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                cx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;12&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                cy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;12&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                r&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;10&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                fill&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                :stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;strokeColor&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                stroke-width&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                :stroke-dasharray&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;circumference&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                :stroke-dashoffset&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;strokeDashoffset&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;transition-all duration-200 ease-out&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;                stroke-linecap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;round&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;svg&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, we use the values from our Alpine.js component here to change the text color, to show the remaining allowed characters as well as the offset of a SVG circle.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This approach worked well for a proof of concept. As mentioned, we’ve collectively decided that we don’t even want to have such indicators in our app, so we scrapped the idea again.&lt;/p&gt;
&lt;p&gt;If this would have landed in production, I definitely would have turned the code into a more reusable system, by either creating a macro or creating a dedicated &lt;code&gt;TextInputWithRemainingCharacterCount&lt;/code&gt;-component for this.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Laracon EU 2026 Recap</title>
        <link href="https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/" />
        <updated>2026-03-12T21:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;It has already been a week since Laracon EU 2026 ended.
I attended with the whole &lt;a href=&quot;https://trenda.ch/&quot;&gt;Trenda&lt;/a&gt;-team, and we remained in Amsterdam for the rest of the week to work on some projects there.&lt;/p&gt;
&lt;p&gt;Laracon is always a great event. I saw many familiar faces from previous installments, internet friends who live far away. And this time I tried to make an effort and talk to more people than usual. (Still forgot to take any selfies.)&lt;/p&gt;
&lt;p&gt;The talks this year were great. There were only 1 or 2 which I think weren’t a hit.&lt;/p&gt;
&lt;p&gt;The following talks were especially great, and I will have to watch the VOD (&lt;a href=&quot;https://www.youtube.com/watch?v=cucIWpAenro&quot;&gt;Day 1&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/watch?v=YJmuKPk3d9M&quot;&gt;Day 2&lt;/a&gt;) again and take notes:&lt;/p&gt;
&lt;h2 id=&quot;dan-harrin-write-better-abstractions-lessons-from-an-import-system&quot; tabindex=&quot;-1&quot;&gt;Dan Harrin: “Write better abstractions: lessons from an import system” &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#dan-harrin-write-better-abstractions-lessons-from-an-import-system&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Dan basically presented how they decide how to abstract functionality in Filament PHP. Their approach of &lt;em&gt;Description over Instructions&lt;/em&gt; was brilliantly explained in his talk.&lt;/p&gt;
&lt;p&gt;Definitely something I would like to adopt in my own packages as well as in work projects, where we build reusable components.&lt;/p&gt;
&lt;h2 id=&quot;ryan-chandler-handling-the-unhappy-path&quot; tabindex=&quot;-1&quot;&gt;Ryan Chandler: “Handling the unhappy path” &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#ryan-chandler-handling-the-unhappy-path&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ryan talked about some not-well-known features and functions of the Laravel framework.&lt;/p&gt;
&lt;p&gt;One thing that stuck out was the… &lt;code&gt;report()&lt;/code&gt; and &lt;code&gt;response()&lt;/code&gt;-function on custom exceptions. I know and used these functions in projects before, but it was great seeing more people exposed to these features and a reminder that they exist.&lt;/p&gt;
&lt;p&gt;Shortly after, Ryan launched &lt;a href=&quot;https://lesserknownlaravel.com/&quot;&gt;lesserknownlaravel.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;tobias-petry-one-billion-rows-with-laravel&quot; tabindex=&quot;-1&quot;&gt;Tobias Petry: “One billion rows with Laravel“ &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#tobias-petry-one-billion-rows-with-laravel&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As always, Tobias shined with great visual slides that complemented his talk perfectly.&lt;/p&gt;
&lt;p&gt;He talked about TimescaleDB in PostgreSQL and how you can use aggregates to precompute large chunks of timeseries events.&lt;/p&gt;
&lt;p&gt;Great talk and a good reminder to explore PostgreSQL more and maybe start migrating my MySQL databases.&lt;/p&gt;
&lt;h2 id=&quot;peter-suhm-unblocking-your-users-with-ai&quot; tabindex=&quot;-1&quot;&gt;Peter Suhm: “Unblocking your users with AI“ &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#peter-suhm-unblocking-your-users-with-ai&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Peter shared some great practical tips on how to help your users get activated in your app.&lt;/p&gt;
&lt;p&gt;He mentions the &lt;em&gt;Three Points of Paralysis&lt;/em&gt; that users might feel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Blank State: “I don’t know where to start.”&lt;/li&gt;
&lt;li&gt;The T-Junction: “I don’t know which one.”&lt;/li&gt;
&lt;li&gt;The Maze: “I don’t know how to find it.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of showing users a cluttered UI, we might show them a prompt that helps them overcome the &lt;em&gt;Blank State&lt;/em&gt;. He explained this with form builders. Instead of manually clicking things together, let the user describe the outcome they want and let the LLM generate a JSON structure for a form.&lt;/p&gt;
&lt;p&gt;This shouldn’t replace the UI form builder, but give users a good starting point.&lt;/p&gt;
&lt;p&gt;For the &lt;em&gt;T-Junction&lt;/em&gt;, he used the same example with form builders. Maybe users don’t know if they have to select a radio or checkbox list for a field. If possible, let the app help your users and provide a good first guess.
For the &lt;em&gt;Maze&lt;/em&gt;, he built basically a little search agent that uses vector embeddings of form answers to answer questions like “Did visitors like the conference?”.&lt;/p&gt;
&lt;h2 id=&quot;simon-hamp-and-shane-rosenthal-nativephp&quot; tabindex=&quot;-1&quot;&gt;Simon Hamp &amp;amp; Shane Rosenthal: “NativePHP” &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#simon-hamp-and-shane-rosenthal-nativephp&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’ve bundled Simon and Shane here, as they both work on NativePHP and both gave talks about NativePHP.&lt;/p&gt;
&lt;p&gt;What they accomplished in the last few months is amazing. They pushed through the negativity they received on Reddit and other social media sites and improved NativePHP more and more. Bundlesize and Android support got better.&lt;/p&gt;
&lt;p&gt;And the advancements Shane showed in this talk – that they now create real-native C code and render real native iOS or Android elements was mind-blowing.&lt;/p&gt;
&lt;p&gt;Simon dropped the phrase &lt;mark&gt;“Just give it 5 minutes”&lt;/mark&gt; and it really stuck with me. I have an idea for a mobile companion app for a side project of mine and will definitely give NativePHP a shot.&lt;/p&gt;
&lt;h2 id=&quot;luke-kuzmish-effective-code-reviews-what-not-to-do&quot; tabindex=&quot;-1&quot;&gt;Luke Kuzmish: “Effective Code Reviews: What NOT to Do“ &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#luke-kuzmish-effective-code-reviews-what-not-to-do&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Luke did a great talk reminding us all to take out the ego of code reviews. He mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standardize the review process.&lt;/li&gt;
&lt;li&gt;Automate as much as possible through code (linting, formatting, etc.).&lt;/li&gt;
&lt;li&gt;Read your own pull request on your own and make sure the CI pipeline is green.&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://conventionalcomments.org/&quot;&gt;Conventional Comments&lt;/a&gt; as a guiding framework when leaving notes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Was a good reminder to use &lt;em&gt;Conventional Comments&lt;/em&gt; categorizes more in my code reviews again.&lt;/p&gt;
&lt;h2 id=&quot;joe-tannenbaum-state-of-the-frontend&quot; tabindex=&quot;-1&quot;&gt;Joe Tannenbaum: “State of the Frontend” &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#joe-tannenbaum-state-of-the-frontend&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Joe gave us an update on the frontend tooling Laravel provides. He showcased some new Inertia features that are coming in the next major version.&lt;/p&gt;
&lt;p&gt;I personally haven’t used Inertia yet. With my limited time, I get more done using Livewire and Filament. … but I really want to build at least one little project using Inertia.
Maybe I will rewrite my little “read-it-later” app using Inertia one day, as that project already relies on being run in Livewire SPA mode.&lt;/p&gt;
&lt;h2 id=&quot;john-drexler-ship-to-production-on-day-1&quot; tabindex=&quot;-1&quot;&gt;John Drexler: “Ship to Production on DAY 1” &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/12/laracon-eu-2026-recap/#john-drexler-ship-to-production-on-day-1&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;John talks about how to tackle big projects by solving the hardest problems first and then shipping the changes to production fast – maybe behind a feature flag to make sure not all users see the changes yet.&lt;/p&gt;
&lt;p&gt;The example he brought up is adding support for &lt;em&gt;organizations&lt;/em&gt; in the Laravel Forge codebase. Adding this had major consequences to the database and required adding new columns to many tables. This is a hard problem that has to be tackled anyway. By solving it first, you already pushed a big rock out of the way of your project.&lt;/p&gt;
&lt;p&gt;He also brings up the concept of a &lt;em&gt;Mind Palace&lt;/em&gt; where you can discuss ideas and solutions, but reminds us to test these hypotheses by shipping to production early on. An assumption made 3 months ago might not hold up today.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/bliki/StranglerFigApplication.html&quot;&gt;&lt;em&gt;The Strangler Pattern&lt;/em&gt;&lt;/a&gt; was also mentioned in which break your project down into vertical slides, small PRs that you can easily ship into production.&lt;/p&gt;
&lt;p&gt;In general, a very good talk I have to revisit and take more notes on.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;As you can see, it’s quite a list. I will have some note taking to do in the next few days.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Podcasts 2026</title>
        <link href="https://stefanzweifel.dev/posts/2026/03/05/podcasts-2026/" />
        <updated>2026-03-05T21:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2026/03/05/podcasts-2026/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;It has been a while since I’ve written about the podcasts I listen to. I had a little series going in &lt;a href=&quot;https://stefanzweifel.dev/posts/2020/11/14/podcasts-2020/&quot;&gt;2020&lt;/a&gt;, &lt;a href=&quot;https://stefanzweifel.dev/posts/2019/11/09/podcasts-2019/&quot;&gt;2019&lt;/a&gt; and &lt;a href=&quot;https://stefanzweifel.dev/posts/2018/10/14/podcasts-2018/&quot;&gt;2018&lt;/a&gt;, but didn’t update you in – &lt;em&gt;checks notes&lt;/em&gt; – the last half decade.&lt;/p&gt;
&lt;p&gt;The trend that picked up in 2019 and 2020 continued over the last few years: My overall podcast listening time continued to drop.&lt;/p&gt;
&lt;p&gt;I previously used to listen to most episodes while working out at the gym, but I’ve basically stopped doing this as I got distracted way to much and didn’t focus on the workout.(I now just listen to various techno playlists during those valuable workout hours)&lt;/p&gt;
&lt;p&gt;The current podcast subscription list goes like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.relay.fm/cortex&quot;&gt;Cortex&lt;/a&gt;: Previously a podcast where Myke Hurley and CGP Grey talked about their productivity setups and general life stuff. CGP Grey left/is on hiatus and the podcast is now a monthly interview with another internet personality about their productivity setup.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.climatetownproductions.com/podcast&quot;&gt;The Climate Denier’s Playbook&lt;/a&gt;: Two comedians with master&#39;s degrees in Climate Science &amp;amp; Policy and Urban Planning, who use that combination to debunk climate misinformation. Each season-based episode picks apart a specific denial argument — things like EVs being worse than trucks, or geoengineering as a substitute for emissions cuts — and tears it apart with research and jokes. A natural companion to the &lt;a href=&quot;https://www.youtube.com/climatetown&quot;&gt;Climate Town&lt;/a&gt; YouTube channel if you&#39;re already watching that.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mostlytechnical.com/&quot;&gt;Mostly Technical&lt;/a&gt;: Podcast by Aaron Francis and Ian Landsman were they mostly talk about technical stuff. In the past few months they talk more about AI and how recent developments in this space changed their daily work.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://99percentinvisible.org/&quot;&gt;99% Invisible&lt;/a&gt;: Stories about design and architecure. Last time I wrote that I didn’t like their trajection their on. I still have them in my feed so they are doing a good job again.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.articlesofinterest.co/&quot;&gt;Articles of Interest&lt;/a&gt;: Started as a mini-series within 99% Invisible — so a natural fit alongside it. Hosted by Avery Trufelman, it covers topics like the rise of casual wear, the environmental impact of the textile industry, and why womenswear doesn&#39;t have pockets. Each season takes a thematic approach, which makes it feel more like an audio documentary series than a regular podcast.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bbc.com/audio/brand/b006s5dp&quot;&gt;BBC: Just a Minute&lt;/a&gt;: A BBC Radio 4 comedy panel game running since 1967, where the goal is to talk for sixty seconds on a given subject without hesitation, repetition or deviation. The comedy comes from contestants trying to bend those rules while catching each other out. Good background listening and a nice palate cleanser between heavier shows.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nosuchthingasafish.com/&quot;&gt;No Such Thing as a Fish&lt;/a&gt;: A weekly podcast by the researchers behind the BBC panel show QI, where each of them shares their favourite fact they&#39;ve come across that week. The facts are genuinely surprising and the hosts have good chemistry. Very easy listening.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://notesonwork.transistor.fm/&quot;&gt;Notes on Work&lt;/a&gt;: Brief thoughts and insights from Caleb Porzio. Caleb is the creator of Livewire and Alpine.js, so if you&#39;re in that corner of the web dev world it&#39;s a nice window into how an indie open source developer thinks. Episodes are short and come out frequently — easy to keep up with.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;swiss-specific-podcasts&quot; tabindex=&quot;-1&quot;&gt;Swiss Specific Podcasts &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2026/03/05/podcasts-2026/#swiss-specific-podcasts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These podcasts are either very related to Swiss news or politics or are spoken in Swiss German.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.srf.ch/audio/echo-der-zeit&quot;&gt;SRF: Echo der Zeit&lt;/a&gt;: My daily ~30 minutes news update on what’s happening in the world.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.srf.ch/audio/news-plus-hintergruende&quot;&gt;SRF: News Plus Hintergründe&lt;/a&gt;: Was previously called &lt;em&gt;Hotspot&lt;/em&gt;. They release a series of podcasts about a certain topic every month or two. Well researched and worth a listen.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.republik.ch/format/dritte-gewalt&quot;&gt;Republik: Dritte Gewalt&lt;/a&gt;: A long-running podcast with two journalists about legal cases in Switzerland. Great crew and episodes.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.republik.ch/format/gute-frage&quot;&gt;Republik: Gute Frage&lt;/a&gt;: A newer podcast in the &lt;em&gt;Republik Podcast universe&lt;/em&gt; where the three hosts discuss a “Gesellschafts” question.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;I’m sure I forgot to add some podcasts I briefly listened for a while or where I just enjoyed a handful of episodes of them.&lt;/p&gt;
&lt;p&gt;Also made myself a note to share my podcasts lists next year again.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Recap 2025</title>
        <link href="https://stefanzweifel.dev/posts/2025/12/31/recap-2025/" />
        <updated>2025-12-31T17:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2025/12/31/recap-2025/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;As has become tradition in my developer bubble, a recap on my 2025.&lt;/p&gt;
&lt;h2 id=&quot;work&quot; tabindex=&quot;-1&quot;&gt;Work &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/12/31/recap-2025/#work&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I start with the work section, as there was a huge change this year: I left 2media/CLARK after 12 years (!) of working there.&lt;/p&gt;
&lt;p&gt;A lot happened at this company in the past 16 months that are worthy of a book, but in short: I couldn’t take management seriously anymore and I got bored. I felt like trapped in a cage. Or like rare Pokémon-card you keep wrapped in plastic in your drawer.&lt;/p&gt;
&lt;p&gt;Every single feature or code change was discussed to death. Meetings to prepare other meetings. I lost all interest and the momentum faded.&lt;/p&gt;
&lt;p&gt;On the day the “You have to leave” thought was locked in, a new job opening was announced for Laravel Forge. I thought “this is your chance” and put everything down, wrote my application and publicly shared that I applied.&lt;br&gt;
Unfortunately I never heard anything back besides the “we received your application email”.&lt;/p&gt;
&lt;p&gt;Sandro from Trenda saw my social media comment and reached out and mentioned, that they are looking for an additional developer to join the team and if I would be interested to rejoin the team.&lt;/p&gt;
&lt;p&gt;After a quick meeting, I’ve signed the contract and handed in my resignation at CLARK and since October 1st I’m &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#work&quot;&gt;back&lt;/a&gt; at Trenda.&lt;/p&gt;
&lt;p&gt;I’m grateful, that Sascha and Sandro took me back. The current economic situation is tough for many and there are layoffs happening everywhere. Grateful that this transition was short and that I’m back at Trenda.&lt;/p&gt;
&lt;p&gt;The first three months are now over and I can sincerely say this was one of the best decision I’ve made in 2025. We as a team work well together and I was able to ship great new features to the platform already.&lt;br&gt;
Something I dearly missed from the last few years at the previous company.&lt;/p&gt;
&lt;h2 id=&quot;personal&quot; tabindex=&quot;-1&quot;&gt;Personal &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/12/31/recap-2025/#personal&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The changes in the work life bleeds in my personal life. Only now I’ve noticed, that I was severely stressed out for many months. Being the sole developer and sole technical person for an entire business unit isn’t great.&lt;/p&gt;
&lt;p&gt;The change of employer and being in a relationship did wonders. I have a positive outlook for my life again, am in a good mood way more and sleep much better.&lt;/p&gt;
&lt;p&gt;I would also say I’m more fit. Not necessarily on a cardio-fitness level (46 VO2 max) or on a strength level (I lift the same weights as a year before), but in an overall way.&lt;br&gt;
I gained 10 kg of mass over the course of the last 12 months and I feel and look much better now.&lt;/p&gt;
&lt;p&gt;Having a partner now to – whom I can talk about anything and everything – helped my mental health a lot. His smile always calms me down and let’s me forget the stress of the current day.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Speaking of mental health; my addiction to social media still plagues me. I check the networks way too much without any benefit.&lt;/p&gt;
&lt;p&gt;In the middle of the year I’ve started blocking the most time consuming networks through &lt;em&gt;Little Snitch&lt;/em&gt;. Now anytime I visit the webpages in my browser, I get a blank page.&lt;/p&gt;
&lt;p&gt;I think I just miss &lt;em&gt;old Twitter&lt;/em&gt; and the whole vibe that came with it. Or I’m still disappointed how Twitter turned out to be after the owner change.&lt;/p&gt;
&lt;p&gt;2026 is the year where I would like to be “less online”. I think &lt;em&gt;do less&lt;/em&gt; will become my general theme for 2026.&lt;br&gt;
Do less doom scrolling. Do less scattered work. Spend more quality time with friends and family and do more things that bring joy.&lt;/p&gt;
&lt;h2 id=&quot;open-source&quot; tabindex=&quot;-1&quot;&gt;Open Source &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/12/31/recap-2025/#open-source&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Speaking of doing less: Open Source work. My contribution to my own open source projects stagnated this year.&lt;/p&gt;
&lt;p&gt;After 2 years, I released a new major version of &lt;em&gt;git-auto-commit&lt;/em&gt; this summer.
I needed the time to get the courage to release the changes. I always had the thought in my head, that I need more time to think about it. Test the change. Make sure nothing breaks. But still, I broke workflows of others and that wasn’t a great feeling.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;My PHP packages didn’t get as much love either. There are many open issues that nag at me.&lt;br&gt;
Over the holiday break I worked on &lt;a href=&quot;https://github.com/stefanzweifel/laravel-backup-restore&quot;&gt;&lt;em&gt;laravel-backup-restore&lt;/em&gt;&lt;/a&gt;, but that was not enough. I will soon go through all those projects and decide, which project I’m going to put my focus on the next year and which will be put on the backburner or be archived.&lt;/p&gt;
&lt;p&gt;I still love working on these projects, but I need to find a good workflow for myself to give me dedicated time to work on them. Like “every second Tuesday evening you work 2 hours on this repository”.&lt;/p&gt;
&lt;h2 id=&quot;swiss-laravel-association&quot; tabindex=&quot;-1&quot;&gt;Swiss Laravel Association &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/12/31/recap-2025/#swiss-laravel-association&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Another big thing in my life right now is the &lt;a href=&quot;https://laravel.swiss/&quot;&gt;Swiss Laravel Association&lt;/a&gt; and the Laravel Switzerland meetups.&lt;br&gt;
This was the first year where we took over from Ruslan and organised 12 meetups (with 2 speakers each) and 1 &lt;a href=&quot;https://lp.jetbrains.com/phpverse-2025/&quot;&gt;PHPverse&lt;/a&gt; watch party.&lt;/p&gt;
&lt;p&gt;The first few months were rough, until we figured out a good workflow how we should organise us. Like who should look for locations, who should look for speakers.&lt;br&gt;
Now after a year, I think we have found an &lt;em&gt;okay&lt;/em&gt; workflow, that keeps the show running.&lt;/p&gt;
&lt;p&gt;I’m especially proud that many new faces showed up to the meetups. This shows us, that there are lot of Laravel devs out there in Switzerland that are looking for a community.&lt;br&gt;
Lots to come in 2026. New locations. A redesigned website. More collaboration with other international meetups.&lt;/p&gt;
&lt;h2 id=&quot;epilogue&quot; tabindex=&quot;-1&quot;&gt;Epilogue &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/12/31/recap-2025/#epilogue&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As I &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#epilogue&quot;&gt;wrote&lt;/a&gt; last year, I have a good outlook on my life. My partner and I celebrated our first anniversary this year and plan to move into a new apartment together next year. Fingers-cross that we find something affordable in the greater Zürich area.&lt;/p&gt;
&lt;p&gt;Work fulfills me again and I see much potential in my work at Trenda.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In 2026 I would like to read, write and publish more. Scrolling through my RSS reader and reading blog posts from other developers or creatives brings me a lot of joy.&lt;br&gt;
I would like to contribute my part here and share more of my thoughts on work or on life.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Health wise I wish that the &lt;em&gt;tremblings&lt;/em&gt; settle in my family. In addition to &lt;a href=&quot;https://stefanzweifel.dev/posts/2023/12/17/lifes-wake-up-call-into-reality/&quot;&gt;the health scare of 2023&lt;/a&gt;, we battled with a cancer diagnosis this year.
Thankfully, the person went to an early screening and the tumor was discovered early on. Removal and treatment went swiftly and there’s nothing left in the body.&lt;/p&gt;
&lt;p&gt;But man. This diagnosis was devastating when I heard about it. Many evenings I was curled up in bed thoughts racing about potentially losing this person or that they had to suffer for a long time.&lt;/p&gt;
&lt;p&gt;So please, tell your loved ones to go do a mammography screening or prostate exam or a general check up, even if it’s not the most comfortable procedure. It can save lives.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Let me bring this back to a positive note. 2025 was a great year for me. I love life and have a great positive outlook for the years to come. See you all again in 2026.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you like these recap-posts, you can find others from &lt;a href=&quot;https://duncanmcclean.com/year-in-review-2025&quot;&gt;Duncan&lt;/a&gt;, &lt;a href=&quot;https://denniskoch.dev/articles/2025-12-30-recap-2025/&quot;&gt;Dennis&lt;/a&gt;, &lt;a href=&quot;https://pawelgrzybek.com/a-look-back-at-2025/&quot;&gt;Paweł&lt;/a&gt;, &lt;a href=&quot;https://www.wking.dev/logs/clarity&quot;&gt;Will&lt;/a&gt;, &lt;a href=&quot;https://rathes.me/blog/en/review-2025&quot;&gt;Rathes&lt;/a&gt; or &lt;a href=&quot;https://www.jmduke.com/posts/2025.html&quot;&gt;Justin&lt;/a&gt;.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Introducing laravel-tfa-confirmation</title>
        <link href="https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/" />
        <updated>2025-02-02T12:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;Earlier today I tagged v1 of a new Laravel package. &lt;a href=&quot;https://github.com/stefanzweifel/laravel-tfa-confirmation/&quot;&gt;laravel-tfa-confirmation&lt;/a&gt; – as the name might suggest – the package has to do with two-factor-authentication.&lt;/p&gt;
&lt;p&gt;Let me explain what the package does and how it could be beneficial for your project.&lt;/p&gt;
&lt;h2 id=&quot;what-problem-does-the-package-solve&quot; tabindex=&quot;-1&quot;&gt;What problem does the package solve? &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#what-problem-does-the-package-solve&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The package was born from my work a &lt;a href=&quot;https://trenda.ch/&quot;&gt;Trenda&lt;/a&gt; in 2024.&lt;br&gt;
In the app, users can enable 2FA for their account and we wanted to force users to enter their 2FA code if they visited certain sections of the app (eg. the team settings page).&lt;/p&gt;
&lt;p&gt;However, they shouldn&#39;t have to enter their 2FA code everytime they visited the page. Only in a given time period.&lt;/p&gt;
&lt;p&gt;This is basically the same behaviour as the &lt;a href=&quot;https://laravel.com/docs/master/authentication#password-confirmation&quot;&gt;&amp;quot;Password Confirmation&amp;quot;&lt;/a&gt; feature in Laravel, but for 2FA codes. It&#39;s also similar to GitHub&#39;s &lt;a href=&quot;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/sudo-mode&quot;&gt;sudo mode&lt;/a&gt; feature.&lt;/p&gt;
&lt;h2 id=&quot;how-does-it-work&quot; tabindex=&quot;-1&quot;&gt;How does it work? &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#how-does-it-work&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After installing the package through composer, you get a new &lt;code&gt;RequireTwoFactorAuthenticationConfirmation&lt;/code&gt;-middleware, which you can apply to a single route or an entire route group.&lt;/p&gt;
&lt;p&gt;Based on a configurable timeout, your users is either forced to enter their 2FA code or is being redirected to the intended route. (Probably important to note, that the package depends on &lt;code&gt;laravel/fortify&lt;/code&gt; and that you have to use Laravel Fortify as your 2FA &amp;quot;provider&amp;quot;.)&lt;/p&gt;
&lt;p&gt;The package provides the routes and controllers to ask for a 2FA code and to verify that the entered 2FA code is valid. The &amp;quot;validation&amp;quot; is then stored in the session for a configured time; thus preventing the user from being asked a 2FA code again.&lt;/p&gt;
&lt;p&gt;The package actually listens to the &lt;code&gt;Laravel&#92;Fortify&#92;Events&#92;ValidTwoFactorAuthenticationCodeProvided &lt;/code&gt;-event fired by Fortify. This has the added benefit, that users don&#39;t have to enter their 2FA code again, if they just have logged in and confirmed their 2FA code.
This streamlines the user experience, if for example you apply the middleware to your admin-panel. Users then don&#39;t have to enter a 2FA code twice, if they just logged in.&lt;/p&gt;
&lt;h2 id=&quot;what-the-package-doesnt-provide&quot; tabindex=&quot;-1&quot;&gt;What the package doesn&#39;t provide &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#what-the-package-doesnt-provide&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The default challenge view presented to users to confirm their 2FA code is not styled at all.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://stefanzweifel.dev/assets/images/posts/20250202-tfa-confirmation/unstyled-confirmation-view.webp&quot; loading=&quot;lazy&quot; alt=&quot;Screenshot of the default challenge view presented to users.&quot;&gt;
    &lt;figcaption&gt;The default challenge view shipped with the package is not styled at all. You as a developer have to apply the styles on your own.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I&#39;ve thought as the the design of each app differs anyway and there is no &lt;em&gt;default&lt;/em&gt;-style for Laravel apps, it does not make sense to ship styled HTML. &lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;If you use this package, you have to publish the view and update the template accordingly.&lt;/p&gt;
&lt;h2 id=&quot;outlook&quot; tabindex=&quot;-1&quot;&gt;Outlook &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#outlook&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&#39;m using v1 of the package already in my side projects to protect my Filament admin panels with a 2FA code.&lt;/p&gt;
&lt;p&gt;In my biased view, I think v1 of the package works great and I encourage you – dear reader – to give it a try in your project.&lt;/p&gt;
&lt;p&gt;Nevertheless, I have ideas for future improvements to the package, but I just haven&#39;t nailed the API yet:&lt;/p&gt;
&lt;h3 id=&quot;custom-session-lengths-for-certain-actions&quot; tabindex=&quot;-1&quot;&gt;Custom session lengths for certain actions &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#custom-session-lengths-for-certain-actions&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Right now the package has 1 timeout configuration. If, for example, you want that users have to enter a 2FA code every 24 hours to access an admin panel, but every 10 minutes if they access their billing settings, this doesn&#39;t work yet.&lt;/p&gt;
&lt;p&gt;There is just one challenge-view and the &amp;quot;challenge confirmation&amp;quot; is stored in the session through a &lt;em&gt;detached&lt;/em&gt; event listener.
The code in the event listener currently doesn&#39;t know, for which &lt;em&gt;challenge&lt;/em&gt; a confirmation should be stored.&lt;/p&gt;
&lt;p&gt;I think I would have to add some sort of hidden &amp;quot;name&amp;quot;-field or &amp;quot;name&amp;quot;-session value to make this all work.&lt;/p&gt;
&lt;h3 id=&quot;confirmstwofactor-contract&quot; tabindex=&quot;-1&quot;&gt;&lt;code&gt;ConfirmsTwoFactor&lt;/code&gt; Contract &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#confirmstwofactor-contract&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Already in the code base, but not used yet is a &lt;code&gt;ConfirmsTwoFactor&lt;/code&gt; contract. My idea is to add this to a model like &lt;code&gt;User&lt;/code&gt; or &lt;code&gt;Team&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In userland, developers could then write code to determine if a challenge should be shown and or how long the confirmations lasts. For example, a team could enable a setting that forces users to confirm a code every 15 minutes for all their users; even when the default value would be every 24 hours.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;As you can see, I&#39;ve got some ideas for this package. For now, I&#39;m happy with v1 and will tinker with a neat API for the other features.&lt;br&gt;
If you have any feedback, please reach out!&lt;/p&gt;
&lt;hr class=&quot;footnotes-sep&quot;&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;If you think this is a bad idea or have a great idea for a default design, feel free to send in a pull request &lt;a href=&quot;https://stefanzweifel.dev/posts/2025/02/02/introducing-laravle-tfa-confirmation/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Recap 2024</title>
        <link href="https://stefanzweifel.dev/posts/2024/12/31/recap-2024/" />
        <updated>2024-12-31T12:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2024/12/31/recap-2024/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;Another eventful year comes to an end. As it has become tradition, here&#39;s my recap of 2024.&lt;/p&gt;
&lt;h2 id=&quot;personal&quot; tabindex=&quot;-1&quot;&gt;Personal &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#personal&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I deviate from my previous recaps and want to share some personal thoughts.&lt;/p&gt;
&lt;p&gt;2024 was a whirlwind of a year.&lt;/p&gt;
&lt;p&gt;It started with an amazing two-week vacation with my gym buddies on a remote island and a trip to Amsterdam for my first-ever conference, which was Laracon EU.&lt;br&gt;
I met so many internet friends there and had a blast. Can&#39;t wait until next February to see many friends again. (If you see me at Laracon, please come by and say &amp;quot;Hi!&amp;quot;.)&lt;/p&gt;
&lt;p&gt;In March, I also started working for &lt;a href=&quot;https://trenda.ch/&quot;&gt;Trenda&lt;/a&gt;. Thanks to attending the Laravel Switzerland meetups, I met Sascha – one of the founders – and got this amazing opportunity to work for them. It was only for one day a week, but it was a breath of fresh air to work in a small team again. Not many meetings. Shipping features and improvements fast to customers.&lt;br&gt;
However, I handed in my resignation and ended the work relationship by the end of this December – for now. Working for two companies got the better of me. As Sascha and Sandro regularly go to the Laravel Switzerland meetups (more on this later), I will stay in contact with them.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Sports-wise, I&#39;ve fallen a bit into a slump. I&#39;m still going to my local gym seven days a week, but didn&#39;t make much progress this year. I had to pause training due to injuries or sickness a couple of times this year.&lt;br&gt;
Next year I&#39;m going to change this. Over this holiday break, I want to change my fitness routine and create a better training plan. I also would like to incorporate cardio more into my training. My goal is to run &lt;a href=&quot;https://www.silvesterlauf.ch/&quot;&gt;a local race&lt;/a&gt; in December 2025.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Summer and autumn were great, though. I met friends and family for hikes, bike rides, dinners, or evenings at the cinema. In general, I was way more open and &lt;em&gt;adventurous&lt;/em&gt; this year. I said &lt;em&gt;Yes!&lt;/em&gt; to more things and didn&#39;t regret it a bit.&lt;/p&gt;
&lt;p&gt;I&#39;ve always been quite introverted, and making new friendships or connections was brutally hard all my life. But since I turned 30, started taking the gym routine more seriously, and attending those Laravel meetups in Switzerland, I&#39;ve changed. Talking to strangers got easier. I enjoy most of the conversations I have and almost always learn something new or get a fresh perspective on things.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Speaking of meetups, &lt;a href=&quot;https://steiger.dev/&quot;&gt;Ruslan&lt;/a&gt; reached out to me and some other regulars to ask if we could help take over the meetup from him, as he is traveling the world by the end of the year.&lt;br&gt;
So together with Jan, Philipp, Sandro, and Sascha, I&#39;ve founded the &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/11/15/on-founding-the-swiss-laravel-association/&quot;&gt;Swiss Laravel Association&lt;/a&gt;. Together we will organize the Laravel meetups going forward. (The next one on January 30th will be awesome.)&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Rambling on here, my social media usage declined further this year. Removing all social media apps from my phone and blocking the URLs in my local network using my Pi-Hole and &lt;a href=&quot;https://www.obdev.at/products/littlesnitch/index.html&quot;&gt;Little Snitch&lt;/a&gt; brought some much-needed calmness to my life.&lt;/p&gt;
&lt;p&gt;I still miss the olden days of Twitter. Discovering new programming projects or fun developers and like-minded people gave me a lot of joy. I felt less alone in this world.&lt;/p&gt;
&lt;p&gt;Mastodon and now Bluesky filled this void a bit, but the fragmentation and general vibe online make me believe that &lt;a href=&quot;https://www.theatlantic.com/technology/archive/2022/11/twitter-facebook-social-media-decline/672074/&quot;&gt;the era of social media&lt;/a&gt; is now truly over.&lt;/p&gt;
&lt;p&gt;I now fill my free time by reading or listening to books or catching up in my RSS reader.&lt;/p&gt;
&lt;h2 id=&quot;work&quot; tabindex=&quot;-1&quot;&gt;Work &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#work&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Work was a whirlwind as well. As mentioned, I worked for Trenda a bit, which was really, really great. It gave me a glimpse into another company and development culture.&lt;/p&gt;
&lt;p&gt;At 2media/CLARK, I was involved in many projects, most of them hidden behind the veil of &amp;quot;internal access only.&amp;quot; My role at this company has also morphed over the last few years from &amp;quot;developer&amp;quot; to &amp;quot;general problem solver.&amp;quot; I like solving problems – big and small – but I don&#39;t necessarily enjoy creating business reports for managers who constantly change the requirements.&lt;/p&gt;
&lt;p&gt;Losing my developer colleague of 12 years from the team by the end of this year was also quite the blow. The next few weeks and months will be challenging, but I&#39;m sure I can find a good solution with my managers/team leader.&lt;/p&gt;
&lt;p&gt;And if not, I&#39;m sure I will find a great team I could join that values my abilities and experience.&lt;/p&gt;
&lt;h2 id=&quot;open-source&quot; tabindex=&quot;-1&quot;&gt;Open Source &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#open-source&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;On the open-source side of my work, I mostly did the regular maintenance work: adding support for new Laravel, PHP, and Node versions and the odd new feature release.&lt;/p&gt;
&lt;p&gt;I wish I would have more energy to work on open-source projects again. My &lt;a href=&quot;https://stefanzweifel.dev/posts/2022/12/18/my-updated-things-3-setup/&quot;&gt;todo list&lt;/a&gt; and Obsidian vault are filled with project ideas I would like to explore. Most of them don&#39;t necessarily have to turn into open-source projects but are still problems I would like to find solutions for.&lt;/p&gt;
&lt;p&gt;Next year, I want to carve out at least one evening per week dedicated to personal coding. Be it working on specific open-source projects or just tinkering with new technology or a new tool. (Watching &lt;a href=&quot;https://rustforphp.dev/&quot;&gt;Ryan&#39;s Rust For PHP course&lt;/a&gt; is very high on my to-do list, for example.)&lt;/p&gt;
&lt;h2 id=&quot;epilogue&quot; tabindex=&quot;-1&quot;&gt;Epilogue &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/12/31/recap-2024/#epilogue&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;2025 looks bright. I think I never had such a positive outlook in my life. At the beginning of the year, I struggled with burnout and depression. I felt alone, unsure what my purpose in life should be.&lt;/p&gt;
&lt;p&gt;These thoughts are all gone now. Starting my first romantic relationship later this year was probably the biggest force that pushed these thoughts away.&lt;br&gt;
My calendar is already sprinkled with lots of events with friends and family or dedicated alone time for concerts or hikes.&lt;/p&gt;
&lt;p&gt;I can&#39;t wait to see what 2025 will bring to me.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Special thanks to &lt;em&gt;Lu&lt;/em&gt; and &lt;em&gt;Ivan&lt;/em&gt; for spending time with me this year. Your presence made an enormous impact on my life.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you enjoy reading these recap posts, here are a few from my internet friends you might enjoy as well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.joe.codes/2024-year-in-review&quot;&gt;https://blog.joe.codes/2024-year-in-review&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.martyfriedel.com/blog/2024-in-review&quot;&gt;https://www.martyfriedel.com/blog/2024-in-review&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>On Founding the Swiss Laravel Association</title>
        <link href="https://stefanzweifel.dev/posts/2024/11/15/on-founding-the-swiss-laravel-association/" />
        <updated>2024-11-15T12:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2024/11/15/on-founding-the-swiss-laravel-association/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;A few weeks ago we &lt;a href=&quot;https://github.com/swiss-laravel-association/policies/commit/ebdc91e8dd712de635a4dd359490be07b2680fea&quot;&gt;signed&lt;/a&gt; the founding documents of the &lt;a href=&quot;https://laravel.swiss/&quot;&gt;&lt;em&gt;Swiss Laravel Association&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While Ruslan goes on an adventure around the world, we want to keep the momentum going and keep organising the monthly meetups happening throughout Switzerland.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The association currently only consists of 5 members and we&#39;re currently still figuring out how membership and sponsoring should actually work.&lt;/p&gt;
&lt;p&gt;Right now we&#39;re focused on organsing the next events in January and February 2025.&lt;/p&gt;
&lt;p&gt;It&#39;s all quite exciting. Eventhough Switzerland has a strong association-culture with sport clubs everywhere, I was never part of one; and especially not part of the executive committee steering the whole thing.&lt;/p&gt;
&lt;p&gt;We&#39;ve shared more about the future at the last meetup. You can find &lt;a href=&quot;https://www.youtube.com/watch?v=AnQv_-EtF2A&quot;&gt;the recording of the talk&lt;/a&gt; on YouTube.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Deployer: Build and Cache Frontend Assets once using GitHub Actions</title>
        <link href="https://stefanzweifel.dev/posts/2024/08/03/deployer-build-and-cache-frontend-assets-once-using-github-actions/" />
        <updated>2024-08-03T12:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2024/08/03/deployer-build-and-cache-frontend-assets-once-using-github-actions/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;When I joined &lt;a href=&quot;https://trenda.ch/&quot;&gt;Trenda&lt;/a&gt; earlier this year, one of the first tasks given to me was adding support for zero downtime deployments.&lt;/p&gt;
&lt;p&gt;I&#39;ve written before how I use &lt;a href=&quot;https://stefanzweifel.dev/posts/2018/07/17/zero-downtime-deployments-for-laravel-apps-with-deployer/&quot;&gt;Deployer&lt;/a&gt; and how I &lt;a href=&quot;https://stefanzweifel.dev/posts/2021/05/24/deployer-on-github-actions/&quot;&gt;trigger deployments using GitHub Actions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In contrast to other projects, at Trenda, we don&#39;t commit the compiled CSS and JavaScript files to the repository. The files need to be compiled on each developers machine and during deployment.&lt;/p&gt;
&lt;p&gt;Originally we ran &lt;code&gt;npm ci; npm run build;&lt;/code&gt; on each deployment on each server. This works, but it makes for slow deployments.&lt;/p&gt;
&lt;p&gt;One thing we noticed is that the compiled assets don&#39;t need to change on &lt;strong&gt;each&lt;/strong&gt; deployment. &lt;mark&gt;Not every release changes the design of the app or adds new JavaScript functionality.&lt;/mark&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gehrisandro&quot;&gt;Sandro&lt;/a&gt; suggested that we somehow could use the &lt;a href=&quot;https://github.com/actions/cache&quot;&gt;&amp;quot;actions/cache&amp;quot;-Action&lt;/a&gt; here.&lt;/p&gt;
&lt;p&gt;At the same time we were discussing the way forward, the  &lt;a href=&quot;https://flareapp.io/&quot;&gt;Flare team&lt;/a&gt; shared &lt;a href=&quot;https://flareapp.io/blog/rethinking-deploys-at-flare&quot;&gt;&amp;quot;Rethinking deploys at Flare&amp;quot;&lt;/a&gt;, in which they mention that they use a package called &lt;a href=&quot;https://github.com/hammerstonedev/airdrop&quot;&gt;Airdrop&lt;/a&gt; to cache build assets.&lt;/p&gt;
&lt;p&gt;A quick test showed that this would work for us as well, but Sandro&#39;s approach with using actions/cache would not require another composer dependency, and we would remain in &amp;quot;GitHub Actions land&amp;quot;.&lt;/p&gt;
&lt;h2 id=&quot;github-actions-workflow-for-deployer&quot; tabindex=&quot;-1&quot;&gt;GitHub Actions Workflow for Deployer &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/08/03/deployer-build-and-cache-frontend-assets-once-using-github-actions/#github-actions-workflow-for-deployer&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We took the workflow I&#39;ve &lt;a href=&quot;https://stefanzweifel.dev/posts/2021/05/24/deployer-on-github-actions/#workflow-example-1-deploy-manually&quot;&gt;shared in 2021 to trigger deploys manually&lt;/a&gt; and added steps to build and cache the frontend assets. The steps look like this:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Workflow triggers and more&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Install and cache composer dependencies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;ramsey/composer-install@v3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Frontend Assets Cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;actions/cache@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;frontend-assets-cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;frontend-assets-${{ hashFiles(&#39;**/vite.config.js&#39;, &#39;**/package-lock.json&#39;, &#39;**/tailwind.config.js&#39;, &#39;**/resources/**&#39;, &#39;**/vendor/filament/**/*.blade.php&#39;) }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    path&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;public/build/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;actions/setup-node@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    node-version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;22&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    cache&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;npm&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Install Dependencies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;npm ci&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Production Assets Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;npm run build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# More steps to deploy the app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part here is the &lt;code&gt;key&lt;/code&gt; we generate in the &amp;quot;Frontend Assets Cache&amp;quot; step.
The &lt;a href=&quot;https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/expressions#hashfiles&quot;&gt;hashFiles&lt;/a&gt;-function generates a unique hash based on the files that match the patterns passed to the method. In this case, the hash is regenerated when one of the following files changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vite.config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resources/**&lt;/code&gt; (any CSS, JavaScript or Blade template file)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vendor/filament/**/*.blade.php&lt;/code&gt; (any Blade templates provided by Filament. We use a custom theme)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The generated cache is stored for 90 days on GitHub servers. If we would start adding Tailwind CSS classes in more places that are not covered in this list, we can just extend the list of files passed to &lt;code&gt;hashFiles()&lt;/code&gt;. Easy-peasy.&lt;/p&gt;
&lt;p&gt;Note that we cache the content of &lt;code&gt;public/build/*&lt;/code&gt;. If your app would rely on code inside &lt;code&gt;node_modules/&lt;/code&gt; you might have to still run &lt;code&gt;npm ci&lt;/code&gt; on your server or find a different solution.&lt;/p&gt;
&lt;p&gt;The entire GitHub Actions workflow now looks like this.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Deploy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  repository_dispatch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    types&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;deploy&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  workflow_dispatch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    inputs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;      deploy_env&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;Environment&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;stag&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;choice&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;stag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;prod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;  deploy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Deploy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Setup PHP&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;shivammathur/setup-php@v2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          php-version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;8.3&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;ramsey/composer-install@v3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Frontend Assets Cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;actions/cache@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;frontend-assets-cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;frontend-assets-${{ hashFiles(&#39;**/vite.config.js&#39;, &#39;**/package-lock.json&#39;, &#39;**/tailwind.config.js&#39;, &#39;**/resources/**&#39;, &#39;**/vendor/filament/**/*.blade.php&#39;) }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          path&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;public/build/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;actions/setup-node@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          node-version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;22&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          cache&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;npm&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Install Dependencies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;npm ci&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Production Assets Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;steps.frontend-assets-cache.outputs.cache-hit != &#39;true&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;npm run build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;Deploy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;deployphp/action@master&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          private-key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;${{ secrets.DEPLOY_KEY }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          known-hosts&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;${{ secrets.KNOWN_HOSTS }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D;--shiki-dark:#85E89D&quot;&gt;          dep&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;deploy ${{ github.event.inputs.deploy_env }} -v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;updating-deployer-to-upload-the-assets&quot; tabindex=&quot;-1&quot;&gt;Updating Deployer to upload the Assets &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/08/03/deployer-build-and-cache-frontend-assets-once-using-github-actions/#updating-deployer-to-upload-the-assets&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The last remaining step is to tell deployer to upload the cached assets to our server.
For this I&#39;ve added a &lt;code&gt;upload:vite-assets&lt;/code&gt; task and referenced it in our &lt;code&gt;deploy&lt;/code&gt;-task.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;upload:vite-assets&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; () {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;    // Check if the public/build directory exists&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    runLocally&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;test -d public/build/&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;    // Upload the assets if they exist.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    upload&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;public/build/&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;{{release_or_current_path}}/public/build/&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;desc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;Deploy application&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;deploy&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;deploy:prepare&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;deploy:vendors&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;upload:vite-assets&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;// ← our new task&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:storage:link&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:view:cache&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:config:cache&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:route:cache&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:view:cache&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:migrate&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:horizon:terminate&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;deploy:publish&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;php-fpm:reload&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;artisan:cache:clear&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we now deploy our app using GitHub Actions, Actions will restore our &lt;code&gt;public/build/&lt;/code&gt; directory if a cache exists, or it will install our NPM dependencies and create a new build and cache that build.&lt;/p&gt;
&lt;p&gt;Deployer will then double-check if the  &lt;code&gt;public/build/&lt;/code&gt; directory exists and will upload the files to our servers.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot; tabindex=&quot;-1&quot;&gt;Wrapping Up &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/08/03/deployer-build-and-cache-frontend-assets-once-using-github-actions/#wrapping-up&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We&#39;re really happy with this solution. It allows us to keep everything deployment related inside the GitHub Actions universe.&lt;/p&gt;
&lt;p&gt;In addition, it allows us to easily upgrade the used Node version by changing &lt;code&gt;node-version&lt;/code&gt; in the workflow. We now don&#39;t need to install Node on the server the app is being deployed to and don&#39;t have to care about upgrades.&lt;/p&gt;
&lt;p&gt;&lt;mark&gt;And the biggest benefit is faster deployments. This change shaved off 1 to 2 minutes of each of our deployments.&lt;/mark&gt; Other CI workflows don&#39;t rely on the frontend assets, but if they would, I would add the same steps to those workflows as well.&lt;/p&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>How I use Shiki in Eleventy</title>
        <link href="https://stefanzweifel.dev/posts/2024/06/03/how-i-use-shiki-in-eleventy/" />
        <updated>2024-06-03T21:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2024/06/03/how-i-use-shiki-in-eleventy/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;When I migrated to &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/20/migrating-from-laravel-to-eleventy/&quot;&gt;Eleventy&lt;/a&gt; one crucial feature I wanted to keep was the server-rendered code blocks.&lt;/p&gt;
&lt;p&gt;In the Laravel version of my site, I used the &lt;a href=&quot;https://github.com/spatie/shiki-php&quot;&gt;spatie/laravel-shiki&lt;/a&gt; package for this, which was a wrapper around the Node binary of Shiki. As Eleventy is already running &lt;em&gt;in&lt;/em&gt; JavaScript, adding Shiki to the build was quite easy.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://www.hoeser.dev/blog/2023-02-07-eleventy-shiki-simple/&quot;&gt;this great post&lt;/a&gt; by Raphael Höser, I got Shiki running in less than 15 minutes.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The perfectionist I am, I noticed that the cold-start of running Eleventy took a bit longer: instead of 0.5 seconds it took 3 seconds to boot the Eleventy dev server.&lt;/p&gt;
&lt;p&gt;I can see you – dear reader – rolling your eyes at what seems like an insignificant delay.&lt;br&gt;
The three seconds to start the dev server is still quite fast compared with the many other static site generators out there.&lt;br&gt;
But still, I wanted to find a solution, that adding more and more posts with code blocks doesn&#39;t increase the boot time of my Eleventy instance further.&lt;/p&gt;
&lt;p&gt;So I&#39;ve added a caching layer to my Shiki-plugin using &lt;a href=&quot;https://www.npmjs.com/package/flat-cache&quot;&gt;flat-cache&lt;/a&gt;.&lt;br&gt;
The plugin now looks like this.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; {createHash} &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;crypto&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; flatCache &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;flat-cache&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; {resolve} &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;node:path&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70;--shiki-dark:#FFAB70&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;  // empty call to notify 11ty that we use this feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;  // eslint-disable-next-line no-empty-function&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;  eleventyConfig.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;amendLibrary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;md&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; {});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;  eleventyConfig.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;eleventy.before&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; shiki&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;shiki&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; highlighter&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; shiki.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;getHighlighter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      themes: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;github-dark&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;github-light&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      langs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;html&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;blade&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;php&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;yaml&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;js&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;ts&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;shell&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;        &#39;diff&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    eleventyConfig.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;amendLibrary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;md&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70;--shiki-dark:#FFAB70&quot;&gt;mdLib&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; mdLib.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;        highlight&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70;--shiki-dark:#FFAB70&quot;&gt;code&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70;--shiki-dark:#FFAB70&quot;&gt;lang&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; hash&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt; createHash&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;md5&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;update&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(code).&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;digest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;hex&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; cache&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; flatCache.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;load&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(hash, &lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;resolve&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;./.cache/shiki&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; cachedValue&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; cache.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;getKey&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(hash);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; (cachedValue) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; cachedValue;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; highlightedCode &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; highlighter.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;codeToHtml&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(code, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            lang: lang,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            themes: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;              light: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;github-dark&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;              dark: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;github-dark&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;          });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;          cache.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;setKey&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;(hash, highlightedCode);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;          cache.&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;          return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; highlightedCode;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;      });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code is very similar to Raphael&#39;s solution. The big difference is that in my markdown &lt;code&gt;highlight&lt;/code&gt;-function, I generate a hash value of the &lt;em&gt;to be highlighted code&lt;/em&gt;. This hash acts as the cache key. If the cache is a hit, the cached value is displayed. If the cache is a miss, Shiki will highlight the given code block and store the value in the file cache.&lt;/p&gt;
&lt;p&gt;This caching mechanism reduced Eleventy&#39;s boot time back to 1 second.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/06/03/how-i-use-shiki-in-eleventy/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr class=&quot;footnotes-sep&quot;&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;I&#39;m sure the file-cache is now the bottleneck and a in-memory cache driver like Redis would bring down boot time even more. But let&#39;s keep things simple. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/06/03/how-i-use-shiki-in-eleventy/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
    <entry>
        <title>Incremental Backups with Borg Backup</title>
        <link href="https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/" />
        <updated>2024-05-25T21:00:00Z</updated>
        <id>https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/</id>
        <content xml:lang="en-GB" type="html">
            &lt;p&gt;When I redid &lt;a href=&quot;https://stefanzweifel.dev/posts/2023/09/16/an-opinionated-personal-folder-structure/&quot;&gt;my personal folder structure and moved to iCloud Drive in 2023&lt;/a&gt;, I also needed a new backup solution.&lt;/p&gt;
&lt;p&gt;Previously, I stored my personal files on my &lt;a href=&quot;https://stefanzweifel.dev/posts/2020/08/04/synology-nas-setup-2020/&quot;&gt;Synology NAS&lt;/a&gt; and its data was – and still is – regularly backed-up to a cloud provider and to local hard drives.&lt;/p&gt;
&lt;p&gt;By changing my &lt;em&gt;primary&lt;/em&gt; storage location to iCloud Drive, creating a backup got a bit more complicated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For one, iCloud Drive is only available on Apple devices or on Windows. As far as I know, it&#39;s impossible to install an iCloud Drive client on a Synology NAS or a Linux server.&lt;/li&gt;
&lt;li&gt;When &lt;a href=&quot;https://support.apple.com/en-us/108756&quot;&gt;&amp;quot;Advanced Data Protection&amp;quot;&lt;/a&gt; is enabled, accessing my iCloud data is even more restricted, as all data is end-to-end encrypted.&lt;/li&gt;
&lt;li&gt;The MacBook Pro I own doesn&#39;t have the storage capacity to hold my entire iCloud Drive content locally. The 400GB I currently store in Apple&#39;s cloud doesn&#39;t fit on my 256GB MacBook Pro disk.&lt;/li&gt;
&lt;li&gt;There is no SDK or web API available to interact with iCloud Drive. (Probably wouldn&#39;t have helped me, as I have &lt;em&gt;Advanced Data Protection&lt;/em&gt; enabled)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As it&#39;s impossible for me to keep an offline snapshot of my iCloud Drive on my MacBook Pro, it became quickly clear, that I won&#39;t be able to create regular backup of all my iCloud Drive data.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;br&gt;
I decided that I therefore only regularly back up my most important files. In my private note on this project I wrote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The definition of &amp;quot;most important files&amp;quot; is quite hard. &lt;mark&gt;For me I consider all the files in the folders 10-29 as most important. The folders contain all personal and financial information from the last 2 decades.&lt;/mark&gt; If I would lose them, life would become complicated quickly.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fn2&quot; id=&quot;fnref2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Given iCloud Drive&#39;s limitations, I set these goals for my backup software search:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;end-to-end encryption: only I should be able to see the contents of my files in the backup.&lt;/li&gt;
&lt;li&gt;fast: creating a backup should not take hours&lt;/li&gt;
&lt;li&gt;cheap: a one time purchase; not a monthly or yearly subscription&lt;/li&gt;
&lt;li&gt;incremental backups: to preserve disk space, the software shouldn&#39;t back up all files whenever a new backup is created. Only the changed files should be copied.&lt;/li&gt;
&lt;li&gt;files need to be able to be restored without relying on third party applications on third party hardware&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fn3&quot; id=&quot;fnref3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this out of the way, the search began.&lt;/p&gt;
&lt;h2 id=&quot;the-beginnings-with-rsync&quot; tabindex=&quot;-1&quot;&gt;The beginnings with rsync &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#the-beginnings-with-rsync&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At first I&#39;ve used &lt;a href=&quot;https://en.wikipedia.org/wiki/Rsync&quot;&gt;rsync&lt;/a&gt; to create incremental backups.&lt;/p&gt;
&lt;p&gt;I used the following script to create incremental backups of my &amp;quot;Personal Documents&amp;quot; folder.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;#!/bin/sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;src&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;~/iCloud/Documents/10-19 Personal-Documents/&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Path to folder for backups&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Note: ULDUAR is the name the USB SSD where I would store my backups.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;dest&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;/Volumes/ULDUAR/Backups/&#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Set the retention period for incremental backups in days&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;retention&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;30&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Start the backup process&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;rsync&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; --archive&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--delete &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--backup &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--inplace &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--recursive &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--verbose &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--exclude=.DS_Store &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;&#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;--backup-dir=${&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;dest}/increment/&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; +%Y-%m-%d-%H%M`&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;src&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;}&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;dest}/full/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Clean up incremental archives older than the specified retention period&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ${dest}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;/increment/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; -mindepth&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; -maxdepth&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; -type&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; d&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; -mtime&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;${retention} &lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;-exec&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; rm&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; -rf&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; {}&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This system met a few of my requirements: cheap, fast, incremental backups and restoring files does not require third party software on third-party hardware.&lt;/p&gt;
&lt;p&gt;But it was finicky. My prior knowledge of rsync was limited to using it as a deploy-tool and debugging the script was a bit of a nightmare.&lt;/p&gt;
&lt;p&gt;My attempt to automate the running of the script using &lt;code&gt;launchd&lt;/code&gt; was also a failure. Somehow macOS doesn&#39;t allow a scheduled script to copy files from the internal hard disk to an external drive. The script always failed with &lt;code&gt;rsync: [sender] opendir &amp;quot;/path/to/destination&amp;quot; failed: Operation not permitted (1)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;End-to-end encryption was also not yet solved. I&#39;ve used this system, while I continued my search for a perfect match, so that I at least got &lt;em&gt;a&lt;/em&gt; backup of my files.&lt;/p&gt;
&lt;h2 id=&quot;borg-backup&quot; tabindex=&quot;-1&quot;&gt;Borg Backup &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#borg-backup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After many hours of research and reading too many Reddit threads I&#39;ve landed on my current solution: &lt;a href=&quot;https://www.borgbackup.org/&quot;&gt;Borg Backup&lt;/a&gt; or Borg for short.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fn4&quot; id=&quot;fnref4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;After creating several proof of concept script to test different backup strategies, I created several scripts in a &lt;code&gt;~/bin/borg-backup/&lt;/code&gt; -older that now do all the heavy lifting of backing up my files.&lt;/p&gt;
&lt;h3 id=&quot;config-sh&quot; tabindex=&quot;-1&quot;&gt;&lt;code&gt;config.sh&lt;/code&gt; &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#config-sh&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At the core sits the &lt;code&gt;config.sh&lt;/code&gt; file which contains helper functions and config values.
Here is an abbreviated version of the file. I use the &lt;a href=&quot;https://developer.1password.com/docs/cli/&quot;&gt;1Password CLI&lt;/a&gt; to get the passphrase for the backups.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;#!/bin/sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Secrets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# ---------------------------------------------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Setting this, so the repo does not need to be given on the commandline:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# export BORG_REPO=ssh://username@example.com:2022/~/backup/main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; BORG_REPO&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#39;/Volumes/ULDUAR/Backups/Borg Repositories/iCloud Drive&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Extract Passphrase from 1Password item&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; BORG_PASSPHRASE&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;op&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; item&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; get&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;1password-i&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; --fields&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; label=password&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Utilities&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# ---------------------------------------------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;colorOutput&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; color&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; content&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$2&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;    # Color mappings&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[0m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; red&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[31m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; green&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[0;32m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; yellow&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[0;33m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; cyan&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[0;35m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;    local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; magenta&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&#92;033[0;36m&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    printf&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;${&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;color&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;$content$default&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;printDefault&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; () {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    colorOutput&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;default&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#92;r&#92;n&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;printInfo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    colorOutput&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;cyan&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&#92;r&#92;n&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;backup-sh&quot; tabindex=&quot;-1&quot;&gt;&lt;code&gt;backup.sh&lt;/code&gt; &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#backup-sh&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The other &lt;em&gt;core&lt;/em&gt; script is obviously &lt;code&gt;backup.sh&lt;/code&gt; which creates the backup.
I didn&#39;t write all of this on my own. I think the basic structure is available in the Borg docs.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-dark github-dark&quot; style=&quot;background-color:#24292e;--shiki-dark-bg:#24292e;color:#e1e4e8;--shiki-dark:#e1e4e8&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;#!/bin/sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# source config.sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;dirname&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$0&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;&quot;)/config.sh&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;printSection&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;Starting backup&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Backup the most important directories into an archive named after&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# the machine this script is currently running on:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;borg&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                                &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --verbose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                              &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --filter&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; AME&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                           &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --list&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                                 &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --stats&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                                &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --show-rc&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                              &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --compression&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; lz4&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                      &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --exclude-caches&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                       &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --exclude&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;/Users/stefan/.cache/*&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;     &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                                           &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    ::&#39;{hostname}-{now}&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                   &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/iCloud/Documents/00-09 INBOX&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/iCloud/Documents/10-19 Personal-Documents&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/iCloud/Documents/20-29 Finances&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/iCloud/Notes&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;backup_exit=$?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# If available, copy the Borg Repositories from the ULDUAR drive (SSD)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# to the NAS. The &quot;Backups&quot; directory on the NAS is regularly&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# being backed up to different locations.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; [ &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;-d&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;/Volumes/NAS/Backups/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ]; &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    printInfo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;🟢 Copy Repository to NAS&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    rsync&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; --archive&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --protect-args&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --verbose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --recursive&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --exclude=.DS_Store&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --delete&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/Volumes/ULDUAR/Backups/Borg Repositories/&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt;    &#39;/Volumes/NAS/Backups/Borg Repositories/&#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;  printCaution&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;⚠️ NAS not available. Backup not mirrored to NAS.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# archives of THIS machine. The &#39;{hostname}-*&#39; matching is very important to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# limit prune&#39;s operation to this machine&#39;s archives and not apply to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# other machines&#39; archives also:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;printInfo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;🧹 Pruning repository&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;borg&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; prune&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                          &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --list&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                          &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --glob-archives&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &#39;{hostname}-*&#39;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;  &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --show-rc&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;                       &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --keep-daily&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    7&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;               &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --keep-weekly&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;   4&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;               &#92;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;    --keep-monthly&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;  6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;prune_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# actually free repo disk space by compacting segments&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;printInfo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;🗜️ Compacting repository&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;borg&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; compact&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;compact_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;$?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D;--shiki-dark:#6A737D&quot;&gt;# use highest exit code as global exit code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;global_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;$(( &lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;backup_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; prune_exit&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; backup_exit&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; prune_exit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;global_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt;$(( &lt;/span&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;compact_exit&lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; global_exit&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; compact_exit&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; global_exit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; [ ${global_exit} &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;-eq&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ]; &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    printSuccess&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;🟢 Backup, Prune, and Compact finished successfully&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;elif&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; [ ${global_exit} &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;-eq&lt;/span&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ]; &lt;/span&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    printCaution&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;⚠️ Backup, Prune, and/or Compact finished with warnings&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0;--shiki-dark:#B392F0&quot;&gt;    printError&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF;--shiki-dark:#9ECBFF&quot;&gt; &quot;🚨 Backup, Prune, and/or Compact finished with errors&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583;--shiki-dark:#F97583&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;--shiki-dark:#79B8FF&quot;&gt;exit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8;--shiki-dark:#E1E4E8&quot;&gt; ${global_exit}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;check-sh-list-sh-and-export-sh&quot; tabindex=&quot;-1&quot;&gt;&lt;code&gt;check.sh&lt;/code&gt;, &lt;code&gt;list.sh&lt;/code&gt; and &lt;code&gt;export.sh&lt;/code&gt; &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#check-sh-list-sh-and-export-sh&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are three other files in my script directory. &lt;code&gt;check.sh&lt;/code&gt; runs &lt;code&gt;borg check&lt;/code&gt; and checks the repository consistency. &lt;code&gt;list.sh&lt;/code&gt; runs &lt;code&gt;borg list&lt;/code&gt; to get a list of backups and &lt;code&gt;export.sh&lt;/code&gt; runs &lt;code&gt;borg export-tar&lt;/code&gt; to extract files from a backup into my &lt;code&gt;~/Downloads&lt;/code&gt; folder.&lt;/p&gt;
&lt;h2 id=&quot;running-the-backup-script&quot; tabindex=&quot;-1&quot;&gt;Running the Backup Script &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#running-the-backup-script&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Running the script is as easy as typing &lt;code&gt;sh ./backup.sh&lt;/code&gt; in a terminal of your choice.
In my case, 1Password will also prompt me to authenticate the request to access the encryption passphrase.&lt;/p&gt;
&lt;p&gt;I&#39;ve added a task to my weekly review project in &lt;a href=&quot;https://stefanzweifel.dev/posts/2022/12/18/my-updated-things-3-setup/&quot;&gt;Things&lt;/a&gt;, to remind me to create a backup.&lt;br&gt;
As I don&#39;t want to open a terminal, navigate to the right directory and run the script (or add an alias for all of this) I&#39;ve added a custom workflow to &lt;a href=&quot;https://stefanzweifel.dev/posts/2021/02/03/my-alfred-setup/&quot;&gt;Alfred&lt;/a&gt; to run the script for me.&lt;br&gt;
All I have to do now is open Alfred and type &amp;quot;borg:backup&amp;quot;.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fn5&quot; id=&quot;fnref5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-bigger-picture&quot; tabindex=&quot;-1&quot;&gt;The Bigger Picture &lt;a class=&quot;heading-permalink&quot; href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#the-bigger-picture&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Borg script and the storage on the USB-SSD is – however – only a small piece in a bigger backup strategy system.&lt;/p&gt;
&lt;p&gt;As you might have noticed in the &lt;code&gt;backup.sh&lt;/code&gt;-script above, the repository Borg creates is also copied to my NAS, if the drive is available. This way, the backups for &amp;quot;Important Docs&amp;quot; is located in 2 locations.&lt;/p&gt;
&lt;p&gt;And my NAS is then also independently backed up to 2 different location.
This illustrations explains my current backup strategy quite well.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://stefanzweifel.dev/assets/images/posts/20240525-backup-strategy/backup-strategy.svg&quot; loading=&quot;lazy&quot; alt=&quot;Diagram showing my backup strategy. It shows rectangles labeled MacBook Pro, iCloud, Google Drive Synology NAS, AWS S3 and USB-SDD. Arrows point to the different rectangles signifining which devices is backed up to which platform.&quot;&gt;
    &lt;figcaption&gt;As you can see, different data is stored on different devices and platforms and is backed up in different ways to different locations.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Another important  piece in my backup strategy are the yearly photo archives. At the end of each year, I export all photos taken during that year, zip them up and upload them into an AWS S3 bucket.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I&#39;ve been using this system for over a year now and I&#39;m quite happy with it. It has already happend that I needed to restore a specific file once or twice and the system held up.&lt;/p&gt;
&lt;p&gt;For now, I&#39;m quite happy with this system and I will use it for the foreseeable future.&lt;/p&gt;
&lt;hr class=&quot;footnotes-sep&quot;&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;You might ask yourself: &amp;quot;Stefan, why do you have 400 GB of data in iCloud Drive? What is all that stuff?&amp;quot;. Fair question. The biggest files are in itself backups/archives. Be it yearly archives of my photo library dating back to 1992, backups of old software I would like to keep or my music library. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn2&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&amp;quot;folder 10-29&amp;quot; refer to my &lt;a href=&quot;https://stefanzweifel.dev/posts/2023/09/16/an-opinionated-personal-folder-structure/&quot;&gt;Johnny Decimal folder structure&lt;/a&gt;. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fnref2&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn3&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;For example &lt;em&gt;Synology Hyper Backup&lt;/em&gt; is a backup software I use on my NAS. In case of an emergency (say my flat burns down and the NAS is destroyed), I would need to purchase another NAS in order to restore my files. In such a situation, purchasing a NAS and setting it up is not the first thing I want to do, just to access my files. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fnref3&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn4&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;Discovering Borg was quite the challenge as its SEO isn&#39;t great. It&#39;s headline isn&#39;t &amp;quot;backup software&amp;quot; but rather the nerdy term &amp;quot;deduplicating archiver with compression and encryption&amp;quot;. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fnref4&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn5&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;I want to give Raycast another try soon. Wonder if I could create a neat extension that would work with my &lt;code&gt;list.sh&lt;/code&gt; script. &lt;a href=&quot;https://stefanzweifel.dev/posts/2024/05/25/incremental-backups-with-borg-backup/#fnref5&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;

            &lt;p&gt;&lt;a href=&quot;mailto:stefan@stefanzweifel.dev?subject=RSS Reply - $this-&gt;title&quot;&gt;Reply to Stefan&lt;/a&gt;&lt;/p&gt;
        </content>
    </entry>
</feed>
