<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://freek.dev/feed</id>
                                <link href="https://freek.dev/feed" rel="self"></link>
                                <title><![CDATA[freek.dev - all blogposts]]></title>
                    
                                <subtitle>All blogposts on freek.dev</subtitle>
                                                    <updated>2026-04-14T12:30:26+02:00</updated>
                        <entry>
            <title><![CDATA[Why use static closures?]]></title>
            <link rel="alternate" href="https://freek.dev/3066-why-use-static-closures" />
            <id>https://freek.dev/3066</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A clear walkthrough of how PHP closures implicitly capture $this, even when they don't use it, and how that can prevent objects from being garbage collected. Also covers what PHP 8.6 will change with automatic static inference.</p>


<a href='https://f2r.github.io/en/static-closures'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-14T12:30:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Instant view switches with Inertia v3 prefetching]]></title>
            <link rel="alternate" href="https://freek.dev/3087-instant-view-switches-with-inertia-v3-prefetching" />
            <id>https://freek.dev/3087</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Over the past few months we've been building <a href="https://there-there.app">There There</a> at Spatie, a support tool shaped by the two decades we've spent running our own customer support. The goal is simple: the helpdesk we always wished we had.</p>
<p>We care about using AI in a particular way. It should help support agents write better replies, not substitute for them. The human stays in charge of the conversation, and the model does the unglamorous work of drafting, rephrasing, and suggesting links. There There is in private beta right now, and you can apply for early access at <a href="https://there-there.app">there-there.app</a>.</p>
<p>We're building There There with Laravel and Inertia, and we lean heavily on the latest features Inertia v3 brings. This post is about another one: prefetching on hover.</p>
<!--more-->
<h2 id="using-prefetch-on-hover">Using prefetch on hover</h2>
<p>In a helpdesk, a support agent flips between views constantly. My open tickets, unassigned, waiting, spam, team views, custom filters. Every switch triggers a fresh page load against a different filter. If each switch blocks on a round-trip, the sidebar starts to feel like dead weight.</p>
<p><a href="https://inertiajs.com/docs/v3/data-props/prefetching">Inertia v3 has a built-in prefetching API</a> that covers this neatly. Instead of waiting for the click, we fetch the view's data as soon as the cursor hovers over the link, and keep the result in a short-lived cache so the actual navigation feels instant.</p>
<p>Here's the core of our sidebar view link.</p>
<pre data-lang="js" class="notranslate">&lt;Link
    href={<span class="hl-property">buildViewUrl</span>(view)}
    prefetch=<span class="hl-value">&quot;hover&quot;</span>
    cacheFor=<span class="hl-value">&quot;30s&quot;</span>
    preserveState={isOnTickets}
    preserveScroll={isOnTickets}
&gt;
    &lt;Icon className=<span class="hl-value">&quot;size-4&quot;</span> /&gt;
    &lt;span&gt;{view.<span class="hl-property">label</span>}&lt;/span&gt;
&lt;/Link&gt;
</pre>
<p><code>prefetch=&quot;hover&quot;</code> is the whole mechanism. After the cursor sits on the link for 75ms, Inertia fires the underlying request in the background. When the agent actually clicks, the response is already in memory, and the page swaps without a network wait.</p>
<p><code>cacheFor</code> is where the real tuning happens. The default is 30 seconds. For our ticket views, that's a sweet spot: long enough that flipping back and forth feels free, short enough that the counts don't go visibly stale. You can push it higher for more static lists, or drop it to <code>&quot;0&quot;</code> for views where freshness matters more than speed.</p>
<p>Here it is in action. The cursor hovers, Inertia warms the cache in the background, and the click lands on data that's already there.</p>
<p><video src="https://freek.dev/admin-uploads/prefetch.mp4" autoplay loop muted playsinline style="max-width: 125%; margin-left: -12.5%;"></video></p>
<p>One small gotcha is worth flagging. When an agent navigates away from a view, you often want to cancel any in-flight requests so you don't paint stale data. The obvious call is <code>router.cancelAll()</code>, but that also cancels prefetches that are happily warming the cache for views the agent is about to visit.</p>
<p>Inertia v3 lets you scope that. Here's how we cancel the active visit without touching prefetches.</p>
<pre data-lang="js" class="notranslate">router.<span class="hl-property">cancelAll</span>({ <span class="hl-keyword">async</span>: <span class="hl-keyword">false</span>, <span class="hl-property">prefetch</span>: <span class="hl-keyword">false</span> });
</pre>
<p>The flags tell Inertia to keep background prefetches and async visits (like an infinite scroll) alive, while cancelling the normal page visit. The agent gets an immediate navigation and the sidebar stays primed for the next move.</p>
<h2 id="in-closing">In closing</h2>
<p>Prefetching on hover is one of those features that costs almost nothing to turn on and changes how the app feels. One prop, a cache duration, and you're done. The cancellation scoping is the one detail worth knowing about, because without it you'll undo your own optimisation the first time you navigate away from a view.</p>
<p>You can read more in the <a href="https://inertiajs.com/docs/v3/data-props/prefetching">Inertia v3 prefetching guide</a>. And if you'd like to try There There yourself, we're in private beta right now and you can apply for early access at <a href="https://there-there.app">there-there.app</a>.</p>
]]>
            </summary>
                                    <updated>2026-04-13T11:57:13+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ How we use Inertia v3 optimistic updates in There There]]></title>
            <link rel="alternate" href="https://freek.dev/3085-how-we-use-inertia-v3-optimistic-updates-in-there-there" />
            <id>https://freek.dev/3085</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A few months ago we started building <a href="https://there-there.app">There There</a>, a helpdesk we're making at Spatie. The premise is simple. After two decades of running customer support for our open source work and our SaaS apps, we wanted the tool we always wished existed.</p>
<p>One thing we care about in particular is using AI to help humans craft better responses, not to replace them. The agent stays in charge of the conversation. The model just helps them reply faster and a little sharper. There There is in private beta right now, and you can apply for early access at <a href="https://there-there.app">there-there.app</a>.</p>
<p>We're building There There with Laravel and Inertia, and we lean heavily on the latest features Inertia v3 brings. In this post I'd like to give one example: optimistic updates.</p>
<!--more-->
<h2 id="using-optimistic-updates">Using optimistic updates</h2>
<p>A support agent toggles a lot of small things in the course of a day. Adding a tag to a ticket, granting a teammate access to a channel, flipping a workflow on or off. Each of those is a single click that triggers a server round-trip.</p>
<p>If the UI waits for that round-trip before showing the change, the app feels sluggish. Not broken, just slow. And in a tool you live in all day, that compounds.</p>
<p><a href="https://inertiajs.com/docs/v3/the-basics/optimistic-updates">Inertia v3 ships with a first-class optimistic updates API</a> that handles this nicely. Instead of waiting on a response, we immediately update the UI to reflect the change and let Inertia roll it back if the request fails.</p>
<p>Here's how it looks on our member detail page. An admin can toggle whether a teammate belongs to a team. The list of teams comes in as a prop, and we render a switch next to each one.</p>
<pre data-lang="js" class="notranslate">{teams.<span class="hl-property">map</span>((team) =&gt; (
    &lt;div key={team.<span class="hl-property">id</span>}&gt;
        &lt;span&gt;{team.<span class="hl-property">name</span>}&lt;/span&gt;
        &lt;Switch
            checked={team.<span class="hl-property">is_member</span>}
            onCheckedChange={() =&gt; <span class="hl-property">handleToggleTeam</span>(team)}
        /&gt;
    &lt;/div&gt;
))}
</pre>
<p>When the switch is flipped, <code>handleToggleTeam</code> runs.</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">function</span> <span class="hl-property">handleToggleTeam</span>(team) {
    <span class="hl-keyword">const</span> optimistic = router.<span class="hl-property">optimistic</span>((props) =&gt; ({
        <span class="hl-property">teams</span>: props.<span class="hl-property">teams</span>.<span class="hl-property">map</span>((t) =&gt;
            t.<span class="hl-property">id</span> === team.<span class="hl-property">id</span> ? { ...<span class="hl-property">t</span>, <span class="hl-property">is_member</span>: !t.<span class="hl-property">is_member</span> } : t,
        ),
    }));

    <span class="hl-keyword">if</span> (team.<span class="hl-property">is_member</span>) {
        optimistic.<span class="hl-property">delete</span>(removeTeamMember.<span class="hl-property">url</span>({
            <span class="hl-property">team</span>: team.<span class="hl-property">ulid</span>,
            <span class="hl-property">user</span>: member.<span class="hl-property">ulid</span>,
        }), { <span class="hl-property">preserveScroll</span>: <span class="hl-keyword">true</span> });

        <span class="hl-keyword">return</span>;
    }

    optimistic.<span class="hl-property">post</span>(addTeamMember.<span class="hl-property">url</span>({
        <span class="hl-property">team</span>: team.<span class="hl-property">ulid</span>,
        <span class="hl-property">user</span>: member.<span class="hl-property">ulid</span>,
    }), {}, { <span class="hl-property">preserveScroll</span>: <span class="hl-keyword">true</span> });
}
</pre>
<p>The function we pass to <code>router.optimistic</code> receives the current props and returns the keys we want to patch. Inertia applies that patch immediately, so the toggle in the UI flips before the request leaves the browser. When the response comes back, Inertia merges the real server props on top.</p>
<p>I like the chained style here because the same optimistic patch can lead to either a <code>post</code> or a <code>delete</code>. We capture the optimistic builder once and pick the verb after.</p>
<p>Here it is in action. The toggle flips immediately, and the request happens in the background.</p>
<p><video src="https://freek.dev/admin-uploads/optimistic.mp4" autoplay loop muted playsinline style="max-width: 125%; margin-left: -12.5%;"></video></p>
<p>For simpler cases there's a second style. You pass <code>optimistic</code> straight as an option to the verb. Here's our workflow toggle:</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">function</span> <span class="hl-property">handleToggle</span>(workflow) {
    router.<span class="hl-property">patch</span>(workflowToggle.<span class="hl-property">url</span>(workflow.<span class="hl-property">ulid</span>), {}, {
        <span class="hl-property">optimistic</span>: (props) =&gt; ({
            <span class="hl-property">workflows</span>: props.<span class="hl-property">workflows</span>.<span class="hl-property">map</span>((w) =&gt;
                w.<span class="hl-property">id</span> === workflow.<span class="hl-property">id</span> ? { ...<span class="hl-property">w</span>, <span class="hl-property">is_enabled</span>: !w.<span class="hl-property">is_enabled</span> } : w,
            ),
        }),
    });
}
</pre>
<p>Same idea. You describe the patched shape, hand it to Inertia, and let it deal with the rest.</p>
<p>If the server returns a non-2xx response, Inertia reverts the optimistic change automatically. There's no manual restore code to write. Validation errors land where you'd expect, and only the keys you touched in the callback are snapshotted, so unrelated state stays untouched. Concurrent requests each carry their own snapshot, which means a slow request won't undo a faster one that already returned.</p>
<h2 id="in-closing">In closing</h2>
<p>Optimistic updates used to mean keeping a parallel copy of state, reverting it on rejection, and reasoning carefully about race conditions. With Inertia v3, you describe the next state and that's it. The whole interaction is about ten lines.</p>
<p>You can read the official documentation in the <a href="https://inertiajs.com/docs/v3/the-basics/optimistic-updates">Inertia v3 optimistic updates guide</a>. And if you'd like to try There There yourself, we're in private beta right now and you can apply for early access at <a href="https://there-there.app">there-there.app</a>.</p>
]]>
            </summary>
                                    <updated>2026-04-13T11:24:11+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Two Soups, Two Cookies]]></title>
            <link rel="alternate" href="https://freek.dev/3065-two-soups-two-cookies" />
            <id>https://freek.dev/3065</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A lovely analogy about software craft. Same ingredients, same features, but the invisible process behind the decisions is what separates &quot;this works&quot; from &quot;this feels right.&quot;</p>


<a href='https://liamhammett.com/two-soups-two-cookies'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-11T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Overcoming AI anxiety]]></title>
            <link rel="alternate" href="https://freek.dev/3063-overcoming-ai-anxiety" />
            <id>https://freek.dev/3063</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Ryan Chandler shares his honest journey from unease to acceptance with AI coding tools. A thoughtful reflection on how your value as a software engineer is not in writing every line, but knowing which lines should exist at all.</p>


<a href='https://ryangjchandler.co.uk/posts/overcoming-ai-anxiety'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-10T12:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Prove that you're human]]></title>
            <link rel="alternate" href="https://freek.dev/3078-prove-that-youre-human" />
            <id>https://freek.dev/3078</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A good reminder that trust matters more than ever when every week brings another AI-made product. The post argues that real faces, founder stories, and a visible reputation help both people and LLMs trust what you build.</p>


<a href='https://marketingfordevelopers.com/lessons/prove-that-youre-human'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-09T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Quantization from the ground up]]></title>
            <link rel="alternate" href="https://freek.dev/3061-quantization-from-the-ground-up" />
            <id>https://freek.dev/3061</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A thorough explainer on how quantization makes LLMs 4x smaller and 2x faster while losing only 5-10% accuracy. Covers floating point precision, compression techniques, and how to measure quality loss, with interactive examples throughout.</p>


<a href='https://ngrok.com/blog/quantization'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-08T12:41:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Designing with Claude Code]]></title>
            <link rel="alternate" href="https://freek.dev/3060-designing-with-claude-code" />
            <id>https://freek.dev/3060</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Steve Schoger shows how he uses Claude Code to design and build UIs, turning natural language prompts into polished interfaces.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/lkKGQVHrXzE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
]]>
            </summary>
                                    <updated>2026-04-07T12:17:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[My Take on Vibe Coding VS Agentic Engineering]]></title>
            <link rel="alternate" href="https://freek.dev/3059-my-take-on-vibe-coding-vs-agentic-engineering" />
            <id>https://freek.dev/3059</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>My thoughts on why Agentic Engineering is a better path than Vibe Coding, and the workflow I use to turn AI agents into a structured engineering process.</p>


<a href='https://wendelladriel.com/blog/my-take-on-vibe-coding-vs-agentic-engineering'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-06T14:33:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[SlideWire: Build Presentations with Livewire and Blade]]></title>
            <link rel="alternate" href="https://freek.dev/3056-slidewire-build-presentations-with-livewire-and-blade" />
            <id>https://freek.dev/3056</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>SlideWire is a Laravel package for building browser-based presentation decks using Livewire components and Blade templates. It comes with built-in navigation, transitions, syntax highlighting, and Mermaid diagrams.</p>


<a href='https://laravel-news.com/slidewire-build-presentations-with-livewire-and-blade'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-04T12:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Under the Hood: How Blaze Speeds Up Blade Templates]]></title>
            <link rel="alternate" href="https://freek.dev/3055-under-the-hood-how-blaze-speeds-up-blade-templates" />
            <id>https://freek.dev/3055</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A deep dive into how Blaze works internally. Matt Stauffer builds two toy versions from scratch to show how Blaze shifts Blade component rendering from runtime to compile time.</p>


<a href='https://tighten.com/insights/blaze-under-the-hood'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-03T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Context Anchoring]]></title>
            <link rel="alternate" href="https://freek.dev/3053-context-anchoring" />
            <id>https://freek.dev/3053</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Martin Fowler explores why AI coding sessions degrade over time and how externalizing decisions into structured documents keeps context reliable across sessions.</p>


<a href='https://martinfowler.com/articles/reduce-friction-ai/context-anchoring.html'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-02T12:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building Multi-Agent Workflows with the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3051-building-multi-agent-workflows-with-the-laravel-ai-sdk" />
            <id>https://freek.dev/3051</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>The Laravel blog walks through how to implement the five multi-agent patterns from Anthropic's &quot;Building Effective Agents&quot; research using the Laravel AI SDK. Prompt chaining, parallelization, routing, orchestrator-workers, and evaluator-optimizer loops, all built with just the agent() helper.</p>


<a href='https://laravel.com/blog/building-multi-agent-workflows-with-the-laravel-ai-sdk'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-01T14:30:05+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Agent responsibly]]></title>
            <link rel="alternate" href="https://freek.dev/3069-agent-responsibly" />
            <id>https://freek.dev/3069</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Vercel shares their internal framework for shipping agent-generated code safely. The core argument: green CI is no longer proof of safety, because agents produce code that looks flawless while remaining blind to production realities. The post outlines how to build systems where agents can act with high autonomy because deployment is safe by default.</p>


<a href='https://vercel.com/blog/agent-responsibly'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-01T12:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Zen of AI Coding]]></title>
            <link rel="alternate" href="https://freek.dev/3050-zen-of-ai-coding" />
            <id>https://freek.dev/3050</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A thoughtful collection of principles for working with coding agents, inspired by the Zen of Python. Covers how cheap code changes prioritization, why refactoring and repaying tech debt got easier, and why your role shifts from typing code to framing problems.</p>


<a href='https://nonstructured.com/zen-of-ai-coding/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-31T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introduction to Delegated Types]]></title>
            <link rel="alternate" href="https://freek.dev/3013-introduction-to-delegated-types" />
            <id>https://freek.dev/3013</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>This post explores what Delegated Types are, highlights their benefits, compares them to alternatives like Single-Table Inheritance (STI), and examines practical use cases they enable.</p>


<a href='https://tighten.com/insights/delegated-types/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-30T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Scotty: a beautiful SSH task runner]]></title>
            <link rel="alternate" href="https://freek.dev/3064-scotty-a-beautiful-ssh-task-runner" />
            <id>https://freek.dev/3064</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We just released <a href="https://github.com/spatie/scotty">Scotty</a>, a beautiful SSH task runner. It lets you define deploy scripts and other remote tasks, run them from your terminal, and watch every step as it happens. It supports both Laravel Envoy's Blade format and a new plain bash format.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-run-deploy.jpg?raw=true" alt="Deploy output" /></p>
<h2 id="why-we-built-scotty">Why we built Scotty</h2>
<p>Even though services like Laravel Cloud make it possible to never think about servers again, I still prefer deploying to my own servers for some projects. I know my way around them, I can pick whichever server provider I want, and I have full control over the environment.</p>
<p>Tools like <a href="https://forge.laravel.com">Laravel Forge</a> have built-in deploy functionality, and it works well. But I've always preferred running deploy scripts manually from my terminal. I want to see exactly what's happening while it runs, and when something goes wrong, I want to be staring at the output the moment it happens, not clicking through a web UI to find a log.</p>
<p>For years, I used <a href="https://laravel.com/docs/envoy">Laravel Envoy</a> for this. It does the job, but I wanted nicer output while tasks are running, and the ability to pause execution mid-deploy. Envoy uses a Blade-based file format, which works fine for us. But not everyone wants to write Blade syntax for their deploy scripts. So Scotty also offers a plain bash format for people who prefer that.</p>
<p>Scotty was built with the help of AI, using the <a href="https://github.com/laravel/envoy">Envoy codebase</a> as a source. Even though Scotty is a rebuild from scratch, in spirit it is a fork of Envoy. Credits to Laravel for providing the foundation. You can read the <a href="https://github.com/spatie/scotty#acknowledgements">full acknowledgement in the Scotty repo</a>.</p>
<h2 id="using-scotty">Using Scotty</h2>
<h3 id="defining-tasks-and-deploying">Defining tasks and deploying</h3>
<p>You define your tasks in a <code>Scotty.sh</code> file. It's just plain bash with some annotation comments. Your editor highlights it correctly, you get full shell support out of the box. Here's what that looks like:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-comment">#!/usr/bin/env scotty</span>

<span class="hl-comment"># @servers remote=deployer@your-server.com</span>

<span class="hl-comment"># @task on:remote</span>
deploy() {
    cd /var/www/my-app
    git pull origin main
    php artisan migrate --force
}
</pre>
<p>Deploy with a single command:</p>
<pre data-lang="bash" class="notranslate">scotty run deploy
</pre>
<p>Scotty connects to your server, runs each task in order, and shows you what's happening in real time. Each task displays its name, a step counter, elapsed time, and the command currently being executed. When everything finishes, you get a summary table with timing for each step.</p>
<p>These screenshots are from deploying this very blog. You can find the <a href="https://github.com/spatie/freek.dev/blob/main/Envoy.blade.php">deploy file on GitHub</a>.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-run-deploy.jpg?raw=true" alt="Deploy output" /></p>
<p>If a task fails, its output is shown and execution stops right there so you can investigate.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-failure.jpg?raw=true" alt="Task failure" /></p>
<h3 id="writing-plain-bash-instead-of-blade">Writing plain bash instead of Blade</h3>
<p>In addition to being able to read <code>Envoy.blade.php</code> files, Scotty introduces a new <code>Scotty.sh</code> format. Every line is real bash. Tasks are just bash functions with a <code># @task</code> annotation that tells Scotty which server to run them on. Macros group tasks into a sequence. Variables are plain bash variables, available everywhere.</p>
<p>Because it's all valid bash, you can use helper functions, computed values like <code>$(date +%Y%m%d-%H%M%S)</code>, and any shell construct you'd normally use. No Blade directives, no PHP <code>@setup</code> blocks, no <code>{{ $variable }}</code> interpolation.</p>
<p>You can also pass variables from the command line. Running <code>scotty run deploy --branch=develop</code> makes <code>$BRANCH</code> available in your tasks. The key is automatically uppercased.</p>
<p>You can list all available tasks and macros with <code>scotty tasks</code>:</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-tasks.jpg?raw=true" alt="scotty tasks" /></p>
<h3 id="pausing-resuming-and-pretending">Pausing, resuming, and pretending</h3>
<p>You can press <code>p</code> at any time during execution to pause after the current task finishes. Press <code>Enter</code> to resume, or <code>Ctrl+C</code> to cancel. This is handy when you want to check something on the server mid-deploy before continuing.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-pause.jpg?raw=true" alt="Pause and resume" /></p>
<p>There's also a pretend mode (<code>scotty run deploy --pretend</code>) that shows you exactly which SSH commands would be executed without actually running them. And a summary mode (<code>scotty run deploy --summary</code>) that hides task output and only shows results.</p>
<h3 id="validating-your-setup-with-doctor">Validating your setup with doctor</h3>
<p>Before your first deploy to a new server, you can run <code>scotty doctor</code> to validate your setup. It checks that your file parses correctly, verifies SSH connectivity to each server, and confirms that tools like <code>php</code>, <code>composer</code>, <code>node</code>, and <code>git</code> are available on the remote machine.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-doctor.jpg?raw=true" alt="scotty doctor" /></p>
<h3 id="migrating-from-envoy">Migrating from Envoy</h3>
<p>If you're already using Laravel Envoy, Scotty can read your <code>Envoy.blade.php</code> file directly. No changes needed. Just run <code>scotty run deploy</code> and it works. From there, you can gradually migrate to the <code>Scotty.sh</code> format if you want to, or keep using Blade.</p>
<p>If you're new to all of this, the <a href="https://spatie.be/docs/scotty/v1/basic-usage/your-first-deploy-script">Your first deploy script</a> page in the docs walks you through everything step by step.</p>
<h2 id="in-closing">In closing</h2>
<p>Scotty gives you a clean, modern way to run deploy scripts and other SSH tasks from your terminal. Plain bash, beautiful output, and full control over every step.</p>
<p>You can find <a href="https://spatie.be/docs/scotty/v1/introduction">the full documentation on our docs site</a> and the source code <a href="https://github.com/spatie/scotty">on GitHub</a>. This is one of the many tools and packages we've created at <a href="https://spatie.be">Spatie</a>. If you want to support our open source work, consider picking up one of <a href="https://spatie.be/products">our paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-03-30T09:44:16+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Generating OG images at the edge on Cloudflare]]></title>
            <link rel="alternate" href="https://freek.dev/3047-generating-og-images-at-the-edge-on-cloudflare" />
            <id>https://freek.dev/3047</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Matt Rothenberg walks through how to generate dynamic Open Graph images on Cloudflare Workers. A practical guide covering the full setup from rendering to caching.</p>


<a href='https://mattrothenberg.com/notes/edge-og-images/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-27T13:30:04+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Speed up your Livewire tests]]></title>
            <link rel="alternate" href="https://freek.dev/3038-speed-up-your-livewire-tests" />
            <id>https://freek.dev/3038</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael Dyrynda found that switching from <code>fill()</code> to <code>set()</code> in Livewire tests reduced his test suite from 22 seconds to 4 seconds. The difference: <code>fill</code> triggers a Livewire round-trip per field, while <code>set</code> batches them into one.</p>


<a href='https://dyrynda.com.au/blog/speed-up-your-livewire-tests'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-26T13:30:29+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Liminal - A full Laravel playground in your browser]]></title>
            <link rel="alternate" href="https://freek.dev/3021-liminal-a-full-laravel-playground-in-your-browser" />
            <id>https://freek.dev/3021</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A static, free, and open-source Laravel playground that runs entirely in the browser! Comes with a sqlite db, artisan commands, two-way file syncing, github imports, and more.</p>


<a href='https://liminal.aschmelyun.com'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-25T13:30:28+01:00</updated>
        </entry>
    </feed>
