<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>alexop.dev</title><description>A Personal Blog from a simple web developer</description><link>https://alexop.dev/</link><language>en</language><lastBuildDate>Thu, 07 May 2026 18:32:58 GMT</lastBuildDate><generator>Astro</generator><copyright>Copyright 2026 Alexander Opalic</copyright><ttl>60</ttl><atom:link href="https://alexop.dev/rss.xml" rel="self" type="application/rss+xml"/><image><url>https://alexop.dev/blog-og.jpg</url><title>alexop.dev</title><link>https://alexop.dev</link></image><webMaster>alex.opalic.dev@gmail.com (Alexander Opalic)</webMaster><managingEditor>alex.opalic.dev@gmail.com (Alexander Opalic)</managingEditor><item><title>How to Write UI Components That Stay Flexible</title><link>https://alexop.dev/posts/compound-components-in-vue-shadcn/</link><guid isPermaLink="true">https://alexop.dev/posts/compound-components-in-vue-shadcn/</guid><description>Most components break under their second or third variant — props pile up, shells get copy-pasted. Here&apos;s how compound components (the pattern behind Reka UI and shadcn-vue) keep a Dialog flexible without flag soup.</description><pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;A Dialog is a great test case for compound components because every product needs three or four versions of one: confirm, edit, share, settings. The naive paths both fail: copy a monolithic dialog into every variant and the shells drift; collapse them into one god component and the props turn into &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;showHeader&lt;/code&gt;, &lt;code&gt;showCancel&lt;/code&gt;, &lt;code&gt;confirmVariant&lt;/code&gt;, &lt;code&gt;headerCentered&lt;/code&gt;. The compound pattern Reka UI uses (and shadcn-vue ships) puts state into a &lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt; context and exposes small primitives (&lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DialogTrigger&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DialogHeader&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DialogFooter&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DialogClose&amp;gt;&lt;/code&gt;) that the consumer arranges into the variant they need. The tree expresses the variant; nothing inside the component branches on flags.&lt;/p&gt;
&lt;p&gt;This post grew out of Fernando Rojo’s talk &lt;a href=&quot;https://www.youtube.com/watch?v=4KvbVq3Eg5w&amp;amp;t=1194s&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Composition Is All You Need&lt;/a&gt;. Watch it first if you have 30 minutes. This post is the Vue translation, with Dialog as the running example.&lt;/p&gt;
&lt;p&gt;The whole pattern fits in one snippet: the tree shape is the variant (&lt;strong&gt;STRUCTURE&lt;/strong&gt;), &lt;code&gt;class=&quot;...&quot;&lt;/code&gt; overrides defaults (&lt;strong&gt;STYLE&lt;/strong&gt;), &lt;code&gt;data-state=&quot;...&quot;&lt;/code&gt; exposes lifecycle as a stylable contract (&lt;strong&gt;STATE&lt;/strong&gt;), and &lt;code&gt;as-child&lt;/code&gt; lets the consumer swap the rendered tag (&lt;strong&gt;ELEMENT&lt;/strong&gt;). The rest of the post is each lever, in detail.&lt;/p&gt;
&lt;h2&gt;See It Before You Read About It&lt;/h2&gt;
&lt;p&gt;Pick a preset. Toggle the children. The dialog on the left and the matching template on the right move together: same primitives, different trees, different variants.&lt;/p&gt;
&lt;p&gt;That is the pattern. One root, a few small children. The consumer arranges them, and the arrangement is the variant.&lt;/p&gt;
&lt;h2&gt;Smell #1: Duplicated Monoliths&lt;/h2&gt;
&lt;p&gt;Imagine you write a Dialog the first way most of us do: as one self-contained component. It opens, it closes, it has a header and a footer, you ship it. A week later product asks for an “edit profile” dialog. You copy the file, change the body to a form, ship it. A month later: a “share” dialog with a wider layout. Copy. A confirm-delete with a destructive action. Copy.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/compose&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;MonolithicConfirmDeleteDialog.vue&lt;/code&gt;&lt;/a&gt; shows one of those copies in practice:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- ANTIPATTERN: shell rebuilt from scratch in every variant. --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const open = ref(false)
const titleId = useId() ?? &apos;monolithic-confirm-title&apos;
const descriptionId = useId() ?? &apos;monolithic-confirm-description&apos;

function setOpen(value: boolean) { open.value = value }
function confirmDelete() { setOpen(false) }

function onKeyDown(e: KeyboardEvent) {
  if (open.value &amp;amp;&amp;amp; e.key === &apos;Escape&apos;) {
    e.preventDefault()
    setOpen(false)
  }
}

watch(open, (now, prev) =&amp;gt; {
  if (now &amp;amp;&amp;amp; !prev) {
    document.body.style.overflow = &apos;hidden&apos;
    window.addEventListener(&apos;keydown&apos;, onKeyDown)
  } else if (!now &amp;amp;&amp;amp; prev) {
    document.body.style.overflow = &apos;&apos;
    window.removeEventListener(&apos;keydown&apos;, onKeyDown)
  }
})

onUnmounted(() =&amp;gt; {
  document.body.style.overflow = &apos;&apos;
  window.removeEventListener(&apos;keydown&apos;, onKeyDown)
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;setOpen(true)&quot;&amp;gt;Delete account&amp;lt;/button&amp;gt;

  &amp;lt;Teleport to=&quot;body&quot;&amp;gt;
    &amp;lt;div v-if=&quot;open&quot; class=&quot;fixed inset-0 z-40&quot;&amp;gt;
      
      &amp;lt;div
        :aria-describedby=&quot;descriptionId&quot;
        :aria-labelledby=&quot;titleId&quot;
        aria-modal=&quot;true&quot;
        class=&quot;fixed top-1/2 left-1/2 ...&quot;
        role=&quot;dialog&quot;
        tabindex=&quot;-1&quot;
      &amp;gt;
        &amp;lt;div class=&quot;flex flex-col gap-2 text-center sm:text-left&quot;&amp;gt;
          &amp;lt;h2 :id=&quot;titleId&quot;&amp;gt;Are you absolutely sure?&amp;lt;/h2&amp;gt;
          &amp;lt;p :id=&quot;descriptionId&quot;&amp;gt;This will permanently delete your account.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- FOOTER: keep in sync with MonolithicProfileEditDialog.vue --&amp;gt;
        &amp;lt;div class=&quot;mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end&quot;&amp;gt;
          &amp;lt;button @click=&quot;setOpen(false)&quot;&amp;gt;Cancel&amp;lt;/button&amp;gt;
          &amp;lt;button @click=&quot;confirmDelete&quot;&amp;gt;Delete account&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/Teleport&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read that comment: &lt;code&gt;FOOTER: keep in sync with MonolithicProfileEditDialog.vue&lt;/code&gt;. That is the smell. Every variant rebuilds the same shell (the overlay, the escape handler, the scroll lock, the title/description ARIA wiring, the footer markup), and the only thing keeping them aligned is grep and discipline.&lt;/p&gt;
&lt;p&gt;You pay the cost every time something has to change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designer wants &lt;code&gt;gap-2&lt;/code&gt; to become &lt;code&gt;gap-6&lt;/code&gt; in every dialog footer? Edit N files.&lt;/li&gt;
&lt;li&gt;Accessibility audit asks for a focus trap? Add it to N files.&lt;/li&gt;
&lt;li&gt;Scroll lock turns out to need a body class instead of inline style? Touch N files.&lt;/li&gt;
&lt;li&gt;New variant (share dialog) appears? Copy the shell again, drift inevitable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each copy is a fork. Each fork ages on its own. Six months in, you can’t be sure the dialogs behave the same way, because they don’t.&lt;/p&gt;
&lt;h2&gt;Smell #2: One God Component With v-ifs&lt;/h2&gt;
&lt;p&gt;The obvious next move is to collapse the duplicates into one component. You make the body a slot, expose &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; as props, add &lt;code&gt;showCancel&lt;/code&gt; and &lt;code&gt;showConfirm&lt;/code&gt; for the footer. It feels clean for one afternoon. Then product asks for a destructive variant; you add &lt;code&gt;confirmVariant&lt;/code&gt;. Then a share dialog needs no description; you add &lt;code&gt;showDescription&lt;/code&gt;. Then someone wants a centered header; &lt;code&gt;headerCentered&lt;/code&gt;. You add a flag for every variant you cram into the component.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/compose&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;BooleanPropDialog.vue&lt;/code&gt;&lt;/a&gt; shows the version that grew out of covering two real call sites (confirm-delete and edit-profile) with one component:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- ANTIPATTERN: boolean prop sprawl. --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
type Mode = &apos;confirm&apos; | &apos;edit&apos;
type Variant = &apos;primary&apos; | &apos;destructive&apos;

const props = withDefaults(
  defineProps&amp;lt;{
    mode?: Mode
    title: string
    description?: string
    showHeader?: boolean
    showDescription?: boolean
    showFooter?: boolean
    showCancel?: boolean
    showConfirm?: boolean
    showSecondary?: boolean
    showCloseIcon?: boolean
    cancelLabel?: string
    confirmLabel?: string
    secondaryLabel?: string
    confirmVariant?: Variant
    triggerLabel: string
    triggerVariant?: Variant
    headerCentered?: boolean
    disableConfirm?: boolean
    editName?: string
    editBio?: string
  }&amp;gt;(),
  { /* …a wall of defaults… */ }
)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button v-if=&quot;props.triggerVariant === &apos;destructive&apos;&quot; ...&amp;gt;{{ props.triggerLabel }}&amp;lt;/button&amp;gt;
  &amp;lt;button v-else ...&amp;gt;{{ props.triggerLabel }}&amp;lt;/button&amp;gt;

  &amp;lt;Teleport to=&quot;body&quot;&amp;gt;
    &amp;lt;div v-if=&quot;open&quot; ...&amp;gt;
      
      &amp;lt;div role=&quot;dialog&quot; ...&amp;gt;
        &amp;lt;button v-if=&quot;props.showCloseIcon&quot; ...&amp;gt;×&amp;lt;/button&amp;gt;

        &amp;lt;div v-if=&quot;props.showHeader&quot; :class=&quot;[
          &apos;flex flex-col gap-2&apos;,
          props.headerCentered ? &apos;text-center&apos; : &apos;text-left&apos;,
        ]&quot;&amp;gt;
          &amp;lt;h2&amp;gt;{{ props.title }}&amp;lt;/h2&amp;gt;
          &amp;lt;p v-if=&quot;props.description &amp;amp;&amp;amp; props.showDescription&quot;&amp;gt;{{ props.description }}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div v-if=&quot;props.mode === &apos;edit&apos;&quot; class=&quot;mt-4 flex flex-col gap-3&quot;&amp;gt;
          &amp;lt;!-- inline form, two-way bound through update:editName / update:editBio --&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div v-if=&quot;props.showFooter&quot; class=&quot;mt-6 ...&quot;&amp;gt;
          &amp;lt;button v-if=&quot;props.showSecondary &amp;amp;&amp;amp; props.secondaryLabel&quot; ...&amp;gt;{{ props.secondaryLabel }}&amp;lt;/button&amp;gt;
          &amp;lt;button v-if=&quot;props.showCancel&quot; ...&amp;gt;{{ props.cancelLabel }}&amp;lt;/button&amp;gt;
          &amp;lt;button v-if=&quot;props.showConfirm &amp;amp;&amp;amp; props.confirmVariant === &apos;destructive&apos;&quot; ...&amp;gt;{{ props.confirmLabel }}&amp;lt;/button&amp;gt;
          &amp;lt;button v-else-if=&quot;props.showConfirm&quot; ...&amp;gt;{{ props.confirmLabel }}&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/Teleport&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call site is the giveaway:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;BooleanPropDialog
  cancel-label=&quot;Cancel&quot;
  confirm-label=&quot;Delete account&quot;
  confirm-variant=&quot;destructive&quot;
  description=&quot;This will permanently delete your account...&quot;
  mode=&quot;confirm&quot;
  :show-cancel=&quot;true&quot;
  :show-confirm=&quot;true&quot;
  title=&quot;Are you absolutely sure?&quot;
  trigger-label=&quot;Delete account&quot;
  trigger-variant=&quot;destructive&quot;
  @confirm=&quot;confirmDelete&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Twelve flags to describe one variant. The edit version needs &lt;code&gt;mode=&quot;edit&quot;&lt;/code&gt;, then plumbs the form’s two-way binding through &lt;code&gt;update:editName&lt;/code&gt; and &lt;code&gt;update:editBio&lt;/code&gt; because the inputs live inside the dialog the consumer can’t reach into.&lt;/p&gt;
&lt;p&gt;Fernando Rojo named this smell in his talk:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“If you have a boolean prop that determines which component tree is getting rendered from the parent, you can kind of imagine me looking over your shoulder and shaking my head.”&lt;/p&gt;
&lt;p&gt;— Fernando Rojo, &lt;em&gt;Composition Is All You Need&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The diagnostic: when a flag changes &lt;em&gt;what&lt;/em&gt; renders rather than &lt;em&gt;how&lt;/em&gt;, lift it into a child component. &lt;code&gt;confirmVariant=&quot;destructive&quot;&lt;/code&gt; is a “how” prop and is fine. &lt;code&gt;mode=&quot;edit&quot;&lt;/code&gt; is a “what” prop. It switches the component tree, so lift it into the tree the consumer controls.&lt;/p&gt;
&lt;h2&gt;The Solution: Compose Trees, Don’t Configure Them&lt;/h2&gt;
&lt;p&gt;The compound component pattern flips the relationship. You ship N small components that share state through a provider. The consumer assembles only the parts they need:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Delete account&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Are you absolutely sure?&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;This cannot be undone.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Cancel&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;bg-red-600 ...&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;@click&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;confirmDelete&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Delete&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A share dialog with no description? Drop &lt;code&gt;&amp;lt;DialogDescription&amp;gt;&lt;/code&gt;. An edit dialog with a form? Put the form inside &lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt;. The inputs are in the consumer’s template, so they bind to the consumer’s state with &lt;code&gt;v-model&lt;/code&gt;. A settings menu where the trigger is a list-row? Pass &lt;code&gt;as-child&lt;/code&gt; to &lt;code&gt;&amp;lt;DialogTrigger&amp;gt;&lt;/code&gt;. No flags inside the component, no &lt;code&gt;update:editName&lt;/code&gt; plumbing across the boundary.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Where are the booleans and the special props telling us what to render? They’re nowhere to be found. We don’t have a monolith — we have shared internals that get reimplemented for each use case.”&lt;/p&gt;
&lt;p&gt;— Fernando Rojo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is how Reka UI is built and how shadcn-vue ships every primitive on top of it.&lt;/p&gt;
&lt;h2&gt;Reading shadcn-vue: The Dialog Primitive&lt;/h2&gt;
&lt;p&gt;Installing the Dialog from shadcn-vue gives you a barrel of small files, not one &lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt; component:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// ui/dialog/index.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; Dialog &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./Dialog.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogClose &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogClose.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogContent &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogContent.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogDescription &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogDescription.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogFooter &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogFooter.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogHeader &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogHeader.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogOverlay &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogOverlay.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogTitle &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogTitle.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; DialogTrigger &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./DialogTrigger.vue&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nine components, each doing one thing. The root that owns the state, &lt;code&gt;Dialog.vue&lt;/code&gt;, looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const props = defineProps&amp;lt;DialogRootProps&amp;gt;();
const emits = defineEmits&amp;lt;DialogRootEmits&amp;gt;();

const forwarded = useForwardPropsEmits(props, emits);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;DialogRoot data-slot=&quot;dialog&quot; v-bind=&quot;forwarded&quot;&amp;gt;
    
  &amp;lt;/DialogRoot&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The root renders no markup of its own. It forwards into Reka UI’s &lt;code&gt;DialogRoot&lt;/code&gt;, which establishes the context. The open/closed state lives there and children read it via &lt;code&gt;inject&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DialogHeader&lt;/code&gt; is a styled wrapper with no logic:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const props = defineProps&amp;lt;{ class?: HTMLAttributes[&quot;class&quot;] }&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div
    data-slot=&quot;dialog-header&quot;
    :class=&quot;cn(&apos;flex flex-col gap-2 text-center sm:text-left&apos;, props.class)&quot;
  &amp;gt;
    
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trigger is a few lines too. It forwards to Reka’s &lt;code&gt;DialogTrigger&lt;/code&gt;, which calls &lt;code&gt;setOpen(true)&lt;/code&gt; and wires up the ARIA attributes. No DOM owned by shadcn-vue, only styling and the &lt;code&gt;data-slot&lt;/code&gt; for theming.&lt;/p&gt;
&lt;p&gt;Compare the call site (the compound version we saw above) to the boolean-prop monolith: same UX, twelve flags fewer, and when a designer asks for a checkbox in the footer or a dialog with no description, you rearrange the tree to fit.&lt;/p&gt;
&lt;h2&gt;A Quick Detour: How &lt;code&gt;provide&lt;/code&gt; and &lt;code&gt;inject&lt;/code&gt; Work&lt;/h2&gt;
&lt;p&gt;Compound components rely on Vue’s &lt;a href=&quot;https://vuejs.org/guide/components/provide-inject.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;provide&lt;/code&gt; and &lt;code&gt;inject&lt;/code&gt;&lt;/a&gt; API. The model matches React Context: a parent publishes a value under a key, and any descendant in its subtree reads that value without the components in between knowing about it.&lt;/p&gt;
&lt;p&gt;The minimum example is two components. Click the button — the child mutates the shared ref, the parent re-renders. The same &lt;code&gt;open&lt;/code&gt; lives in both:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const open = ref(false);
provide(&quot;dialog-open&quot;, open);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;p&amp;gt;Parent sees: open = &amp;lt;strong&amp;gt;{{ open }}&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;
  
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const open = inject(&quot;dialog-open&quot;);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;open = !open&quot;&amp;gt;
    Toggle from child (now {{ open }})
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Vue docs frame this as the answer to &lt;em&gt;prop drilling&lt;/em&gt;: passing the same prop through three intermediate components that do not care about it, only to reach the one that does. With &lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt;, the descendant reads from the closest matching provider in the tree, however many components deep it is.&lt;/p&gt;
&lt;p&gt;Toggle the root state below. On the left, every component on the path carries &lt;code&gt;open&lt;/code&gt; as a prop. On the right, only the provider and the consumers know about it. &lt;code&gt;&amp;lt;Header&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Body&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;Footer&amp;gt;&lt;/code&gt; are unchanged.&lt;/p&gt;
&lt;p&gt;Three properties of the API matter for &lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reactivity passes through.&lt;/strong&gt; Provide a &lt;code&gt;ref&lt;/code&gt; and the descendant gets the same &lt;code&gt;ref&lt;/code&gt;. This is what lets &lt;code&gt;&amp;lt;DialogClose&amp;gt;&lt;/code&gt; flip &lt;code&gt;open&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt; react with a transition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String keys are untyped.&lt;/strong&gt; &lt;code&gt;inject(&quot;dialog-open&quot;)&lt;/code&gt; returns &lt;code&gt;unknown&lt;/code&gt;. Vue ships &lt;a href=&quot;https://vuejs.org/guide/typescript/composition-api.html#typing-provide-inject&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;InjectionKey&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt;, a typed &lt;code&gt;Symbol&lt;/code&gt;, so the injected type matches the provided type at the call site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing providers fail without warning.&lt;/strong&gt; &lt;code&gt;inject(&quot;missing&quot;)&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt; and the component renders broken with no compile-time error. Robust compound APIs wrap &lt;code&gt;inject&lt;/code&gt; in a function that throws when the provider is absent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vue also lets you pass &lt;a href=&quot;https://vuejs.org/api/reactivity-core.html#readonly&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;readonly()&lt;/code&gt;&lt;/a&gt; around a provided ref so descendants can read but not mutate. Dialog skips this because &lt;code&gt;&amp;lt;DialogClose&amp;gt;&lt;/code&gt; &lt;em&gt;should&lt;/em&gt; mutate, but &lt;code&gt;readonly&lt;/code&gt; fits when only the provider holds the write path.&lt;/p&gt;
&lt;h2&gt;Building Your Own: A Dialog Component&lt;/h2&gt;
&lt;p&gt;Let’s build the compound Dialog from scratch. This is the part you will reuse on your own components.&lt;/p&gt;
&lt;h3&gt;Step 1: The provider owns state&lt;/h3&gt;
&lt;p&gt;Start with a composable that defines the shared interface. This is your contract:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// composables/useDialog.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;DialogContext&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  open&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;setOpen&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  triggerRef&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;HTMLElement &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  contentId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  titleId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  descriptionId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; DialogKey&lt;span&gt;:&lt;/span&gt; InjectionKey&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContext&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Symbol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dialog&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;provideDialog&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx&lt;span&gt;:&lt;/span&gt; DialogContext&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;provide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;DialogKey&lt;span&gt;,&lt;/span&gt; ctx&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useDialog&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; DialogContext &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; ctx &lt;span&gt;=&lt;/span&gt; &lt;span&gt;inject&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;DialogKey&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;ctx&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;useDialog must be used inside &amp;lt;Dialog&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; ctx&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;InjectionKey&lt;/code&gt; is typed, so children get full type safety on the context. &lt;code&gt;useDialog&lt;/code&gt; throws when used outside the provider, which prevents silent bugs in consumer code.&lt;/p&gt;
&lt;h4&gt;If you use VueUse, there is a helper for this&lt;/h4&gt;
&lt;p&gt;The compound pattern is two halves: the root &lt;em&gt;provides&lt;/em&gt;, the leaves &lt;em&gt;inject&lt;/em&gt;. VueUse’s &lt;a href=&quot;https://vueuse.org/shared/createInjectionState/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;createInjectionState&lt;/code&gt;&lt;/a&gt; is shaped exactly around that — it takes a setup function and returns a &lt;code&gt;[useProvide, useInject]&lt;/code&gt; tuple. The same &lt;code&gt;useDialog.ts&lt;/code&gt; becomes:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// composables/useDialog.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;useProvideDialog&lt;span&gt;,&lt;/span&gt; useDialogRaw&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createInjectionState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; open &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; triggerRef &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;HTMLElement &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; contentId &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;&quot;dialog-content&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; titleId &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;&quot;dialog-title&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; descriptionId &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;&quot;dialog-description&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;setOpen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    open&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; open&lt;span&gt;,&lt;/span&gt; setOpen&lt;span&gt;,&lt;/span&gt; triggerRef&lt;span&gt;,&lt;/span&gt; contentId&lt;span&gt;,&lt;/span&gt; titleId&lt;span&gt;,&lt;/span&gt; descriptionId &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; useProvideDialog &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useDialog&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; ctx &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useDialogRaw&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;ctx&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;useDialog must be used inside &amp;lt;Dialog&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; ctx&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The state and the ARIA ids now live inside the setup function, so the root’s &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; collapses to a single &lt;code&gt;useProvideDialog()&lt;/code&gt; call — no manually defined &lt;code&gt;InjectionKey&lt;/code&gt;, no separate &lt;code&gt;provideDialog(ctx)&lt;/code&gt; wiring. The leaves still get a typed &lt;code&gt;useDialog()&lt;/code&gt; that throws when used outside the provider. (&lt;code&gt;createInjectionState&lt;/code&gt; also accepts a &lt;code&gt;defaultValue&lt;/code&gt; option that returns a fallback instead of &lt;code&gt;undefined&lt;/code&gt;; for compound components you usually want the throw, since a missing provider is a real bug, not a graceful-degradation case.)&lt;/p&gt;
&lt;p&gt;Two payoffs beyond saved boilerplate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You can unit-test the state machine without mounting a component.&lt;/strong&gt; With the manual version the state sits in &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; of &lt;code&gt;Dialog.vue&lt;/code&gt;, so to assert “&lt;code&gt;setOpen(false)&lt;/code&gt; flips &lt;code&gt;open&lt;/code&gt;” you have to mount the SFC. The setup function passed to &lt;code&gt;createInjectionState&lt;/code&gt; is a plain function. Call it in a test, assert on the returned refs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The root SFC becomes near-empty and swappable.&lt;/strong&gt; It is ``. You can wrap the root in a Teleport, or ship a second root variant that provides the same state, since the root holds nothing to copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this section uses the manual version because it shows the raw &lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt; shape with no library in the way. Reach for &lt;code&gt;createInjectionState&lt;/code&gt; once you have a few compound components in the codebase, or once you want to test the state machine in isolation.&lt;/p&gt;
&lt;h3&gt;Step 2: The root injects, doesn’t render&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Dialog.vue&lt;/code&gt; root creates the state, generates ARIA ids, and provides the bundle. It renders no markup of its own. It is a logical container:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- Dialog.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const open = defineModel&amp;lt;boolean&amp;gt;(&quot;open&quot;, { default: false });
const triggerRef = ref&amp;lt;HTMLElement | null&amp;gt;(null);

const contentId = useId() ?? &quot;dialog-content&quot;;
const titleId = useId() ?? &quot;dialog-title&quot;;
const descriptionId = useId() ?? &quot;dialog-description&quot;;

function setOpen(value: boolean) {
  open.value = value;
}

provideDialog({ open, setOpen, triggerRef, contentId, titleId, descriptionId });
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;defineModel&lt;/code&gt; exposes &lt;code&gt;v-model:open&lt;/code&gt; so the consumer can read or control the open state from outside. &lt;code&gt;useId&lt;/code&gt; gives stable IDs for the title/description so &lt;code&gt;&amp;lt;DialogTitle&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;DialogDescription&amp;gt;&lt;/code&gt; can publish them and &lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt; can reference them via &lt;code&gt;aria-labelledby&lt;/code&gt; / &lt;code&gt;aria-describedby&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Step 3: Each child is small and focused&lt;/h3&gt;
&lt;p&gt;Children only know about the slice of context they need.&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- DialogTrigger.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { open, setOpen, triggerRef, contentId } = useDialog();

function handleClick(e: MouseEvent) {
  triggerRef.value = e.currentTarget as HTMLElement;
  setOpen(true);
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    :aria-controls=&quot;contentId&quot;
    :aria-expanded=&quot;open&quot;
    :data-state=&quot;open ? &apos;open&apos; : &apos;closed&apos;&quot;
    @click=&quot;handleClick&quot;
  &amp;gt;
    
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- DialogClose.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { setOpen } = useDialog();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button type=&quot;button&quot; @click=&quot;setOpen(false)&quot;&amp;gt;
    &amp;lt;slot&amp;gt;Cancel&amp;lt;/slot&amp;gt;
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- DialogTitle.vue — publishes the id to satisfy aria-labelledby --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { titleId } = useDialog();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;/h2&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt; is the heaviest leaf because it owns the open/close transition, focus management, scroll lock, and the escape handler. But all of those used to be duplicated across every monolithic dialog. Lifting them into one place is the entire point. When accessibility audits ask for a focus trap, you change one file:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- DialogContent.vue (focus + escape, abridged) --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { open, setOpen, triggerRef, contentId, titleId, descriptionId } = useDialog();
const contentRef = ref&amp;lt;HTMLElement | null&amp;gt;(null);

function onKeyDown(e: KeyboardEvent) {
  if (open.value &amp;amp;&amp;amp; e.key === &quot;Escape&quot;) {
    e.preventDefault();
    setOpen(false);
  }
}

watch(open, async (now, prev) =&amp;gt; {
  if (now &amp;amp;&amp;amp; !prev) {
    document.body.style.overflow = &quot;hidden&quot;;
    window.addEventListener(&quot;keydown&quot;, onKeyDown);
    await nextTick();
    contentRef.value?.querySelector&amp;lt;HTMLElement&amp;gt;(&quot;button, input, [tabindex]&quot;)?.focus();
  } else if (!now &amp;amp;&amp;amp; prev) {
    document.body.style.overflow = &quot;&quot;;
    window.removeEventListener(&quot;keydown&quot;, onKeyDown);
    triggerRef.value?.focus();
  }
});

onUnmounted(() =&amp;gt; {
  document.body.style.overflow = &quot;&quot;;
  window.removeEventListener(&quot;keydown&quot;, onKeyDown);
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;Teleport to=&quot;body&quot;&amp;gt;
    &amp;lt;Transition&amp;gt;
      &amp;lt;div v-if=&quot;open&quot; class=&quot;fixed inset-0 z-40&quot;&amp;gt;
        
        &amp;lt;div
          :id=&quot;contentId&quot;
          ref=&quot;contentRef&quot;
          :aria-describedby=&quot;descriptionId&quot;
          :aria-labelledby=&quot;titleId&quot;
          aria-modal=&quot;true&quot;
          role=&quot;dialog&quot;
          tabindex=&quot;-1&quot;
        &amp;gt;
          
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/Transition&amp;gt;
  &amp;lt;/Teleport&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The work that lived inside every monolithic dialog now lives in one component. Consumers compose instead of copy.&lt;/p&gt;
&lt;h3&gt;Step 4: Compose distinct variants&lt;/h3&gt;
&lt;p&gt;Now the same primitives produce every variant. Confirm delete:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTrigger&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;bg-red-600 ...&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Delete account&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Are you absolutely sure?&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;This cannot be undone.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Cancel&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;bg-red-600 ...&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;@click&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;confirmDelete&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Delete account&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Edit profile, with the form sitting in the consumer’s template so &lt;code&gt;v-model&lt;/code&gt; binds to the consumer’s state without crossing the dialog boundary:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Edit profile&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Edit profile&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Make changes here. Click save when you&apos;re done.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogDescription&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span&gt;@submit.prevent&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;save&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Cancel&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;submit&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Save changes&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice what’s gone: no &lt;code&gt;update:editName&lt;/code&gt; plumbing, no &lt;code&gt;mode=&quot;edit&quot;&lt;/code&gt; flag, no shell rebuilt. The form is the consumer’s template; the dialog scaffolding is the primitive.&lt;/p&gt;
&lt;p&gt;Share dialog, wider content, icon close instead of a button. Same primitives, different tree:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Share project&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTrigger&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContent&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;max-w-2xl&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Share &quot;Project Phoenix&quot;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogTitle&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogHeader&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogClose&lt;/span&gt; &lt;span&gt;as-child&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;absolute top-4 right-4&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;aria-label&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;Close&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;✕&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogClose&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;bg-zinc-900 text-white&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Done&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogClose&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogFooter&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;DialogContent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;Dialog&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The whole thing, running&lt;/h3&gt;
&lt;p&gt;Five small files. Click through the tabs to see the implementation, then click the trigger. The provider lives in &lt;code&gt;Dialog.vue&lt;/code&gt;; trigger and close call &lt;code&gt;setOpen&lt;/code&gt; on the same context; content reads &lt;code&gt;open&lt;/code&gt; to render.&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
function confirmDelete() {
  alert(&quot;Account deleted!&quot;);
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;Dialog&amp;gt;
    &amp;lt;DialogTrigger&amp;gt;Delete account&amp;lt;/DialogTrigger&amp;gt;
    &amp;lt;DialogContent&amp;gt;
      &amp;lt;h2&amp;gt;Are you absolutely sure?&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;This will permanently delete your account.&amp;lt;/p&amp;gt;
      &amp;lt;div class=&quot;footer&quot;&amp;gt;
        &amp;lt;DialogClose&amp;gt;Cancel&amp;lt;/DialogClose&amp;gt;
        &amp;lt;button class=&quot;danger&quot; @click=&quot;confirmDelete&quot;&amp;gt;
          Delete account
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/DialogContent&amp;gt;
  &amp;lt;/Dialog&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.footer {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin-top: 1.25rem;
}
.danger {
  background: #dc2626;
  color: white;
  border-color: #dc2626;
}
h2 {
  margin: 0 0 0.5rem;
  font-size: 1.05rem;
  font-weight: 600;
}
p {
  margin: 0;
  opacity: 0.8;
  font-size: 0.9rem;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const open = ref(false);

provide(&quot;dialog&quot;, {
  open,
  setOpen: (value) =&amp;gt; {
    open.value = value;
  },
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const { open, setOpen } = inject(&quot;dialog&quot;);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    :aria-expanded=&quot;open&quot;
    :data-state=&quot;open ? &apos;open&apos; : &apos;closed&apos;&quot;
    @click=&quot;setOpen(true)&quot;
  &amp;gt;
    
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const { open, setOpen } = inject(&quot;dialog&quot;);

function onKeyDown(e) {
  if (e.key === &quot;Escape&quot;) setOpen(false);
}

watch(open, (now) =&amp;gt; {
  if (now) window.addEventListener(&quot;keydown&quot;, onKeyDown);
  else window.removeEventListener(&quot;keydown&quot;, onKeyDown);
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;Teleport to=&quot;body&quot;&amp;gt;
    &amp;lt;div v-if=&quot;open&quot; class=&quot;overlay&quot; @click.self=&quot;setOpen(false)&quot;&amp;gt;
      &amp;lt;div class=&quot;content&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot;&amp;gt;
        
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/Teleport&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
.content {
  background: rgb(24, 24, 36);
  color: rgb(228, 228, 235);
  border: 1px solid rgba(255, 255, 255, 0.08);
  padding: 1.5rem;
  border-radius: 0.5rem;
  max-width: 22rem;
  width: 90%;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const { setOpen } = inject(&quot;dialog&quot;);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button type=&quot;button&quot; @click=&quot;setOpen(false)&quot;&amp;gt;
    &amp;lt;slot&amp;gt;Cancel&amp;lt;/slot&amp;gt;
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No flag inside any file decides what gets rendered. The variant is the tree in &lt;code&gt;App.vue&lt;/code&gt;. Swap &lt;code&gt;&amp;lt;DialogContent&amp;gt;&lt;/code&gt;&apos;s children for a form and you have edit-profile; swap them for a share sheet and you have share. Same primitives, same provider, different tree.&lt;/p&gt;
&lt;h2&gt;State Lives in the Provider, Not the Layout&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;“If there’s one thing to take away from this talk, it would be this. I’ve solved so many problems in my React code bases by simply lifting state higher up in the tree.”&lt;/p&gt;
&lt;p&gt;— Fernando Rojo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The share example tucked an icon &lt;code&gt;&amp;lt;DialogClose&amp;gt;&lt;/code&gt; outside the footer, absolutely positioned in the top-right corner. It still closed the same dialog because the open state lives in the provider, not in any specific child. Slots alone cannot reach across siblings like that; a provider can.&lt;/p&gt;
&lt;p&gt;Open the dialog below, then move the cancel button between the footer and a toolbar that sits next to the dialog. Both consumers call &lt;code&gt;setOpen(false)&lt;/code&gt; on the same provider. The visual layout and the state graph are decoupled.&lt;/p&gt;
&lt;p&gt;In a slot-based design without a provider you would have to do one of these by hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hoist the open state up to the parent: boilerplate, repeated per variant&lt;/li&gt;
&lt;li&gt;Pass an &lt;code&gt;onClose&lt;/code&gt; callback through every layer: prop drilling&lt;/li&gt;
&lt;li&gt;Use a scoped slot to pass state to a child: solves the &lt;em&gt;direct child&lt;/em&gt; case, then collapses the moment a sibling outside the visual frame needs the same state&lt;/li&gt;
&lt;li&gt;Reach into the dialog with a template ref: escape hatch, untyped, brittle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt;, none of that exists. A child calls &lt;code&gt;useDialog()&lt;/code&gt; and the closest provider answers, no matter how deep in the tree or how far apart the children sit in the layout. “Lift state to the provider” means lifting it out of the layout. The same state can then be read from any DOM position the consumer wants.&lt;/p&gt;
&lt;h2&gt;Styling Without Locking Consumers In: &lt;code&gt;cn()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;That covers structure. The consumer composes the tree, the provider keeps state in sync no matter how the children are laid out. Styling is the second axis. The primitive ships sensible defaults: &lt;code&gt;flex flex-col gap-2&lt;/code&gt; on a header, &lt;code&gt;max-w-md&lt;/code&gt; on the content. Every real call site eventually wants to bend one of them: a wider share dialog, a tighter footer, a destructive button that breaks the default palette. The consumer needs a way in without forking the file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cn(...)&lt;/code&gt; is how shadcn-vue solves that. It is a small utility every shadcn project writes itself, by convention living in &lt;code&gt;src/lib/utils.ts&lt;/code&gt; (not an npm package).&lt;/p&gt;
&lt;p&gt;The implementation is four lines:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// lib/utils.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;cn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;inputs&lt;span&gt;:&lt;/span&gt; ClassValue&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;twMerge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;clsx&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputs&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;clsx&lt;/code&gt; joins truthy class values into a string. &lt;code&gt;tailwind-merge&lt;/code&gt; deduplicates conflicting Tailwind utilities so the &lt;em&gt;last&lt;/em&gt; one wins, so &lt;code&gt;p-2&lt;/code&gt; followed by &lt;code&gt;p-4&lt;/code&gt; collapses to &lt;code&gt;p-4&lt;/code&gt;. Together, whatever the consumer passes overrides whatever the primitive sets, with no specificity wars.&lt;/p&gt;
&lt;p&gt;In every shadcn-vue primitive it shows up as the same one-liner. From &lt;code&gt;DialogHeader&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;div :class=&quot;cn(&apos;flex flex-col gap-2 text-center sm:text-left&apos;, props.class)&quot;&amp;gt;
  
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The primitive holds the default styling, &lt;code&gt;props.class&lt;/code&gt; carries the override, and the merge runs defaults first so the override wins on conflicts.&lt;/p&gt;
&lt;h3&gt;Watch the merge happen&lt;/h3&gt;
&lt;p&gt;Toggle classes on either side. The left panel shows what naive concatenation produces: conflicting utilities sit side-by-side, flagged with ⚠. The right panel runs the same input through the real &lt;code&gt;cn()&lt;/code&gt; (using &lt;code&gt;clsx&lt;/code&gt; + &lt;code&gt;tailwind-merge&lt;/code&gt; from npm) and shows which classes survive and which get dropped.&lt;/p&gt;
&lt;p&gt;Conflict chips on the left are the ones whose winner depends on stylesheet order; fragile. Strikethrough chips on the right are the ones &lt;code&gt;tailwind-merge&lt;/code&gt; dropped. Hover any dropped chip to see who beat it. Non-conflicting classes (like &lt;code&gt;text-white&lt;/code&gt; when no other text-color is set) flow through untouched on both sides, since &lt;code&gt;cn()&lt;/code&gt; deduplicates only when there is a real conflict.&lt;/p&gt;
&lt;p&gt;So the share dialog’s &lt;code&gt;&amp;lt;DialogContent class=&quot;max-w-2xl&quot;&amp;gt;&lt;/code&gt; works: the primitive’s &lt;code&gt;max-w-md&lt;/code&gt; default loses to the consumer’s &lt;code&gt;max-w-2xl&lt;/code&gt;, no specificity war, no fork.&lt;/p&gt;
&lt;h2&gt;State as Contract: The &lt;code&gt;data-*&lt;/code&gt; Pattern&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cn()&lt;/code&gt; covers static overrides. But every dialog also has &lt;em&gt;state&lt;/em&gt;: open, closed, animating in, animating out, disabled. The consumer wants to style against those too. A fade on open, a slide on close, a muted border when disabled. The primitive cannot ship every animation, and it shouldn’t have to.&lt;/p&gt;
&lt;p&gt;Headless libraries solve this by writing state to the DOM as data attributes, then letting the consumer style against them. You already saw both halves of the pattern in the snippets above.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;data-state&lt;/code&gt;: the current state of the component&lt;/h3&gt;
&lt;p&gt;The library sets it automatically. Look at the trigger we built earlier:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;:data-state=&quot;open ? &apos;open&apos; : &apos;closed&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The consumer styles against the attribute:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;DialogContent&lt;/span&gt;
  &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;data-[state=open]:animate-in data-[state=closed]:fade-out&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tailwind v4 ships &lt;code&gt;data-[...]&lt;/code&gt; as a first-class variant, so no plugin is needed. The same vocabulary shows up across the ecosystem: &lt;code&gt;data-state=&quot;open&quot; | &quot;closed&quot;&lt;/code&gt; on overlays, &lt;code&gt;&quot;on&quot; | &quot;off&quot;&lt;/code&gt; on toggles, &lt;code&gt;&quot;checked&quot; | &quot;unchecked&quot; | &quot;indeterminate&quot;&lt;/code&gt; on checkboxes, &lt;code&gt;&quot;active&quot; | &quot;inactive&quot;&lt;/code&gt; on tabs. Reka UI and Radix share the contract, so a class targeting &lt;code&gt;data-[state=open]:&lt;/code&gt; works against either.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;data-slot&lt;/code&gt;: stable identity for each part&lt;/h3&gt;
&lt;p&gt;shadcn-vue v4 stamps a &lt;code&gt;data-slot&lt;/code&gt; on every primitive. The dialog root carries &lt;code&gt;data-slot=&quot;dialog&quot;&lt;/code&gt;, the header &lt;code&gt;data-slot=&quot;dialog-header&quot;&lt;/code&gt;, and so on. You saw it in the shadcn source above:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;DialogRoot data-slot=&quot;dialog&quot; v-bind=&quot;forwarded&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;div data-slot=&quot;dialog-header&quot; :class=&quot;cn(...)&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why a separate attribute when &lt;code&gt;class&lt;/code&gt; exists? Because &lt;code&gt;class&lt;/code&gt; belongs to the consumer (&lt;code&gt;cn()&lt;/code&gt; already settled that), and Tailwind utility names are not semantic. &lt;code&gt;data-slot&lt;/code&gt; is. A parent layout can target a dialog part without knowing or caring which utilities live inside it today:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;[&amp;amp;_[data-slot=dialog-footer]]:gap-3&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;!-- Any dialog mounted inside picks up gap-3 on its footer, no matter how deep. --&amp;gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Other attributes you’ll see&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data-side&lt;/code&gt;: &lt;code&gt;top&lt;/code&gt; / &lt;code&gt;right&lt;/code&gt; / &lt;code&gt;bottom&lt;/code&gt; / &lt;code&gt;left&lt;/code&gt; on positioned elements (tooltip, popover, menu)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-align&lt;/code&gt;: &lt;code&gt;start&lt;/code&gt; / &lt;code&gt;center&lt;/code&gt; / &lt;code&gt;end&lt;/code&gt; for the same&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-orientation&lt;/code&gt;: &lt;code&gt;horizontal&lt;/code&gt; / &lt;code&gt;vertical&lt;/code&gt; on tabs, separators, sliders&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-disabled&lt;/code&gt;: present when the part is disabled&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-highlighted&lt;/code&gt;: present when keyboard focus is on a menu item&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-placeholder&lt;/code&gt;: present while a select is showing its placeholder&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why attributes, not classes&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The consumer owns &lt;code&gt;class&lt;/code&gt;.&lt;/strong&gt; If the library wrote state classes like &lt;code&gt;is-open&lt;/code&gt;, it would have to merge with the consumer’s input or fight specificity. Attributes sidestep both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable surface.&lt;/strong&gt; Refactor the primitive’s utility classes tomorrow; &lt;code&gt;data-state=&quot;open&quot;&lt;/code&gt; survives because it is the public contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inspectable.&lt;/strong&gt; Open DevTools, see the state in the DOM. No closure spelunking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable with Tailwind variants.&lt;/strong&gt; &lt;code&gt;data-[state=open]:&lt;/code&gt;, &lt;code&gt;group-data-[state=open]:&lt;/code&gt;, and &lt;code&gt;has-[[data-slot=dialog-footer]]:&lt;/code&gt; are first-class variants in v4.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Element Handoff: &lt;code&gt;as-child&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;One more escape hatch shows up in shadcn-vue source: &lt;code&gt;as-child&lt;/code&gt; (inherited from Reka UI, which inherits it from Radix). Pass it to a primitive and the primitive &lt;strong&gt;clones the consumer’s child&lt;/strong&gt; and forwards its behavior into that element rather than rendering its own DOM.&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- Default: DialogTrigger renders its own &amp;lt;button&amp;gt; --&amp;gt;
&amp;lt;DialogTrigger&amp;gt;Open&amp;lt;/DialogTrigger&amp;gt;

&amp;lt;!-- as-child: DialogTrigger borrows your &amp;lt;RouterLink&amp;gt;&apos;s DOM,
     keeps the open-on-click behavior, but the rendered tag is &amp;lt;a&amp;gt; --&amp;gt;
&amp;lt;DialogTrigger as-child&amp;gt;
  &amp;lt;RouterLink to=&quot;/profile&quot;&amp;gt;Open profile&amp;lt;/RouterLink&amp;gt;
&amp;lt;/DialogTrigger&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contract: the consumer’s element receives all the props and event handlers the primitive would have applied to its own DOM. The primitive owns behavior; the consumer owns presentation. &lt;em&gt;(Reka UI implements this via its &lt;code&gt;&amp;lt;Primitive&amp;gt;&lt;/code&gt; component and slot forwarding, so you do not need to write the cloning machinery yourself.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is the fourth axis of consumer override the pattern gives you. Compound children let the consumer compose &lt;strong&gt;structure&lt;/strong&gt;, &lt;code&gt;cn()&lt;/code&gt; lets them override &lt;strong&gt;style&lt;/strong&gt;, &lt;code&gt;data-*&lt;/code&gt; exposes &lt;strong&gt;state and identity&lt;/strong&gt; as a stylable contract, and &lt;code&gt;as-child&lt;/code&gt; lets them swap the &lt;strong&gt;rendered element&lt;/strong&gt;. All four are decided at the call site.&lt;/p&gt;
&lt;h2&gt;The Convenience Layer&lt;/h2&gt;
&lt;p&gt;Once a compound API exists, someone will ask for a flat `` shortcut. Don’t add it as flags on &lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt;. Build it &lt;em&gt;on top&lt;/em&gt; of the primitives:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- ConfirmDialog.vue: a higher-level wrapper, NOT a flag added to Dialog --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import {
  Dialog, DialogClose, DialogContent, DialogDescription,
  DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from &quot;@/components/dialog&quot;;

withDefaults(
  defineProps&amp;lt;{
    title: string;
    description?: string;
    confirmLabel?: string;
    cancelLabel?: string;
    destructive?: boolean;
  }&amp;gt;(),
  { confirmLabel: &quot;Confirm&quot;, cancelLabel: &quot;Cancel&quot;, destructive: false },
);

const emit = defineEmits&amp;lt;{ confirm: [] }&amp;gt;();
const open = defineModel&amp;lt;boolean&amp;gt;(&quot;open&quot;, { default: false });

function handleConfirm() {
  emit(&quot;confirm&quot;);
  open.value = false;
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;Dialog v-model:open=&quot;open&quot;&amp;gt;
    &amp;lt;DialogTrigger v-if=&quot;$slots.trigger&quot; as-child&amp;gt;
      
    &amp;lt;/DialogTrigger&amp;gt;
    &amp;lt;DialogContent&amp;gt;
      &amp;lt;DialogHeader&amp;gt;
        &amp;lt;DialogTitle&amp;gt;{{ title }}&amp;lt;/DialogTitle&amp;gt;
        &amp;lt;DialogDescription v-if=&quot;description&quot;&amp;gt;{{ description }}&amp;lt;/DialogDescription&amp;gt;
      &amp;lt;/DialogHeader&amp;gt;
      &amp;lt;DialogFooter&amp;gt;
        &amp;lt;DialogClose&amp;gt;{{ cancelLabel }}&amp;lt;/DialogClose&amp;gt;
        &amp;lt;button :class=&quot;destructive ? &apos;bg-red-600 ...&apos; : &apos;bg-zinc-900 ...&apos;&quot; @click=&quot;handleConfirm&quot;&amp;gt;
          {{ confirmLabel }}
        &amp;lt;/button&amp;gt;
      &amp;lt;/DialogFooter&amp;gt;
    &amp;lt;/DialogContent&amp;gt;
  &amp;lt;/Dialog&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now a sign-out confirm is one tag:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;ConfirmDialog
  title=&quot;Sign out of your account?&quot;
  description=&quot;You&apos;ll need to sign in again next time you visit.&quot;
  confirm-label=&quot;Sign out&quot;
  cancel-label=&quot;Stay signed in&quot;
  @confirm=&quot;onSignOut&quot;
&amp;gt;
  &amp;lt;template #trigger&amp;gt;
    &amp;lt;button&amp;gt;Sign out&amp;lt;/button&amp;gt;
  &amp;lt;/template&amp;gt;
&amp;lt;/ConfirmDialog&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The convenience layer is a &lt;em&gt;consumer&lt;/em&gt; of the primitives, not an extension of them. When a designer asks for a non-standard variant (checkbox in the footer, custom layout), the consumer drops back down to the primitives without anyone touching &lt;code&gt;ConfirmDialog&lt;/code&gt;. This is what stops the compound API from collapsing back into &lt;code&gt;BooleanPropDialog&lt;/code&gt; over time.&lt;/p&gt;
&lt;h2&gt;How to Apply This in Your Codebase&lt;/h2&gt;
&lt;p&gt;I use this workflow to migrate an existing prop-heavy component:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Find the smell.&lt;/strong&gt; List every boolean prop. For each one, ask: does this prop change &lt;em&gt;what renders&lt;/em&gt; or &lt;em&gt;how it renders&lt;/em&gt;? “What” props are composition opportunities. “How” props (variant, size, color) are fine; leave them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sketch the tree per variant.&lt;/strong&gt; Take three real call sites and write down the JSX/template you wish you could write. The compound API drops out of that exercise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract the provider.&lt;/strong&gt; Move state and handlers into a composable with a typed &lt;code&gt;InjectionKey&lt;/code&gt;. The consumer-facing interface is the type signature of that context object.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split the children.&lt;/strong&gt; Each child component reads only the slice it needs from &lt;code&gt;useX()&lt;/code&gt;. Keep them dumb. No business logic in the leaves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete the old props one variant at a time.&lt;/strong&gt; You can ship the new API alongside the old monolith and migrate call sites incrementally.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A few rules that have saved me pain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fail when context is missing.&lt;/strong&gt; &lt;code&gt;useDialog&lt;/code&gt; throws. Otherwise consumers will render broken UI without warning when they forget the root.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not build a generic primitive before you have three concrete users.&lt;/strong&gt; One use case is a component. Two is a coincidence. Three is a pattern worth abstracting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resist the “convenience prop” temptation.&lt;/strong&gt; Once you have the compound API, someone will ask for a `` shortcut. Add it as a separate higher-level component (&lt;code&gt;&amp;lt;ConfirmDialog&amp;gt;&lt;/code&gt; built &lt;em&gt;on top&lt;/em&gt; of the primitives), not as flags on the root.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When Not to Use It&lt;/h2&gt;
&lt;p&gt;The compound pattern has real costs. Skip it when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The component has one shape and you are inventing variants that do not exist.&lt;/li&gt;
&lt;li&gt;Consumers are LLM-generated code or external API users who benefit from a flat, predictable signature.&lt;/li&gt;
&lt;li&gt;The state is trivial and &lt;code&gt;provide&lt;/code&gt; / &lt;code&gt;inject&lt;/code&gt; adds more ceremony than the boolean it replaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A `` does not need to be compound. A modal with a form, a footer, and three layouts does.&lt;/p&gt;
&lt;h2&gt;Connections&lt;/h2&gt;
&lt;p&gt;This pattern lives across the ecosystem under different names. The unifying idea, &lt;em&gt;push the variant decision out of the component and into the call site&lt;/em&gt;, shows up in three traditions worth reading.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On the React side&lt;/strong&gt;, Kent C. Dodds canonized the modern compound-components-with-context approach in &lt;a href=&quot;https://kentcdodds.com/blog/compound-components-with-react-hooks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Compound Components with React Hooks&lt;/a&gt;. His framing of the pattern as “an implicit contract between parent and children, eliminating verbose prop passing” is the cleanest single statement of why this beats props. &lt;a href=&quot;https://www.patterns.dev/react/compound-pattern/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;patterns.dev’s Compound Pattern&lt;/a&gt; is the canonical reference for the React variant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Radix UI&lt;/strong&gt; (which Reka UI mirrors for Vue) builds compound on top of two primitives: a context provider and the &lt;a href=&quot;https://www.radix-ui.com/primitives/docs/guides/composition&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;asChild&lt;/code&gt;&lt;/a&gt; composition prop, which lets consumers swap the rendered element while keeping the behavior. The &lt;a href=&quot;https://github.com/radix-ui/primitives/blob/main/philosophy.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Radix philosophy doc&lt;/a&gt; is the architecture-in-one-page version: &lt;em&gt;“primitives ship with zero presentational styles”&lt;/em&gt; and &lt;em&gt;“components are designed with an open API that provides consumers with direct access to the underlying DOM node.”&lt;/em&gt; The whole shadcn-vue ecosystem flows from those two lines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On the Vue side&lt;/strong&gt;, Adam Wathan’s &lt;a href=&quot;https://adamwathan.me/advanced-vue-component-design/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Advanced Vue Component Design&lt;/a&gt; (2018) was the first widely-shared treatment of compound, slots, and providers as a single toolkit. Michael Thiessen catalogs the per-pattern variants in &lt;a href=&quot;https://michaelnthiessen.com/12-design-patterns-vue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;12 Design Patterns in Vue&lt;/a&gt;, &lt;a href=&quot;https://michaelnthiessen.com/6-levels-of-reusability/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The 6 Levels of Reusability&lt;/a&gt;, and the small applied exercise &lt;a href=&quot;https://michaelnthiessen.com/building-unnecessary-if-else-component&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Building a (Totally) Unnecessary If/Else Component&lt;/a&gt;. The last one builds an &lt;code&gt;&amp;lt;If&amp;gt;&lt;/code&gt; pair from scratch using &lt;code&gt;provide&lt;/code&gt;/&lt;code&gt;inject&lt;/code&gt;, a tighter playground than Dialog if you want to see the wiring on a smaller surface.&lt;/p&gt;
&lt;p&gt;All three traditions share the same shift from configuration to composition: hand the component its pieces instead of describing them with flags.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.shadcn-vue.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;shadcn-vue&lt;/a&gt;: the source code referenced in this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reka-ui.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Reka UI&lt;/a&gt;: the headless primitives shadcn-vue is built on&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.radix-ui.com/primitives&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Radix Primitives&lt;/a&gt;: the React parent of Reka UI; same patterns, different binding&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=4KvbVq3Eg5w&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Fernando Rojo, Composition Is All You Need&lt;/a&gt;: the talk that prompted this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kentcdodds.com/blog/compound-components-with-react-hooks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Kent C. Dodds, Compound Components with React Hooks&lt;/a&gt;: canonical React treatment&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/components/provide-inject.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue docs: provide / inject&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vueuse.org/shared/createInjectionState/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VueUse: createInjectionState&lt;/a&gt;: the factory that collapses the manual provider boilerplate&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><category>design-patterns</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>A Modern Quality Pipeline and Testing Strategy for Frontend Projects</title><link>https://alexop.dev/posts/modern-frontend-quality-pipeline/</link><guid isPermaLink="true">https://alexop.dev/posts/modern-frontend-quality-pipeline/</guid><description>A short, framework-agnostic concept of what a modern quality pipeline and testing strategy look like for any JavaScript or TypeScript frontend project.</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;An agent writes most of my frontend code now. I review what it produces and tighten the architecture where it overreaches.&lt;/p&gt;
&lt;p&gt;That changes what a quality pipeline is for. You used to write tests and types so the next person on the file stayed sane. Now you write them so the agent can check its own work. Give it more ways to verify a change (types, lint, unit, component, real browser, a11y, bundle budget) and it finishes more of the ticket on its own. A red check tells it what to try next.&lt;/p&gt;
&lt;p&gt;Frontend has also grown more complicated since 2024. SSR, streaming, partial prerendering, server components, edge runtimes. Each adds a place where a change can break silently. “TypeScript plus a couple of unit tests” no longer covers it.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;quality pipeline&lt;/em&gt; is the set of checks that run on every change (locally, on commit, in CI) to give layered confidence the change is correct, accessible, performant, and safe to ship. A &lt;em&gt;testing strategy&lt;/em&gt; is the part of that pipeline that asserts behaviour: what the app should do, at which level, at what cost.&lt;/p&gt;
&lt;p&gt;Plan them as one system. The pipeline decides when checks run; the strategy decides which checks are worth running. Design them together so that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each check has a clear job and runs at the cheapest stage where it can&lt;br /&gt;
catch the problem.&lt;/li&gt;
&lt;li&gt;The feedback loop is short enough that no developer or agent skips&lt;br /&gt;
ahead.&lt;/li&gt;
&lt;li&gt;The same checks run on a contributor’s laptop, in an agent’s sandbox,&lt;br /&gt;
and on the CI runner.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same pipeline shape applies whether you build with Next, Nuxt, Astro, SvelteKit, Remix, or a plain Vite app. The framework choice changes which adapter you import, nothing else.&lt;/p&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;Frontend tooling consolidated between 2023 and 2026. &lt;a href=&quot;https://vitejs.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vite&lt;/a&gt;&lt;br /&gt;
became the default dev/build engine across the major frameworks.&lt;br /&gt;
&lt;a href=&quot;https://vitest.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest&lt;/a&gt; replaced &lt;a href=&quot;https://jestjs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jest&lt;/a&gt;.&lt;br /&gt;
&lt;a href=&quot;https://playwright.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright&lt;/a&gt; became the default for E2E.&lt;br /&gt;
&lt;a href=&quot;https://eslint.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint&lt;/a&gt; adopted flat config; &lt;a href=&quot;https://biomejs.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Biome&lt;/a&gt;&lt;br /&gt;
and &lt;strong&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/linter&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Oxlint&lt;/a&gt;&lt;/strong&gt; emerged as much faster&lt;br /&gt;
alternatives in Rust. &lt;a href=&quot;https://www.typescriptlang.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeScript&lt;/a&gt; strict mode&lt;br /&gt;
became table stakes. &lt;a href=&quot;https://docs.renovatebot.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Renovate&lt;/a&gt; replaced&lt;br /&gt;
&lt;a href=&quot;https://github.com/dependabot&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dependabot&lt;/a&gt;. In March 2026,&lt;br /&gt;
&lt;a href=&quot;https://voidzero.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VoidZero&lt;/a&gt; shipped &lt;strong&gt;&lt;a href=&quot;https://viteplus.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vite+&lt;/a&gt;&lt;/strong&gt; as&lt;br /&gt;
the open-source culmination of that trend: one CLI that wraps Vite,&lt;br /&gt;
&lt;a href=&quot;https://rolldown.rs/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Rolldown&lt;/a&gt;, Vitest, Oxlint,&lt;br /&gt;
&lt;a href=&quot;https://github.com/oxc-project/oxc&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Oxfmt&lt;/a&gt;, and &lt;a href=&quot;https://tsdown.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Tsdown&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A modern quality pipeline’s pieces look the same regardless of framework, so I’ll describe the concept first and my stack second.&lt;/p&gt;
&lt;h2&gt;My default stack&lt;/h2&gt;
&lt;p&gt;For new frontend projects in 2026 I reach for &lt;strong&gt;Vite+&lt;/strong&gt; instead of wiring&lt;br /&gt;
the toolchain by hand. Vite+ (&lt;a href=&quot;https://viteplus.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;viteplus.dev&lt;/a&gt;) is&lt;br /&gt;
the unified toolchain from VoidZero, &lt;a href=&quot;https://evanyou.me/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Evan You&lt;/a&gt;’s company.&lt;br /&gt;
It bundles Vite, Rolldown, Vitest, Oxlint, Oxfmt, and Tsdown behind a single&lt;br /&gt;
CLI (&lt;code&gt;vp dev&lt;/code&gt;, &lt;code&gt;vp check&lt;/code&gt;, &lt;code&gt;vp test&lt;/code&gt;, &lt;code&gt;vp build&lt;/code&gt;) and one config file. The&lt;br /&gt;
alpha shipped open source under MIT.&lt;/p&gt;
&lt;p&gt;If you adopt the pieces one at a time, the swaps I would make are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old default&lt;/th&gt;
&lt;th&gt;What I use&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ESLint&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oxlint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~50× faster, fast enough to run on every keystroke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://prettier.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Prettier&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oxfmt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~30× faster, Prettier-compatible defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vitest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ESM-native, browser mode, same matchers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://webpack.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;webpack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vite + Rolldown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~40× faster production builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;four separate configs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vp check&lt;/code&gt; / &lt;code&gt;vp test&lt;/code&gt; / &lt;code&gt;vp build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one CLI, one config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each piece holds up on its own. I have shipped Vitest and Oxlint in&lt;br /&gt;
production for some time; swapping Prettier for Oxfmt and webpack for&lt;br /&gt;
Rolldown took a day in the projects I tried. Vite+ removes the&lt;br /&gt;
integration cost that kept teams on the older stack.&lt;/p&gt;
&lt;h2&gt;The layers&lt;/h2&gt;
&lt;p&gt;Think of the pipeline as concentric layers, each cheaper and faster than&lt;br /&gt;
the one outside it. Run cheap checks first. Save the expensive ones for&lt;br /&gt;
the things only they can catch.&lt;/p&gt;
&lt;h3&gt;1. Type safety&lt;/h3&gt;
&lt;p&gt;Type safety is your first line of defence. Run your framework’s type&lt;br /&gt;
checker in CI on every PR. Treat any new type error as a build failure.&lt;/p&gt;
&lt;p&gt;If you use TypeScript, that means &lt;code&gt;tsc --noEmit&lt;/code&gt; (or your framework’s wrapper around it; most frameworks ship one to handle their template syntax and project references). If you don’t use TypeScript yet, adopting it is the highest-leverage change you can make.&lt;/p&gt;
&lt;p&gt;Validate untyped boundaries with a schema library (&lt;a href=&quot;https://zod.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Zod&lt;/a&gt;, &lt;a href=&quot;https://valibot.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Valibot&lt;/a&gt;, &lt;a href=&quot;https://arktype.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ArkType&lt;/a&gt;). Parse anywhere data crosses a boundary: route params, API responses, env vars, form input. TypeScript trusts the types you write; schemas check that the data matches them at runtime. With runtime parsing in place you stop reaching for &lt;code&gt;as&lt;/code&gt;. See why &lt;code&gt;as&lt;/code&gt; is a shortcut to avoid.&lt;/p&gt;
&lt;h3&gt;2. Lint and format&lt;/h3&gt;
&lt;p&gt;Catches style and a wide class of bugs (unused vars, unsafe &lt;code&gt;any&lt;/code&gt;, missing&lt;br /&gt;
deps in effects) without running the code.&lt;/p&gt;
&lt;p&gt;The conventional choice is ESLint flat config plus&lt;br /&gt;
&lt;a href=&quot;https://typescript-eslint.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;typescript-eslint&lt;/code&gt;&lt;/a&gt; and your framework’s plugin. The 2026 alternative is &lt;strong&gt;Oxlint&lt;/strong&gt; (Rust-based,&lt;br /&gt;
~50× faster) paired with Oxfmt for formatting, or &lt;strong&gt;Biome&lt;/strong&gt; for a&lt;br /&gt;
single-binary lint+format combo. The trade-off: Oxlint and Biome have&lt;br /&gt;
smaller rule sets than ESLint’s mature ecosystem, but they cover most of&lt;br /&gt;
the high-value cases and are fast enough to run on every keystroke. For a&lt;br /&gt;
working setup that uses Oxlint as a fast first pass and keeps ESLint for&lt;br /&gt;
the rules Oxlint doesn’t yet cover, see my opinionated ESLint setup for Vue projects.&lt;/p&gt;
&lt;p&gt;Add these two rule families regardless of which linter you pick. They&lt;br /&gt;
catch bugs the type system misses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ota-meshi.github.io/eslint-plugin-regexp/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;eslint-plugin-regexp&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; – ~60 correctness rules for regular&lt;br /&gt;
expressions. Cheap to add, catches real bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://e18e.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@e18e/eslint-plugin&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; – small performance lints (e.g., prefer&lt;br /&gt;
&lt;code&gt;Set.has&lt;/code&gt; over &lt;code&gt;Array.includes&lt;/code&gt;) that compound across a codebase.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Unit tests&lt;/h3&gt;
&lt;p&gt;For pure functions, hooks, stores, and utilities. Cheap, fast, and where&lt;br /&gt;
most logic should live.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool:&lt;/strong&gt; Vitest. Run on every save in watch mode; run all of them in CI.&lt;/li&gt;
&lt;li&gt;Aim for high coverage of pure modules; don’t chase coverage on UI glue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Vue, see my guide to testing Vue composables with Vitest.&lt;/p&gt;
&lt;h3&gt;4. Component tests&lt;/h3&gt;
&lt;p&gt;For components in isolation, with a real DOM and real user interactions. The biggest win in 2026 is &lt;strong&gt;Vitest browser mode&lt;/strong&gt;: your component tests run in a real Chromium via Playwright instead of jsdom. Hover states, focus, layout, intersection observers, and scroll behaviour all work as they do in production.&lt;/p&gt;
&lt;p&gt;Pair this with &lt;a href=&quot;https://testing-library.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@testing-library/*&lt;/code&gt;&lt;/a&gt; for&lt;br /&gt;
whichever framework you use; accessibility assertions on each mounted&lt;br /&gt;
component live in layer 8 below. For a deeper walkthrough of how this&lt;br /&gt;
fits into a full testing pyramid, see my Vue 3 testing pyramid guide.&lt;/p&gt;
&lt;h3&gt;5. API mocking&lt;/h3&gt;
&lt;p&gt;Hard-coded fixtures go stale. Tests that hit a real backend are flaky.&lt;br /&gt;
Mock at the network layer once and reuse the same handlers everywhere.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool:&lt;/strong&gt; &lt;strong&gt;&lt;a href=&quot;https://mswjs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MSW&lt;/a&gt;&lt;/strong&gt; (Mock Service Worker). It intercepts &lt;code&gt;fetch&lt;/code&gt;,&lt;br /&gt;
XHR, and GraphQL with a service worker in the browser and a&lt;br /&gt;
request interceptor in Node, so the same handler definitions&lt;br /&gt;
work in Vitest, Vitest browser mode, Playwright, and the dev&lt;br /&gt;
server.&lt;/li&gt;
&lt;li&gt;Define handlers once in &lt;code&gt;src/mocks/handlers.ts&lt;/code&gt;; load them in&lt;br /&gt;
your test setup and (optionally) in the dev server for&lt;br /&gt;
offline-first development.&lt;/li&gt;
&lt;li&gt;Combined with Zod (or Valibot/ArkType) schemas at the same&lt;br /&gt;
boundary, you get mocks that are typed, schema-validated, and&lt;br /&gt;
shared across every layer that hits the network. One source of&lt;br /&gt;
truth instead of three drifting fixture folders.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. Contract testing&lt;/h3&gt;
&lt;p&gt;The mocks in layer 5 are only as good as the assumptions you bake into them. If the backend renames a field or changes a status code without telling you, every green unit and component test still passes while production breaks. Contract testing closes that gap by tying the mock to a verifiable artefact that the provider checks against.&lt;/p&gt;
&lt;p&gt;There are three styles, and they fit different team setups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer-driven contracts (&lt;a href=&quot;https://pact.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pact&lt;/a&gt;).&lt;/strong&gt; The frontend&lt;br /&gt;
writes a test that records the requests it makes and the responses it&lt;br /&gt;
expects. Pact generates a JSON contract and publishes it to a broker&lt;br /&gt;
(the open-source &lt;a href=&quot;https://docs.pact.io/pact_broker&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pact Broker&lt;/a&gt;, or&lt;br /&gt;
hosted &lt;a href=&quot;https://pactflow.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PactFlow&lt;/a&gt;). The provider runs &lt;em&gt;its&lt;/em&gt; real&lt;br /&gt;
test suite against that contract; if it satisfies every recorded&lt;br /&gt;
interaction, both sides can deploy. Pact has libraries for JS/TS, JVM,&lt;br /&gt;
.NET, Go, Rust, Python, Ruby, PHP, and Swift, so the same broker spans&lt;br /&gt;
a polyglot estate. Best when you control both ends of the wire and want&lt;br /&gt;
the consumer to drive the schema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provider-driven / OpenAPI-based.&lt;/strong&gt; The provider publishes an&lt;br /&gt;
&lt;a href=&quot;https://www.openapis.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenAPI&lt;/a&gt; spec and the contract &lt;em&gt;is&lt;/em&gt; the&lt;br /&gt;
spec. Consumers validate their requests and assertions against it with&lt;br /&gt;
&lt;a href=&quot;https://schemathesis.readthedocs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Schemathesis&lt;/a&gt; (property-based&lt;br /&gt;
fuzzing of every operation), &lt;a href=&quot;https://dredd.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dredd&lt;/a&gt; (replays&lt;br /&gt;
example requests from the spec against the running provider), or&lt;br /&gt;
&lt;a href=&quot;https://stoplight.io/open-source/spectral&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spectral&lt;/a&gt; (lints the spec&lt;br /&gt;
itself). Best when the provider already maintains an OAS and you don’t&lt;br /&gt;
want to add Pact on their side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bi-directional contracts (&lt;a href=&quot;https://docs.pactflow.io/docs/bi-directional-contract-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PactFlow&lt;/a&gt;).&lt;/strong&gt;&lt;br /&gt;
The consumer publishes a Pact contract; the provider publishes its&lt;br /&gt;
OpenAPI spec; PactFlow proves the two are compatible without the&lt;br /&gt;
provider having to run consumer-supplied tests. Best when consumers&lt;br /&gt;
want consumer-driven semantics but the provider team won’t (or can’t)&lt;br /&gt;
run Pact verification themselves.&lt;/p&gt;
&lt;p&gt;What you get for the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Independent deploys.&lt;/strong&gt; A contract gate replaces “is the matching&lt;br /&gt;
E2E green?” with “does the provider satisfy every consumer’s&lt;br /&gt;
contract?”. Consumer and provider can ship on different cadences&lt;br /&gt;
without coordinating a release train.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster than E2E.&lt;/strong&gt; Verifying a contract is a unit test for the&lt;br /&gt;
boundary; E2E spins up the real services. You catch the same class&lt;br /&gt;
of bug an order of magnitude sooner.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catches drift the linter can’t.&lt;/strong&gt; A field renamed on the backend&lt;br /&gt;
fails the contract before MSW handlers or Playwright flows would&lt;br /&gt;
notice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skip this layer if you own both services and ship them as one unit. E2E covers the same boundary in that case, and the contract overhead doesn’t pay off. Add it the moment consumer and provider deploy on different cadences, you don’t own the provider, or a single backend serves multiple frontends that all need to keep working.&lt;/p&gt;
&lt;p&gt;For a deep dive, &lt;a href=&quot;https://www.oreilly.com/library/view/contract-testing-in/9781633437241/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;Contract Testing in Action&lt;/em&gt;&lt;/a&gt;&lt;br /&gt;
(Marie Cruz &amp;amp; Lewis Prescott, Manning) walks through Pact, bi-directional&lt;br /&gt;
contracts, and how to introduce the practice without stalling delivery.&lt;/p&gt;
&lt;h3&gt;7. End-to-end tests&lt;/h3&gt;
&lt;p&gt;For critical user journeys across real pages: signup, checkout, the one or two flows that must never break. Keep the suite small. E2E is expensive.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool:&lt;/strong&gt; Playwright.&lt;/li&gt;
&lt;li&gt;Run against a built preview, not the dev server.&lt;/li&gt;
&lt;li&gt;Two assertions worth wiring into a custom fixture, regardless of&lt;br /&gt;
framework, because they catch silent regressions:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hydration mismatches.&lt;/strong&gt; Listen for hydration warnings on &lt;code&gt;console&lt;/code&gt;&lt;br /&gt;
and fail the test if any appear. SSR/CSR drift is one of the most&lt;br /&gt;
common silent regressions in modern frameworks. I wrote a dedicated&lt;br /&gt;
post on&lt;br /&gt;
catching hydration errors in Playwright tests&lt;br /&gt;
with a reusable fixture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSP violations.&lt;/strong&gt; Listen for &lt;code&gt;securitypolicyviolation&lt;/code&gt; events. If&lt;br /&gt;
your CSP is real, this turns every E2E run into a CSP regression&lt;br /&gt;
test.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. Accessibility&lt;/h3&gt;
&lt;p&gt;Accessibility cuts across lint, component, E2E, and preview. Treat it as a single discipline and check the same WCAG rule set at every cheap-enough stage.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lint.&lt;/strong&gt; &lt;a href=&quot;https://github.com/jsx-eslint/eslint-plugin-jsx-a11y&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt;&lt;/a&gt;&lt;br /&gt;
(React/JSX), &lt;a href=&quot;https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;eslint-plugin-vuejs-accessibility&lt;/code&gt;&lt;/a&gt;&lt;br /&gt;
(Vue), &lt;a href=&quot;https://ota-meshi.github.io/eslint-plugin-astro/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;eslint-plugin-astro&lt;/code&gt;&lt;/a&gt; — catch&lt;br /&gt;
missing &lt;code&gt;alt&lt;/code&gt;, role mismatches, and other static violations before tests run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component.&lt;/strong&gt; &lt;a href=&quot;https://github.com/dequelabs/axe-core&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;axe-core&lt;/code&gt;&lt;/a&gt; via&lt;br /&gt;
&lt;a href=&quot;https://github.com/nickcolley/jest-axe&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;jest-axe&lt;/code&gt;&lt;/a&gt; for jsdom, or&lt;br /&gt;
&lt;a href=&quot;https://github.com/dequelabs/axe-core-npm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@axe-core/playwright&lt;/code&gt;&lt;/a&gt; in Vitest browser mode.&lt;br /&gt;
Assert no violations on every mounted component, and add a meta-test that fails if any&lt;br /&gt;
component test lacks an a11y assertion so the practice doesn’t slide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E2E.&lt;/strong&gt; &lt;a href=&quot;https://github.com/dequelabs/axe-core-npm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@axe-core/playwright&lt;/code&gt;&lt;/a&gt; on each critical&lt;br /&gt;
journey — same engine as the component layer, but on the real composed page where many&lt;br /&gt;
violations only appear once everything is wired together.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preview.&lt;/strong&gt; Lighthouse’s accessibility category (run as part of layer 10) or&lt;br /&gt;
&lt;a href=&quot;https://github.com/pa11y/pa11y-ci&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pa11y CI&lt;/a&gt; on a list of routes for a dedicated, auditable&lt;br /&gt;
report.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual.&lt;/strong&gt; Storybook’s &lt;a href=&quot;https://storybook.js.org/addons/@storybook/addon-a11y&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a11y addon&lt;/a&gt;,&lt;br /&gt;
keyboard-only walkthroughs of new flows, and screen-reader spot-checks. Automated tools catch&lt;br /&gt;
roughly 30% of WCAG issues; the rest needs a human.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is no longer optional in the EU: the&lt;br /&gt;
&lt;a href=&quot;https://ec.europa.eu/social/main.jsp?catId=1202&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;European Accessibility Act&lt;/a&gt; took effect in&lt;br /&gt;
mid-2025, so most B2C and many B2B products operating in EU markets are now legally required&lt;br /&gt;
to meet WCAG 2.1 AA equivalence. For a framework-specific checklist, see&lt;br /&gt;
my Vue accessibility blueprint.&lt;/p&gt;
&lt;h3&gt;9. Visual regression&lt;/h3&gt;
&lt;p&gt;Catches unintended UI drift that unit and E2E tests miss.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.chromatic.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Chromatic&lt;/a&gt; (hosted,&lt;br /&gt;
&lt;a href=&quot;https://storybook.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Storybook&lt;/a&gt;-native) or Playwright screenshots + a&lt;br /&gt;
diff tool like &lt;a href=&quot;https://lost-pixel.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lost Pixel&lt;/a&gt; for self-hosted. For a Vitest-native approach, see&lt;br /&gt;
how to do visual regression testing in Vue with Vitest.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onlyChanged: true&lt;/code&gt; keeps it cheap: only re-snapshot stories whose&lt;br /&gt;
dependencies changed.&lt;/li&gt;
&lt;li&gt;Gate on PR; review diffs as part of code review.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10. Performance and bundle size&lt;/h3&gt;
&lt;p&gt;Performance regressions are silent unless you measure them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lighthouse CI&lt;/a&gt;&lt;/strong&gt; on a&lt;br /&gt;
preview deployment. Run it against both a &lt;em&gt;light&lt;/em&gt; and &lt;em&gt;dark&lt;/em&gt; color scheme;&lt;br /&gt;
contrast regressions show up only in one.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ai/size-limit&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;size-limit&lt;/code&gt;&lt;/a&gt; or your framework’s bundle&lt;br /&gt;
analyzer on PR for bundle deltas. Set explicit budgets and fail the build&lt;br /&gt;
when they’re exceeded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lab measurements catch regressions before merge. To see what real&lt;br /&gt;
users experience, and to find bottlenecks while you’re writing the&lt;br /&gt;
code, see layer 15 below.&lt;/p&gt;
&lt;h3&gt;11. Dead code and dependency hygiene&lt;/h3&gt;
&lt;p&gt;Unused code is a tax on every other check.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://knip.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Knip&lt;/a&gt;&lt;/strong&gt; to find unused files, exports, and&lt;br /&gt;
dependencies. Configure per-workspace if you have a monorepo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Renovate&lt;/strong&gt; for automated dependency updates with grouping and a sane&lt;br /&gt;
schedule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://google.github.io/osv-scanner/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OSV-Scanner&lt;/a&gt;&lt;/strong&gt; for vulnerabilities&lt;br /&gt;
and &lt;strong&gt;&lt;a href=&quot;https://github.com/gitleaks/gitleaks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Gitleaks&lt;/a&gt;&lt;/strong&gt; for secrets, gated&lt;br /&gt;
to high-severity only to avoid alert fatigue.&lt;/li&gt;
&lt;li&gt;Generate an &lt;strong&gt;SBOM&lt;/strong&gt; (Software Bill of Materials) on every build with&lt;br /&gt;
&lt;a href=&quot;https://github.com/anchore/syft&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Syft&lt;/a&gt;, &lt;a href=&quot;https://trivy.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Trivy&lt;/a&gt;, or&lt;br /&gt;
&lt;a href=&quot;https://github.com/CycloneDX/cdxgen&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cdxgen&lt;/a&gt;, in&lt;br /&gt;
&lt;a href=&quot;https://cyclonedx.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CycloneDX&lt;/a&gt; or &lt;a href=&quot;https://spdx.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SPDX&lt;/a&gt; format. This&lt;br /&gt;
is shifting from “nice to have” to “regulated requirement” in 2026&lt;br /&gt;
(&lt;a href=&quot;https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;EU CRA&lt;/a&gt;,&lt;br /&gt;
US executive orders), and it’s the same artefact your security team uses&lt;br /&gt;
to answer customer vulnerability questionnaires.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;12. Internationalisation drift&lt;/h3&gt;
&lt;p&gt;If you ship in more than one language, untranslated strings slip through. A mature i18n library plus a drift checker in CI catches them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;i18n libraries&lt;/strong&gt; – &lt;a href=&quot;https://www.i18next.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;i18next&lt;/a&gt;,&lt;br /&gt;
&lt;a href=&quot;https://vue-i18n.intlify.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;vue-i18n&lt;/a&gt;,&lt;br /&gt;
&lt;a href=&quot;https://formatjs.github.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FormatJS / react-intl&lt;/a&gt;, and&lt;br /&gt;
&lt;a href=&quot;https://lingui.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lingui&lt;/a&gt; all expose a missing-key handler you can&lt;br /&gt;
fail the build on, plus extractor CLIs that refuse to ship if a string&lt;br /&gt;
has no translation entry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lint your source for hardcoded strings.&lt;/strong&gt; ESLint has&lt;br /&gt;
&lt;a href=&quot;https://github.com/edvardchen/eslint-plugin-i18next&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;eslint-plugin-i18next&lt;/code&gt;&lt;/a&gt;&lt;br /&gt;
and&lt;br /&gt;
&lt;a href=&quot;https://eslint-plugin-vue-i18n.intlify.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@intlify/eslint-plugin-vue-i18n&lt;/code&gt;&lt;/a&gt;&lt;br /&gt;
to flag bare strings in JSX/templates and unused or missing keys.&lt;br /&gt;
Oxlint doesn’t yet ship i18n-specific rules, so run it as the fast&lt;br /&gt;
first pass and keep these ESLint plugins for the i18n layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://lunaria.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lunaria&lt;/a&gt;&lt;/strong&gt; compares each locale against a source&lt;br /&gt;
locale and reports missing or stale keys. It works with any project that has translation&lt;br /&gt;
files; you can publish a public status dashboard from the same data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;13. Preview deployments&lt;/h3&gt;
&lt;p&gt;The cheapest way to enable manual review and to give E2E, Lighthouse, and&lt;br /&gt;
visual-regression checks something realistic to run against.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vercel&lt;/a&gt;, &lt;a href=&quot;https://www.netlify.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Netlify&lt;/a&gt;, or&lt;br /&gt;
&lt;a href=&quot;https://pages.cloudflare.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare Pages&lt;/a&gt; will give you a unique URL&lt;br /&gt;
per PR for free.&lt;/li&gt;
&lt;li&gt;Wire your downstream checks to that URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;14. Automated code review&lt;/h3&gt;
&lt;p&gt;In 2026, AI code review is a standard pipeline stage. It runs before any&lt;br /&gt;
human reviewer touches the PR and catches issues the layers above miss:&lt;br /&gt;
logic mistakes, missing edge cases, security smells, and the small&lt;br /&gt;
inconsistencies that lint rules can’t express.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.coderabbit.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CodeRabbit&lt;/a&gt;&lt;/strong&gt;,&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://www.greptile.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Greptile&lt;/a&gt;&lt;/strong&gt;, and&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://vercel.com/docs/agent&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vercel Agent&lt;/a&gt;&lt;/strong&gt; are the main options. Recent benchmarks put Greptile’s bug-catch rate around 82%&lt;br /&gt;
versus CodeRabbit’s ~44%, but Greptile produces more false positives&lt;br /&gt;
and runs slower; CodeRabbit covers more git platforms.&lt;/li&gt;
&lt;li&gt;Have it run alongside specialist scanners (secrets, vulnerabilities,&lt;br /&gt;
workflow lint, shell/yaml lint) so a single bot comment summarises&lt;br /&gt;
every machine-checkable concern on the PR.&lt;/li&gt;
&lt;li&gt;Pause the bot on Renovate / Dependabot PRs to avoid noise on&lt;br /&gt;
mechanical updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Treat the AI reviewer as a high-recall first pass that reduces human review without replacing it. If you want to add an AI agent that goes one step further and exercises the app in a real browser, see how I run automated QA with Claude Code, Agent Browser, and GitHub Actions.&lt;/p&gt;
&lt;h3&gt;15. Runtime observability&lt;/h3&gt;
&lt;p&gt;Layers 1–13 give you confidence at merge time. Once a change is in&lt;br /&gt;
production, and while you’re writing it, you also want a live view of&lt;br /&gt;
what the app is doing. The same instrumentation answers both questions.&lt;/p&gt;
&lt;p&gt;Use &lt;strong&gt;&lt;a href=&quot;https://opentelemetry.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenTelemetry&lt;/a&gt;&lt;/strong&gt; as the SDK. It’s the&lt;br /&gt;
only vendor-neutral option, and the JS ecosystem caught up in 2025–2026:&lt;br /&gt;
stable web SDK, official auto-instrumentations for &lt;code&gt;document-load&lt;/code&gt;,&lt;br /&gt;
&lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;xhr&lt;/code&gt;, and &lt;code&gt;user-interaction&lt;/code&gt;, and OTLP support in every backend&lt;br /&gt;
that matters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Browser SDK.&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/@opentelemetry/sdk-trace-web&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;@opentelemetry/sdk-trace-web&lt;/code&gt;&lt;/a&gt;&lt;br /&gt;
plus the auto-instrumentations emits OTLP/HTTP. Wrap&lt;br /&gt;
&lt;a href=&quot;https://github.com/GoogleChrome/web-vitals&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;web-vitals&lt;/code&gt;&lt;/a&gt; into OTel&lt;br /&gt;
metrics so LCP/INP/CLS land on the same backend as the trace that&lt;br /&gt;
produced them. One pipe instead of two.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR / edge.&lt;/strong&gt; Next, Nuxt, SvelteKit, and Astro all expose OTel hooks.&lt;br /&gt;
Set a single &lt;code&gt;service.name&lt;/code&gt; resource attribute and one request stitches&lt;br /&gt;
together: edge → SSR → hydration → client interaction,&lt;br /&gt;
all in one trace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One collector, two backends.&lt;/strong&gt; Run an&lt;br /&gt;
&lt;a href=&quot;https://opentelemetry.io/docs/collector/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenTelemetry Collector&lt;/a&gt; with&lt;br /&gt;
two exporters. In dev, point it at &lt;a href=&quot;https://www.jaegertracing.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jaeger&lt;/a&gt;&lt;br /&gt;
or &lt;a href=&quot;https://grafana.com/oss/tempo/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Grafana Tempo&lt;/a&gt; running via&lt;br /&gt;
&lt;code&gt;docker-compose&lt;/code&gt;; open &lt;code&gt;localhost:16686&lt;/code&gt; and you can watch every fetch,&lt;br /&gt;
render, and hydration span as you click through the app. In prod, swap&lt;br /&gt;
the exporter to &lt;a href=&quot;https://www.honeycomb.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Honeycomb&lt;/a&gt;,&lt;br /&gt;
&lt;a href=&quot;https://grafana.com/products/cloud/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Grafana Cloud&lt;/a&gt;,&lt;br /&gt;
&lt;a href=&quot;https://www.dash0.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dash0&lt;/a&gt;, or&lt;br /&gt;
&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/tracing/instrumentation/opentelemetry/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Sentry’s OTel ingest&lt;/a&gt;.&lt;br /&gt;
Same SDK, same instrumentations, different OTLP endpoint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sample, or pay.&lt;/strong&gt; &lt;code&gt;ParentBased(TraceIdRatioBased(0.05))&lt;/code&gt; in prod,&lt;br /&gt;
&lt;code&gt;AlwaysOn&lt;/code&gt; in dev. Tail-sample at the collector to keep the slow and&lt;br /&gt;
error traces and drop the rest, so the signal that matters survives&lt;br /&gt;
without renting cloud storage for every render.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dev-time payoff is the part most teams underuse. The next time&lt;br /&gt;
someone asks why a page is slow on a real device, you already have the&lt;br /&gt;
trace from when they loaded it.&lt;/p&gt;
&lt;h2&gt;Where each layer runs&lt;/h2&gt;
&lt;p&gt;Same layers, different stages. Pick the cheapest stage where each check&lt;br /&gt;
can catch the problem.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editor&lt;/td&gt;
&lt;td&gt;Type checker LSP, linter, Vitest watch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-commit&lt;/td&gt;
&lt;td&gt;Format and lint on staged files only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI on PR&lt;/td&gt;
&lt;td&gt;Typecheck, full lint, unit, component, contract verify, build, knip, size-limit, AI review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI on preview URL&lt;/td&gt;
&lt;td&gt;E2E, accessibility (axe + Lighthouse), visual regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-merge / nightly&lt;/td&gt;
&lt;td&gt;Full E2E matrix, dependency updates, security scans, SBOM publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev server / production&lt;/td&gt;
&lt;td&gt;OpenTelemetry traces and metrics: live in dev, sampled in prod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For wiring local hooks themselves, &lt;strong&gt;&lt;a href=&quot;https://lefthook.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lefthook&lt;/a&gt;&lt;/strong&gt; has&lt;br /&gt;
become the default modern alternative to&lt;br /&gt;
&lt;a href=&quot;https://typicode.github.io/husky/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Husky&lt;/a&gt;: a single Go binary, declarative&lt;br /&gt;
YAML config, and parallel execution of lint/format/test commands on staged&lt;br /&gt;
files. Commit a &lt;code&gt;lefthook.yml&lt;/code&gt; to the repo, run &lt;code&gt;lefthook install&lt;/code&gt; once, and&lt;br /&gt;
contributors get the same hook setup automatically. Pair it with&lt;br /&gt;
&lt;a href=&quot;https://github.com/lint-staged/lint-staged&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;lint-staged&lt;/code&gt;&lt;/a&gt; (or Lefthook’s&lt;br /&gt;
built-in &lt;code&gt;{staged_files}&lt;/code&gt; substitution) so pre-commit only runs against the&lt;br /&gt;
files that changed. That fast-check pattern keeps the hook under a couple&lt;br /&gt;
of seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://git-scm.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Git 2.54&lt;/a&gt;&lt;/strong&gt; added config-based hooks, which means a small project no&lt;br /&gt;
longer needs an external hook manager at all. You define hooks in&lt;br /&gt;
&lt;code&gt;.gitconfig&lt;/code&gt; instead of as scripts under &lt;code&gt;.git/hooks&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;hook &quot;linter&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
   &lt;span&gt;event&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pre-commit&lt;/span&gt;
   &lt;span&gt;command&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pnpm exec oxlint --staged&lt;/span&gt;

&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;hook &quot;format&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
   &lt;span&gt;event&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pre-commit&lt;/span&gt;
   &lt;span&gt;command&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pnpm exec oxfmt --check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Multiple hooks per event run in order. Disable a single hook with&lt;br /&gt;
&lt;code&gt;hook.&amp;lt;name&amp;gt;.enabled = false&lt;/code&gt; (useful for opting one repo out of a&lt;br /&gt;
system-wide config), and list the active ones with &lt;code&gt;git hook list pre-commit&lt;/code&gt;. The traditional &lt;code&gt;.git/hooks/*&lt;/code&gt; scripts still run last,&lt;br /&gt;
so existing setups keep working. For a small project this covers most&lt;br /&gt;
of what Lefthook does without an extra binary. Lefthook still has the&lt;br /&gt;
edge for parallel execution and staged-file substitution.&lt;/p&gt;
&lt;p&gt;One valid alternative skips commit hooks and runs &lt;em&gt;everything&lt;/em&gt; server-side in CI as required status checks. You trade a slower red-CI feedback loop for never blocking a contributor with a flaky local hook.&lt;/p&gt;
&lt;p&gt;If GitHub Actions is the CI you’re wiring this onto, &lt;a href=&quot;https://www.manning.com/books/github-actions-in-action&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;GitHub Actions in Action&lt;/em&gt;&lt;/a&gt; (Kaufmann, Bos &amp;amp; de Vries, Manning) covers workflow design, reusable actions, matrix builds, secrets, and self-hosted runners. It pays off once you outgrow the default templates and start wiring the layers above into shared workflows.&lt;/p&gt;
&lt;h2&gt;What this gets you&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Regressions caught before merge.&lt;/strong&gt; A typed schema at the boundary plus&lt;br /&gt;
a Playwright check on the critical path catches more shipped bugs than&lt;br /&gt;
any single layer alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactors get safer.&lt;/strong&gt; Strict types and a healthy unit and component&lt;br /&gt;
test suite let you change internals without breaking surface behaviour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding gets shorter.&lt;/strong&gt; A new contributor can run one command, see&lt;br /&gt;
green, and trust that CI will tell them if they break something.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No one becomes the bottleneck.&lt;/strong&gt; The pipeline enforces the standard&lt;br /&gt;
for accessibility, performance, and i18n, so quality stops riding on a&lt;br /&gt;
single contributor.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Picking your testing shape&lt;/h2&gt;
&lt;p&gt;The layers above tell you &lt;em&gt;what&lt;/em&gt; to run. They don’t tell you &lt;em&gt;how much weight to give each one&lt;/em&gt;. A solo dev shipping a Vite app needs a different mix than a fifty-engineer team coordinating across a dozen services.&lt;/p&gt;
&lt;p&gt;The industry argues about this in shapes. The classical&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://martinfowler.com/articles/practical-test-pyramid.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pyramid&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
(Mike Cohn) puts most of the weight on unit tests and very little on&lt;br /&gt;
E2E. Kent C. Dodds’&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Trophy&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
moves the weight to integration tests because frontend bugs live at the&lt;br /&gt;
component-interaction layer. Spotify’s&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://engineering.atspotify.com/2018/01/testing-of-microservices&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Honeycomb&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
pushes weight onto integrated and contract tests because in a microservices&lt;br /&gt;
world, isolation tests prove little and full E2E is brittle. The&lt;br /&gt;
&lt;strong&gt;Ice-cream cone&lt;/strong&gt; is the anti-pattern you end up with by accident: lots of&lt;br /&gt;
slow E2E on top, almost nothing underneath.&lt;/p&gt;
&lt;p&gt;The right answer is “it depends,” but you can be precise about what&lt;br /&gt;
it depends on. As web.dev puts it in &lt;em&gt;Pyramid or Crab&lt;/em&gt;, &lt;a href=&quot;https://web.dev/articles/ta-strategies&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;“the testing&lt;br /&gt;
strategy that’s right for your team is unique to your project’s&lt;br /&gt;
context”&lt;/a&gt;. Pactflow, from the&lt;br /&gt;
contract-testing camp, goes further: at scale, full E2E is&lt;br /&gt;
&lt;a href=&quot;https://pactflow.io/blog/proving-e2e-tests-are-a-scam/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a tax with diminishing returns&lt;/a&gt;&lt;br /&gt;
once teams and services multiply.&lt;/p&gt;
&lt;p&gt;The four inputs that change the answer most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Team size.&lt;/strong&gt; Six developers can keep a real E2E suite green together;&lt;br /&gt;
sixty cannot. Coordination cost is what kills E2E suites at scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend control.&lt;/strong&gt; If you don’t own the backend, contract testing&lt;br /&gt;
goes from “nice” to “the only way you can change anything safely”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of services in the flow.&lt;/strong&gt; Each new service multiplies the&lt;br /&gt;
surface that has to be set up, seeded, and reset for an E2E run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment cadence.&lt;/strong&gt; A daily-merge team can’t afford a flaky&lt;br /&gt;
twenty-minute suite; a quarterly-release team can.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick yours and the shape that fits will appear:&lt;/p&gt;
&lt;p&gt;The same pyramid that helps one team can block another from shipping. Pick the shape that fits your constraints.&lt;/p&gt;
&lt;h2&gt;Picking your battles&lt;/h2&gt;
&lt;p&gt;You don’t need every layer on day one. A reasonable order to add them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TypeScript strict + a fast linter + a formatter, wired into Lefthook&lt;br /&gt;
so format and lint run on staged files at commit time.&lt;/li&gt;
&lt;li&gt;Vitest for utilities, run on PR.&lt;/li&gt;
&lt;li&gt;MSW handlers for any module that hits the network, shared between&lt;br /&gt;
tests and the dev server.&lt;/li&gt;
&lt;li&gt;Playwright for the single most important user journey, with hydration&lt;br /&gt;
and CSP listeners wired into a shared fixture.&lt;/li&gt;
&lt;li&gt;Contract testing the moment consumer and provider deploy on different&lt;br /&gt;
cadences. Pact if you control both sides, OpenAPI validation if the&lt;br /&gt;
provider already publishes a spec.&lt;/li&gt;
&lt;li&gt;Preview deployments and Lighthouse CI (light &lt;em&gt;and&lt;/em&gt; dark).&lt;/li&gt;
&lt;li&gt;OpenTelemetry traces and metrics. Point the SDK at a local Jaeger&lt;br /&gt;
via &lt;code&gt;docker-compose&lt;/code&gt; the moment a “why is this slow?” question takes&lt;br /&gt;
more than five minutes to answer. Same SDK in prod (different OTLP&lt;br /&gt;
endpoint) once you have real users.&lt;/li&gt;
&lt;li&gt;Storybook + visual regression once the design system stabilises.&lt;/li&gt;
&lt;li&gt;Accessibility audits in component tests and E2E.&lt;/li&gt;
&lt;li&gt;Knip and bundle-size budgets once the codebase has weight.&lt;/li&gt;
&lt;li&gt;i18n drift checking once you ship a second locale.&lt;/li&gt;
&lt;li&gt;AI code review and SBOM generation once the project has external&lt;br /&gt;
stakeholders to answer to: reviewers, customers, or compliance.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each layer should pay for itself in caught regressions or saved review&lt;br /&gt;
time. Remove the ones that don’t.&lt;/p&gt;
&lt;h2&gt;Supply chain defaults&lt;/h2&gt;
&lt;p&gt;The pipeline above catches the bugs you write. None of it stops a compromised dependency from running code on your laptop. The 2025–2026 wave of &lt;a href=&quot;https://www.npmjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;npm&lt;/a&gt; attacks (Shai-Hulud, the &lt;a href=&quot;https://rspack.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Rspack&lt;/a&gt; postinstall cryptominer, the &lt;a href=&quot;https://axios-http.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;axios&lt;/a&gt; 1.14.1 hijack) made the package manager’s defaults a real part of your security posture. I use &lt;strong&gt;&lt;a href=&quot;https://pnpm.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pnpm&lt;/a&gt;&lt;/strong&gt; on every new project. Three of its settings do most of the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Lifecycle scripts blocked by default.&lt;/strong&gt; Since pnpm 10, &lt;code&gt;preinstall&lt;/code&gt;&lt;br /&gt;
and &lt;code&gt;postinstall&lt;/code&gt; scripts in dependencies do not run on &lt;code&gt;pnpm install&lt;/code&gt;.&lt;br /&gt;
You opt specific packages in via &lt;code&gt;pnpm.onlyBuiltDependencies&lt;/code&gt; in&lt;br /&gt;
&lt;code&gt;package.json&lt;/code&gt;. Most historical supply chain payloads shipped through&lt;br /&gt;
postinstall, so this default removes one of the biggest attack vectors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;minimumReleaseAge&lt;/code&gt;.&lt;/strong&gt; A pnpm 10.16+ setting that refuses to&lt;br /&gt;
resolve a published version until it is at least &lt;em&gt;N&lt;/em&gt; minutes old. Set&lt;br /&gt;
it to &lt;code&gt;1440&lt;/code&gt; (one day) or &lt;code&gt;10080&lt;/code&gt; (one week) in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;.&lt;br /&gt;
Most compromised packages get detected and unpublished within hours,&lt;br /&gt;
so a 24-hour delay covers the common published-and-pulled incidents.&lt;br /&gt;
pnpm 11 makes one day the default. Use &lt;code&gt;minimumReleaseAgeExclude&lt;/code&gt; for&lt;br /&gt;
the few internal or first-party packages you need to install the moment&lt;br /&gt;
they ship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;blockExoticSubdeps&lt;/code&gt;.&lt;/strong&gt; Refuses transitive dependencies pinned to&lt;br /&gt;
git repositories or tarball URLs. Closes a common path for&lt;br /&gt;
typo-squatting and dependency confusion.&lt;/p&gt;
&lt;p&gt;A minimal &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; for a new project:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;minimumReleaseAge&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1440&lt;/span&gt;
&lt;span&gt;blockExoticSubdeps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;onlyBuiltDependencies&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; esbuild
  &lt;span&gt;-&lt;/span&gt; sharp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair this with &lt;strong&gt;OSV-Scanner&lt;/strong&gt; and &lt;strong&gt;Gitleaks&lt;/strong&gt; in CI (layer 11 of the&lt;br /&gt;
pipeline) and you cover both the install-time and the audit-time sides&lt;br /&gt;
of supply chain security.&lt;/p&gt;
&lt;h2&gt;Related resources&lt;/h2&gt;
&lt;p&gt;If you want to read more or start implementing this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://viteplus.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vite+&lt;/a&gt; – unified toolchain from VoidZero (Vite, Rolldown, Vitest, Oxlint, Oxfmt, Tsdown)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pnpm.io/supply-chain-security&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pnpm supply chain security&lt;/a&gt; – the full list of defaults and settings discussed above&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest&lt;/a&gt; – including &lt;a href=&quot;https://vitest.dev/guide/browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;browser mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mswjs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MSW&lt;/a&gt; – network-level API mocking for browser and Node&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pact.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pact&lt;/a&gt; and &lt;a href=&quot;https://pactflow.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PactFlow&lt;/a&gt; – consumer-driven and bi-directional contract testing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://schemathesis.readthedocs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Schemathesis&lt;/a&gt;, &lt;a href=&quot;https://dredd.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dredd&lt;/a&gt;, &lt;a href=&quot;https://stoplight.io/open-source/spectral&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Spectral&lt;/a&gt; – OpenAPI-based contract testing and linting&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lefthook.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lefthook&lt;/a&gt; – fast Git hooks manager (modern Husky alternative)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Storybook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://knip.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Knip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/linter&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Oxlint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Biome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lighthouse CI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lunaria.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Lunaria&lt;/a&gt; – i18n drift detection&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ai/size-limit&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;size-limit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dequelabs/axe-core&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;axe-core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anchore/syft&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Syft&lt;/a&gt; – SBOM generation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.coderabbit.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CodeRabbit&lt;/a&gt;, &lt;a href=&quot;https://www.greptile.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Greptile&lt;/a&gt; – AI code review&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/practical-test-pyramid.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Practical Test Pyramid&lt;/a&gt; – Ham Vocke’s canonical write-up of Mike Cohn’s pyramid.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Testing Trophy&lt;/a&gt; – Kent C. Dodds’ model for where to put the weight of your tests.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.atspotify.com/2018/01/testing-of-microservices&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing of Microservices (Honeycomb)&lt;/a&gt; – Spotify’s case for integrated tests over isolated unit tests in a microservices world.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/ta-strategies&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pyramid or Crab? Find a testing strategy that fits&lt;/a&gt; – web.dev on choosing a shape for your context.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pactflow.io/blog/proving-e2e-tests-are-a-scam/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Proving E2E tests are a scam&lt;/a&gt; – Pactflow’s contract-first counter-position.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/contract-testing-in/9781633437241/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Contract Testing in Action&lt;/a&gt; – Marie Cruz &amp;amp; Lewis Prescott (Manning) on consumer-driven and bi-directional contracts in practice.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.manning.com/books/github-actions-in-action&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub Actions in Action&lt;/a&gt; – Kaufmann, Bos &amp;amp; de Vries (Manning) on workflows, reusable actions, matrix builds, and self-hosted runners.&lt;/li&gt;
&lt;li&gt;Frontend testing guide: 10 essential rules for naming tests&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>testing</category><category>tooling</category><category>ci</category><category>performance</category><category>observability</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Exploratory QA with AI Agents: Building a Site-Agnostic Harness</title><link>https://alexop.dev/posts/exploratory-qa-ai-agents-site-agnostic-harness/</link><guid isPermaLink="true">https://alexop.dev/posts/exploratory-qa-ai-agents-site-agnostic-harness/</guid><description>A thin Bun runner that hands a coding agent one charter and lets it drive a real browser through an exploratory QA session. Works with Claude, Codex, or Copilot, and any browser CLI.</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;&lt;code&gt;explore-qa&lt;/code&gt; is a small Bun runner I built to drive AI agents through exploratory QA sessions. You give it a charter (“explore the basket flow with a mobile viewport”), pick an agent (Claude, Codex, or Copilot), pick a browser CLI (&lt;code&gt;agent-browser&lt;/code&gt; or &lt;code&gt;playwright-cli&lt;/code&gt;), and it runs the session. You get back a Markdown report shaped like a real tester’s notes: SBTM sections, PROOF debrief, screenshots, full JSONL session log.&lt;/p&gt;
&lt;p&gt;About 500 lines of TypeScript plus a folder of Markdown. Site-agnostic by construction.&lt;/p&gt;

  Repo: [github.com/alexanderop/explore-qa](https://github.com/alexanderop/explore-qa). Use it as a template, clone it, and run the `/onboard-site` skill to wire it up to your own site. You&apos;ll need one of [Claude Code](https://docs.claude.com/en/docs/claude-code/overview), [Codex CLI](https://developers.openai.com/codex/cli), or [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) installed to drive the runs.
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git clone&lt;/code&gt; the repo, &lt;code&gt;bun install&lt;/code&gt;, &lt;code&gt;bun link&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Open the folder in Claude Code and run &lt;code&gt;/onboard-site https://yoursite.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qa&lt;/code&gt; to pick a charter, agent, and browser in an interactive wizard&lt;/li&gt;
&lt;li&gt;Read the report under &lt;code&gt;qa-runs/charters/&amp;lt;charter&amp;gt;/&lt;/code&gt;&lt;br /&gt;
&lt;/li&gt;&lt;/ol&gt;

&lt;h2&gt;A real finding on &lt;a href=&quot;http://otto.de&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;otto.de&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before the deep dive, here’s what this actually produces. On a recent run against &lt;a href=&quot;http://otto.de&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;otto.de&lt;/a&gt;’s mobile nav, Codex flagged a small UX bug: the &lt;strong&gt;Filter&lt;/strong&gt; button (bottom right, highlighted in pink) is rendered on the category overview panel, where there are no products to filter yet. It only belongs on the product listing screen, one level deeper.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Nothing broken, no broken-image 404, no crash. The kind of small consistency bug a scripted E2E suite would never notice because nobody wrote a test for “is the filter button absent on this screen.” An agent browsing the menu the way a user would noticed it in under seven minutes.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;Most “AI testing” demos I see lean one way: Playwright on autopilot. A script, some selectors, an LLM picking the next click. It passes green, nobody learns anything about the site, and the bug that shipped to production wasn’t in the happy path anyway.&lt;/p&gt;
&lt;p&gt;I wanted the opposite. A tool that behaves like a QA engineer on their first day. Point it at a website. Give it a one-line mission. Let it browse, notice what’s weird, file what it finds, and hand back a report I can triage in ten minutes. Agents are the first tool that make this economical: one tester per charter, parallel, at roughly the cost of a cup of coffee per run.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Charter&lt;/strong&gt;, one file, one mission. “Explore X with Y to discover Z.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Site profile&lt;/strong&gt;, one file per site. Base URL, viewport, known quirks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness&lt;/strong&gt;, composes a prompt from Markdown fragments, spawns the agent, captures the session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;, a Markdown report with SBTM + PROOF sections, screenshots, and a full JSONL session log.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No hard-coded selectors. No &lt;code&gt;page.click(&apos;.btn-checkout&apos;)&lt;/code&gt;. The agent reads &lt;code&gt;agent-browser --help&lt;/code&gt; at the start of every run and figures out the CLI from scratch. That keeps the harness site-agnostic.&lt;/p&gt;
&lt;p&gt;In one picture:&lt;/p&gt;
&lt;p&gt;You write the charter. The harness builds the prompt. The agent runs the session. You read the report.&lt;/p&gt;
&lt;h2&gt;What scripted E2E tests are bad at&lt;/h2&gt;
&lt;p&gt;Scripted Playwright and Cypress suites do one job well: catch regressions in flows you &lt;em&gt;already know about&lt;/em&gt;. They are bad at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ Finding issues nobody thought to write a test for&lt;/li&gt;
&lt;li&gt;❌ Catching layout breaks on the mobile viewport&lt;/li&gt;
&lt;li&gt;❌ Noticing 404s on the third-party tracking pixel&lt;/li&gt;
&lt;li&gt;❌ Spotting a consent banner that ate the tap target&lt;/li&gt;
&lt;li&gt;❌ Doing an a11y pass on the page you just changed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Exploratory testing is the opposite discipline. Kaner &amp;amp; Bach’s definition is worth quoting because most people get it wrong:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Exploratory testing is simultaneous learning, test design, and test execution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Learning, test design, and execution happen &lt;strong&gt;in parallel&lt;/strong&gt;, not sequentially. Scripts come after, not before. Humans are great at it. LLMs turn out to be surprisingly good at it too, if you build the harness right.&lt;/p&gt;
&lt;h2&gt;Six moving parts&lt;/h2&gt;
&lt;p&gt;Each one does exactly one thing.&lt;/p&gt;
&lt;p&gt;You write a charter. You point it at a site. The harness composes a system prompt plus a user prompt, spawns the agent CLI in fully permissive mode, and the agent drives the browser for the next 5 to 10 minutes while writing a &lt;code&gt;report.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A charter looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; otto&lt;span&gt;-&lt;/span&gt;pdp&lt;span&gt;-&lt;/span&gt;basket
&lt;span&gt;includeFragments&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; _browser&lt;span&gt;-&lt;/span&gt;workflow
  &lt;span&gt;-&lt;/span&gt; _report&lt;span&gt;-&lt;/span&gt;format&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Charter: add to basket from PDP&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Mission&lt;/span&gt;

&lt;span&gt;&amp;gt;&lt;/span&gt; Explore &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;the product detail page → basket flow&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&amp;gt;&lt;/span&gt; with &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;a mobile viewport, no login, a cold cookie&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&amp;gt;&lt;/span&gt; to discover &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;whether a user can add an item and see it in the basket&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;.

Time box: ~5 minutes.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Risks / oracles&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; Size picker blocks add-to-basket on mobile
&lt;span&gt;-&lt;/span&gt; Consent banner obscures the CTA
&lt;span&gt;-&lt;/span&gt; Basket count doesn&apos;t update after add
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. No code. The harness handles everything else.&lt;/p&gt;
&lt;h2&gt;The prompt is the config&lt;/h2&gt;
&lt;p&gt;The prompt isn’t in TypeScript. It’s in Markdown fragments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prompts/
├── _system.md           ← always loaded: &quot;you are a precise, honest QA engineer&quot;
├── _honesty-checks.md   ← always loaded: &quot;don&apos;t invent findings&quot;
├── _browser-workflow.md ← opt-in: &quot;read {{browser}} --help first&quot;
└── _report-format.md    ← opt-in: SBTM + PROOF report shape
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Want to make the agent more aggressive? Edit the Markdown. Want a new report section? Edit the Markdown. There’s no DSL, no YAML template, no indirection layer between you and what the agent reads. &lt;strong&gt;The prompt &lt;em&gt;is&lt;/em&gt; the config.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Rules in &lt;code&gt;_system.md&lt;/code&gt; apply to every run. Rules in &lt;code&gt;_browser-workflow.md&lt;/code&gt; only load when a charter opts in via &lt;code&gt;includeFragments&lt;/code&gt;. Charter bodies stay short because the rules aren’t duplicated across every file.&lt;/p&gt;
&lt;p&gt;Composition is a fan-in:&lt;/p&gt;
&lt;p&gt;Five or six Markdown files in, one deterministic prompt bundle out. The &lt;code&gt;promptHash&lt;/code&gt; is what later lets &lt;code&gt;compare-runs.ts&lt;/code&gt; separate a prompt change from model variance.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;{{browser}} --help&lt;/code&gt; trick&lt;/h2&gt;
&lt;p&gt;Early versions of the harness had backend-specific CLI snippets baked into the prompt. Things like &lt;code&gt;agent-browser session start --mobile&lt;/code&gt;. It worked, until &lt;code&gt;agent-browser&lt;/code&gt; shipped a new subcommand and half my charters broke overnight.&lt;/p&gt;
&lt;p&gt;The fix is one line in &lt;code&gt;_browser-workflow.md&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Before your first action, run &lt;code&gt;{{browser}} --help&lt;/code&gt; and read it. Use the commands you see in the live help, not commands you remember.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now the harness is backend-agnostic by construction. Swap &lt;code&gt;agent-browser&lt;/code&gt; for &lt;code&gt;playwright-cli&lt;/code&gt; in &lt;code&gt;qa.local.json&lt;/code&gt;, and the same charter runs against a different CLI with zero prompt changes.&lt;/p&gt;
&lt;p&gt;✅ Agent reads live help, uses current commands&lt;br /&gt;
❌ Agent relies on memorized subcommands, breaks on every CLI update&lt;/p&gt;
&lt;h2&gt;Three agents, three incompatible CLIs&lt;/h2&gt;
&lt;p&gt;Here’s what the actual invocations look like for the same charter on each backend. I pulled these from &lt;a href=&quot;https://code.claude.com/docs/en/cli-reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Code’s CLI reference&lt;/a&gt;, &lt;a href=&quot;https://developers.openai.com/codex/cli/reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Codex’s &lt;code&gt;exec&lt;/code&gt; reference&lt;/a&gt;, and &lt;a href=&quot;https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub Copilot CLI’s docs&lt;/a&gt;. They all do roughly the same thing. None of them agree on how to do it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$PROMPT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--model&lt;/span&gt; claude-opus-4-6 &lt;span&gt;\&lt;/span&gt;
  --output-format stream-json &lt;span&gt;\&lt;/span&gt;
  --include-partial-messages &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--verbose&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  --append-system-prompt &lt;span&gt;&quot;&lt;span&gt;$SYSTEM_PROMPT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  --add-dir &lt;span&gt;&quot;&lt;span&gt;$RUN_DIR&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  --permission-mode bypassPermissions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Codex CLI:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Codex has no --system-prompt flag. It reads AGENTS.md from --cd.&lt;/span&gt;
&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$SYSTEM_PROMPT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$RUN_DIR&lt;/span&gt;/AGENTS.md&quot;&lt;/span&gt;

codex &lt;span&gt;exec&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--cd&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$RUN_DIR&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;-m&lt;/span&gt; gpt-5.4 &lt;span&gt;\&lt;/span&gt;
  --dangerously-bypass-approvals-and-sandbox &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--json&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;-o&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$LOG_DIR&lt;/span&gt;/codex-last-message.txt&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;&quot;&lt;span&gt;$PROMPT&lt;/span&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;GitHub Copilot CLI:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Copilot also reads AGENTS.md from its working directory.&lt;/span&gt;
&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$SYSTEM_PROMPT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$RUN_DIR&lt;/span&gt;/AGENTS.md&quot;&lt;/span&gt;

&lt;span&gt;cd&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$RUN_DIR&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; copilot &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$PROMPT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--model&lt;/span&gt; gpt-5.4 &lt;span&gt;\&lt;/span&gt;
  --allow-all-tools &lt;span&gt;\&lt;/span&gt;
  --add-dir &lt;span&gt;&quot;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;pwd&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  --output-format json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at how many things differ between three CLIs that all do the same job:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-interactive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System prompt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--append-system-prompt &quot;TEXT&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write &lt;code&gt;AGENTS.md&lt;/code&gt; in &lt;code&gt;--cd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;write &lt;code&gt;AGENTS.md&lt;/code&gt; in cwd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission bypass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--allow-all-tools&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--add-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--cd&lt;/code&gt; / &lt;code&gt;-C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;process &lt;code&gt;cwd&lt;/code&gt; + &lt;code&gt;--add-dir&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model flag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--model&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stream-json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--json&lt;/code&gt; (NDJSON)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final answer&lt;/td&gt;
&lt;td&gt;in the stream&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--output-last-message FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;in the JSON payload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the argument for a runner. Not because the CLIs are hard on their own, they aren’t. The moment you want to run the &lt;em&gt;same&lt;/em&gt; charter across all three, you end up with three invocation templates, three ways of handing over a system prompt, and three output formats to parse on the way back.&lt;/p&gt;
&lt;p&gt;Without the runner, A/B testing one prompt across three agents means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write three shell scripts (one per CLI) that each build the right flag combo.&lt;/li&gt;
&lt;li&gt;Materialize &lt;code&gt;AGENTS.md&lt;/code&gt; for Codex and Copilot but use &lt;code&gt;--append-system-prompt&lt;/code&gt; for Claude.&lt;/li&gt;
&lt;li&gt;Parse &lt;code&gt;stream-json&lt;/code&gt; for Claude, NDJSON events for Codex, and a JSON blob for Copilot, with different “where’s the final answer” rules for each.&lt;/li&gt;
&lt;li&gt;Re-read all three docs every time a CLI cuts a release, because the flags drift.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With the runner, it’s one switch statement in &lt;code&gt;scripts/lib/agents.ts&lt;/code&gt; and one command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;qa otto-pdp-basket claude  agent-browser otto
qa otto-pdp-basket codex   agent-browser otto
qa otto-pdp-basket copilot agent-browser otto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same prompt (identical &lt;code&gt;promptHash&lt;/code&gt;), same charter, same report shape. The per-CLI quirks are one file deep.&lt;/p&gt;
&lt;h2&gt;Two browsers, same charter&lt;/h2&gt;
&lt;p&gt;The same story plays out one layer down at the browser tool. &lt;code&gt;explore-qa&lt;/code&gt; works with two browser CLIs today, &lt;a href=&quot;https://www.npmjs.com/package/agent-browser&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;agent-browser&lt;/code&gt;&lt;/a&gt; and Microsoft’s &lt;a href=&quot;https://github.com/microsoft/playwright-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/a&gt;. They’re also incompatible in all the ways that matter to a script.&lt;/p&gt;
&lt;p&gt;Here’s the same “open a page, snapshot it, click the primary button, screenshot the result” flow on each:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;agent-browser&lt;/strong&gt; (flat, stateful, one global session):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser &lt;span&gt;open&lt;/span&gt; https://example.com
agent-browser &lt;span&gt;set&lt;/span&gt; device &lt;span&gt;&quot;iPhone 15 Pro&quot;&lt;/span&gt;
agent-browser snapshot                          &lt;span&gt;# accessibility tree with @refs&lt;/span&gt;
agent-browser click @ref_07
agent-browser screenshot /abs/path/after-click.png
agent-browser console                            &lt;span&gt;# view logs&lt;/span&gt;
agent-browser close
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;playwright-cli&lt;/strong&gt; (named sessions, YAML snapshots with &lt;code&gt;e&lt;/code&gt;-refs):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;PLAYWRIGHT_CLI_DEVICE&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;iPhone 15 Pro&quot;&lt;/span&gt;     &lt;span&gt;# env-var device emulation&lt;/span&gt;
playwright-cli &lt;span&gt;-s&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;otto &lt;span&gt;open&lt;/span&gt; https://example.com
playwright-cli &lt;span&gt;-s&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;otto snapshot                  &lt;span&gt;# writes YAML with e1, e15, e34…&lt;/span&gt;
playwright-cli &lt;span&gt;-s&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;otto click e15
playwright-cli &lt;span&gt;-s&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;otto screenshot &lt;span&gt;--filename&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;after-click.png
playwright-cli &lt;span&gt;-s&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;otto close
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same flow, very different shape:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;&lt;code&gt;agent-browser&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;playwright-cli&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Navigate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;open &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;open [url]&lt;/code&gt; then &lt;code&gt;goto &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Element identity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@ref&lt;/code&gt; from live snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e1&lt;/code&gt;, &lt;code&gt;e15&lt;/code&gt;, &lt;code&gt;e34&lt;/code&gt; from YAML snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device emulation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set device &quot;iPhone 15 Pro&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PLAYWRIGHT_CLI_DEVICE&lt;/code&gt; env var&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;one implicit global session&lt;/td&gt;
&lt;td&gt;named sessions via &lt;code&gt;-s=&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typing into an input&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type &amp;lt;sel&amp;gt; &amp;lt;text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fill e5 &amp;lt;text&amp;gt;&lt;/code&gt; (or &lt;code&gt;type &quot;text&quot;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot target&lt;/td&gt;
&lt;td&gt;positional path arg&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--filename=&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allow-tool pattern&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bash(agent-browser:*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bash(playwright-cli:*)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The runner abstracts this with two lines of code in &lt;a href=&quot;https://github.com/alexanderopalic/explore-qa/blob/main/scripts/lib/browsers.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;scripts/lib/browsers.ts&lt;/code&gt;&lt;/a&gt; and one line of prompt in &lt;code&gt;_browser-workflow.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;Run &lt;span&gt;`{{browser}} --help`&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;first&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; to learn the available commands and flags.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole trick. Instead of teaching the agent two CLI syntaxes up front, we teach it to &lt;em&gt;discover&lt;/em&gt; the CLI at runtime. The agent reads the live help, picks the subcommands it sees, and runs. Same charter, same prompt, same report. When &lt;code&gt;agent-browser&lt;/code&gt; ships a new subcommand tomorrow, the agent picks it up on the next run without a prompt edit.&lt;/p&gt;
&lt;p&gt;Swap backends with one line in &lt;code&gt;qa.local.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;browser&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;playwright-cli&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or override per invocation:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;qa otto-pdp-basket claude agent-browser  otto
qa otto-pdp-basket claude playwright-cli otto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two runs, two different browser CLIs, identical &lt;code&gt;promptHash&lt;/code&gt;. If the results disagree, the delta is the browser tool. Not a prompt change, not model drift, not you. That’s the kind of signal scripted suites never give you.&lt;/p&gt;
&lt;h2&gt;Replaying one prompt across agents&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;/agent-battle&lt;/code&gt; skill runs the same charter in parallel across all three agent CLIs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /agent-battle otto-cart-to-checkout.md
• Charter: otto-cart-to-checkout, site: otto. Starting all three in parallel.
• Bash(qa otto-cart-to-checkout claude  agent-browser otto)
• Bash(qa otto-cart-to-checkout codex   agent-browser otto)
• Bash(qa otto-cart-to-checkout copilot agent-browser otto)
• All three agents launched.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fan-out:&lt;/p&gt;
&lt;p&gt;Output: a side-by-side comparison on speed, findings, discipline, and report quality. Useful for two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Picking which agent to trust on a given site.&lt;/strong&gt; They don’t perform equally. Claude tends to be more thorough, Codex tends to be faster, Copilot tends to follow the report format most literally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catching prompt regressions.&lt;/strong&gt; If two agents suddenly diverge after a prompt tweak, the tweak probably hurts one of them.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;A real disagreement&lt;/h3&gt;
&lt;p&gt;Here’s an actual run against &lt;a href=&quot;http://otto.de&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;otto.de&lt;/a&gt;’s product detail to basket flow from a week ago (&lt;code&gt;qa-runs/charters/otto-pdp-basket/2026-04-18_battle-summary.md&lt;/code&gt;):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;agent&lt;/th&gt;
&lt;th&gt;duration&lt;/th&gt;
&lt;th&gt;findings&lt;/th&gt;
&lt;th&gt;severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;claude&lt;/td&gt;
&lt;td&gt;07:57&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codex&lt;/td&gt;
&lt;td&gt;06:53&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Major&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;09:26&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Codex flagged a Major finding: “Basket totals stay at single-item values after quantity increases to 2.” Claude and Copilot ran the &lt;em&gt;same&lt;/em&gt; prompt on the &lt;em&gt;same&lt;/em&gt; product in the &lt;em&gt;same&lt;/em&gt; timeframe (identical &lt;code&gt;promptHash&lt;/code&gt;), and both observed the subtotal updating correctly from 39,99 € to 79,98 €. Copilot specifically reran to rule out transient UI state.&lt;/p&gt;
&lt;p&gt;One of them had to be wrong. Because the prompt hashes matched, the disagreement was genuinely agent variance. Not prompt drift, not me editing a fragment, not a model upgrade. That narrowed triage to “whose evidence is stronger?” in about two minutes. (Codex’s finding came from a single DOM read after a 4-second wait. The other two did a reload and a second read. Codex was wrong.)&lt;/p&gt;
&lt;p&gt;Without the harness I’d have run one agent, trusted its report, and either shipped a non-bug as Major or dismissed a real one. Three reports under one &lt;code&gt;promptHash&lt;/code&gt; made the disagreement itself the signal.&lt;/p&gt;
&lt;h2&gt;SBTM + PROOF: why the report looks like a tester’s notes&lt;/h2&gt;
&lt;p&gt;The report format isn’t my invention. It’s &lt;a href=&quot;https://www.satisfice.us/articles/sbtm.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Session-Based Test Management&lt;/a&gt; (James and Jonathan Bach, 2000) plus PROOF, Jon Bach’s five-point debrief shape.&lt;/p&gt;
&lt;p&gt;Every report has these sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session&lt;/strong&gt;, what was tested, what wasn’t&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task breakdown&lt;/strong&gt;, how time was split (design, investigation, setup)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Findings&lt;/strong&gt;, each one with severity, repro, screenshot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;, landmarks, focus order, alt text&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PROOF debrief&lt;/strong&gt;, Past, Results, Obstacles, Outlook, Feelings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one matters. &lt;em&gt;Feelings&lt;/em&gt; is where the agent rates its own confidence, and it’s the first thing I read when triaging. A low-confidence PROOF entry is a signal to rerun with a different agent, not to ship the finding.&lt;/p&gt;
&lt;p&gt;Read Elisabeth Hendrickson’s &lt;a href=&quot;https://pragprog.com/titles/ehxta/explore-it/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;Explore It!&lt;/em&gt;&lt;/a&gt; Chapter 2 and Michael Bolton’s &lt;a href=&quot;https://www.developsense.com/presentations/2007-10-PNSQC-AnExploratoryTestersNotebook.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;An Exploratory Tester’s Notebook&lt;/em&gt;&lt;/a&gt; for the full version. Everything in the prompt fragments is downstream of those three sources.&lt;/p&gt;
&lt;h2&gt;What I’d do differently&lt;/h2&gt;
&lt;p&gt;A few things I learned the hard way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don’t stuff the charter with oracles.&lt;/strong&gt; Three or four risks, each with a one-line oracle, is the sweet spot. More than that and the agent turns the charter into a checklist, which kills exploratory behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Screenshots only on findings and key states.&lt;/strong&gt; Every screenshot is tokens on the return trip. An agent that takes a screenshot after every click runs out of context before it reaches the bug.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let the agent break script.&lt;/strong&gt; &lt;code&gt;_system.md&lt;/code&gt; explicitly tells the agent: “If something unusual catches your eye, follow the trail even if it isn’t in the scenarios.” Most of the interesting findings come from the deviation, not the script.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission mode is all-or-nothing.&lt;/strong&gt; Claude runs with &lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt;, Codex with &lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;, Copilot with &lt;code&gt;--allow-all-tools&lt;/code&gt;. Non-interactive charter runs cannot stop to ask for approval. You either trust the agent with full shell access for the run, or you don’t run it at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Related posts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;How to Use Claude Code as an AI QA Tester with Agent Browser, the single-charter version that predated this harness&lt;/li&gt;
&lt;li&gt;Building an AI QA Engineer with Claude Code and Playwright MCP, the MCP-based cousin&lt;/li&gt;
&lt;li&gt;Spec-Driven Development with Claude Code in Action, how I plan non-trivial work with parallel subagents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent forgets. Your charters don’t.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>testing</category><category>automation</category><category>ai</category><category>agent-browser</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Use Claude Code as an AI QA Tester with Agent Browser</title><link>https://alexop.dev/posts/automated-qa-claude-code-agent-browser-cli-github-actions/</link><guid isPermaLink="true">https://alexop.dev/posts/automated-qa-claude-code-agent-browser-cli-github-actions/</guid><description>Claude Code and Agent Browser let you test your web app in a real browser without hardcoded selectors. Manual browser control, AI-driven exploration, and structured JSON output.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;Claude Code and &lt;a href=&quot;https://github.com/vercel-labs/agent-browser&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent Browser&lt;/a&gt; let you test your web app in a real browser without hardcoded selectors. You point Claude at your app, and it clicks through pages, fills forms, and checks for errors. If the layout changes or a new onboarding flow appears, Claude adjusts. No selectors to maintain.&lt;/p&gt;
&lt;p&gt;You’ll need a web app with a dev server and basic familiarity with the command line and Claude Code.&lt;/p&gt;
&lt;p&gt;You’ll walk away knowing how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Control a browser from the terminal using Agent Browser’s snapshot-and-ref model&lt;/li&gt;
&lt;li&gt;Run one-shot AI-powered browser exploration with &lt;code&gt;claude -p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Separate QA persona from task instructions using &lt;code&gt;--system-prompt&lt;/code&gt; and &lt;code&gt;--append-system-prompt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Get machine-readable structured JSON output with &lt;code&gt;--output-format json&lt;/code&gt; and &lt;code&gt;--json-schema&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Part 1 covers Agent Browser and &lt;code&gt;claude -p&lt;/code&gt; locally. Part 2 wraps it into a GitHub Actions workflow that runs on every PR.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;h3&gt;The problem&lt;/h3&gt;
&lt;p&gt;I’m building a workout tracking PWA, a Vue 3 app with exercises, workout flows, templates, timers, and interactive forms. You fix something on one page and break something on another. I had unit tests and integration tests with Vitest, but no answer to: &lt;strong&gt;does the app work when a real user clicks through it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Manual QA is tedious. I’d deploy a change, open the app, click around for a few minutes, and call it done. Half the time I’d skip pages I didn’t think were affected. I shipped bugs to production: a form that fails without feedback, a navigation link pointing nowhere, a JS error on a page I forgot to check.&lt;/p&gt;
&lt;p&gt;I wanted something that could open the app in a real browser, click around like a user, and flag what’s broken. Scripted Playwright tests with hardcoded selectors need constant maintenance. I wanted a test runner that could handle a changed layout without a rewrite.&lt;/p&gt;
&lt;h3&gt;What we’re building&lt;/h3&gt;
&lt;p&gt;An AI-powered QA workflow where Claude Code drives a real browser via Agent Browser. A single terminal command that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Opens your app in a real browser&lt;/li&gt;
&lt;li&gt;Claude explores it like a user: clicking navigation, filling forms, checking for JS errors&lt;/li&gt;
&lt;li&gt;Returns a structured JSON report with any bugs found&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Part 2 wraps this into a GitHub Actions workflow that runs on PRs and posts bug reports as comments.&lt;/p&gt;
&lt;h3&gt;How Agent Browser works&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/vercel-labs/agent-browser&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent Browser&lt;/a&gt; is a native Rust CLI for browser automation. You issue commands one at a time, which makes it a natural fit for AI agents. It uses Chrome for Testing under the hood, so no Playwright or Node.js runtime is required.&lt;/p&gt;
&lt;p&gt;The key concept is &lt;strong&gt;element refs&lt;/strong&gt;. &lt;code&gt;snapshot&lt;/code&gt; returns an accessibility tree where every interactive element has a ref like &lt;code&gt;@e1&lt;/code&gt;, &lt;code&gt;@e2&lt;/code&gt;. Claude reads the snapshot, decides what to click, and uses the ref.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser &lt;span&gt;open&lt;/span&gt; https://www.otto.de    &lt;span&gt;# Open a URL&lt;/span&gt;
agent-browser snapshot &lt;span&gt;-i&lt;/span&gt;                  &lt;span&gt;# Get interactive elements with refs&lt;/span&gt;
agent-browser click @e2                    &lt;span&gt;# Click element by ref&lt;/span&gt;
agent-browser fill @e3 &lt;span&gt;&quot;value&quot;&lt;/span&gt;             &lt;span&gt;# Fill an input&lt;/span&gt;
agent-browser console                      &lt;span&gt;# Check for JS errors&lt;/span&gt;
agent-browser screenshot                   &lt;span&gt;# Take a screenshot&lt;/span&gt;
agent-browser close                        &lt;span&gt;# Close browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude repeats this loop until it has enough information to report:&lt;/p&gt;
&lt;p&gt;A concrete example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude runs &lt;code&gt;snapshot -i&lt;/code&gt; and sees: &lt;code&gt;nav[@e10] &amp;gt; a[@e11] &quot;Home&quot; | a[@e12] &quot;Workouts&quot; | a[@e13] &quot;Settings&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Claude decides to test navigation and runs &lt;code&gt;click @e12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Claude runs &lt;code&gt;snapshot -i&lt;/code&gt; again and sees the Workouts page loaded&lt;/li&gt;
&lt;li&gt;Claude concludes navigation works&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Agent Browser uses a client-daemon architecture. The daemon starts on the first command and persists between commands, so subsequent operations are fast.&lt;/p&gt;
&lt;h2&gt;Before you start&lt;/h2&gt;
&lt;p&gt;Before you start, you should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have a web app with a dev server (we use Vite, but any web app works; the examples use &lt;code&gt;https://www.otto.de&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Have Node.js and npm installed&lt;/li&gt;
&lt;li&gt;Have Claude Code installed (&lt;code&gt;npm install -g @anthropic-ai/claude-code&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 1: Agent Browser by hand&lt;/h2&gt;
&lt;p&gt;Before letting Claude drive, get comfortable with Agent Browser yourself. Any public website works.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Agent Browser globally and set up the browser:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; &lt;span&gt;-g&lt;/span&gt; agent-browser
agent-browser &lt;span&gt;install&lt;/span&gt;  &lt;span&gt;# Download Chrome from Chrome for Testing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open a website:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser &lt;span&gt;open&lt;/span&gt; https://www.otto.de
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll see:&lt;/p&gt;
&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;✓ OTTO - Mode, Möbel &amp;amp; Technik » Zum Online-Shop
  https://www.otto.de/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Take a snapshot to see the page structure:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser snapshot &lt;span&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-i&lt;/code&gt; flag shows only interactive elements. You’ll see something like:&lt;/p&gt;
&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;- button &quot;Zur Hauptnavigation&quot; [ref=e1]
- link &quot;zur Homepage&quot; [ref=e2]
- searchbox &quot;Wonach suchst du?&quot; [ref=e3]
- button &quot;Suche abschicken&quot; [ref=e4]
- link &quot;s Service&quot; [ref=e5]
- link &quot;Θ Mein Konto&quot; [ref=e6]
- link &quot;Merkzettel&quot; [ref=e7]
- link &quot;Inspiration&quot; [ref=e8]
- link &quot;Damen-Mode&quot; [ref=e9]
- link &quot;Herren-Mode&quot; [ref=e10]
- link &quot;Baby &amp;amp; Kind&quot; [ref=e11]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;[ref=eN]&lt;/code&gt; is a handle you can interact with using &lt;code&gt;@eN&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try clicking a nav link:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser click @e9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser navigates to the Damen-Mode page. Take another snapshot to see the new page:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser snapshot &lt;span&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try filling the search box and submitting:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser fill @e3 &lt;span&gt;&quot;Sneaker&quot;&lt;/span&gt;
agent-browser press Enter
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check for JavaScript errors:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser console
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Close the browser when you’re done:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-browser close
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Claude runs the same commands in the same order. It reads the snapshot output, picks what to click next, and loops until it has a report.&lt;/p&gt;
&lt;h2&gt;Step 2: Let Claude drive with &lt;code&gt;claude -p&lt;/code&gt;&lt;/h2&gt;
&lt;aside&gt;
`claude -p` turns Claude Code into a general-purpose agent. Browser testing is one use case, but the same flags work for data processing, file organization, API calls. If you can script it in a terminal, you can hand it to `claude -p`.
&lt;p&gt;The official &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/sdk&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude SDK&lt;/a&gt; (&lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt;) exposes the same capabilities programmatically. The flags you learn here (&lt;code&gt;--allowedTools&lt;/code&gt;, &lt;code&gt;--json-schema&lt;/code&gt;, &lt;code&gt;--append-system-prompt&lt;/code&gt;) map directly to SDK parameters in TypeScript and Python.&lt;/p&gt;
&lt;p&gt;You can also wire &lt;code&gt;claude -p&lt;/code&gt; into shell aliases in your &lt;code&gt;.zshrc&lt;/code&gt; for one-word commands like &lt;code&gt;cpush&lt;/code&gt; (auto-commit and push) or &lt;code&gt;clint&lt;/code&gt; (lint your project). See Claude Code Slash Commands Guide for examples.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;&lt;code&gt;claude -p&lt;/code&gt; runs Claude Code with a one-shot prompt from the terminal. No interactive session, no back-and-forth.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run Claude with a simple exploration prompt. Say “agent-browser cli” in the prompt. If you leave out “cli”, Claude doesn’t know which tool to use:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;Open https://www.otto.de using agent-browser cli. \
Take a snapshot. Click one navigation link. \
Take another snapshot. \
Tell me what you found.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
&lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Watch it work. You’ll see each tool call in real time.&lt;/p&gt;
&lt;p&gt;Outcome:&lt;/p&gt;
&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;Here&apos;s what I found:

Snapshot 1 — otto.de Homepage:
- OTTO is a major German online shop for fashion, furniture, and electronics
- The top navigation has categories: Inspiration, Damen-Mode, Herren-Mode,
  Baby &amp;amp; Kind, Sport, Beauty &amp;amp; Drogerie, Multimedia, Haushalt, Küche,
  Heimtextilien, Möbel, Baumarkt, Marken, Sale
- Several promotional banners were visible:
  - 10 EUR coupon for new customers (valid until April 30)
  - 20% extra on fashion &amp;amp; sport (valid until April 13)
  - 20% extra for OTTO-Up members on selected furniture
  - 10% extra on Samsung TVs &amp;amp; soundbars (valid until April 20)

Snapshot 2 — Multimedia category page:
After clicking &quot;Multimedia&quot;, the page showed a structured product catalog
with subcategories:
- Fernseher (TVs): OLED (69), 4K (480), QLED (236), Smart-TVs (428),
  LED (555), 8K (2)
- Heimkino: Surround systems (186), Soundbars (228),
  Beamer/Projectors (603), Blu-ray players (20), Subwoofers (651),
  DVD players (76)
- Additional categories for audio, computing, gaming, and accessories

The site is fully functional, well-structured with clear navigation,
and currently running several spring sales promotions.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now give Claude a more structured QA prompt:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;You are a QA tester. Open https://www.otto.de using agent-browser cli.

Test the following:
1. Does the homepage load? (take a snapshot)
2. Does navigation work? (click 2 links, snapshot each)
3. Are there any JS errors? (check console)

Report your findings as:
- PASS or FAIL for each test
- Any bugs found&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
&lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outcome:&lt;/p&gt;
&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;QA Report: otto.de

Test 1: Homepage Load — PASS
- URL: https://www.otto.de/
- Title: &quot;OTTO - Mode, Möbel &amp;amp; Technik » Zum Online-Shop&quot;
- Page rendered fully with banner, search bar, navigation,
  promotional coupons, and product categories

Test 2: Navigation — PASS (both links)
1. Damen-Mode → https://www.otto.de/damen/ — PASS
2. Multimedia → https://www.otto.de/technik/multimedia/ — PASS
- Both pages loaded correctly with proper titles and content

Test 3: JS Errors — PASS
- agent-browser errors returned no page errors across all 3 pages
- agent-browser console returned no console warnings/errors

Bugs Found: None.
All tests passed cleanly with no JS errors or navigation issues.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step 2b: Separate persona from task with system prompts&lt;/h2&gt;
&lt;p&gt;In the previous step, the QA persona (“You are a QA tester”) was mixed into the task prompt. Claude Code has flags to separate these concerns: &lt;code&gt;--system-prompt&lt;/code&gt; replaces the default system prompt, and &lt;code&gt;--append-system-prompt&lt;/code&gt; adds to it.&lt;/p&gt;
&lt;p&gt;The Step 2 example rewritten with &lt;code&gt;--append-system-prompt&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;Open https://www.otto.de using agent-browser cli.

Test the following:
1. Does the homepage load? (take a snapshot)
2. Does navigation work? (click 2 links, snapshot each)
3. Are there any JS errors? (check console)

Report your findings as:
- PASS or FAIL for each test
- Any bugs found&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    --append-system-prompt &lt;span&gt;&quot;You are a senior QA engineer. Always use \
&apos;agent-browser snapshot -i&apos; for interactive elements only. Be concise \
and structured in your reports.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    &lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    --max-turns &lt;span&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outcome:&lt;/p&gt;
&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;## QA Report: otto.de

| # | Test | Result |
|---|------|--------|
| 1 | **Homepage loads** | **PASS** — Title: &quot;OTTO - Mode, Möbel &amp;amp; Technik » Zum Online-Shop&quot;. Full nav bar, search, product recommendations, and brand sections all rendered. |
| 2a | **Navigation: Multimedia** | **PASS** — Clicked &quot;Multimedia&quot; nav link. Category page loaded with subcategories (Fernseher, Heimkino, Audio, etc.) with product counts. |
| 2b | **Navigation: Möbel** | **PASS** — Clicked &quot;Möbel&quot; nav link. Category page loaded with subcategories (Wohnzimmer, Schlafzimmer, etc.) with product counts. |
| 3 | **JS console errors** | **PASS** — No JavaScript errors detected. No failed resource loads (HTTP 4xx/5xx). |

**Bugs found:** None.

All 4 checks passed. The site loads correctly, navigation between category pages works as expected, and no client-side errors were observed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the task prompt describes &lt;em&gt;what&lt;/em&gt; to test. You put the persona and behavioral rules in &lt;code&gt;--append-system-prompt&lt;/code&gt;, which applies them on every turn without cluttering the task.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;--system-prompt&lt;/code&gt; vs &lt;code&gt;--append-system-prompt&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--append-system-prompt&lt;/code&gt;&lt;/strong&gt; adds your instructions on top of Claude Code’s defaults. Claude still knows how to use Bash, handle errors, etc. Best for most cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--system-prompt&lt;/code&gt;&lt;/strong&gt; replaces the entire default system prompt. You get full control but lose built-in behaviors. Use this when you want a stripped-down agent that only runs &lt;code&gt;agent-browser&lt;/code&gt; commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Other useful flags&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-turns N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caps how many tool-call turns Claude gets. Match to your prompt’s scope: 5-8 for a single assertion, 15 for a smoke test, 30+ for deep exploration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pick the model. Use &lt;code&gt;opus&lt;/code&gt; for thorough QA where you want Claude to reason carefully, &lt;code&gt;sonnet&lt;/code&gt; for fast smoke tests.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--output-format json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Machine-readable output (covered in Step 4).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Example with all flags&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;Open https://www.otto.de using agent-browser cli.
Navigate to every page in the main navigation.
Check console for JS errors on each page.
Report PASS/FAIL per page.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    --append-system-prompt &lt;span&gt;&quot;You are a senior QA engineer testing a \
web shop. Use &apos;agent-browser snapshot -i&apos; for interactive \
elements only. Be concise.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    &lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    --max-turns &lt;span&gt;20&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
    &lt;span&gt;--model&lt;/span&gt; opus
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Try it on your own app&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Point Claude at your app (here we use &lt;a href=&quot;http://otto.de&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;otto.de&lt;/a&gt;, but swap in your own URL):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;Open https://www.otto.de using agent-browser. \
Take a snapshot. Navigate to 3 different pages. \
Check console for JS errors on each page. \
Report what you found.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
&lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step 4: Get structured JSON output&lt;/h2&gt;
&lt;p&gt;Free-text output works for manual review, but you can’t branch a shell script on “PASS” buried in a markdown table.&lt;/p&gt;
&lt;p&gt;Claude’s output is non-deterministic. You can’t predict the exact wording. But your downstream decisions are deterministic: fail the build or don’t, post a comment or don’t.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--json-schema&lt;/code&gt; closes that gap. You define the output shape upfront, and Claude conforms to it. Claude still explores pages, interprets errors, and judges severity, but you get a structured JSON object with known fields and value types. Your script reads &lt;code&gt;verdict&lt;/code&gt; and branches.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--output-format json&lt;/code&gt; returns a JSON object with session metadata and the result. &lt;code&gt;--json-schema&lt;/code&gt; forces Claude’s response to conform to your schema, landing in a &lt;code&gt;structured_output&lt;/code&gt; field.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run Claude with a JSON schema to get a structured QA report:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;Open https://www.otto.de using agent-browser cli.
Take a snapshot to verify the page loads.
Click one navigation link and take another snapshot.
Check the console for JS errors.
Return your findings as structured JSON.&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
--output-format json &lt;span&gt;\&lt;/span&gt;
--json-schema &lt;span&gt;&apos;{
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;verdict&quot;: {
      &quot;type&quot;: &quot;string&quot;,
      &quot;enum&quot;: [&quot;HEALTHY&quot;, &quot;MINOR_ISSUES&quot;, &quot;CRITICAL_BUGS&quot;]
    },
    &quot;summary&quot;: { &quot;type&quot;: &quot;string&quot; },
    &quot;bugs&quot;: {
      &quot;type&quot;: &quot;array&quot;,
      &quot;items&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
          &quot;severity&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;enum&quot;: [&quot;critical&quot;, &quot;major&quot;, &quot;minor&quot;] },
          &quot;title&quot;: { &quot;type&quot;: &quot;string&quot; },
          &quot;description&quot;: { &quot;type&quot;: &quot;string&quot; }
        },
        &quot;required&quot;: [&quot;severity&quot;, &quot;title&quot;, &quot;description&quot;]
      }
    }
  },
  &quot;required&quot;: [&quot;verdict&quot;, &quot;summary&quot;, &quot;bugs&quot;]
}&apos;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
&lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full JSON response looks like this (trimmed for readability):&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;result&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;subtype&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;is_error&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;duration_ms&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;78276&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;duration_api_ms&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;71827&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;num_turns&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;13&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;result&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;**Results:**\n\n| Step | Result |\n|------|--------|\n| Open https://www.otto.de | Loaded — title: \&quot;OTTO - Mode, Möbel &amp;amp; Technik » Zum Online-Shop\&quot; |\n| Snapshot #1 (homepage) | Full navigation rendered |\n| Click \&quot;Multimedia\&quot; nav link | Navigated to /technik/multimedia/ |\n| Snapshot #2 (Multimedia page) | Page rendered with breadcrumb, category heading, and sub-navigation |\n| Console JS errors | **None detected** |\n\n**Verdict: HEALTHY**&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;total_cost_usd&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0.32&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;structured_output&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;verdict&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;HEALTHY&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;summary&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;otto.de loads successfully with full navigation, promotional banners, and product content. Navigation to the Multimedia section (/technik/multimedia/) works correctly. No JavaScript console errors or failed resource loads were detected.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;bugs&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;result&lt;/code&gt; field contains Claude’s free-text response. The &lt;code&gt;structured_output&lt;/code&gt; field contains the validated JSON that conforms to your schema. You also get metadata like &lt;code&gt;duration_ms&lt;/code&gt;, &lt;code&gt;num_turns&lt;/code&gt;, and &lt;code&gt;total_cost_usd&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Extract the structured output with &lt;code&gt;jq&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Pipe the full output through jq to get just the structured data&lt;/span&gt;
claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;...&quot;&lt;/span&gt; --output-format json --json-schema &lt;span&gt;&apos;...&apos;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;&apos;.structured_output&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;verdict&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;HEALTHY&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;summary&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;otto.de loads successfully with full navigation, promotional banners, and product content. Navigation to the Multimedia section (/technik/multimedia/) works correctly. No JavaScript console errors or failed resource loads were detected.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;bugs&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use it in a script to make pass/fail decisions:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;VERDICT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;claude &lt;span&gt;-p&lt;/span&gt; &lt;span&gt;&quot;...&quot;&lt;/span&gt; --output-format json --json-schema &lt;span&gt;&apos;...&apos;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;--allowedTools&lt;/span&gt; &lt;span&gt;&quot;Bash(agent-browser*)&quot;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.structured_output.verdict&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;[&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$VERDICT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;CRITICAL_BUGS&quot;&lt;/span&gt; &lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;then&lt;/span&gt;
  &lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;QA failed!&quot;&lt;/span&gt;
  &lt;span&gt;exit&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;
&lt;span&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside &lt;code&gt;claude -p&lt;/code&gt;, Claude explores the app, interprets what it sees, and judges severity. Outside, the &lt;code&gt;if&lt;/code&gt; statement and &lt;code&gt;exit 1&lt;/code&gt; are plain bash. &lt;code&gt;--json-schema&lt;/code&gt; gives you a contract between the two. Without it, you’d grep Claude’s prose for the word “CRITICAL” and hope it doesn’t rephrase.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This same mechanism powers the GitHub Actions workflow in Part 2. Get it working locally first, fewer surprises in CI.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;You now have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser control from the terminal with Agent Browser’s snapshot-and-ref model&lt;/li&gt;
&lt;li&gt;One-shot AI-powered exploration with &lt;code&gt;claude -p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Persona separation via &lt;code&gt;--append-system-prompt&lt;/code&gt;, execution control via &lt;code&gt;--max-turns&lt;/code&gt; and &lt;code&gt;--model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Structured JSON output with &lt;code&gt;--output-format json&lt;/code&gt; and &lt;code&gt;--json-schema&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this runs locally. Part 2 moves it into CI: a GitHub Actions workflow using Claude Code Action that runs Claude as a QA agent on every PR, posts structured bug reports as comments, and sets commit statuses (green/yellow/red). Coming soon.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Building an AI QA Engineer with Playwright MCP, the MCP-based version with richer browser control&lt;/li&gt;
&lt;li&gt;Claude Code Skills, Hooks, and Subagents, creating reusable skills for your QA prompts&lt;/li&gt;
&lt;li&gt;Claude Code Customization Guide, deeper coverage of &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;, skills, and subagents&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vercel-labs/agent-browser&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent Browser documentation&lt;/a&gt;, full command reference and setup guide&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>claude-code</category><category>testing</category><category>automation</category><category>agent-browser</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Catch Hydration Errors in Playwright Tests (Astro, Nuxt, React SSR)</title><link>https://alexop.dev/posts/catch-hydration-errors-playwright-tests/</link><guid isPermaLink="true">https://alexop.dev/posts/catch-hydration-errors-playwright-tests/</guid><description>A Playwright fixture that listens to the browser console for hydration mismatch warnings and fails your E2E tests when server and client HTML disagree.</description><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Your E2E tests pass. The page loads, buttons work.&lt;/p&gt;
&lt;p&gt;But open the browser console:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Hydration failed because the server rendered HTML didn&apos;t match the client.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a hydration mismatch. The server sent one thing and the client replaced it with something else. The page still works, so you don’t notice. Your tests don’t check for it, so they pass.&lt;/p&gt;
&lt;h2&gt;What are SSR and hydration?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SSR (server-side rendering)&lt;/strong&gt; means the server generates HTML and sends it to the browser before JavaScript loads. Users see content before client code boots, and search engines can index it.&lt;/p&gt;
&lt;p&gt;Astro and Nuxt build on this model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hydration&lt;/strong&gt; is the next step: client JavaScript takes over the server-rendered HTML, attaching event handlers and state to the existing markup. The contract: the first client render must match what the server sent.&lt;/p&gt;
&lt;p&gt;When it does not match, the framework discards the server HTML and re-renders on the client. That re-render is a hydration mismatch.&lt;/p&gt;
&lt;h3&gt;Common causes&lt;/h3&gt;
&lt;p&gt;Anything that produces different HTML on client and server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reading &lt;code&gt;localStorage&lt;/code&gt; or &lt;code&gt;window.matchMedia()&lt;/code&gt; during render&lt;/li&gt;
&lt;li&gt;Calling &lt;code&gt;new Date()&lt;/code&gt; or &lt;code&gt;Math.random()&lt;/code&gt; during render&lt;/li&gt;
&lt;li&gt;Formatting dates or numbers differently across server and client&lt;/li&gt;
&lt;li&gt;Rendering conditional branches based on browser-only state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Theme toggles and locale formatting cause most of them.&lt;/p&gt;
&lt;p&gt;If you use Vue with SSR, the &lt;code&gt;window is not defined&lt;/code&gt; error comes from the same root cause. VueUse has a pattern for it:&lt;/p&gt;
&lt;h2&gt;A real bug I found&lt;/h2&gt;
&lt;p&gt;I was working on an Astro page and my theme hook was reading browser state during the first render:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;getInitialTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Theme &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; stored &lt;span&gt;=&lt;/span&gt; localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SITE&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;themeStorageKey&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;stored &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;light&quot;&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; stored &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; stored&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matchMedia&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;(prefers-color-scheme: dark)&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;matches &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;light&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;theme&lt;span&gt;,&lt;/span&gt; setTheme&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Theme&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;getInitialTheme&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server defaulted to &lt;code&gt;dark&lt;/code&gt;, but the browser picked &lt;code&gt;light&lt;/code&gt;. React saw the mismatch, logged a hydration warning, and re-rendered from scratch.&lt;/p&gt;
&lt;p&gt;The page still worked, the button still existed. Normal E2E tests passed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; start with a deterministic value, resolve browser state after mount.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;theme&lt;span&gt;,&lt;/span&gt; setTheme&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Theme&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;mounted&lt;span&gt;,&lt;/span&gt; setMounted&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;useEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; preferredTheme &lt;span&gt;=&lt;/span&gt; &lt;span&gt;getPreferredTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    document&lt;span&gt;.&lt;/span&gt;documentElement&lt;span&gt;.&lt;/span&gt;classList&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; preferredTheme &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;setTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;preferredTheme&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;setMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;useEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;mounted&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; root &lt;span&gt;=&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;documentElement&lt;span&gt;;&lt;/span&gt;
    root&lt;span&gt;.&lt;/span&gt;classList&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; theme &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SITE&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;themeStorageKey&lt;span&gt;,&lt;/span&gt; theme&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;mounted&lt;span&gt;,&lt;/span&gt; theme&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; theme&lt;span&gt;,&lt;/span&gt; setTheme&lt;span&gt;,&lt;/span&gt; &lt;span&gt;toggleTheme&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setTheme&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;t &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&quot;light&quot;&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The core idea&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Listen to the browser console during a Playwright test. If a hydration warning appears, fail the test.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;React and Vue log hydration mismatches to the console. You don’t check the console during automated tests, so this fixture does.&lt;/p&gt;
&lt;h2&gt;The fixture&lt;/h2&gt;

Fixtures are Playwright&apos;s way of setting up and tearing down what each test needs. Built-in fixtures like `page` and `browser` come for free. You create custom ones with `base.extend()`. Each fixture runs when a test requests it and gets cleaned up afterward. The fixture below injects `hydrationErrors` and `runtimeErrors` into every test that asks for them.

&lt;p&gt;I first saw this approach in the &lt;a href=&quot;https://npmx.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;npmx.dev&lt;/a&gt; open source project and adapted it for my Astro site. My version covers React and Vue hydration strings and catches uncaught runtime exceptions:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;HYDRATION_ERROR_PATTERNS&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;hydration failed because the server rendered html didn&apos;t match the client&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;hydration completed but contains mismatches&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;hydration text content mismatch&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;hydration node mismatch&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;hydration attribute mismatch&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;isHydrationError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;HYDRATION_ERROR_PATTERNS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;pattern&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; pattern&lt;span&gt;.&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;toConsoleText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;message&lt;span&gt;:&lt;/span&gt; ConsoleMessage&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; test &lt;span&gt;=&lt;/span&gt; base&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  hydrationErrors&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  runtimeErrors&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;hydrationErrors&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; page &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; use&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; hydrationErrors&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;handleConsole&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;message&lt;span&gt;:&lt;/span&gt; ConsoleMessage&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; text &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toConsoleText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isHydrationError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        hydrationErrors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;console&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handleConsole&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;hydrationErrors&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;off&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;console&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handleConsole&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;

  &lt;span&gt;runtimeErrors&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; page &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; use&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; runtimeErrors&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;handleConsole&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;message&lt;span&gt;:&lt;/span&gt; ConsoleMessage&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; text &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toConsoleText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;message&lt;span&gt;.&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; text&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;&lt;span&gt;isHydrationError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        runtimeErrors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;handlePageError&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;:&lt;/span&gt; Error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      runtimeErrors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;error&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;console&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handleConsole&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;pageerror&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handlePageError&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;runtimeErrors&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;off&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;console&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handleConsole&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;off&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;pageerror&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; handlePageError&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; expect &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop this into &lt;code&gt;test/e2e/test-utils.ts&lt;/code&gt; and import from there instead of &lt;code&gt;@playwright/test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Related: a full AI-driven QA workflow with Playwright:&lt;/p&gt;
&lt;h2&gt;Using it&lt;/h2&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;home page hydrates cleanly&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; page&lt;span&gt;,&lt;/span&gt; hydrationErrors&lt;span&gt;,&lt;/span&gt; runtimeErrors &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; waitUntil&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;domcontentloaded&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;heading&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Home&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;hydrationErrors&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toEqual&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;runtimeErrors&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toEqual&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start with your homepage. Add one interactive route, then one with a theme toggle or client-only widget. That surfaces most bugs.&lt;/p&gt;
&lt;h2&gt;How npmx.dev does it at scale&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://npmx.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;npmx.dev&lt;/a&gt; project tests hydration correctness for every combination of user settings across every page, around 48 checks from a single fixture.&lt;/p&gt;
&lt;p&gt;They inject &lt;code&gt;localStorage&lt;/code&gt; values via Playwright’s &lt;code&gt;page.addInitScript()&lt;/code&gt; before navigation, simulating a returning user with saved preferences. Returning users with non-default settings trigger most hydration mismatches.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;PAGES&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/about&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/settings&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/compare&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/search&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/package/nuxt&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

test&lt;span&gt;.&lt;/span&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;color mode: dark&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; page &lt;span&gt;of&lt;/span&gt; &lt;span&gt;PAGES&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;page&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; page&lt;span&gt;:&lt;/span&gt; pw&lt;span&gt;,&lt;/span&gt; goto&lt;span&gt;,&lt;/span&gt; hydrationErrors &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;injectLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;pw&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;npmx-color-mode&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;dark&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; waitUntil&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;hydration&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;hydrationErrors&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toEqual&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;injectLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;:&lt;/span&gt; Page&lt;span&gt;,&lt;/span&gt; entries&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addInitScript&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;e&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; value&lt;span&gt;]&lt;/span&gt; &lt;span&gt;of&lt;/span&gt; Object&lt;span&gt;.&lt;/span&gt;&lt;span&gt;entries&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; entries&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They repeat this for every setting type, locale, accent color, background theme, package manager, relative dates, each with a non-default value. If any combination causes a hydration mismatch on any page, the test fails.&lt;/p&gt;
&lt;p&gt;Their fixture uses Vue-specific error strings (&lt;code&gt;&quot;Hydration completed but contains mismatches&quot;&lt;/code&gt;) while mine uses React patterns. The approach is the same, only the strings you match against change.&lt;/p&gt;
&lt;p&gt;More on how E2E tests relate to unit and integration tests:&lt;/p&gt;
&lt;p&gt;If you ship an SSR app and do not check for hydration errors in your browser tests, you have one in production right now.&lt;/p&gt;

          </content:encoded><category>testing</category><category>javascript</category><category>performance</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>App Screenshots: An AI Coding Agent Skill for Visual Documentation</title><link>https://alexop.dev/posts/app-screenshots-claude-code-skill/</link><guid isPermaLink="true">https://alexop.dev/posts/app-screenshots-claude-code-skill/</guid><description>Automate annotated screenshot documentation for any web app or live website using an AI coding agent skill. Works with Claude Code, Cursor, Windsurf, or any agent that supports custom skills.</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;I built a &lt;a href=&quot;https://github.com/alexanderop/app-screenshots&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;skill&lt;/a&gt; that takes annotated screenshots of any web app or live website and generates a markdown visual guide. It works with Claude Code, Cursor, Windsurf, or any AI coding agent that supports custom skills.&lt;/p&gt;
&lt;h2&gt;What Is a Skill?&lt;/h2&gt;
&lt;p&gt;A skill is a markdown file with instructions for a coding agent. It describes how to perform a specific task and can reference CLI tools, APIs, or other resources. You write the steps in markdown, the agent follows them. A general-purpose coding agent with the right skill becomes a specialist, no code changes needed. For a deeper look at how skills fit into the broader ecosystem, see the guide to &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;, skills, and subagents.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Documenting UIs is tedious. You open the app, take a screenshot, annotate it in some tool, write descriptions, repeat for every page. Automate it.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The skill uses &lt;a href=&quot;https://github.com/vercel-labs/agent-browser&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;agent-browser&lt;/code&gt;&lt;/a&gt;, a headless browser automation CLI built by Vercel specifically for AI agents. Instead of heavy tools like Playwright, it provides a lightweight snapshot + refs system that lets agents navigate, click, and screenshot pages with minimal context usage. It works with Claude Code, Codex, Cursor, Gemini CLI, and more.&lt;/p&gt;
&lt;p&gt;You point your coding agent at a local dev server or a public URL and it does the rest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Discovers pages&lt;/strong&gt; by reading the site’s navigation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Screenshots each page&lt;/strong&gt; with SVG annotations injected directly into the DOM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generates a markdown file&lt;/strong&gt; with numbered references to each annotation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Annotations come in three types: &lt;code&gt;box&lt;/code&gt; for sections, &lt;code&gt;click&lt;/code&gt; for interactive elements, and &lt;code&gt;circle&lt;/code&gt; for general callouts. Each screenshot gets up to 3 annotations with auto-rotating colors. If you like auto-generated visual docs, the walkthrough skill does something similar for codebases with interactive Mermaid diagrams.&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Homepage&lt;/span&gt;

The landing page shows a hero banner with seasonal promotions.
Use the &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Search&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; bar (1) to find products.
The &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Category navigation&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; (2) provides access to all departments.

&lt;span&gt;&lt;span&gt;!&lt;/span&gt;[&lt;span&gt;Homepage&lt;/span&gt;](&lt;span&gt;screenshots/01-homepage.png&lt;/span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Live Sites Just Work&lt;/h2&gt;
&lt;p&gt;It handles cookie banners, lazy-loaded content, bot protection. You can document &lt;code&gt;otto.de&lt;/code&gt;, &lt;code&gt;github.com&lt;/code&gt;, or your own staging environment with the same command.&lt;/p&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Install the prerequisite:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; &lt;span&gt;-g&lt;/span&gt; agent-browser &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; agent-browser &lt;span&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then tell your coding agent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Screenshot the app&quot;
&quot;Document otto.de with screenshots&quot;
&quot;Give me a visual guide of the checkout flow&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skill figures out whether you mean a local dev server or a live URL and adapts accordingly.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;GitHub: &lt;a href=&quot;https://github.com/alexanderop/app-screenshots&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/alexanderop/app-screenshots&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you’re building your own skill library, see how I built a skill for searching Claude’s conversation history.&lt;/p&gt;

          </content:encoded><category>ai</category><category>coding-agents</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Software Factory: Why Your Team Will Never Work the Same Again</title><link>https://alexop.dev/posts/the-software-factory/</link><guid isPermaLink="true">https://alexop.dev/posts/the-software-factory/</guid><description>We already have everything we need to build software factories. Teams will change. The only variable is speed.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;p&gt;&amp;lt;TLDR items={[&lt;br /&gt;
“AI coding agents like Claude Code already have everything needed to build software factories today”,&lt;br /&gt;
“Teams shrink from 12 specialists to 5 generalists, everyone becomes a builder with agents as their execution layer”,&lt;br /&gt;
“Skills (markdown files that teach agents new capabilities) are the key to scaling what your factory can do”,&lt;br /&gt;
“The best factories are self-improving: agents read user feedback, A/B tests, and error logs to automatically populate the backlog”,&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;h2&gt;We Already Have Everything We Need&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;The current models and tooling are enough to build software factories. Today.&lt;/p&gt;
&lt;p&gt;In a software factory, developers stop writing code by hand. AI coding agents implement features and fix bugs while developers design and improve the factory.&lt;/p&gt;
&lt;p&gt;Or as Lee Edwards from Root Ventures &lt;a href=&quot;https://sfstandard.com/2026/02/19/ai-writes-code-now-s-left-software-engineers/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;put it&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“It’s like giving them a nuclear-powered six-axis mill. It’s a single-person software factory.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anthropic ships the building blocks today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Code&lt;/a&gt; is an agentic coding tool that lives in your terminal, understands your codebase, and can write code, run tests, and open pull requests&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/headless&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Headless mode&lt;/a&gt; lets you run Claude Code programmatically in CI/CD pipelines, GitHub Actions, or any automation script&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/scheduled-tasks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Scheduled tasks&lt;/a&gt; let agents run on a timer: daily code reviews, dependency checks, morning briefings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/sub-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Subagents&lt;/a&gt; are specialized agents you can create for specific tasks, each with their own tools and permissions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/agent-teams&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent teams&lt;/a&gt; let you orchestrate multiple Claude Code sessions working in parallel on different parts of a problem&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/skills&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Skills&lt;/a&gt; extend what your agents can do with simple Markdown files&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://platform.claude.com/docs/en/agent-sdk/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Agent SDK&lt;/a&gt; lets you build production multi-agent systems with orchestration, guardrails, and tracing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.blog/changelog/2026-02-04-claude-and-codex-are-now-available-in-public-preview-on-github/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude on GitHub&lt;/a&gt; lets Claude commit code and comment on PRs directly from GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You delegate a ticket, review the output, and tighten the architecture. These pieces fit together:&lt;/p&gt;
&lt;p&gt;Understanding Claude Code’s Full Stack: MCP, Skills, Subagents, and Hooks&lt;/p&gt;
&lt;p&gt;But first, how we used to work.&lt;/p&gt;
&lt;h2&gt;How We Worked Before: Beer Commerce&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;Beer Commerce is a fictional company that sells craft beer online. Mid-size, multiple teams, microservices backend, microfrontend architecture.&lt;/p&gt;
&lt;p&gt;One of their teams, the Checkout team, has 12 people:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5 developers (2 frontend, 2 backend, 1 full-stack)&lt;/li&gt;
&lt;li&gt;1 product owner&lt;/li&gt;
&lt;li&gt;1 scrum master&lt;/li&gt;
&lt;li&gt;2 QA engineers&lt;/li&gt;
&lt;li&gt;1 UX designer&lt;/li&gt;
&lt;li&gt;1 business analyst&lt;/li&gt;
&lt;li&gt;1 tech lead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They run two-week sprints. Here’s what happens when a new feature comes in. Say, “Add a discount code field to the checkout page”:&lt;/p&gt;
&lt;p&gt;Business request → BA writes user story → PO refines and prioritizes → sprint planning (2-4 hrs) → UX designs mockups (2-3 days) → dev picks up ticket (day 3-4) → dev writes code (2-3 days) → code review (1-2 days) → QA testing (1-2 days) → deployed (day 10-14).&lt;/p&gt;
&lt;p&gt;Business request to production: &lt;strong&gt;10 to 14 days&lt;/strong&gt;. That’s the happy path, with no blocked dependencies and no sick days. Many features eat an entire sprint or spill into the next one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Handoffs&lt;/strong&gt; are the bottleneck. The BA hands off to the PO. The PO hands off to the dev. The dev hands off to QA. Each handoff adds waiting time and context loss. The coding itself takes maybe 2 to 3 days out of a 14-day cycle.&lt;/p&gt;
&lt;p&gt;Most of the industry still works this way.&lt;/p&gt;
&lt;h2&gt;Enter the Software Factory&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;Claude Code runs as a scheduled agent in the cloud. You give it a task and it works on its own: reading the codebase, writing code, running tests, opening PRs. This is a production workflow.&lt;/p&gt;
&lt;p&gt;In a software factory, &lt;strong&gt;each team member is a product builder&lt;/strong&gt;. The UX designer who gets a customer complaint can write a spec, hand it to an agent, and babysit the implementation. The business analyst who spots a conversion drop can describe the fix, kick off an agent, and watch it ship. If you understand the problem, you can drive the solution.&lt;/p&gt;
&lt;p&gt;The workflow:&lt;/p&gt;
&lt;p&gt;Business request → builder writes spec (30 min) → agent picks up ticket → agent writes frontend + backend + tests in parallel → agent runs full test suite and lint → builder reviews PR (1-2 hrs) → deployed (same day).&lt;/p&gt;
&lt;p&gt;A designer, PM, or domain expert can write that spec. A good spec goes in, working software comes out.&lt;/p&gt;
&lt;p&gt;Business request to production: &lt;strong&gt;hours, not weeks&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The builder writes a good spec, delegates to the agent, and reviews the output.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;Steve Yegge built this for real with &lt;a href=&quot;https://github.com/steveyegge/gastown&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Gas Town&lt;/a&gt;. He calls it &lt;a href=&quot;https://cloudnativenow.com/features/gas-town-what-kubernetes-for-ai-coding-agents-actually-looks-like/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;“Kubernetes for AI coding agents”&lt;/a&gt;, and the comparison is architecturally accurate. You talk to the “Mayor” (the factory foreman), and it coordinates 20 to 30 parallel coding agents called “Polecats” that each work on feature branches. A “Refinery” manages the merge queue so parallel work doesn’t collide. Git persists everything. If the system crashes, it reads the history and resumes.&lt;/p&gt;
&lt;p&gt;Stripe built the same thing at enterprise scale. They call their agents &lt;a href=&quot;https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Minions&lt;/a&gt;: one-shot coding agents that merge over 1,300 pull requests per week. A task starts in a Slack message and ends in a pull request that passes CI, ready for human review, with no interaction in between.&lt;/p&gt;
&lt;p&gt;Before the LLM even runs, a deterministic orchestrator prefetches context. It scans threads for links, pulls tickets, and searches code via MCP. Each Minion gets its own isolated devbox. Same machines human engineers use. Spins up in 10 seconds.&lt;/p&gt;
&lt;p&gt;They built &lt;a href=&quot;https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents-part-2&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Toolshed&lt;/a&gt;, an internal MCP server with nearly 500 tools. The orchestrator curates a subset per task. The agent starts focused, not drowning.&lt;/p&gt;
&lt;p&gt;The architecture uses what Stripe calls &lt;strong&gt;blueprints&lt;/strong&gt;: hybrid systems that combine deterministic code nodes (run linters, push changes) with agentic subtasks (implement feature, fix CI failures). This is the factory pattern: you don’t give the agent full autonomy and hope for the best. You build a pipeline with guardrails.&lt;/p&gt;
&lt;p&gt;Linters run in under a second before the first push. If CI fails, the Minion gets one more attempt to fix it. After that, humans take over. Two CI rounds max, then ship or hand off.&lt;/p&gt;
&lt;p&gt;Why did Stripe build this in-house? Hundreds of millions of lines of Ruby, Sorbet types, and proprietary libraries no public model has seen. Generic agents couldn’t navigate it. Stripe’s insight: “If a tool is good for human engineers, it’s good for LLMs.” Every investment in developer tooling, documentation, devboxes, and CI directly improved agent performance. The factory runs on the same infrastructure humans use.&lt;/p&gt;
&lt;p&gt;Boris Cherny, the creator of Claude Code, &lt;a href=&quot;https://sfstandard.com/2026/02/19/ai-writes-code-now-s-left-software-engineers/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;said&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“We’re going to start to see the title of software engineer go away. It’s just going to be ‘builder’ or ‘product manager.’”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anthropic &lt;a href=&quot;https://claude.com/blog/eight-trends-defining-how-software-gets-built-in-2026&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;describes the shift&lt;/a&gt; as: “Engineers are shifting from writing code to coordinating agents that write code, focusing their own expertise on architecture, system design, and strategic decisions.”&lt;/p&gt;
&lt;p&gt;The job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing clear specs that agents can execute on&lt;/li&gt;
&lt;li&gt;Reviewing pull requests from agents&lt;/li&gt;
&lt;li&gt;Improving the architecture so agents can work more effectively&lt;/li&gt;
&lt;li&gt;Tightening testing and CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Monitoring the factory’s output quality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You spend less time on boilerplate and more on architecture and deciding what to build. I explored this shift in detail here:&lt;/p&gt;
&lt;p&gt;Spec-Driven Development with Claude Code in Action&lt;/p&gt;
&lt;h2&gt;What Are Skills?&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;A factory is only as good as its tooling. For AI coding agents, that tooling is &lt;strong&gt;skills&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As Simon Willison &lt;a href=&quot;https://simonwillison.net/2025/Oct/16/claude-skills/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;explains&lt;/a&gt;, a skill is “a Markdown file telling the model how to do something, optionally accompanied by extra documents and pre-written scripts.” He predicts “a Cambrian explosion in Skills which will make this year’s MCP rush look pedestrian.”&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud debug skill&lt;/strong&gt; (the agent can SSH into production, read logs, find the root cause of an incident, and propose a fix)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database migration skill&lt;/strong&gt; (it understands your ORM, generates migrations, and knows how to handle rollbacks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design-to-code skill&lt;/strong&gt; (it takes a Figma design and translates it into frontend components)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring skill&lt;/strong&gt; (it reads Grafana dashboards, interprets metrics, and creates alerts)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security audit skill&lt;/strong&gt; (it scans for OWASP vulnerabilities and fixes them)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You add skills, and your agents handle more types of work without you. They debug production issues, handle incidents, set up infrastructure.&lt;/p&gt;
&lt;p&gt;Building and maintaining these skills is the highest-leverage engineering task. You’re programming the factory. I’ve written about this hands-on:&lt;/p&gt;
&lt;p&gt;How to Speed Up Your Claude Code Experience with Slash Commands&lt;/p&gt;
&lt;h2&gt;The New Team: Everyone Is a Builder&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;If AI agents handle the implementation, you don’t need 12 people on the Checkout team. You need 5, maybe fewer.&lt;/p&gt;
&lt;p&gt;Elad Gil &lt;a href=&quot;https://anshadameenza.com/blog/technology/ai-small-teams-software-development-revolution/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;called it&lt;/a&gt; “the dirty secret of 2024: the actual engineering team size needed for most software products has collapsed by 5-10x.” Gergely Orosz from The Pragmatic Engineer &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/the-future-of-software-engineering-with-ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;writes&lt;/a&gt; that “we are already seeing the end of two-pizza teams (6-10 people) thanks to AI.” Andres Max &lt;a href=&quot;https://andresmax.com/large-software-teams-ai-age/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;goes further&lt;/a&gt;: “A 5-person team in 2026 can ship what a 50-person team shipped in 2016.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fixed roles dissolve.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Good early-stage startups have no “frontend developers” or “QA engineers.” They have builders. The designer writes code. The engineer talks to customers. The PM debugs production.&lt;/p&gt;
&lt;p&gt;The software factory brings that to companies of any size. Agents handle the implementation, and people become builders with agents as their execution layer.&lt;/p&gt;
&lt;p&gt;Your expertise still matters. Someone who spent ten years doing UX research asks better questions about user flows. Someone with deep backend experience makes better architectural decisions. Expertise becomes a &lt;strong&gt;superpower, not a job boundary&lt;/strong&gt;. The UX expert can act on their insights by instructing an agent to build a prototype, instead of writing a ticket and waiting two weeks.&lt;/p&gt;
&lt;p&gt;AKF Partners &lt;a href=&quot;https://akfpartners.com/growth-blog/engineering-team-sizes-are-evolving-rapidly-with-agentic-AI-platforms-the-limits-challenges-and-principles-we-must-consider&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;describe a similar model&lt;/a&gt;: “A team of 2 or 3 humans, a lead developer, a product manager, and a designer, could leverage AI agents to cover coding, testing, deployment, and analytics.”&lt;/p&gt;
&lt;p&gt;The data backs this up. According to the &lt;a href=&quot;https://getdx.com/blog/ai-assisted-engineering-q4-impact-report-2025/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DX Q4 Impact Report&lt;/a&gt;, roughly 60% of non-engineers like managers, designers, and PMs now use AI to contribute code daily.&lt;/p&gt;
&lt;p&gt;The gap between “the person who knows what to build” and “the person who can build it” is closing. Your ideas and judgment matter. Your job title doesn’t.&lt;/p&gt;
&lt;h2&gt;The Self-Improving Factory&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;The factory can also figure out what to build next.&lt;/p&gt;
&lt;p&gt;Your product generates signals: user feedback in support tickets, behavior data from analytics, A/B test results, error logs. Right now, a human reads all of that, synthesizes it, and turns it into tickets. That takes time and drops context.&lt;/p&gt;
&lt;p&gt;In a software factory, agents do this on their own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An agent monitors user feedback channels, clusters recurring complaints, and creates backlog items with context&lt;/li&gt;
&lt;li&gt;An agent reads A/B test results, identifies winning variants, and proposes follow-up experiments&lt;/li&gt;
&lt;li&gt;An agent watches error rates in production, detects patterns, and creates bug tickets before anyone files a report&lt;/li&gt;
&lt;li&gt;An agent analyzes usage data, spots features that users struggle with, and suggests UX improvements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The product owner no longer curates a static list once a week. Ideas stream in ranked by impact, updated as data arrives.&lt;/p&gt;
&lt;p&gt;Tools like &lt;a href=&quot;https://linear.app&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Linear&lt;/a&gt; are building toward this. They call their model a triad: &lt;strong&gt;humans decide and remain accountable, agents execute within defined scopes, the platform manages interactions and visibility&lt;/strong&gt;. Issues can only be &lt;em&gt;assigned&lt;/em&gt; to humans, but &lt;em&gt;delegated&lt;/em&gt; to agents. As they &lt;a href=&quot;https://linear.app/now/our-approach-to-building-the-agent-interaction-sdk&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;put it&lt;/a&gt;: “An agent cannot be held accountable.” The platform becomes the orchestration layer where humans and agents interact.&lt;/p&gt;
&lt;p&gt;Combined with their &lt;a href=&quot;https://linear.app/now/how-cursor-integrated-with-linear-for-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cursor integration&lt;/a&gt;, the picture becomes clear: “Starting an issue used to mean manually creating a feature branch. Now it means assembling the right context so your coding agent can take a first pass.” Linear provides the product and customer context, the coding agent provides the codebase expertise, and the human provides judgment. That’s the factory’s coordination layer.&lt;/p&gt;
&lt;p&gt;Because the factory can also implement those ideas, you get a closed loop: &lt;strong&gt;observe, decide, build, ship, observe again&lt;/strong&gt;. You use the factory to improve the product, and you read the product’s data to improve the factory.&lt;/p&gt;
&lt;p&gt;Andrej Karpathy pushes this to its extreme with &lt;a href=&quot;https://github.com/karpathy/autoresearch&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;autoresearch&lt;/a&gt;. AI agents run ML experiments overnight on a single GPU. Humans write a &lt;code&gt;program.md&lt;/code&gt; (a high-level spec) instead of training code, and agents iterate through experiments. Each run takes 5 minutes, producing about 12 experiments per hour, all optimizing toward a single metric. As Karpathy puts it: “Research is now entirely the domain of autonomous swarms of AI agents running across compute cluster megastructures.”&lt;/p&gt;
&lt;p&gt;The same pattern applies to product development. Replace “training runs” with “feature experiments” and “val_bpb” with “conversion rate.” Your factory runs experiments, measures outcomes, and feeds the results back into the next cycle.&lt;/p&gt;
&lt;p&gt;Anthropic supports &lt;a href=&quot;https://code.claude.com/docs/en/scheduled-tasks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;scheduled tasks&lt;/a&gt; that let agents run on a timer. Set up a daily agent that reads your support inbox, one that checks your analytics dashboard, one that reviews your error logs. Your team reviews what they find, prioritizes, and lets the factory execute.&lt;/p&gt;
&lt;p&gt;For a deeper look at how agent teams work in practice:&lt;/p&gt;
&lt;p&gt;7 Agent Team Patterns for Claude Code&lt;/p&gt;
&lt;h2&gt;The Factory Is the Product&lt;/h2&gt;
&lt;p&gt;In a software factory, the software you ship is one product. &lt;strong&gt;The factory is another.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Skills you build, pipelines you improve, architectural decisions that make your codebase more agent-friendly: these compound. A team that spends a week improving their testing infrastructure ships future features faster. A team that builds a cloud debugging skill lets an agent resolve future incidents.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;Moss calls this &lt;a href=&quot;https://banay.me/dont-waste-your-backpressure/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;backpressure&lt;/a&gt;: the automated feedback that keeps agents on track. Type checkers, linters, test suites, build systems. If an agent misses an import, the build fails and the agent fixes it. You don’t spend your time pointing out syntax errors. Without backpressure, you’re stuck reviewing trivial mistakes. With it, agents self-correct and you focus on architecture and product decisions. Investing in backpressure is investing in your factory’s throughput.&lt;/p&gt;
&lt;p&gt;The teams that build the best factories will ship the best products.&lt;/p&gt;
&lt;h2&gt;This Is Happening Now&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;Claude Code runs in the cloud. You can schedule agents and build skills. Early adopters are doing this now.&lt;/p&gt;
&lt;p&gt;As Andres Max &lt;a href=&quot;https://andresmax.com/large-software-teams-ai-age/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;put it&lt;/a&gt;: “Engineers who only wrote boilerplate are at risk. Engineers who make good decisions, architect systems, and understand users are more valuable than ever.”&lt;/p&gt;
&lt;p&gt;Are you going to build a factory, or keep doing two-week sprints while your competitors ship in hours?&lt;/p&gt;
&lt;p&gt;In Five Years, Developers Won’t Write Code By Hand&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The tools for building software factories exist &lt;strong&gt;right now&lt;/strong&gt;: Claude Code, headless mode, scheduled tasks, skills, agent teams&lt;/li&gt;
&lt;li&gt;Stripe’s Minions prove the model works at scale: 1,300+ PRs merged per week, fully agent-written, human-reviewed&lt;/li&gt;
&lt;li&gt;12-person teams running two-week sprints give way to small groups of generalists who all build&lt;/li&gt;
&lt;li&gt;Skills turn general-purpose agents into specialized factory workers, and that specialization is your competitive edge&lt;/li&gt;
&lt;li&gt;Self-improving factories use agents to read user feedback, A/B tests, and production data, then populate the backlog without human curation&lt;/li&gt;
&lt;li&gt;The factory is a product. Investments in tooling, testing, and architecture compound&lt;/li&gt;
&lt;li&gt;The role of “software engineer” shifts toward “factory builder”: less boilerplate, more architecture, judgment, and deciding what to build next&lt;/li&gt;
&lt;/ul&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>ai</category><category>software-engineering</category><category>teams</category><category>future</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building a Real-Time Todo App with Jazz and Vue 3</title><link>https://alexop.dev/posts/building-real-time-todo-app-jazz-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/building-real-time-todo-app-jazz-vue/</guid><description>Build a real-time, offline-capable todo app with Jazz and Vue 3. Learn CoValues, drag-and-drop with fractional indexing, and shareable URLs. No backend needed.</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Build a real-time, offline-capable todo app with Jazz and Vue 3. Jazz replaces your entire backend with collaborative data structures called CoValues. Combined with &lt;code&gt;community-jazz-vue&lt;/code&gt;, you get composables like &lt;code&gt;useCoState&lt;/code&gt; that plug into Vue’s reactivity system. The app syncs across tabs and devices, supports drag-and-drop with fractional indexing, and generates shareable URLs.&lt;/p&gt;
&lt;h2&gt;Why Jazz Replaces Your Entire Backend&lt;/h2&gt;
&lt;p&gt;You want real-time collaboration, offline support, and shareable URLs. In a traditional stack, that means building a sync server, a conflict resolution strategy, a permissions layer, and an API. Jazz collapses all of that into collaborative data structures called CoValues.&lt;/p&gt;
&lt;p&gt;Vue’s &lt;code&gt;ref&lt;/code&gt; keeps the virtual DOM in sync with the real DOM. CoValues extend that idea: they sync your data across devices and users, across network boundaries. You mutate data locally. Jazz handles the rest.&lt;/p&gt;
&lt;p&gt;This post builds a complete Vue 3 app on top of Jazz. You can find the official Jazz documentation at &lt;a href=&quot;https://jazz.tools/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;jazz.tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Combined with &lt;code&gt;community-jazz-vue&lt;/code&gt;, a community-maintained Vue binding, you get composables like &lt;code&gt;useCoState&lt;/code&gt; and a provider component that plug into Vue’s reactivity system.&lt;/p&gt;
&lt;p&gt;If you’re new to local-first, start with the first post in this series:&lt;/p&gt;
&lt;p&gt;For comparison, the same app built with Dexie.js and IndexedDB:&lt;/p&gt;
&lt;p&gt;Anselm Eickhoff, the creator of Jazz, gave a workshop on local-first at VueConf 2025 where he live-coded with Jazz:&lt;/p&gt;
&lt;h2&gt;Your First CoValue: A Synced Counter&lt;/h2&gt;
&lt;p&gt;Vue’s &lt;code&gt;ref&lt;/code&gt; keeps the virtual DOM in sync with the real DOM. You update a &lt;code&gt;ref&lt;/code&gt;, Vue re-renders. That’s reactive state within one browser tab. Jazz’s &lt;code&gt;useCoState&lt;/code&gt; extends that across devices, users, and network boundaries. You mutate locally, Jazz propagates the change to every connected peer.&lt;/p&gt;
&lt;p&gt;The simplest CoValue is a single field:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; Counter &lt;span&gt;=&lt;/span&gt; co&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; count&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s one line of schema. Now subscribe to it in a Vue component:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// Create a counter and grab its ID
const counterId = ref&amp;lt;string | undefined&amp;gt;();
const newCounter = Counter.create({ count: 0 });
counterId.value = newCounter.$jazz.id;

// Subscribe — returns a reactive ref that updates on local and remote changes
const counter = useCoState(Counter, counterId);

function increment() {
  if (counter.value?.$isLoaded) {
    counter.value.$jazz.set(&quot;count&quot;, (counter.value.count ?? 0) + 1);
  }
}

function decrement() {
  if (counter.value?.$isLoaded) {
    counter.value.$jazz.set(&quot;count&quot;, Math.max(0, (counter.value.count ?? 0) - 1));
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div v-if=&quot;!counter?.$isLoaded&quot;&amp;gt;Loading...&amp;lt;/div&amp;gt;
  &amp;lt;div v-else&amp;gt;
    &amp;lt;p&amp;gt;{{ counter.count }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&quot;decrement&quot;&amp;gt;−&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&quot;increment&quot;&amp;gt;+&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Counter.create({ count: 0 })&lt;/code&gt; creates a new CoValue instance and persists it locally.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCoState(Counter, counterId)&lt;/code&gt; subscribes to that CoValue. It returns a reactive &lt;code&gt;Ref&lt;/code&gt; that starts unloaded, populates once available, and re-renders on every change. Same as Vue’s &lt;code&gt;ref&lt;/code&gt;, but across the network.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$jazz.set(&quot;count&quot;, ...)&lt;/code&gt; mutates the CoValue. No API call, no action dispatch. Jazz syncs the change to all connected peers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This works for a single user. But there’s a problem: &lt;strong&gt;by default, CoValues are private&lt;/strong&gt;. Only the creator can read or write them. If you shared this counter’s ID with another user, they’d get nothing. The CoValue is invisible to them.&lt;/p&gt;
&lt;h3&gt;Making It Shareable with Groups&lt;/h3&gt;
&lt;p&gt;To let others access the counter, you need a &lt;strong&gt;Group&lt;/strong&gt;. Groups are how Jazz controls who gets access and what they can do. Every CoValue has an owner: either an Account (private) or a Group (shared).&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; group &lt;span&gt;=&lt;/span&gt; Group&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
group&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addMember&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;everyone&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;writer&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; newCounter &lt;span&gt;=&lt;/span&gt; Counter&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; count&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; owner&lt;span&gt;:&lt;/span&gt; group &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s three lines. &lt;code&gt;Group.create()&lt;/code&gt; makes a new permission group. &lt;code&gt;addMember(&quot;everyone&quot;, &quot;writer&quot;)&lt;/code&gt; grants public write access. The &lt;code&gt;&quot;everyone&quot;&lt;/code&gt; keyword is a special shorthand that includes all users, even anonymous guests. Passing &lt;code&gt;{ owner: group }&lt;/code&gt; to &lt;code&gt;Counter.create&lt;/code&gt; assigns the counter to that group instead of the default private account.&lt;/p&gt;
&lt;p&gt;Now anyone who knows the CoValue ID can read and write the same counter. No API keys, no auth server. Sharing the ID is all it takes.&lt;/p&gt;
&lt;p&gt;Try it. Click the + button in one panel and watch the count update in the other. Both panels are independent Jazz clients syncing via Jazz Cloud:&lt;/p&gt;
&lt;aside&gt;
The `&quot;everyone&quot;` + `&quot;writer&quot;` combination is permissive, great for demos and public collaboration. For real apps, Jazz supports granular roles (`reader`, `writer`, `admin`) and per-user membership. We&apos;ll use the same pattern for the todo app below.
&lt;/aside&gt;
&lt;p&gt;That’s the entire model: define your data with &lt;code&gt;co.map&lt;/code&gt;, subscribe with &lt;code&gt;useCoState&lt;/code&gt;, mutate. But we glossed over something important. How does Jazz decide who can access a CoValue, and what stops someone from escalating their own permissions?&lt;/p&gt;
&lt;h2&gt;How Jazz Permissions Work&lt;/h2&gt;
&lt;p&gt;The counter example used &lt;code&gt;addMember(&quot;everyone&quot;, &quot;writer&quot;)&lt;/code&gt; to make data publicly accessible.&lt;/p&gt;
&lt;p&gt;Jazz defines a role hierarchy where each role includes the permissions of the roles below it:&lt;/p&gt;
&lt;p&gt;When we create a Group and grant public write access, the permission structure looks like this:&lt;/p&gt;
&lt;p&gt;So what stops a malicious user from upgrading their own role from &lt;code&gt;writer&lt;/code&gt; to &lt;code&gt;admin&lt;/code&gt;? Jazz enforces permissions cryptographically, in the data itself.&lt;/p&gt;
&lt;p&gt;Every change to a CoValue is signed with the author’s private key (Ed25519). When a peer receives a transaction, it verifies the signature and checks whether that account had permission to make that change. A writer trying to modify Group roles? The signature is valid, but the Group’s internal rules reject it. Only admins can change roles. The transaction gets discarded.&lt;/p&gt;
&lt;p&gt;The Jazz Cloud sync server can’t tamper with your data. It relays signed transactions. If the server modified anything, the signatures wouldn’t match and peers would reject it. Jazz also encrypts data with a shared read key (XSalsa20), so the server can’t read what it’s relaying.&lt;/p&gt;
&lt;aside&gt;
For more complex apps, Jazz supports granular permissions on nested CoValues. Each nested CoValue can have its own Group with different roles. A project board where managers can edit columns but clients can only read specific tasks. Groups can also be members of other groups, enabling cascading permission hierarchies.
&lt;/aside&gt;
&lt;p&gt;Now for something more ambitious.&lt;/p&gt;
&lt;h2&gt;What We’re Building: A Real-Time Vue 3 Todo App&lt;/h2&gt;
&lt;p&gt;A todo list app that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Syncs in real-time across tabs and devices via Jazz Cloud&lt;/li&gt;
&lt;li&gt;Works offline with a toggle to simulate going on/off the network&lt;/li&gt;
&lt;li&gt;Supports drag-and-drop reordering via &lt;code&gt;useSortable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Shows a dynamic page title with the count of incomplete todos&lt;/li&gt;
&lt;li&gt;Generates a shareable URL so anyone with the link sees the same list&lt;/li&gt;
&lt;li&gt;Includes the Jazz Inspector for debugging CoValues in development&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Try it out. Both todo apps below are independent Jazz clients, each running as a Vue component embedded in this page. Add a todo in one and watch it sync to the other via Jazz Cloud:&lt;/p&gt;
&lt;aside&gt;
Click the Jazz icon in the bottom-right corner to open the Jazz Inspector. You can copy any `co_z...` CoValue ID shown in the app UI, paste it into the inspector, and hit &quot;Inspect CoValue&quot; to see the raw data inside: its fields, current values, and sync status.
&lt;/aside&gt;
&lt;p&gt;The high-level architecture:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    &lt;span&gt;subgraph&lt;/span&gt; BrowserA&lt;span&gt;[&quot;Browser A (Tab 1)&quot;]&lt;/span&gt;
        AppA&lt;span&gt;[&quot;Vue App&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; IDB_A&lt;span&gt;[&quot;Local IndexedDB&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Cloud&lt;span&gt;[&quot;Jazz Cloud&quot;]&lt;/span&gt;
        Sync&lt;span&gt;[&quot;Sync Server&quot;]&lt;/span&gt;
        Store&lt;span&gt;[&quot;Persistent Store&quot;]&lt;/span&gt;
        Sync &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Store
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; BrowserB&lt;span&gt;[&quot;Browser B (Tab 2)&quot;]&lt;/span&gt;
        AppB&lt;span&gt;[&quot;Vue App&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; IDB_B&lt;span&gt;[&quot;Local IndexedDB&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    IDB_A &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|WebSocket|&lt;/span&gt; Sync
    IDB_B &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|WebSocket|&lt;/span&gt; Sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’ll build it step by step.&lt;/p&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;You’ll need Node.js v20+ and a package manager (we’ll use pnpm, but npm/yarn work too).&lt;/p&gt;
&lt;p&gt;Start with a fresh Vue project (select TypeScript when prompted) and install the dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; create vue@latest vue-jazz-todo
&lt;span&gt;cd&lt;/span&gt; vue-jazz-todo

&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; jazz-tools community-jazz-vue @vueuse/core @vueuse/integrations sortablejs fractional-indexing tailwindcss @tailwindcss/vite
&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; &lt;span&gt;-D&lt;/span&gt; @types/sortablejs vite-plugin-vue-devtools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the project structure we’ll end up with:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “vue-jazz-todo”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “main.ts”, comment: “App entry point” },&lt;br /&gt;
{ name: “App.vue”, comment: “JazzVueProvider wrapper” },&lt;br /&gt;
{ name: “TodoApp.vue”, comment: “Main todo UI” },&lt;br /&gt;
{ name: “schema.ts”, comment: “Jazz CoValue definitions” },&lt;br /&gt;
{&lt;br /&gt;
name: “assets”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “main.css”, comment: “Tailwind CSS import” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “vite.config.ts”, comment: “Build config” },&lt;br /&gt;
{ name: “index.html”, comment: “HTML shell” },&lt;br /&gt;
{ name: “package.json” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Defining the Schema&lt;/h2&gt;
&lt;p&gt;Jazz uses Collaborative Values (CoValues) as its building blocks: reactive, synced data structures. If you’ve used Zod before, the schema API will feel familiar. Create &lt;code&gt;src/schema.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; ToDo &lt;span&gt;=&lt;/span&gt; co&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; title&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; completed&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; order&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; ToDoList &lt;span&gt;=&lt;/span&gt; co&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ToDo&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lines. &lt;code&gt;co.map&lt;/code&gt; defines a collaborative object with typed fields, &lt;code&gt;co.list&lt;/code&gt; defines an ordered list. The &lt;code&gt;order&lt;/code&gt; field stores a fractional index string for drag-and-drop (more on that in the reordering section). Jazz handles sync, persistence, and conflict resolution for you.&lt;/p&gt;
&lt;aside&gt;
Jazz&apos;s CoValues are built on [CRDTs](https://jakelazaroff.com/words/an-interactive-intro-to-crdts/) (Conflict-free Replicated Data Types). Unlike Dexie.js where the server handles conflicts, Jazz bakes conflict resolution into the data structures. You never have to think about it.
&lt;/aside&gt;
&lt;p&gt;In practice, a &lt;code&gt;CoMap&lt;/code&gt; tracks every change from every device. When Alice sets &lt;code&gt;completed: false&lt;/code&gt; at 8:05 AM and Bob sets &lt;code&gt;completed: true&lt;/code&gt; at 8:03 AM, the later timestamp wins. Step through the timeline to see how the resolved state changes as transactions arrive:&lt;/p&gt;
&lt;h2&gt;Entry Point and Build Configuration&lt;/h2&gt;
&lt;p&gt;The entry point is standard Vue. Update &lt;code&gt;src/main.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;&quot;./assets/main.css&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;createApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;App&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#app&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Tailwind CSS v4, replace &lt;code&gt;src/assets/main.css&lt;/code&gt; with a single import, no &lt;code&gt;tailwind.config.js&lt;/code&gt; needed:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;&lt;span&gt;@import&lt;/span&gt; &lt;span&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to tell Vue about the Jazz Inspector custom element so Vue doesn’t try to resolve it as a component. Update &lt;code&gt;vite.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;vue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        compilerOptions&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;isCustomElement&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;tag&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; tag &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&apos;jazz-inspector&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;vueDevTools&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;tailwindcss&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  resolve&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    alias&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&apos;@&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;fileURLToPath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt; &lt;span&gt;&lt;span&gt;URL&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;./src&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;meta&lt;span&gt;.&lt;/span&gt;url&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;isCustomElement&lt;/code&gt; line matters because Jazz ships with a built-in inspector for browsing and debugging CoValues at runtime. It appears as a floating panel where you can click into any CoValue to see its current state, history, and sync status. Since the inspector is a Web Component (&lt;code&gt;&amp;lt;jazz-inspector /&amp;gt;&lt;/code&gt;), you need to tell Vue not to resolve it as a Vue component.&lt;/p&gt;
&lt;h2&gt;Setting Up the Jazz Provider&lt;/h2&gt;
&lt;p&gt;Instead of manually creating a context manager, we use the &lt;code&gt;JazzVueProvider&lt;/code&gt; component from &lt;code&gt;community-jazz-vue&lt;/code&gt;. It handles context creation, cleanup, and provides Jazz to all child components via Vue’s dependency injection.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;src/App.vue&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import &quot;jazz-tools/inspector/register-custom-element&quot;;
const isOnline = ref(true);
const syncConfig = computed(() =&amp;gt; ({
  peer: &quot;wss://cloud.jazz.tools/?key=vue-jazz-todo-example&quot; as const,
  when: isOnline.value ? (&quot;always&quot; as const) : (&quot;never&quot; as const),
}));
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;JazzVueProvider :sync=&quot;syncConfig&quot;&amp;gt;
    
    &amp;lt;jazz-inspector /&amp;gt;
  &amp;lt;/JazzVueProvider&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things worth understanding about this setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JazzVueProvider&lt;/code&gt; wraps the entire app and takes a &lt;code&gt;sync&lt;/code&gt; prop. It only renders children once the Jazz context is ready, so downstream components can safely assume Jazz is available. The sync config is reactive: the &lt;code&gt;computed&lt;/code&gt; derives the config from &lt;code&gt;isOnline&lt;/code&gt;. When the user toggles the switch, &lt;code&gt;when&lt;/code&gt; flips between &lt;code&gt;&quot;always&quot;&lt;/code&gt; and &lt;code&gt;&quot;never&quot;&lt;/code&gt;, and Jazz switches between syncing and offline mode.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import &quot;jazz-tools/inspector/register-custom-element&quot;&lt;/code&gt; registers the &lt;code&gt;&amp;lt;jazz-inspector&amp;gt;&lt;/code&gt; Web Component as a side-effect import. The &lt;code&gt;isOnline&lt;/code&gt; state lives here and is passed down via &lt;code&gt;v-model:is-online&lt;/code&gt;, keeping the sync config and toggle UI separated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Building the Todo App&lt;/h2&gt;
&lt;p&gt;Now for the main component. Create &lt;code&gt;src/TodoApp.vue&lt;/code&gt; and we’ll build it piece by piece.&lt;/p&gt;
&lt;h3&gt;Imports and Setup&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; isOnline &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;defineModel&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;isOnline&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; required&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; newTitle &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; copy&lt;span&gt;,&lt;/span&gt; copied &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useClipboard&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; copiedDuring&lt;span&gt;:&lt;/span&gt; &lt;span&gt;2000&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We receive &lt;code&gt;isOnline&lt;/code&gt; from the parent via &lt;code&gt;defineModel&lt;/code&gt; and set up a ref for the input field plus VueUse’s clipboard composable for the “Copy link” button.&lt;/p&gt;
&lt;h3&gt;Routing via URL&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; params &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useUrlSearchParams&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;history&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; listId &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;params&lt;span&gt;.&lt;/span&gt;id &lt;span&gt;as&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;listId&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; group &lt;span&gt;=&lt;/span&gt; Group&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  group&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addMember&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;everyone&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;writer&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; newList &lt;span&gt;=&lt;/span&gt; ToDoList&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; owner&lt;span&gt;:&lt;/span&gt; group &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  listId&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; newList&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;;&lt;/span&gt;
  params&lt;span&gt;.&lt;/span&gt;id &lt;span&gt;=&lt;/span&gt; newList&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use VueUse’s &lt;code&gt;useUrlSearchParams(&quot;history&quot;)&lt;/code&gt; instead of raw &lt;code&gt;URLSearchParams&lt;/code&gt;. The &lt;code&gt;&quot;history&quot;&lt;/code&gt; mode uses the History API (no page reload) and gives us a reactive object. If the URL has an &lt;code&gt;?id=&lt;/code&gt; parameter, we subscribe to that existing list. Otherwise, we create a new list owned by a Group with public write access (the same pattern from the permissions section) so anyone with the link can read and write.&lt;/p&gt;
&lt;h3&gt;Subscribing to the List&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; todoList &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useCoState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ToDoList&lt;span&gt;,&lt;/span&gt; listId&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  resolve&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; $each&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; todos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; list &lt;span&gt;=&lt;/span&gt; todoList&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;list&lt;span&gt;?.&lt;/span&gt;$isLoaded&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;list&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;.&lt;/span&gt;order &lt;span&gt;&amp;lt;&lt;/span&gt; b&lt;span&gt;.&lt;/span&gt;order &lt;span&gt;?&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; a&lt;span&gt;.&lt;/span&gt;order &lt;span&gt;&amp;gt;&lt;/span&gt; b&lt;span&gt;.&lt;/span&gt;order &lt;span&gt;?&lt;/span&gt; &lt;span&gt;1&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useCoState&lt;/code&gt; is the core composable from &lt;code&gt;community-jazz-vue&lt;/code&gt;. It takes a CoValue schema, an ID (which can be a ref), and a resolve query. It returns a Vue &lt;code&gt;Ref&lt;/code&gt; that starts as unloaded, populates once available, and re-renders on both local and remote changes.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;resolve: { $each: true }&lt;/code&gt; tells Jazz to deeply load each item in the list. Without it, you’d get the list but each ToDo entry would be an unresolved reference. Items are sorted by their &lt;code&gt;order&lt;/code&gt; field rather than their position in the CoList.&lt;/p&gt;
&lt;p&gt;If you’re interested in how Vue composables like this are designed, I wrote about the patterns here:&lt;/p&gt;
&lt;p&gt;Vue Composables Style Guide: Lessons from VueUse’s Codebase&lt;/p&gt;
&lt;h3&gt;Dynamic Page Title&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; pageTitle &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;To Do&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;t&lt;span&gt;.&lt;/span&gt;completed&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;;&lt;/span&gt;
  pageTitle&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; count &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;count&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;) To Do&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;To Do&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A small UX touch: the browser tab shows “(3) To Do” when there are 3 incomplete items.&lt;/p&gt;
&lt;h3&gt;CRUD Operations&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; inputEl &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useTemplateRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;HTMLInputElement&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;inputEl&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; focused&lt;span&gt;:&lt;/span&gt; inputFocused &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFocus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputEl&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;addTodo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; list &lt;span&gt;=&lt;/span&gt; todoList&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; title &lt;span&gt;=&lt;/span&gt; newTitle&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;title &lt;span&gt;||&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;list&lt;span&gt;?.&lt;/span&gt;$isLoaded&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; sorted &lt;span&gt;=&lt;/span&gt; todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; lastOrder &lt;span&gt;=&lt;/span&gt; sorted&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; sorted&lt;span&gt;[&lt;/span&gt;sorted&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;-&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;order &lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; order &lt;span&gt;=&lt;/span&gt; &lt;span&gt;generateKeyBetween&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;lastOrder&lt;span&gt;,&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  list&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; title&lt;span&gt;,&lt;/span&gt; completed&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; order &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  newTitle&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  inputFocused&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;toggleTodo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;:&lt;/span&gt; co&lt;span&gt;.&lt;/span&gt;loaded&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; ToDo&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  todo&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;completed&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;todo&lt;span&gt;.&lt;/span&gt;completed&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;deleteTodo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;:&lt;/span&gt; co&lt;span&gt;.&lt;/span&gt;loaded&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; ToDo&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; list &lt;span&gt;=&lt;/span&gt; todoList&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;list&lt;span&gt;?.&lt;/span&gt;$isLoaded&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; listIndex &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;list&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findIndex&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; t&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;id &lt;span&gt;===&lt;/span&gt; todo&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;listIndex &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; list&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;listIndex&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; &lt;span&gt;copyLink&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;copy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;window&lt;span&gt;.&lt;/span&gt;location&lt;span&gt;.&lt;/span&gt;href&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every loaded CoValue exposes a &lt;code&gt;$jazz&lt;/code&gt; accessor for mutations: &lt;code&gt;$jazz.push()&lt;/code&gt;, &lt;code&gt;$jazz.set()&lt;/code&gt;, &lt;code&gt;$jazz.remove()&lt;/code&gt;. These mutations apply locally and sync to all connected peers. No “save” button, no API call.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;addTodo&lt;/code&gt; generates a fractional index via &lt;code&gt;generateKeyBetween(lastOrder, null)&lt;/code&gt;, which creates a key that sorts after all existing items. &lt;code&gt;deleteTodo&lt;/code&gt; finds the item by its CoValue ID instead of relying on the array index, since our &lt;code&gt;todos&lt;/code&gt; computed is sorted by &lt;code&gt;order&lt;/code&gt; and the display index won’t match the &lt;code&gt;CoList&lt;/code&gt; position.&lt;/p&gt;
&lt;h3&gt;The Sync Lifecycle&lt;/h3&gt;
&lt;p&gt;The lifecycle of a mutation like &lt;code&gt;list.$jazz.push(...)&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[&quot;list.$jazz.push(...)&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[&quot;Local IndexedDB&quot;]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[&quot;Vue re-renders via shallowRef&quot;]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[&quot;Jazz Cloud Sync Server&quot;]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[&quot;Other Browsers&quot;]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[&quot;useCoState() triggers update&quot;]&lt;/span&gt;
    F &lt;span&gt;--&amp;gt;&lt;/span&gt; G&lt;span&gt;[&quot;Vue re-renders&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The mutation returns before the sync starts. Your UI updates first. When the data reaches other peers, their &lt;code&gt;useCoState&lt;/code&gt; subscriptions fire and Vue re-renders on their end too.&lt;/p&gt;
&lt;h2&gt;The TodoApp.vue Template&lt;/h2&gt;
&lt;p&gt;The complete template ties together the add form, sortable list, online toggle, and shareable link:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;min-h-screen bg-gray-950 flex items-start justify-center pt-16 px-4&quot;&amp;gt;
    &amp;lt;div class=&quot;w-full max-w-md&quot;&amp;gt;
      &amp;lt;!-- Header with online toggle --&amp;gt;
      &amp;lt;div class=&quot;flex items-center justify-between mb-8&quot;&amp;gt;
        &amp;lt;div class=&quot;flex items-center gap-2&quot;&amp;gt;
          &amp;lt;svg class=&quot;w-7 h-7 text-blue-500&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&amp;gt;
            
          &amp;lt;/svg&amp;gt;
          &amp;lt;span class=&quot;text-white text-xl font-semibold tracking-tight&quot;&amp;gt;jazz&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;flex items-center gap-2&quot;&amp;gt;
          &amp;lt;span class=&quot;text-gray-300 text-sm&quot;&amp;gt;Online&amp;lt;/span&amp;gt;
          &amp;lt;button
            @click=&quot;isOnline = !isOnline&quot;
            :class=&quot;[
              &apos;relative inline-flex h-6 w-11 items-center rounded-full transition-colors&apos;,
              isOnline ? &apos;bg-blue-600&apos; : &apos;bg-gray-600&apos;,
            ]&quot;
          &amp;gt;
            &amp;lt;span
              :class=&quot;[
                &apos;inline-block h-4 w-4 rounded-full bg-white transition-transform&apos;,
                isOnline ? &apos;translate-x-6&apos; : &apos;translate-x-1&apos;,
              ]&quot;
            /&amp;gt;
          &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;!-- Card --&amp;gt;
      &amp;lt;div class=&quot;bg-gray-900 border border-gray-700 rounded-xl p-6&quot;&amp;gt;
        &amp;lt;h1 class=&quot;text-3xl font-bold text-white mb-6&quot;&amp;gt;To Do&amp;lt;/h1&amp;gt;

        &amp;lt;div v-if=&quot;!todoList?.$isLoaded&quot; class=&quot;text-gray-400 text-center py-8&quot;&amp;gt;
          Loading...
        &amp;lt;/div&amp;gt;

        &amp;lt;template v-else&amp;gt;
          &amp;lt;form @submit.prevent=&quot;addTodo&quot; class=&quot;mb-6 space-y-3&quot;&amp;gt;
            &amp;lt;input
              ref=&quot;inputEl&quot;
              v-model=&quot;newTitle&quot;
              placeholder=&quot;New task&quot;
              class=&quot;w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent&quot;
            /&amp;gt;
            &amp;lt;button
              type=&quot;submit&quot;
              class=&quot;w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium&quot;
            &amp;gt;
              Add
            &amp;lt;/button&amp;gt;
          &amp;lt;/form&amp;gt;

          &amp;lt;ul ref=&quot;todoListEl&quot; class=&quot;space-y-2&quot;&amp;gt;
            &amp;lt;li
              v-for=&quot;(todo, index) in todos&quot;
              :key=&quot;todo.$jazz.id&quot;
              class=&quot;group flex items-center gap-3 p-2 rounded-lg hover:bg-gray-800&quot;
            &amp;gt;
              &amp;lt;span
                class=&quot;drag-handle cursor-grab active:cursor-grabbing text-gray-600 group-hover:text-gray-400 transition-colors select-none&quot;
                title=&quot;Drag to reorder&quot;
              &amp;gt;
                &amp;lt;svg class=&quot;w-4 h-4&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;currentColor&quot;&amp;gt;

                &amp;lt;/svg&amp;gt;
              &amp;lt;/span&amp;gt;
              &amp;lt;input
                type=&quot;checkbox&quot;
                :checked=&quot;todo.completed&quot;
                @change=&quot;toggleTodo(todo)&quot;
                class=&quot;h-4 w-4 rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500 focus:ring-offset-gray-900&quot;
              /&amp;gt;
              &amp;lt;span
                class=&quot;flex-1&quot;
                :class=&quot;todo.completed ? &apos;line-through text-gray-500&apos; : &apos;text-gray-200&apos;&quot;
              &amp;gt;
                {{ todo.title }}
              &amp;lt;/span&amp;gt;
              &amp;lt;button
                @click=&quot;deleteTodo(todo)&quot;
                class=&quot;opacity-0 group-hover:opacity-100 transition-opacity text-gray-500 hover:text-red-400 p-1&quot;
                title=&quot;Delete&quot;
              &amp;gt;
                &amp;lt;svg class=&quot;w-4 h-4&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&amp;gt;
                  
                &amp;lt;/svg&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/li&amp;gt;
          &amp;lt;/ul&amp;gt;

          &amp;lt;p v-if=&quot;todos.length === 0&quot; class=&quot;text-gray-500 text-center py-4&quot;&amp;gt;
            No todos yet. Add one above!
          &amp;lt;/p&amp;gt;

          &amp;lt;div v-if=&quot;listId&quot; class=&quot;mt-6 flex items-center gap-2&quot;&amp;gt;
            &amp;lt;p class=&quot;text-xs text-gray-500 break-all flex-1&quot;&amp;gt;
              List ID: {{ listId }}
            &amp;lt;/p&amp;gt;
            &amp;lt;button
              @click=&quot;copyLink&quot;
              class=&quot;shrink-0 px-3 py-1 text-xs rounded-md border transition-colors&quot;
              :class=&quot;copied
                ? &apos;border-green-600 text-green-400&apos;
                : &apos;border-gray-600 text-gray-400 hover:text-gray-200 hover:border-gray-500&apos;&quot;
            &amp;gt;
              {{ copied ? &quot;Copied!&quot; : &quot;Copy link&quot; }}
            &amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the template, &lt;code&gt;:key=&quot;todo.$jazz.id&quot;&lt;/code&gt; gives Vue a stable, globally unique key for each item. The &lt;code&gt;ref=&quot;inputEl&quot;&lt;/code&gt; connects to &lt;code&gt;useFocus&lt;/code&gt; so the input regains focus after adding a todo. &lt;code&gt;@change=&quot;toggleTodo(todo)&quot;&lt;/code&gt; shows how clean mutations are: no actions, no dispatching, no reducers.&lt;/p&gt;
&lt;h2&gt;Drag-and-Drop Reordering&lt;/h2&gt;
&lt;p&gt;We use &lt;a href=&quot;https://vueuse.org/integrations/useSortable/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;useSortable&lt;/code&gt;&lt;/a&gt; from VueUse for drag-and-drop. The simplest approach would be to call &lt;code&gt;splice&lt;/code&gt; on the &lt;code&gt;CoList&lt;/code&gt;: remove the item from its old position, insert it at the new one. For a single user, that works fine.&lt;/p&gt;
&lt;h3&gt;The Problem with &lt;code&gt;splice&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Jazz’s &lt;code&gt;CoList&lt;/code&gt; implements &lt;code&gt;splice&lt;/code&gt; as a delete-plus-insert, not as an atomic move. That distinction matters as soon as two users are involved. If both users reorder the same item while offline, each one generates an independent delete-plus-insert. When they sync back up, the list CRDT sees two separate insert operations, and the item appears twice.&lt;/p&gt;
&lt;p&gt;List CRDTs have no “move” operation. Only deletes and inserts. So we need a different strategy for ordering.&lt;/p&gt;
&lt;h3&gt;Fractional Indexing&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/fractional-indexing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Fractional indexing&lt;/a&gt; (by Rocicorp, based on &lt;a href=&quot;https://observablehq.com/@dgreensp/implementing-fractional-indexing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Observable post&lt;/a&gt; by David Greenspan) sidesteps the problem. Instead of moving an item in the list, you store an &lt;code&gt;order&lt;/code&gt; field on each item and update only that field when reordering. When you move an item between positions A and C, &lt;code&gt;generateKeyBetween(&quot;a&quot;, &quot;c&quot;)&lt;/code&gt; returns a key like &lt;code&gt;&quot;b&quot;&lt;/code&gt; that sorts between them. The rest of the list stays untouched.&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;order&lt;/code&gt; is a field on a &lt;code&gt;CoMap&lt;/code&gt;, updates use last-writer-wins semantics. The worst case during a concurrent edit is that one user’s reorder “wins.” The item never duplicates.&lt;/p&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;useSortable&lt;/code&gt; implementation:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; todoListEl &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useTemplateRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;HTMLElement&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todoListEl&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;useSortable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todoListEl&lt;span&gt;,&lt;/span&gt; todos&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  handle&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;.drag-handle&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  animation&lt;span&gt;:&lt;/span&gt; &lt;span&gt;150&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;onUpdate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Revert SortableJS DOM manipulation — let Vue re-render from data&lt;/span&gt;
    &lt;span&gt;removeNode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;item&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;insertNodeAt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;from&lt;span&gt;,&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;item&lt;span&gt;,&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;oldIndex&lt;span&gt;!&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; list &lt;span&gt;=&lt;/span&gt; todoList&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;list&lt;span&gt;?.&lt;/span&gt;$isLoaded &lt;span&gt;||&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;oldIndex &lt;span&gt;==&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;newIndex &lt;span&gt;==&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; sorted &lt;span&gt;=&lt;/span&gt; todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; before &lt;span&gt;=&lt;/span&gt; sorted&lt;span&gt;[&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;newIndex &lt;span&gt;-&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;order &lt;span&gt;??&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; after &lt;span&gt;=&lt;/span&gt; sorted&lt;span&gt;[&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;newIndex &lt;span&gt;+&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;newIndex &lt;span&gt;&amp;gt;&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;oldIndex &lt;span&gt;?&lt;/span&gt; &lt;span&gt;1&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;order &lt;span&gt;??&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; item &lt;span&gt;=&lt;/span&gt; sorted&lt;span&gt;[&lt;/span&gt;e&lt;span&gt;.&lt;/span&gt;oldIndex&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;item&lt;span&gt;)&lt;/span&gt; item&lt;span&gt;.&lt;/span&gt;$jazz&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;order&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;generateKeyBetween&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;before&lt;span&gt;,&lt;/span&gt; after&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;onUpdate&lt;/code&gt; handler contains a subtle DOM revert pattern: SortableJS directly manipulates the DOM when the user drags an item, but our source of truth is the &lt;code&gt;order&lt;/code&gt; field on each &lt;code&gt;CoMap&lt;/code&gt;. So we first undo the DOM change, then update the &lt;code&gt;order&lt;/code&gt; field. Vue’s computed re-sorts and re-renders correctly.&lt;/p&gt;
&lt;p&gt;The reorder syncs to all peers just like any other mutation. If you’ve worked with cross-tab state syncing before, you’ll know how tricky this can be:&lt;/p&gt;
&lt;p&gt;Building a Pinia Plugin for Cross-Tab State Syncing&lt;/p&gt;
&lt;h2&gt;Running It&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;pnpm&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open two browser tabs. Add a todo in one, watch it appear in the other. Toggle “Online” off, add more todos, toggle back on. They sync up. Copy the link, open it in an incognito window. Same list.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The full project is four files of application code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/schema.ts&lt;/code&gt; (4 lines): CoValue definitions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/App.vue&lt;/code&gt; (20 lines): Provider + inspector + online toggle&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/TodoApp.vue&lt;/code&gt; (~80 lines script + ~125 lines template): The complete app&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vite.config.ts&lt;/code&gt; (26 lines): Build config with custom element support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Coming from the Dexie.js approach, the amount of code surprised me. Jazz eliminates the backend-sync-API stack. You define your data, mutate it locally, and it syncs.&lt;/p&gt;
&lt;p&gt;Two things worth highlighting: fractional indexing for drag-and-drop avoids the duplicate-item problem that &lt;code&gt;CoList&lt;/code&gt; splice-based reordering can cause during concurrent offline edits. Jazz Groups with &lt;code&gt;addMember(&quot;everyone&quot;, &quot;writer&quot;)&lt;/code&gt; make the shared URL accessible. Without this, CoValues are private to the creator.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://jazz.tools&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jazz documentation&lt;/a&gt; for more, and the &lt;a href=&quot;https://www.npmjs.com/package/community-jazz-vue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;community-jazz-vue package&lt;/a&gt; (&lt;a href=&quot;https://github.com/garden-co/jazz/tree/main/packages/community-jazz-vue/src&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;source code&lt;/a&gt;) for the full Vue API.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href=&quot;https://github.com/alexanderop/vue-jazz-todo-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/alexanderop/vue-jazz-todo-example&lt;/a&gt;&lt;/p&gt;

          </content:encoded><category>vue</category><category>local-first</category><category>javascript</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Turned Claude Code&apos;s Thinking Indicator into a One Piece Adventure</title><link>https://alexop.dev/posts/claude-code-spinner-verbs-one-piece/</link><guid isPermaLink="true">https://alexop.dev/posts/claude-code-spinner-verbs-one-piece/</guid><description>Replace Claude Code&apos;s default spinner verbs with custom One Piece references using the spinnerVerbs config in settings.json.</description><pubDate>Thu, 12 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Every time Claude Code processes a request, it shows a spinner with a verb like “Considering…” or “Analyzing…”. That gets old fast. So I replaced all of them with One Piece references.&lt;/p&gt;

  You can just ask Claude Code to change the spinner verbs for you. Tell it which theme you want (Star Wars, cooking, etc.) and it&apos;ll update `~/.claude/settings.json` directly. This post shows you how it works under the hood.

&lt;h2&gt;The Setting: &lt;code&gt;spinnerVerbs&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Claude Code exposes a &lt;code&gt;spinnerVerbs&lt;/code&gt; config in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. You set a mode and pass an array of strings. That’s it.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;spinnerVerbs&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;mode&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;replace&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;verbs&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;&quot;Stretching like Luffy...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Reading the Poneglyph...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Activating Gear Fifth...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Three-Sword Styling...&quot;&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Two Modes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/strong&gt; swaps out every default verb. Claude Code only cycles through your list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;append&lt;/code&gt;&lt;/strong&gt; mixes your verbs with the built-in ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I picked &lt;code&gt;replace&lt;/code&gt; because I want full One Piece immersion, no “Considering…” sneaking in.&lt;/p&gt;
&lt;h2&gt;My Full Verb List&lt;/h2&gt;
&lt;p&gt;I wrote 25 verbs that cover the Straw Hat crew and the broader world:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;&quot;Stretching like Luffy...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Setting sail on the Grand Line...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Reading the Poneglyph...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Activating Gear Fifth...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Searching for the One Piece...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Navigating with Nami...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Three-Sword Styling...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Cooking with Sanji...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Consulting Nico Robin...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Upgrading with Franky...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Playing Brook&apos;s violin...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Haki awakening...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Dodging a Buster Call...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Entering the New World...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Deciphering the Will of D...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Gathering the crew...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Unfurling the Jolly Roger...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Bounty hunting...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Conquering the Calm Belt...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Dreaming of All Blue...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Mapping the world with Nami...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Yohoho-ing with Brook...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Tanuki-ing with Chopper...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Observing with Haki...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Sailing the Thousand Sunny...&quot;&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each verb references a character, location, or concept from the series. Chopper gets called a tanuki (he hates that), Sanji dreams of All Blue, and Brook does his signature laugh.&lt;/p&gt;
&lt;h2&gt;How to Apply It&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;~/.claude/settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;spinnerVerbs&lt;/code&gt; block anywhere at the top level.&lt;/li&gt;
&lt;li&gt;Restart Claude Code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now every thinking phase picks a random verb from your list. Simple customization, big personality boost.&lt;/p&gt;
&lt;p&gt;If you’re looking for more ways to personalize Claude Code, check out how to customize the status line or how I added sound effects with hooks. For a broader overview of all the customization options, see the full customization guide.&lt;/p&gt;
&lt;h2&gt;Make Your Own&lt;/h2&gt;
&lt;p&gt;Swap the verbs for any theme you like, Star Wars, cooking terms, gym motivation, whatever fits your vibe. The only rule: keep them short so they render cleanly in the terminal.&lt;/p&gt;

          </content:encoded><category>ai</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Added Sound Effects to Claude Code with Hooks</title><link>https://alexop.dev/posts/how-i-added-sound-effects-to-claude-code-with-hooks/</link><guid isPermaLink="true">https://alexop.dev/posts/how-i-added-sound-effects-to-claude-code-with-hooks/</guid><description>Claude Code hooks let you run shell commands on lifecycle events. I wired up Age of Empires sound effects to session start, prompt submission, task completion, and context compaction.</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Claude Code has a hooks system that lets you run shell commands on lifecycle events. I wired up Age of Empires sound effects to four key moments: session start, prompt submission, task completion, and context compaction. Now my terminal sounds like a medieval battlefield and I love it.&lt;/p&gt;
&lt;h2&gt;Why Sounds?&lt;/h2&gt;
&lt;p&gt;I spend a lot of time in Claude Code. Sometimes I send a prompt and switch to another window while it works. The problem is I never know when it’s done. I could keep checking, but that breaks my flow.&lt;/p&gt;
&lt;p&gt;I wanted audio cues. Something that tells me “hey, I’m done” without me having to look. And if I’m going to add sounds, why not make them fun?&lt;/p&gt;
&lt;h2&gt;How Claude Code Hooks Work&lt;/h2&gt;
&lt;p&gt;Claude Code lets you define hooks in your &lt;code&gt;settings.json&lt;/code&gt; file at &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. Hooks fire on specific lifecycle events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SessionStart&lt;/strong&gt;: When a new Claude Code session begins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UserPromptSubmit&lt;/strong&gt;: Right after you hit enter on a prompt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop&lt;/strong&gt;: When Claude finishes its response&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PreCompact&lt;/strong&gt;: Before context compaction happens (when the conversation gets too long)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each hook runs a shell command. That’s it. Simple and powerful.&lt;/p&gt;
&lt;p&gt;If you’re new to hooks, I covered the basics in my post about setting up Claude Code notification hooks. This post goes in a different direction: instead of desktop notifications, we’re adding sound effects.&lt;/p&gt;
&lt;p&gt;For a broader look at what Claude Code can do, see my overview of the full Claude Code feature stack.&lt;/p&gt;
&lt;h2&gt;My Setup&lt;/h2&gt;
&lt;p&gt;I put four MP3 files in &lt;code&gt;~/.claude/sounds/&lt;/code&gt; and used &lt;code&gt;afplay&lt;/code&gt; (macOS built-in audio player) to play them. Here’s my full hooks config:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// ~/.claude/settings.json&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;SessionStart&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;startup|clear&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;afplay ~/.claude/sounds/horn.mp3 &amp;amp;&quot;&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;UserPromptSubmit&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;afplay ~/.claude/sounds/yes.mp3 &amp;amp;&quot;&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;Stop&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;afplay ~/.claude/sounds/allhail.mp3 &amp;amp;&quot;&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;PreCompact&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;afplay ~/.claude/sounds/wololo.mp3 &amp;amp;&quot;&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; at the end is important. It runs &lt;code&gt;afplay&lt;/code&gt; in the background so the sound doesn’t block Claude Code from continuing.&lt;/p&gt;
&lt;h2&gt;The Sound Choices&lt;/h2&gt;
&lt;p&gt;All sounds are from Age of Empires. Here’s why I picked each one:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Sound&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SessionStart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;horn.mp3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A battle horn. The session begins, time to work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UserPromptSubmit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes.mp3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The villager “yes” response. Claude acknowledges your command.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allhail.mp3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;“All hail!” when Claude finishes. Victory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PreCompact&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wololo.mp3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The priest conversion sound. Your context is being… converted.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;wololo&lt;/code&gt; for context compaction is my favorite. If you’ve played Age of Empires, you know what happens when a priest starts chanting “wololo”: things change. That’s exactly what context compaction does. It transforms your conversation to fit the context window.&lt;/p&gt;
&lt;h2&gt;Bonus: Touch Files for Status Line&lt;/h2&gt;
&lt;p&gt;You might have noticed my actual config also touches files like &lt;code&gt;touch ~/.claude/.claude-done&lt;/code&gt;. I use these as signals for my custom status line script. The status line reads these files to show the current Claude Code state in my terminal prompt.&lt;/p&gt;
&lt;h2&gt;How to Set This Up Yourself&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create the sounds directory: &lt;code&gt;mkdir -p ~/.claude/sounds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Drop your MP3 files in there (any short sound clips work)&lt;/li&gt;
&lt;li&gt;Add the hooks config to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Restart Claude Code&lt;/li&gt;
&lt;/ol&gt;

On Linux, replace `afplay` with `aplay` or `paplay`. On Windows WSL, you can use `powershell.exe -c (New-Object Media.SoundPlayer &quot;path&quot;).PlaySync()`.

&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code hooks run shell commands on lifecycle events&lt;/li&gt;
&lt;li&gt;You can use &lt;code&gt;afplay&lt;/code&gt; on macOS to play sounds in the background&lt;/li&gt;
&lt;li&gt;Age of Empires sounds make the terminal more fun&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; suffix is key so sounds don’t block execution&lt;/li&gt;
&lt;li&gt;Touch files can signal other tools like status line scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Small things like audio feedback make tools feel more alive. It took five minutes to set up and now I actually enjoy waiting for Claude to finish.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>hooks</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building a Walkthrough Skill for AI Coding Agents</title><link>https://alexop.dev/posts/building-walkthrough-skill-claude-code/</link><guid isPermaLink="true">https://alexop.dev/posts/building-walkthrough-skill-claude-code/</guid><description>How I built a skill that generates interactive codebase walkthroughs with clickable Mermaid diagrams—works with Claude Code, Amp, and any agent that supports the skills standard.</description><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I’m a visual learner. When I join a new codebase or try to understand a complex flow, I need a diagram. Reading files one by one doesn’t give me the mental model I need—I need to see the connections.&lt;/p&gt;
&lt;p&gt;That’s why Amp’s &lt;a href=&quot;https://ampcode.com/news/walkthrough&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Shareable Walkthroughs&lt;/a&gt; caught my attention. The idea is simple: ask your AI coding tool to explain a feature, and it generates an interactive diagram where you can click through nodes to drill into the details.&lt;/p&gt;
&lt;p&gt;I couldn’t find the source code for Amp’s implementation. So I built my own.&lt;/p&gt;
&lt;aside&gt;
The skill is open source and ready to install:
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/walkthrough&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Quick install: &lt;code&gt;npx skills add https://github.com/alexanderop/walkthrough --skill walkthrough&lt;/code&gt;&lt;/p&gt;
&lt;/aside&gt;
&lt;h2&gt;What It Does&lt;/h2&gt;
&lt;p&gt;You ask your agent something like “walkthrough how does authentication work” and it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spawns 2–4 subagents to explore relevant parts of your codebase in parallel&lt;/li&gt;
&lt;li&gt;Synthesizes findings into 5–12 key concepts with connections&lt;/li&gt;
&lt;li&gt;Generates a self-contained HTML file with an interactive Mermaid diagram&lt;/li&gt;
&lt;li&gt;Opens it in your browser&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each node in the diagram is clickable. Click one and a detail panel slides in with a plain-English description, file paths, and optional code snippets. The whole thing is designed to give you a mental model in under two minutes.&lt;/p&gt;
&lt;p&gt;Here’s a live example—this walkthrough explains how the skill itself works. Click on any node to see the details:&lt;/p&gt;

&lt;aside&gt;
Skills aren&apos;t exclusive to Claude Code. They&apos;re a [shared standard](https://skills.dev/) supported by multiple AI coding agents—Claude Code, Amp, and others. A skill you write once works across all of them. If you&apos;re new to skills in Claude Code specifically, check out my guide to CLAUDE.md, skills, and subagents.
&lt;/aside&gt;
&lt;h2&gt;How the Skill Works&lt;/h2&gt;
&lt;p&gt;The walkthrough above shows the full pipeline. Let me walk through the four phases.&lt;/p&gt;
&lt;h3&gt;Phase 1: Trigger&lt;/h3&gt;
&lt;p&gt;Everything starts with a natural-language prompt. When you type something like “walkthrough how does auth work”, your agent matches the trigger pattern in the skill definition and activates the walkthrough workflow.&lt;/p&gt;
&lt;h3&gt;Phase 2: Skill Configuration&lt;/h3&gt;
&lt;p&gt;The skill consists of two files:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{ name: “skills/walkthrough”, open: true, children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://skill.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;skill.md&lt;/a&gt;”, comment: “// workflow and trigger definitions” },&lt;br /&gt;
{ name: “references”, open: true, children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://html-patterns.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;html-patterns.md&lt;/a&gt;”, comment: “// HTML, CSS, JS templates” }&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;skill.md&lt;/code&gt; defines the four-step workflow: scope the question, launch subagents, synthesize findings, generate HTML. It also includes a quality checklist—things like keeping diagrams to 5–12 nodes and writing descriptions in plain English.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;html-patterns.md&lt;/code&gt; is the complete reference for the generated output: React components, Mermaid configuration, the color palette, pan/zoom implementation, and Shiki syntax highlighting setup. The agent reads this file and follows the patterns when generating the HTML.&lt;/p&gt;
&lt;h3&gt;Phase 3: Exploration&lt;/h3&gt;
&lt;p&gt;This is where it gets interesting. Instead of having one agent read through your entire codebase, the skill uses subagents to parallelize the exploration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scope understanding&lt;/strong&gt; — The main agent clarifies what you’re asking about and identifies which areas of the codebase are relevant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel subagents&lt;/strong&gt; — 2–4 Explore subagents investigate specific regions concurrently, each returning a structured report with purpose, connections, node suggestions, file paths, and key code snippets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Synthesis&lt;/strong&gt; — The main agent combines all reports into a single list of nodes, edges, and subgroup groupings&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The subagents do the reading. The main agent does the thinking. This keeps the output coherent while making exploration fast.&lt;/p&gt;
&lt;h3&gt;Phase 4: Generation&lt;/h3&gt;
&lt;p&gt;With the synthesized data, the agent picks the right diagram type—flowchart for feature flows and architecture, ER diagram for database schemas—and generates a self-contained HTML file. No build step, no dependencies to install. It loads everything from CDNs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React 18&lt;/strong&gt; for the interactive UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mermaid 11&lt;/strong&gt; for diagram rendering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for styling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shiki&lt;/strong&gt; for syntax highlighting in code snippets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The design is dark-only with a pure black background and purple accents. Every diagram node is clickable via Mermaid’s callback system, and there’s scroll-to-zoom and drag-to-pan for larger diagrams. The file opens directly in your browser.&lt;/p&gt;
&lt;p&gt;And here’s what happens when I ask the skill to onboard me on this very blog’s codebase:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;walkthrough help me to onboard on this project
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Five content collections, a presentation engine, educational visualizers, OG image generation—all mapped out in one interactive diagram. Click any node to see what it does and where it lives.&lt;/p&gt;
&lt;h2&gt;Example Prompts&lt;/h2&gt;
&lt;p&gt;Here are some ways I use it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;walkthrough how does the presentation mode work
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;walkthrough explain the authentication flow when a user signs up
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;walkthrough database schema for the content management system, use an ER diagram
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why I Built This&lt;/h2&gt;
&lt;p&gt;I’ve written about using AI for diagrams before. The pattern is always the same—I need to see the system before I can reason about it.&lt;/p&gt;
&lt;p&gt;Existing tools either require you to manually write diagram code or generate static images. What I wanted was something interactive where I could start at the high level and drill down only into the parts that matter.&lt;/p&gt;
&lt;p&gt;The walkthrough skill fills that gap. It’s the onboarding tool I wish every codebase had.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Skills are a standard—this works with Claude Code, Amp, and any agent that supports them&lt;/li&gt;
&lt;li&gt;Subagents make codebase exploration fast by parallelizing reads&lt;/li&gt;
&lt;li&gt;Self-contained HTML with CDN dependencies means zero setup for viewers&lt;/li&gt;
&lt;li&gt;The skill is open source at &lt;a href=&quot;https://github.com/alexanderop/walkthrough&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/alexanderop/walkthrough&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re a visual thinker like me, give it a try. Ask it to explain something in your codebase and see what comes out.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><category>skills</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>From Tasks to Swarms: Agent Teams in Claude Code</title><link>https://alexop.dev/posts/from-tasks-to-swarms-agent-teams-in-claude-code/</link><guid isPermaLink="true">https://alexop.dev/posts/from-tasks-to-swarms-agent-teams-in-claude-code/</guid><description>Agent teams let multiple Claude Code sessions coordinate, communicate, and self-organize. Here&apos;s how they work, when to use them, and what they cost.</description><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;In my previous post I covered spec-driven development with Claude Code—using the task system to break large refactors into subagent-driven work. Subagents are powerful, but they have one fundamental limitation: they can only report back to the parent. They can’t talk to each other.&lt;/p&gt;
&lt;p&gt;Agent teams remove that limitation. They’re a new experimental feature in Claude Code where multiple sessions coordinate as a team—with a shared task list, direct messaging between teammates, and a team lead that orchestrates the whole thing.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;The Evolution: From Subagents to Agent Teams&lt;/h2&gt;
&lt;p&gt;This is the progression of delegation in Claude Code. Each step gives the AI more autonomy and less handholding:&lt;/p&gt;

Agent teams are disabled by default. Enable them by adding `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` to your `settings.json` or environment.

&lt;h2&gt;Subagents vs Agent Teams&lt;/h2&gt;
&lt;p&gt;The critical difference is communication. Subagents are fire-and-forget workers. Agent teams are collaborators.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Subagents&lt;/th&gt;
&lt;th&gt;Agent Teams&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Own window, results return&lt;/td&gt;
&lt;td&gt;Own window, fully independent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Communication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Report to main only&lt;/td&gt;
&lt;td&gt;Message each other directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coordination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Main manages everything&lt;/td&gt;
&lt;td&gt;Shared task list, self-claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Token cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Higher—each teammate is a full session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Focused tasks, research&lt;/td&gt;
&lt;td&gt;Complex work needing collaboration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;The Seven Team Primitives&lt;/h2&gt;
&lt;p&gt;Agent teams aren’t magic. They’re built from seven tools that Claude can call. Understanding these tools is the key to understanding how teams actually work under the hood.&lt;/p&gt;
&lt;p&gt;Here’s each one, with the real calls from my QA session.&lt;/p&gt;
&lt;h3&gt;TeamCreate — Start a Team&lt;/h3&gt;
&lt;p&gt;Creates the team directory and config file. This is always the first call.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Tool call&lt;/span&gt;
TeamCreate(&lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;team_name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;blog-qa&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;QA team testing the blog&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;)

&lt;span&gt;// Creates on disk:&lt;/span&gt;
&lt;span&gt;// ~/.claude/teams/blog-qa/config.json&lt;/span&gt;
&lt;span&gt;// ~/.claude/tasks/blog-qa/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;team_name&lt;/code&gt; is the namespace that links everything together—tasks, messages, and the config file all live under it.&lt;/p&gt;
&lt;h3&gt;TaskCreate — Define a Unit of Work&lt;/h3&gt;
&lt;p&gt;Each task is a JSON file on disk. The lead creates these before spawning any teammates.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Tool call&lt;/span&gt;
TaskCreate(&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;subject&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;QA: Core pages respond with 200 and valid HTML&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &quot;Fetch all major pages at localhost&lt;span&gt;:&lt;/span&gt;&lt;span&gt;4321&lt;/span&gt; and verify
    they return HTTP &lt;span&gt;200&lt;/span&gt;. Test&lt;span&gt;:&lt;/span&gt; /&lt;span&gt;,&lt;/span&gt; /posts&lt;span&gt;,&lt;/span&gt; /tags&lt;span&gt;,&lt;/span&gt; /notes&lt;span&gt;,&lt;/span&gt; /tils&lt;span&gt;,&lt;/span&gt;
    /search&lt;span&gt;,&lt;/span&gt; /projects&lt;span&gt;,&lt;/span&gt; /&lt;span&gt;404&lt;/span&gt; (should be &lt;span&gt;404&lt;/span&gt;)&lt;span&gt;,&lt;/span&gt; /rss.xml...&quot;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;activeForm&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Testing core page responses&quot;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;)

&lt;span&gt;// Creates: ~/.claude/tasks/blog-qa/1.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; is what the teammate actually reads to know what to do. It’s essentially a prompt for the agent. The more detail you pack in here (or the lead packs in), the better the agent performs.&lt;/p&gt;
&lt;h3&gt;TaskUpdate — Claim and Complete Work&lt;/h3&gt;
&lt;p&gt;Teammates use this to change task status. It’s how work moves through the pipeline.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Teammate claims a task&lt;/span&gt;
TaskUpdate(&lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;taskId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;in_progress&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;owner&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-pages&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;)

&lt;span&gt;// Teammate finishes&lt;/span&gt;
TaskUpdate(&lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;taskId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;completed&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The status field prevents two agents from working on the same thing. When a task is &lt;code&gt;in_progress&lt;/code&gt; with an owner, other agents skip it.&lt;/p&gt;
&lt;h3&gt;TaskList — Find Available Work&lt;/h3&gt;
&lt;p&gt;Returns all tasks with their current status. Teammates call this after completing a task to find what’s next.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Tool call&lt;/span&gt;
TaskList()

&lt;span&gt;// Returns:&lt;/span&gt;
&lt;span&gt;// { id: &quot;1&quot;, subject: &quot;Core pages...&quot;,  status: &quot;completed&quot;,   owner: &quot;qa-pages&quot; }&lt;/span&gt;
&lt;span&gt;// { id: &quot;2&quot;, subject: &quot;Blog posts...&quot;,  status: &quot;in_progress&quot;, owner: &quot;qa-posts&quot; }&lt;/span&gt;
&lt;span&gt;// { id: &quot;3&quot;, subject: &quot;Links...&quot;,       status: &quot;pending&quot;,     owner: &quot;&quot;         }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the shared coordination mechanism. No centralized scheduler—each teammate polls &lt;code&gt;TaskList&lt;/code&gt;, finds unowned pending tasks, and claims one.&lt;/p&gt;
&lt;h3&gt;Task (with team_name) — Spawn a Teammate&lt;/h3&gt;
&lt;p&gt;The existing &lt;code&gt;Task&lt;/code&gt; tool gets a &lt;code&gt;team_name&lt;/code&gt; parameter that turns a regular subagent into a team member. Once spawned, the teammate can see the shared task list and message other teammates.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Tool call&lt;/span&gt;
Task(&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;QA: Core page responses&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;subagent_type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-pages&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;team_name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;blog-qa&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;model&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;sonnet&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;prompt&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &quot;You are a QA agent testing a blog at localhost&lt;span&gt;:&lt;/span&gt;&lt;span&gt;4321&lt;/span&gt;.
    Your task is Task #&lt;span&gt;1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; verify all core pages respond correctly...&quot;
&lt;span&gt;}&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;model: &quot;sonnet&quot;&lt;/code&gt; — the lead ran on Opus but spawned all teammates on Sonnet. This is a common cost optimization pattern: expensive model for coordination, cheaper model for execution.&lt;/p&gt;
&lt;h3&gt;SendMessage — Talk to Each Other&lt;/h3&gt;
&lt;p&gt;This is what makes teams different from subagents. Any teammate can message any other teammate directly.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Teammate → Lead: report findings&lt;/span&gt;
SendMessage(&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;recipient&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;team-lead&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Task #1 complete. 16/16 pages pass. No issues found.&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;summary&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;All core pages pass&quot;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;)

&lt;span&gt;// Lead → Teammate: request shutdown&lt;/span&gt;
SendMessage(&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;shutdown_request&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;recipient&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-pages&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;All tasks complete, shutting down team.&quot;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;)

&lt;span&gt;// Teammate → Lead: acknowledge shutdown&lt;/span&gt;
SendMessage(&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;shutdown_response&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;request_id&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;shutdown-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;approve&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SendMessage&lt;/code&gt; supports several message types: &lt;code&gt;message&lt;/code&gt; for direct messages, &lt;code&gt;broadcast&lt;/code&gt; to reach all teammates at once, &lt;code&gt;shutdown_request&lt;/code&gt;/&lt;code&gt;shutdown_response&lt;/code&gt; for graceful teardown, and &lt;code&gt;plan_approval_response&lt;/code&gt; for quality gates.&lt;/p&gt;
&lt;h3&gt;TeamDelete — Clean Up&lt;/h3&gt;
&lt;p&gt;Removes the team config and all task files from disk. Called after all teammates have shut down.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// Tool call&lt;/span&gt;
TeamDelete()

&lt;span&gt;// Removes:&lt;/span&gt;
&lt;span&gt;// ~/.claude/teams/blog-qa/&lt;/span&gt;
&lt;span&gt;// ~/.claude/tasks/blog-qa/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How They Fit Together&lt;/h3&gt;
&lt;p&gt;Every team session follows the same tool sequence:&lt;/p&gt;
&lt;p&gt;Each teammate is a full Claude Code session with its own context window. They load the same project context (&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;, MCP servers, skills) but don’t inherit the lead’s conversation history. The task files on disk and &lt;code&gt;SendMessage&lt;/code&gt; are the only coordination channels—there’s no shared memory.&lt;/p&gt;
&lt;h2&gt;The Team Lead’s Control Layer&lt;/h2&gt;
&lt;p&gt;What makes agent teams more than “just parallel subagents” is the team lead. The lead is an abstraction layer that gives AI more control over coordination:&lt;/p&gt;
&lt;h2&gt;Task Lifecycle in a Team&lt;/h2&gt;
&lt;h2&gt;Use Case: Building a Large Feature&lt;/h2&gt;
&lt;p&gt;When you ask Claude Code to implement something big, agent teams let it parallelize the work the way a real engineering team would:&lt;/p&gt;

Create an agent team to build the new dashboard feature.
One teammate on the API layer, one on the frontend components,
one on the test suite. Use Sonnet for each teammate.

&lt;p&gt;The key difference from subagents: when the API teammate finishes the type definitions, it messages the UI teammate directly. No round-trip through the main agent. The test teammate can ask the API teammate to spin up a dev server. They self-coordinate.&lt;/p&gt;
&lt;h2&gt;Real Example: QA Swarm Against My Blog&lt;/h2&gt;
&lt;p&gt;This isn’t a hypothetical. I ran this against my own blog before a production deploy. Here’s the exact prompt I used:&lt;/p&gt;

Use a team of agents that will do QA against my blog.
It&apos;s running at http://localhost:4321/

&lt;p&gt;That’s it. Claude took over from there.&lt;/p&gt;
&lt;h3&gt;What the Lead Did&lt;/h3&gt;
&lt;p&gt;The lead verified the site was running (&lt;code&gt;curl&lt;/code&gt; returned 200), created a team called &lt;code&gt;blog-qa&lt;/code&gt;, then broke the work into 5 tasks and spawned 5 agents—all using Sonnet to keep costs down:&lt;/p&gt;
&lt;h3&gt;What the Files Look Like&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;TeamCreate&lt;/code&gt; runs, it writes two things to disk. Here’s what they actually look like:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// ~/.claude/teams/blog-qa/config.json&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;members&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-pages&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;abc-123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentType&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-posts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;def-456&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentType&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-links&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;ghi-789&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentType&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-seo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;   &lt;span&gt;&quot;agentId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;jkl-012&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentType&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-a11y&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;  &lt;span&gt;&quot;agentId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;mno-345&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;agentType&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;general-purpose&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the tasks lived as individual files under &lt;code&gt;~/.claude/tasks/blog-qa/&lt;/code&gt;. Each task had a subject, a detailed description telling the agent exactly what to check, and a status field:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// ~/.claude/tasks/blog-qa/1.json&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;subject&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;QA: Core pages respond with 200 and valid HTML&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &quot;Fetch all major pages on the blog at localhost&lt;span&gt;:&lt;/span&gt;&lt;span&gt;4321&lt;/span&gt; and verify
    they return HTTP &lt;span&gt;200&lt;/span&gt; with valid HTML content. Test&lt;span&gt;:&lt;/span&gt; /&lt;span&gt;,&lt;/span&gt; /posts&lt;span&gt;,&lt;/span&gt; /tags&lt;span&gt;,&lt;/span&gt; /notes&lt;span&gt;,&lt;/span&gt;
    /tils&lt;span&gt;,&lt;/span&gt; /search&lt;span&gt;,&lt;/span&gt; /projects&lt;span&gt;,&lt;/span&gt; /talks&lt;span&gt;,&lt;/span&gt; /goals&lt;span&gt;,&lt;/span&gt; /prompts&lt;span&gt;,&lt;/span&gt; /&lt;span&gt;404&lt;/span&gt; (should be &lt;span&gt;404&lt;/span&gt;)&lt;span&gt;,&lt;/span&gt;
    /robots.txt&lt;span&gt;,&lt;/span&gt; /rss.xml&lt;span&gt;,&lt;/span&gt; /llms.txt&lt;span&gt;,&lt;/span&gt; /llms-full.txt. Also check that the HTML
    contains expected elements (title&lt;span&gt;,&lt;/span&gt; nav&lt;span&gt;,&lt;/span&gt; footer).&quot;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;completed&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;owner&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;qa-pages&quot;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lead created these 5 tasks:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;What It Checked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Core page responses&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qa-pages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16 URLs return correct HTTP status codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Blog post rendering&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qa-posts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;83 posts have h1, meta tags, working images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Navigation &amp;amp; link integrity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qa-links&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;146 internal URLs for broken links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;RSS, sitemap, SEO metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qa-seo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSS validity, robots.txt, og:tags, JSON-LD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Accessibility &amp;amp; HTML structure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qa-a11y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heading hierarchy, ARIA, theme toggle, lang attr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;How the Agents Reported Back&lt;/h3&gt;
&lt;p&gt;Each agent finished independently and sent a structured report back via &lt;code&gt;SendMessage&lt;/code&gt;. Here’s what &lt;code&gt;qa-pages&lt;/code&gt; sent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Task #1 Complete: Core Page Response Testing

### Summary: ALL PAGES PASS

| URL         | Expected | Actual | Result |
|-------------|----------|--------|--------|
| /           | 200      | 200    | PASS   |
| /posts      | 200      | 200    | PASS   |
| /tags       | 200      | 200    | PASS   |
| /notes      | 200      | 200    | PASS   |
| /404        | 404      | 404    | PASS   |
| /rss.xml    | 200      | 200    | PASS   |
| /llms.txt   | 200      | 200    | PASS   |
  ...16 URLs tested, 0 failures.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Meanwhile &lt;code&gt;qa-posts&lt;/code&gt; tested all 83 blog posts and found 2 with broken OG images. &lt;code&gt;qa-seo&lt;/code&gt; found the &lt;code&gt;og:type&lt;/code&gt; meta tag was missing. &lt;code&gt;qa-a11y&lt;/code&gt; caught that &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; had &lt;code&gt;class=&quot;false&quot;&lt;/code&gt; (a boolean stringified as a class name) and a heading hierarchy issue.&lt;/p&gt;
&lt;h3&gt;The Lead’s Synthesis&lt;/h3&gt;
&lt;p&gt;Once all 5 agents reported back, the lead compiled a single prioritized report:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## QA Report — alexop.dev

5 agents | 146+ URLs tested | 83 blog posts checked

### Issues by Severity

MAJOR (4):
  1. &amp;lt;html class=&quot;false&quot;&amp;gt; — boolean stringified as CSS class
  2. &amp;lt;h2&amp;gt; before &amp;lt;h1&amp;gt; — newsletter banner breaks heading order
  3. Theme toggle button missing from DOM
  4. Theme hardcoded to dark only

MEDIUM (2):
  5. og:type meta tag missing on all pages
  6. 2 blog posts with broken OG images

MINOR (4):
  7-10. Various small accessibility gaps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the lead sent &lt;code&gt;shutdown_request&lt;/code&gt; to each agent, they acknowledged, and &lt;code&gt;TeamDelete&lt;/code&gt; cleaned up the files.&lt;/p&gt;
&lt;h3&gt;The Full Lifecycle&lt;/h3&gt;
&lt;p&gt;The whole thing—from prompt to final report—took about 3 minutes. Each agent used &lt;code&gt;curl&lt;/code&gt; to fetch pages and parse the HTML. No browser automation, no test framework. Just 5 Claude sessions hammering a dev server in parallel.&lt;/p&gt;
&lt;h2&gt;The Cost Trade-off&lt;/h2&gt;
&lt;p&gt;Agent teams are token-heavy. Every teammate is a full Claude Code session with its own context window.&lt;/p&gt;
&lt;p&gt;The math is simple: more agents = more tokens = more cost. Use teams when the coordination benefit justifies it. For routine tasks, a single session or subagents are more cost-effective.&lt;/p&gt;
&lt;h3&gt;When Teams Are Worth It&lt;/h3&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Enable agent teams:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// .claude/settings.json&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;env&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then tell Claude what you want:&lt;/p&gt;

Create an agent team to refactor the authentication module.
One teammate on the backend logic, one on the frontend hooks,
one running tests continuously. Require plan approval before
any teammate makes changes.

&lt;h3&gt;Recipe: Plan First, Parallelize Second&lt;/h3&gt;
&lt;p&gt;The most effective pattern I’ve found isn’t jumping straight into a team. It’s a two-step approach: &lt;strong&gt;plan first with plan mode, then hand the plan to a team for parallel execution.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here’s the workflow:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Get a plan.&lt;/strong&gt; Start with plan mode (&lt;code&gt;/plan&lt;/code&gt; or tell Claude to plan). Let it explore the codebase, identify files, and produce a step-by-step implementation plan. Review it. Adjust it. This is cheap—plan mode only reads files.&lt;/p&gt;

Plan the refactor of our authentication module. I want to split the
monolithic auth.ts into separate files for JWT handling, session
management, and middleware. Show me the plan before doing anything.

&lt;p&gt;Claude produces something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Plan:
1. Create src/auth/jwt.ts — extract token signing/verification
2. Create src/auth/sessions.ts — extract session logic
3. Create src/auth/middleware.ts — extract Express middleware
4. Update src/auth/index.ts — re-export public API
5. Update 12 import sites across the codebase
6. Update tests in src/auth/__tests__/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Parallelize the plan with a team.&lt;/strong&gt; Once you approve the plan, tell Claude to execute it as a team. The key insight: the plan already has the task breakdown. You’re just telling Claude to run independent tracks in parallel instead of sequentially.&lt;/p&gt;

Now execute this plan using an agent team. Parallelize where possible—
steps 1-3 can run in parallel since they&apos;re independent extractions.
Step 4-5 depends on 1-3. Step 6 depends on everything.
Use Sonnet for the teammates.

&lt;p&gt;The lead sees the dependency graph and spawns teammates accordingly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Wave 1 (parallel):  jwt.ts + sessions.ts + middleware.ts  → 3 teammates
Wave 2 (after wave 1): index.ts barrel + update imports   → 1-2 teammates
Wave 3 (after wave 2): update tests                       → 1 teammate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because plan mode gives you the checkpoint to review before spending tokens on a full team. Without the plan step, the team lead has to figure out the task breakdown itself—which it can do, but you lose the chance to steer it. With the plan, you’ve already shaped the work. The team just executes it faster.&lt;/p&gt;

Plan mode costs ~10k tokens. A team that goes in the wrong direction costs 500k+. Spending a few seconds reviewing a plan saves you from expensive course corrections mid-swarm.

&lt;h3&gt;Display Modes&lt;/h3&gt;
&lt;h2&gt;The Abstraction Ladder&lt;/h2&gt;
&lt;p&gt;Each level trades control for compute. Solo sessions give you full control but limited throughput. Agent teams give you maximum compute but you’re trusting the AI to coordinate itself. The sweet spot depends on your task.&lt;/p&gt;
&lt;aside&gt;
Agent teams are experimental. Key limitations:
- No session resumption for in-process teammates
- Task status can lag—teammates sometimes forget to mark tasks complete
- One team per session, no nested teams
- Split panes require tmux or iTerm2 (not VS Code terminal)
- All teammates start with the lead&apos;s permission settings
&lt;/aside&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Agent teams let multiple Claude Code sessions &lt;strong&gt;communicate with each other&lt;/strong&gt;, not just report to a parent&lt;/li&gt;
&lt;li&gt;A team lead orchestrates, observes, and synthesizes—another abstraction layer where AI manages AI&lt;/li&gt;
&lt;li&gt;Best use cases: &lt;strong&gt;large features&lt;/strong&gt; (parallel tracks), &lt;strong&gt;QA swarms&lt;/strong&gt; (multiple testing perspectives), &lt;strong&gt;competing hypotheses&lt;/strong&gt; (debate and converge)&lt;/li&gt;
&lt;li&gt;The trade-off is real: more agents = more tokens = more cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best recipe&lt;/strong&gt;: plan first with plan mode (cheap), then hand the plan to a team for parallel execution (expensive but fast). The plan gives you a checkpoint before committing tokens.&lt;/li&gt;
&lt;li&gt;Start with subagents for focused work, graduate to teams when workers need to coordinate&lt;/li&gt;
&lt;li&gt;For background on the task system that teams build on, see my spec-driven development post&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>VMark: Hand-Drawn Annotations for Presentations</title><link>https://alexop.dev/posts/vmark-tutorial/</link><guid isPermaLink="true">https://alexop.dev/posts/vmark-tutorial/</guid><description>Learn how to add animated hand-drawn annotations to your presentations using VMark. A complete tutorial covering all annotation types, colors, and timing controls.</description><pubDate>Tue, 03 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h1&gt;VMark: Hand-Drawn Annotations&lt;/h1&gt;
&lt;p&gt;Add emphasis to your presentations with animated hand-drawn marks&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Static text in presentations lacks emphasis.&lt;/p&gt;
&lt;p&gt;We want &lt;strong&gt;this&lt;/strong&gt; to stand out.&lt;/p&gt;
&lt;p&gt;VMark creates a “live drawing” effect that grabs attention and guides your audience’s focus.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Import VMark from the presentation feature:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basic usage wraps text in the component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;text to emphasize&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default is underline, and it animates on click.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Annotation Types: Underline &amp;amp; Circle&lt;/h2&gt;
&lt;p&gt;Underline is the default. Use it for general emphasis.&lt;/p&gt;
&lt;p&gt;Circle draws attention to key terms or important words.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;underline&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;emphasis&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;circle&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;color&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;red&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;key term&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Annotation Types: Box &amp;amp; Highlight&lt;/h2&gt;
&lt;p&gt;Box frames content like code or definitions.&lt;/p&gt;
&lt;p&gt;Highlight creates a marker effect. It uses 40% opacity so text stays readable.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;box&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;color&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;green&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;boxed&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;highlight&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;color&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;yellow&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;highlighted&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Annotation Types: Strike-through &amp;amp; Crossed-off&lt;/h2&gt;
&lt;p&gt;Use these for corrections or showing what to avoid:&lt;/p&gt;
&lt;p&gt;This approach is wrong&lt;/p&gt;
&lt;p&gt;Don’t do this&lt;/p&gt;
&lt;p&gt;Common pattern: show the wrong approach, then reveal the correct one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Annotation Type: Bracket&lt;/h2&gt;
&lt;p&gt;&amp;lt;VMark type=“bracket” brackets={[‘left’, ‘right’]} color=“purple”&amp;gt;&lt;br /&gt;
Brackets emphasize entire phrases or sentences&lt;br /&gt;
&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;brackets&lt;/code&gt; prop controls direction:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;bracket&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;brackets&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;left&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;right&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
  phrase
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Options: &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;bottom&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Color Presets&lt;/h2&gt;
&lt;p&gt;VMark includes 24 color presets:&lt;/p&gt;
&lt;p&gt;red | blue | green | yellow | purple | orange&lt;/p&gt;
&lt;p&gt;cyan | teal | pink | indigo | lime | amber&lt;/p&gt;
&lt;p&gt;Custom CSS colors also work: &lt;code&gt;#ff6b6b&lt;/code&gt;, &lt;code&gt;rgb(100, 200, 150)&lt;/code&gt;, &lt;code&gt;rgba(255, 0, 0, 0.5)&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Controlling Timing: Sequential&lt;/h2&gt;
&lt;p&gt;By default, VMark annotations appear sequentially with each click.&lt;/p&gt;
&lt;p&gt;This one appears second.&lt;/p&gt;
&lt;p&gt;And this one third.&lt;/p&gt;
&lt;p&gt;Just place VMark components in order. Each one advances the click counter.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Controlling Timing: Explicit&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;at&lt;/code&gt; to specify an exact click number:&lt;/p&gt;
&lt;p&gt;Third (at=3)&lt;/p&gt;
&lt;p&gt;First (at=1)&lt;/p&gt;
&lt;p&gt;Second (at=2)&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;at&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;appears on click 3&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;at&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;appears on click 1&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Advanced Timing: Relative &amp;amp; Ranges&lt;/h2&gt;
&lt;p&gt;Relative positioning with &lt;code&gt;+N&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;+1 from previous&lt;/p&gt;
&lt;p&gt;Ranges show content only during a window:&lt;/p&gt;
&lt;p&gt;&amp;lt;VMark at={[3, 5]} type=“highlight” color=“yellow”&amp;gt;Visible from click 3 to 4&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;at&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;+1&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;offset from previous&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt; &lt;span&gt;at&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;5&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;visible during clicks 2-4&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;VMark&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Under the Hood&lt;/h2&gt;
&lt;p&gt;VMark is built on the rough-notation library.&lt;/p&gt;
&lt;p&gt;Key behaviors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generates SVG annotations at runtime&lt;/li&gt;
&lt;li&gt;Respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt; (instant show, no animation)&lt;/li&gt;
&lt;li&gt;Works with print/export mode (&lt;code&gt;?print=true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Annotations are visual only. Screen readers see just the text&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Import from &lt;code&gt;@features/presentation&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;7 annotation types: underline, circle, box, highlight, strike-through, crossed-off, bracket&lt;/p&gt;
&lt;p&gt;24 color presets plus custom CSS colors&lt;/p&gt;
&lt;p&gt;Full click system integration with &lt;code&gt;at&lt;/code&gt; prop for timing control&lt;/p&gt;
&lt;p&gt;Press &lt;strong&gt;Escape&lt;/strong&gt; to exit presentation mode.&lt;/p&gt;

          </content:encoded><category>presentation</category><category>tutorial</category><category>documentation</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Spec-Driven Development with Claude Code in Action</title><link>https://alexop.dev/posts/spec-driven-development-claude-code-in-action/</link><guid isPermaLink="true">https://alexop.dev/posts/spec-driven-development-claude-code-in-action/</guid><description>A practical workflow for tackling large refactors with Claude Code using parallel research subagents, written specs, and the new task system for context-efficient implementation.</description><pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I’m building a &lt;a href=&quot;https://github.com/alexanderop/nuxt-sync-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;simplified sync engine from scratch&lt;/a&gt; using Nuxt 4.&lt;br /&gt;
My approach: study how production-grade frameworks solve the hard problems, then implement a minimal version myself.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jazz.tools&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jazz&lt;/a&gt; is my primary reference a local-first framework with elegant patterns for persistence, conflict resolution, and cross-tab sync. Rather than reading through their codebase manually,&lt;br /&gt;
I use Claude Code to research, extract patterns, and help me implement them.&lt;/p&gt;
&lt;p&gt;This post documents the workflow I call &lt;strong&gt;Spec-Driven Development with Claude Code&lt;/strong&gt; the exact prompts, tools, and patterns I used to migrate my storage layer from SQLite/WASM to IndexedDB in a single day.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;My sync engine was using &lt;code&gt;sql.js&lt;/code&gt; (SQLite compiled to WASM) for client-side storage. It worked, but had issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large WASM bundle (~1MB)&lt;/li&gt;
&lt;li&gt;Complex COOP/COEP header requirements&lt;/li&gt;
&lt;li&gt;No native cross-tab sync&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to migrate to IndexedDB, borrowing patterns from Jazz. But this was a significant refactor touching 15+ files. (For background on local-first web development and why it matters, see my earlier post.)&lt;/p&gt;
&lt;h2&gt;The Workflow&lt;/h2&gt;
&lt;p&gt;Instead of diving into code, I used Claude Code as an &lt;strong&gt;AI development team&lt;/strong&gt;with myself as the product owner, Claude as the tech lead, and subagents as developers.&lt;/p&gt;
&lt;p&gt;Important: I also cloned the source code of Jazz into my Project so Claude could reference it during research and implementation.&lt;/p&gt;
&lt;figure&gt;
&lt;h2&gt;Phase 1: Research with Parallel Subagents&lt;/h2&gt;
&lt;h3&gt;The Prompt&lt;/h3&gt;

you have access to jazz source repo explain to me how they use
indexdb in the client to persist state our project is using sqlite
but we want to change to indexdb with jazz your goal is to write
a report spin up multiple subagents for your research task

&lt;h3&gt;What Happened&lt;/h3&gt;
&lt;p&gt;Claude spawned &lt;strong&gt;5 parallel research agents&lt;/strong&gt;, each investigating a specific aspect of Jazz:&lt;/p&gt;

These are Claude Code&apos;s **built-in subagents**. The `Task` tool is a native tool in Claude Code, just like `Read` or `Bash`. When you ask Claude to &quot;spin up subagents,&quot; it uses this built-in tool automatically with the `general-purpose` subagent type. No custom agent definition or setup is required.

&lt;figure&gt;
&lt;p&gt;Each agent explored the Jazz codebase independently and reported back:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Key Findings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRDT&lt;/td&gt;
&lt;td&gt;Data structures&lt;/td&gt;
&lt;td&gt;CoMap, CoList use operation-based CRDTs with LWW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;Real-time sync&lt;/td&gt;
&lt;td&gt;4-message protocol: load, known, content, done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push/Pull&lt;/td&gt;
&lt;td&gt;Sync strategy&lt;/td&gt;
&lt;td&gt;Hybrid model with known-state tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;IndexedDB with &lt;code&gt;coValues&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;, &lt;code&gt;transactions&lt;/code&gt; stores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Overall design&lt;/td&gt;
&lt;td&gt;Monorepo with platform adapters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Follow-up Prompt&lt;/h3&gt;

research longer and improve the plan

&lt;p&gt;This triggered deeper investigation into edge cases and implementation details.&lt;/p&gt;
&lt;h2&gt;Phase 2: Spec Creation&lt;/h2&gt;
&lt;p&gt;After research, Claude wrote a comprehensive technical specification to &lt;code&gt;docs/indexeddb-migration-spec.md&lt;/code&gt;:&lt;br /&gt;
&lt;a href=&quot;https://gist.github.com/alexanderop/70ef80ac6dda5166c5085cc9bb269df1&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Full spec&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; IndexedDB Migration Specification&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Part 1: How Jazz Uses IndexedDB&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Database schema (coValues, sessions, transactions stores)
&lt;span&gt;-&lt;/span&gt; Transaction queuing pattern
&lt;span&gt;-&lt;/span&gt; Entity caching layer
&lt;span&gt;-&lt;/span&gt; Session-based conflict resolution

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Part 2: Current SQLite Architecture Analysis&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; sql.js WASM setup
&lt;span&gt;-&lt;/span&gt; Existing sync protocol
&lt;span&gt;-&lt;/span&gt; Pain points and limitations

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Part 3: Migration Plan (4 Phases)&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Phase 1: Core IndexedDB utilities
&lt;span&gt;-&lt;/span&gt; Phase 2: Composables layer
&lt;span&gt;-&lt;/span&gt; Phase 3: Cross-tab sync
&lt;span&gt;-&lt;/span&gt; Phase 4: Cleanup and testing

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Part 4: Implementation Checklist&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; [ ] idb-helpers.ts
&lt;span&gt;-&lt;/span&gt; [ ] useIndexedDB.ts
&lt;span&gt;-&lt;/span&gt; [ ] useSessionTracking.ts
&lt;span&gt;-&lt;/span&gt; ... (14 items total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: The spec becomes the source of truth. It’s a document Claude can reference during implementation, ensuring consistency across all tasks.&lt;br /&gt;
It also becomes a Pin that we can use if something went wrong during implementation.&lt;/p&gt;
&lt;h2&gt;Phase 3: Spec Refinement via Interview&lt;/h2&gt;
&lt;p&gt;Before implementation, I wanted to ensure the spec was solid. I used Claude’s &lt;code&gt;AskUserQuestion&lt;/code&gt; tool:&lt;/p&gt;
&lt;h3&gt;The Prompt&lt;/h3&gt;

use the ask_user_question tool do you have any questions regarding
@docs/indexeddb-migration-spec.md before we implement it we want
to improve the specs

&lt;p&gt;Claude asked clarifying questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Should we support migration from existing SQLite data?&lt;/li&gt;
&lt;li&gt;What’s the preferred conflict resolution strategy?&lt;/li&gt;
&lt;li&gt;Should cross-tab sync use BroadcastChannel or SharedWorker?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After answering, I requested Vue-specific improvements:&lt;/p&gt;

we want to use provide and inject you have access to the source
code of pinia spin up multiple subagents how they do it so we can
use same patterns

&lt;p&gt;Claude researched Pinia’s patterns and updated the spec with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Symbol-based injection keys&lt;/li&gt;
&lt;li&gt;Provider composables with fallback patterns&lt;/li&gt;
&lt;li&gt;Proper cleanup on unmount&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Phase 4: Implementation with Task Delegation&lt;/h2&gt;
&lt;p&gt;This is where the new &lt;strong&gt;Claude Code Task System&lt;/strong&gt; shines. (If you’re unfamiliar with subagents and how they work in Claude Code, my customization guide covers the fundamentals.)&lt;/p&gt;
&lt;h3&gt;The Prompt&lt;/h3&gt;

implement @docs/indexeddb-migration-spec.md use the task tool and
each task should only be done by a subagent so that context is
clear after each task do a commit before you continue you are the
main agent and your subagents are your devs

&lt;h3&gt;Understanding Claude Code’s Task System&lt;/h3&gt;
&lt;p&gt;Claude Code’s task systeminspired by &lt;a href=&quot;https://github.com/beads-ai/beads&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Beads&lt;/a&gt;, Steve Yegge’s distributed git-backed issue trackersolves two critical problems with AI coding agents:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent Amnesia&lt;/strong&gt;: Starting a new session mid-task loses all progress unless you manually document remaining work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context Pollution&lt;/strong&gt;: A full context window makes the agent drop discovered bugs instead of tracking them.&lt;/p&gt;
&lt;p&gt;The previous todo list lived in session memory and vanished on restart. The new task system persists tasks to disk, making them shareable across sessions and subagents.&lt;/p&gt;
&lt;h3&gt;How Tasks Persist&lt;/h3&gt;
&lt;p&gt;Tasks are stored in &lt;code&gt;.claude/tasks/{session-id}/&lt;/code&gt; as JSON files:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;task-1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;subject&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Create idb-helpers.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Implement IndexedDB promise wrappers...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pending | in_progress | completed&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;blocks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;task-3&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;task-4&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;blockedBy&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;task-0&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Four Task Tools&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TaskCreate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new task with subject, description, and dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TaskUpdate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update status (pending → in_progress → completed) or modify dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TaskList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View all tasks, their status, and what’s blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TaskGet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get full details of a specific task including description&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Task System Architecture&lt;/h3&gt;
&lt;figure&gt;
&lt;h3&gt;Why Subagents + Tasks = Context Efficiency&lt;/h3&gt;
&lt;p&gt;By delegating each task to a subagent, the main session stays leanit only handles orchestration (creating tasks, tracking progress, committing). Each subagent gets a fresh context window focused entirely on its specific task, reads what it needs, implements, and returns. This means the main agent won’t run out of context even for larger refactors with dozens of tasks.&lt;/p&gt;
&lt;p&gt;For truly massive projects spanning days or weeks, a full autonomous agent like &lt;a href=&quot;https://ghuntley.com/ralph/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ralph&lt;/a&gt; would be more appropriate. Ralph is elegantly simplea bash loop that feeds a markdown file into Claude Code repeatedly:&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;The key difference: Ralph executes each iteration in a completely new Claude session, using the markdown file as the only persistent memory. This makes it truly stateless and capable of running for days.&lt;/p&gt;
&lt;p&gt;This spec-driven approach hits a middle ground: subagents get fresh context but the main orchestrator maintains state within a single session. Structured enough to maintain coherence, flexible enough to handle complexity, without the setup overhead of a full autonomous system.&lt;/p&gt;
&lt;h3&gt;The Execution Flow&lt;/h3&gt;
&lt;figure&gt;
&lt;h3&gt;Why This Pattern Works&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context isolation&lt;/strong&gt;: Each subagent starts fresh, reading only what it needsno accumulated cruft&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent progress&lt;/strong&gt;: Tasks survive session restarts; pick up where you left off&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency-aware parallelism&lt;/strong&gt;: Claude identifies which tasks can run concurrently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atomic commits&lt;/strong&gt;: Every task = one commit, making rollbacks trivial&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spec as contract&lt;/strong&gt;: Subagents reference the spec, ensuring consistency&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Backpressure: Let the System Catch Mistakes&lt;/h3&gt;
&lt;p&gt;One crucial element that makes atomic commits powerful: &lt;a href=&quot;https://banay.me/dont-waste-your-backpressure/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;backpressure&lt;/a&gt;. Instead of manually reviewing every change, set up pre-commit hooks that run tests, linting, and type checking automatically.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# .husky/pre-commit&lt;/span&gt;
&lt;span&gt;pnpm&lt;/span&gt; typecheck &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;pnpm&lt;/span&gt; lint &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;pnpm&lt;/span&gt; test-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a subagent commits, the hook runs immediately. If tests fail, the commit is rejected and the agent sees the error outputgiving it a chance to self-correct before moving on. This creates automated feedback that catches issues at the source rather than accumulating bugs across multiple tasks.&lt;/p&gt;
&lt;p&gt;The result: you stop being the bottleneck for quality control. The system validates correctness while you focus on higher-level decisions.&lt;/p&gt;
&lt;h3&gt;When Things Go Wrong&lt;/h3&gt;
&lt;p&gt;The first execution wasn’t perfectI started the project and hit some errors. But here’s where the spec pays off: I opened a new chat, pinned the spec document, pasted the error, and Claude fixed it immediately. No context rebuilding, no re-explaining the architecture.&lt;/p&gt;
&lt;p&gt;The spec acts as a recovery point. When a session goes sideways or context gets polluted, you don’t lose everythingyou have a document that captures the full intent and design decisions.&lt;/p&gt;
&lt;h3&gt;The Results&lt;/h3&gt;
&lt;p&gt;After ~45 minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git log-oneline | head20

9dc1c96 refactor: clean up code structure
9fce16b feat(storage): migrate from SQLite to IndexedDB
835c494 feat: integrate IDB sync engine provider
d2cd7b7 refactor: remove SQLite/sql.js dependencies
2fb7656 feat: add browser mode test stubs
... (14 commits total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;14 tasks completed&lt;/strong&gt;, &lt;strong&gt;14 commits&lt;/strong&gt;, &lt;strong&gt;15+ files changed&lt;/strong&gt;, &lt;strong&gt;one PR ready for review&lt;/strong&gt;. See the &lt;a href=&quot;https://github.com/alexanderop/nuxt-sync-engine/pull/3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;full pull request&lt;/a&gt; (includes additional manual changes).&lt;/p&gt;
&lt;p&gt;And despite orchestrating 14 subagents, the main session’s context stayed manageable:&lt;/p&gt;
&lt;p&gt;&amp;lt;ContextUsage&lt;br /&gt;
model=“claude-opus-4-5-20251101”&lt;br /&gt;
used=“143k”&lt;br /&gt;
total=“200k”&lt;br /&gt;
percent={71}&lt;br /&gt;
categories={[&lt;br /&gt;
{ name: “System prompt”, tokens: “2.8k”, percent: 1.4, color: “system-prompt” },&lt;br /&gt;
{ name: “System tools”, tokens: “16.2k”, percent: 8.1, color: “system-tools” },&lt;br /&gt;
{ name: “MCP tools”, tokens: “293”, percent: 0.1, color: “mcp-tools” },&lt;br /&gt;
{ name: “Custom agents”, tokens: “641”, percent: 0.3, color: “custom-agents” },&lt;br /&gt;
{ name: “Memory files”, tokens: “431”, percent: 0.2, color: “memory-files” },&lt;br /&gt;
{ name: “Skills”, tokens: “1.6k”, percent: 0.8, color: “skills” },&lt;br /&gt;
{ name: “Messages”, tokens: “122.9k”, percent: 61.4, color: “messages” },&lt;br /&gt;
{ name: “Free space”, tokens: “22k”, percent: 11.1, color: “free” },&lt;br /&gt;
{ name: “Autocompact buffer”, tokens: “33.0k”, percent: 16.5, color: “buffer” },&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;This proves the delegation pattern worksthe main agent handled orchestration while subagents did the heavy lifting in isolated contexts.&lt;/p&gt;
&lt;h2&gt;The Prompt Patterns&lt;/h2&gt;
&lt;p&gt;Here are the key prompt patterns that make this workflow effective:&lt;/p&gt;
&lt;h3&gt;1. Parallel Research&lt;/h3&gt;

spin up multiple subagents for your research task

&lt;p&gt;Triggers Claude to spawn parallel agents, each investigating independently. Much faster than sequential research.&lt;/p&gt;
&lt;h3&gt;2. Spec-First Development&lt;/h3&gt;

your goal is to write a report/document

&lt;p&gt;Forces Claude to produce a written artifact before any code. This becomes the source of truth.&lt;/p&gt;
&lt;h3&gt;3. Interview Before Implementation&lt;/h3&gt;

use the ask_user_question tool... before we implement

&lt;p&gt;Surfaces ambiguities and design decisions before they become bugs.&lt;/p&gt;
&lt;h3&gt;4. Task Delegation with Commits&lt;/h3&gt;

use the task tool and each task should only be done by a subagent
after each task do a commit before you continue

&lt;p&gt;Creates the orchestration pattern with atomic commits.&lt;/p&gt;
&lt;h3&gt;5. Role Assignment&lt;/h3&gt;

you are the main agent and your subagents are your devs

&lt;p&gt;Sets expectations for how Claude should behaveas a coordinator, not a solo implementer.&lt;/p&gt;
&lt;h2&gt;Comparison: Traditional vs Spec-Driven&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Traditional AI Coding&lt;/th&gt;
&lt;th&gt;Spec-Driven Development&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prompt → Code → Debug → Repeat&lt;/td&gt;
&lt;td&gt;Research → Spec → Refine → Tasks → Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fills up with failed attempts&lt;/td&gt;
&lt;td&gt;Each task gets fresh context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No persistence across sessions&lt;/td&gt;
&lt;td&gt;Spec is persistent source of truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bug tracking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discovered late, forgotten&lt;/td&gt;
&lt;td&gt;Bugs become new tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Completion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No clear stopping point&lt;/td&gt;
&lt;td&gt;Clear completion criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Advanced: Multi-Session Workflows&lt;/h2&gt;
&lt;p&gt;The task system supports coordination across multiple Claude Code sessions. Set a shared task list ID:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;CLAUDE_CODE_TASK_LIST_ID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;myproject claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or add to &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;env&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;CLAUDE_CODE_TASK_LIST_ID&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;myproject&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One session acts as &lt;strong&gt;orchestrator&lt;/strong&gt;; another becomes a &lt;strong&gt;checker&lt;/strong&gt; that monitors completed tasks, verifies implementation quality, and adds follow-up tasks for anything missing.&lt;/p&gt;
&lt;h2&gt;When to Use This Workflow&lt;/h2&gt;
&lt;p&gt;This pattern excels for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Large refactors&lt;/strong&gt; touching many files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrations&lt;/strong&gt; requiring research into external codebases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature implementations&lt;/strong&gt; with unclear requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning new libraries&lt;/strong&gt; by studying their source&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s overkill for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small bug fixes&lt;/li&gt;
&lt;li&gt;Single-file changes&lt;/li&gt;
&lt;li&gt;Well-defined, simple features&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Tools You Need&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Claude Code CLI&lt;/strong&gt; (latest version with task tools)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A spec document&lt;/strong&gt; (markdown works great)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference codebases&lt;/strong&gt; if learning from existing implementations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git&lt;/strong&gt; for atomic commits&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/beads-ai/beads&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Beads&lt;/a&gt; Steve Yegge’s git-backed issue tracker that inspired the task system&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.humanlayer.dev/blog/12-factor-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;12 Factor Agents&lt;/a&gt; Design principles for AI coding agents&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Building Effective Agents&lt;/a&gt; Anthropic’s research on agent architectures&lt;/li&gt;
&lt;li&gt;For a broader overview of Claude Code’s feature stack, see my comprehensive guide&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Spec-Driven Development with Claude Code mirrors real engineering workflows: parallel work, handoffs, blockers, and dependencies. Instead of treating Claude as a solo coder, you treat it as a team.&lt;/p&gt;
&lt;p&gt;The key insight from Beads applies here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“By having each task that you give a coding agent isolated into its own context window, you can now give it the ability to log any bugs for later.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The SQLite to IndexedDB migration would have taken me 2-3 days manually. With this workflow, it took one afternoonand produced better code thanks to the research phase uncovering patterns from Jazz I wouldn’t have found on my own.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Try it yourself: Start your next significant feature with “write a spec for X, spin up subagents for research” and see how it changes your workflow.&lt;/em&gt;&lt;/p&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>claude-code</category><category>ai</category><category>local-first</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>My Opinionated ESLint Setup for Vue Projects</title><link>https://alexop.dev/posts/opinionated-eslint-setup-vue-projects/</link><guid isPermaLink="true">https://alexop.dev/posts/opinionated-eslint-setup-vue-projects/</guid><description>A battle-tested linting configuration that catches real bugs, enforces clean architecture, and runs fast using Oxlint and ESLint together.</description><pubDate>Sat, 31 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Over the last 7+ years as a Vue developer, I’ve developed a highly opinionated style for writing Vue components. Some of these rules might not be useful for you, but I thought it was worth sharing so you can pick what fits your project. The goal is to enforce code structure that’s readable for both developers and AI agents.&lt;/p&gt;
&lt;p&gt;These rules aren’t arbitrary—they encode patterns I’ve written about extensively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to Write Clean Vue Components explains why I separate business logic into pure functions&lt;/li&gt;
&lt;li&gt;How to Structure Vue Projects covers my feature-based architecture approach&lt;/li&gt;
&lt;li&gt;Building a Modular Monolith with Nuxt Layers applies feature isolation to Nuxt projects&lt;/li&gt;
&lt;li&gt;The Problem with &lt;code&gt;as&lt;/code&gt; in TypeScript covers why I ban type assertions&lt;/li&gt;
&lt;li&gt;Robust Error Handling in TypeScript introduces the Result pattern behind my &lt;code&gt;tryCatch&lt;/code&gt; rule&lt;/li&gt;
&lt;li&gt;Vue 3 Testing Pyramid explains my integration-first testing strategy&lt;/li&gt;
&lt;li&gt;Frontend Testing Guide shares my test naming conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ESLint rules are how I enforce these patterns automatically—so the codebase stays consistent even as the team grows.&lt;/p&gt;

**Why linting matters more in the AI era:** As AI agents write more of our code, strict linting becomes essential. It&apos;s a form of [back pressure](https://banay.me/dont-waste-your-backpressure/?ref=ghuntley.com)—automated feedback mechanisms that tell an agent when it&apos;s made a mistake, allowing it to self-correct without your intervention. You have a limited budget of feedback (your time and attention). If you spend that budget telling the agent &quot;you missed an import&quot; or &quot;that type is wrong,&quot; you can&apos;t spend it on architectural decisions or complex logic. Type checkers, linters, and test suites act as back pressure: they push back against bad code so you don&apos;t have to. Your ESLint config is now part of your prompt—it&apos;s the automated quality gate that lets agents iterate until they pass.

&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Why Two Linters? Oxlint + ESLint&lt;/h2&gt;
&lt;p&gt;I run two linters: &lt;strong&gt;Oxlint&lt;/strong&gt; first, then &lt;strong&gt;ESLint&lt;/strong&gt;. Why? Speed and coverage.&lt;/p&gt;
&lt;h3&gt;Oxlint: The Speed Demon&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/linter.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Oxlint&lt;/a&gt; is written in Rust. It runs 50-100x faster than ESLint on large codebases. My pre-commit hook completes in milliseconds instead of seconds.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# In package.json&lt;/span&gt;
&lt;span&gt;&quot;lint:oxlint&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;oxlint . --fix --ignore-path .gitignore&quot;&lt;/span&gt;,
&lt;span&gt;&quot;lint:eslint&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;eslint . --fix --cache&quot;&lt;/span&gt;,
&lt;span&gt;&quot;lint&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;run-s lint:*&quot;&lt;/span&gt;  &lt;span&gt;# Runs oxlint first, then eslint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Oxlint supports fewer rules. It handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correctness &amp;amp; suspicious patterns&lt;/strong&gt; - catches bugs early&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core ESLint equivalents&lt;/strong&gt; - &lt;code&gt;no-console&lt;/code&gt;, &lt;code&gt;no-explicit-any&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript basics&lt;/strong&gt; - &lt;code&gt;array-type&lt;/code&gt;, &lt;code&gt;consistent-type-definitions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But Oxlint lacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue-specific rules (&lt;code&gt;vue/*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Import boundary rules (&lt;code&gt;import-x/*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Vitest testing rules (&lt;code&gt;vitest/*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;i18n rules (&lt;code&gt;@intlify/vue-i18n/*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Custom local rules&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Setup&lt;/h3&gt;
&lt;p&gt;Oxlint runs first for fast feedback. ESLint runs second for comprehensive checks. The &lt;code&gt;eslint-plugin-oxlint&lt;/code&gt; package tells ESLint to skip rules that Oxlint already handles.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfigWithVueTs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;// ... other configs&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;pluginOxlint&lt;span&gt;.&lt;/span&gt;&lt;span&gt;buildFromOxlintConfigFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;./.oxlintrc.json&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;// .oxlintrc.json&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;./node_modules/oxlint/configuration_schema.json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;categories&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;correctness&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;suspicious&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;warn&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;rules&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;typescript/no-explicit-any&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;eslint/no-console&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;allow&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;warn&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Must-Have Rules&lt;/h2&gt;
&lt;p&gt;These rules catch real bugs and enforce maintainable code. Enable them on every Vue project.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Cyclomatic Complexity&lt;/h3&gt;
&lt;p&gt;Complex functions are hard to test and understand. This rule limits branching logic per function.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;complexity&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; max&lt;span&gt;:&lt;/span&gt; &lt;span&gt;10&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
function processOrder(order: Order) {
  if (order.status === &apos;pending&apos;) {
    if (order.items.length &amp;gt; 0) {
      if (order.payment) {
        if (order.payment.verified) {
          if (order.shipping) {
            // 5 levels deep, complexity keeps growing...
          }
        }
      }
    }
  }
}
```
  
  
```typescript
function processOrder(order: Order) {
  if (!isValidOrder(order)) return
&lt;p&gt;processPayment(order.payment)&lt;br /&gt;
scheduleShipping(order.shipping)&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;function isValidOrder(order: Order): boolean {&lt;br /&gt;
return order.status === ‘pending’&lt;br /&gt;
&amp;amp;&amp;amp; order.items.length &amp;gt; 0&lt;br /&gt;
&amp;amp;&amp;amp; order.payment?.verified === true&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

**Threshold guidance:**
- ESLint default: `20` (lenient)
- This project uses: `10` (stricter)
- Consider `15` as a middle ground for legacy codebases

&amp;gt; [ESLint: complexity](https://eslint.org/docs/latest/rules/complexity)

---

### No Nested Ternaries

Nested ternaries are hard to read. Use early returns or separate variables instead.

```typescript
// eslint.config.ts
{
  rules: {
    &apos;no-nested-ternary&apos;: &apos;error&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
const label = isLoading ? &apos;Loading...&apos; : hasError ? &apos;Failed&apos; : &apos;Success&apos;
```
  
  
```typescript
function getLabel() {
  if (isLoading) return &apos;Loading...&apos;
  if (hasError) return &apos;Failed&apos;
  return &apos;Success&apos;
}
&lt;p&gt;const label = getLabel()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

&amp;gt; [ESLint: no-nested-ternary](https://eslint.org/docs/rules/no-nested-ternary)

---

### No Type Assertions

Type assertions (`as Type`) bypass TypeScript&apos;s type checker. They hide bugs. Use type guards or proper typing instead.

```typescript
// eslint.config.ts
{
  rules: {
    &apos;@typescript-eslint/consistent-type-assertions&apos;: [&apos;error&apos;, {
      assertionStyle: &apos;never&apos;
    }]
  }
}
&lt;/code&gt;&lt;/pre&gt;

`as const` assertions are always allowed, even with `assertionStyle: &apos;never&apos;`. Const assertions don&apos;t bypass type checking—they make types more specific.


  
```typescript
const user = response.data as User  // What if it&apos;s not a User?
&lt;p&gt;const element = document.querySelector(‘.btn’) as HTMLButtonElement&lt;br /&gt;
element.click()  // Runtime error if element is null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
  &amp;lt;Fragment slot=&quot;good&quot;&amp;gt;
```typescript
// Use type guards
function isUser(data: unknown): data is User {
  return typeof data === &apos;object&apos;
    &amp;amp;&amp;amp; data !== null
    &amp;amp;&amp;amp; &apos;id&apos; in data
    &amp;amp;&amp;amp; &apos;name&apos; in data
}

if (isUser(response.data)) {
  const user = response.data  // TypeScript knows it&apos;s User
}

// Handle nulls properly
const element = document.querySelector(&apos;.btn&apos;)
if (element instanceof HTMLButtonElement) {
  element.click()
}
&lt;/code&gt;&lt;/pre&gt;
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://typescript-eslint.io/rules/consistent-type-assertions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeScript ESLint: consistent-type-assertions&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No Enums&lt;/h3&gt;
&lt;p&gt;TypeScript enums have quirks. They generate JavaScript code, have numeric reverse mappings, and behave differently from union types. Use literal unions or const objects instead.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-syntax&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;TSEnumDeclaration&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use literal unions or `as const` objects instead of enums.&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
enum Status {
  Pending,
  Active,
  Done
}
&lt;p&gt;const status: Status = Status.Pending&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
  &amp;lt;Fragment slot=&quot;good&quot;&amp;gt;
```typescript
// Literal union - simplest
type Status = &apos;pending&apos; | &apos;active&apos; | &apos;done&apos;

// Or const object when you need values
const Status = {
  Pending: &apos;pending&apos;,
  Active: &apos;active&apos;,
  Done: &apos;done&apos;
} as const

type Status = typeof Status[keyof typeof Status]
&lt;/code&gt;&lt;/pre&gt;
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/rules/no-restricted-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint: no-restricted-syntax&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No else/else-if&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;else&lt;/code&gt; and &lt;code&gt;else-if&lt;/code&gt; blocks increase nesting. Early returns are easier to read and reduce cognitive load.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-syntax&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;IfStatement &amp;gt; IfStatement.alternate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Avoid `else if`. Prefer early returns or ternary operators.&apos;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;IfStatement &amp;gt; :not(IfStatement).alternate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Avoid `else`. Prefer early returns or ternary operators.&apos;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
function getDiscount(user: User) {
  if (user.isPremium) {
    return 0.2
  } else if (user.isMember) {
    return 0.1
  } else {
    return 0
  }
}
```
  
  
```typescript
function getDiscount(user: User) {
  if (user.isPremium) return 0.2
  if (user.isMember) return 0.1
  return 0
}
```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/rules/no-restricted-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint: no-restricted-syntax&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No Native try/catch&lt;/h3&gt;
&lt;p&gt;Native try/catch blocks are verbose and error-prone. Use a utility function that returns a result tuple instead.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-syntax&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;TryStatement&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use tryCatch() from @/lib/tryCatch instead of try/catch. Returns Result&amp;lt;T&amp;gt; tuple: [error, null] | [null, data].&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
async function fetchUser(id: string) {
  try {
    const response = await api.get(`/users/${id}`)
    return response.data
  } catch (error) {
    console.error(error)
    return null
  }
}
```
  
  
```typescript
async function fetchUser(id: string) {
  const [error, response] = await tryCatch(api.get(`/users/${id}`))
&lt;p&gt;if (error) {&lt;br /&gt;
console.error(error)&lt;br /&gt;
return null&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;return response.data&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

The `tryCatch` utility returns `[error, null]` or `[null, data]`, similar to Go&apos;s error handling.

&amp;gt; [ESLint: no-restricted-syntax](https://eslint.org/docs/rules/no-restricted-syntax)

---

### No Direct DOM Manipulation

Vue manages the DOM. Calling `document.querySelector` bypasses Vue&apos;s reactivity and template refs. Use `useTemplateRef()` instead. If you&apos;re on Vue 3.5+, the built-in rule already enforces this.

```typescript
// eslint.config.ts
{
  files: [&apos;src/**/*.vue&apos;],
  rules: {
    &apos;vue/prefer-use-template-ref&apos;: &apos;error&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;

  
```vue



```
  
  
```vue
&lt;p&gt;&lt;/p&gt;


```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/rules/no-restricted-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint: no-restricted-syntax&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Feature Boundary Enforcement&lt;/h3&gt;
&lt;p&gt;Features should not import from other features. This keeps code modular and prevents circular dependencies. If you’re using a feature-based architecture, this rule is essential—see How to Structure Vue Projects for more on this approach.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&apos;import-x&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; pluginImportX &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;import-x/no-restricted-paths&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      zones&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;// === CROSS-FEATURE ISOLATION ===&lt;/span&gt;
        &lt;span&gt;// Features cannot import from other features&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/workout&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./workout&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/exercises&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./exercises&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/settings&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./settings&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/timers&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./timers&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/templates&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./templates&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features/benchmarks&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; except&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./benchmarks&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;

        &lt;span&gt;// === UNIDIRECTIONAL FLOW ===&lt;/span&gt;
        &lt;span&gt;// Shared code cannot import from features or views&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt;
          target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./src/components&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/composables&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/lib&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/db&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/types&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/stores&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./src/views&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;

        &lt;span&gt;// Features cannot import from views (views are top-level orchestrators)&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/features&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; from&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;./src/views&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Unidirectional Flow:&lt;/strong&gt; The architecture enforces a strict dependency hierarchy. Views orchestrate features, features use shared code, but never the reverse.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;views → features → shared (components, composables, lib, db, types, stores)
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
// src/features/workout/composables/useWorkout.ts
// Cross-feature import!
```
  
  
```typescript
// src/features/workout/composables/useWorkout.ts
// Use shared database layer instead
```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/un-ts/eslint-plugin-import-x&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-import-x: no-restricted-paths&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Vue Component Naming&lt;/h3&gt;
&lt;p&gt;Consistent naming makes components easy to find and identify.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vue/multi-word-component-names&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      ignores&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;App&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;Layout&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/component-definition-name-casing&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;PascalCase&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/component-name-in-template-casing&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;PascalCase&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      registeredComponentsOnly&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/match-component-file-name&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      extensions&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      shouldMatchCase&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/prop-name-casing&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;camelCase&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/attribute-hyphenation&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;always&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/custom-event-name-casing&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;kebab-case&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue


  Click

```
  
  
```vue


  Click

```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.vuejs.org/rules/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue: component rules&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Dead Code Detection in Vue&lt;/h3&gt;
&lt;p&gt;Find unused props, refs, and emits before they become tech debt.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vue/no-unused-properties&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      groups&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;props&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;data&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;computed&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;methods&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/no-unused-refs&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/no-unused-emit-declarations&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue
&lt;p&gt;&lt;/p&gt;

  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  Click

```
  
  
```vue
&lt;p&gt;&lt;/p&gt;

  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  Click

```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.vuejs.org/rules/no-unused-properties.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue: no-unused-properties&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No Hardcoded i18n Strings&lt;/h3&gt;
&lt;p&gt;Hardcoded strings break internationalization. The &lt;code&gt;@intlify/vue-i18n&lt;/code&gt; plugin catches them.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&apos;@intlify/vue-i18n&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; pluginVueI18n &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;@intlify/vue-i18n/no-raw-text&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      ignorePattern&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;^[-#:()&amp;amp;+×/°′″%]+&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      ignoreText&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;kg&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;lbs&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;cm&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;ft/in&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;—&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;•&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;✓&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;›&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;→&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;·&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;.&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;Close&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      attributes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&apos;/.+/&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;title&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;aria-label&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;aria-placeholder&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;placeholder&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;alt&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;attributes&lt;/code&gt; option catches hardcoded strings in accessibility attributes too.&lt;/p&gt;

  
```vue

  Save Changes
  &lt;p&gt;No items found&lt;/p&gt;

```
  
  
```vue

  {{ t(&apos;common.save&apos;) }}
  &lt;p&gt;{{ t(&apos;items.empty&apos;) }}&lt;/p&gt;

```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint-plugin-vue-i18n.intlify.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue-i18n&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No Disabling i18n Rules&lt;/h3&gt;
&lt;p&gt;Prevent developers from bypassing i18n checks with &lt;code&gt;eslint-disable&lt;/code&gt; comments.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;@eslint-community/eslint-comments&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; pluginEslintComments
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;@eslint-community/eslint-comments/no-restricted-disable&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&apos;@intlify/vue-i18n/*&apos;&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue

Save Changes
```
  
  
```vue
{{ t(&apos;common.save&apos;) }}
```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/eslint-community/eslint-plugin-eslint-comments&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@eslint-community/eslint-plugin-eslint-comments&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;No Hardcoded Route Strings&lt;/h3&gt;
&lt;p&gt;Use named routes instead of hardcoded path strings for maintainability.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-syntax&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;CallExpression[callee.property.name=&quot;push&quot;][callee.object.name=&quot;router&quot;] &amp;gt; Literal:first-child&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use named routes with RouteNames instead of hardcoded path strings.&apos;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;CallExpression[callee.property.name=&quot;push&quot;][callee.object.name=&quot;router&quot;] &amp;gt; TemplateLiteral:first-child&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use named routes with RouteNames instead of template literals.&apos;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
router.push(&apos;/workout/123&apos;)
router.push(`/workout/${id}`)
```
  
  
```typescript
router.push({ name: RouteNames.WorkoutDetail, params: { id } })
```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/latest/rules/no-restricted-syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint: no-restricted-syntax&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Enforce Integration Test Helpers&lt;/h3&gt;
&lt;p&gt;Ban direct &lt;code&gt;render()&lt;/code&gt; or &lt;code&gt;mount()&lt;/code&gt; calls in tests. Use a centralized test helper instead. For more on testing strategies in Vue, see Vue 3 Testing Pyramid: A Practical Guide with Vitest Browser Mode.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/__tests__/**/*.{ts,spec.ts}&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  ignores&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/__tests__/helpers/**&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-imports&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      paths&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt;
          name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;vitest-browser-vue&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          importNames&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;render&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use createTestApp() from @/__tests__/helpers/createTestApp instead.&apos;&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt;
          name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;@vue/test-utils&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          importNames&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;mount&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;shallowMount&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Use createTestApp() instead of mounting components directly.&apos;&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
const { getByText } = render(MyComponent)
const wrapper = mount(MyComponent)
```
  
  
```typescript
const { page } = await createTestApp({ route: &apos;/workout&apos; })
```
  

&lt;p&gt;This ensures all tests use consistent setup with routing, i18n, and database.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/latest/rules/no-restricted-imports&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ESLint: no-restricted-imports&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Enforce pnpm Catalogs&lt;/h3&gt;
&lt;p&gt;When using pnpm workspaces, enforce that dependencies use catalog references.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfigWithVueTs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;// ... other configs&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;pnpmConfigs&lt;span&gt;.&lt;/span&gt;recommended&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures dependencies are managed centrally in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nickmccurdy/eslint-plugin-pnpm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-pnpm&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Nice-to-Have Rules&lt;/h2&gt;
&lt;p&gt;These rules improve code quality but are less critical. Enable them after the must-haves are in place.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Vue 3.5+ API Enforcement&lt;/h3&gt;
&lt;p&gt;Use the latest Vue 3.5 APIs for cleaner code.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vue/define-props-destructuring&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/prefer-use-template-ref&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue
&lt;p&gt;&lt;/p&gt;

  Click

```
  
  
```vue
&lt;p&gt;&lt;/p&gt;

  Click

```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.vuejs.org/rules/define-props-destructuring.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue: define-props-destructuring&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Explicit Component APIs&lt;/h3&gt;
&lt;p&gt;Require &lt;code&gt;defineExpose&lt;/code&gt; and &lt;code&gt;defineSlots&lt;/code&gt; to make component interfaces explicit.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vue/require-expose&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/require-explicit-slots&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;warn&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue



```
  
  
```vue
&lt;p&gt;&lt;/p&gt;


```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.vuejs.org/rules/require-expose.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue: require-expose&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Template Depth Limit&lt;/h3&gt;
&lt;p&gt;Deep template nesting is hard to read. Extract nested sections into components. This one matters a lot—it helps you avoid ending up with components that are 2000 lines long.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/*.vue&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vue/max-template-depth&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; maxDepth&lt;span&gt;:&lt;/span&gt; &lt;span&gt;8&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vue/max-props&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; maxProps&lt;span&gt;:&lt;/span&gt; &lt;span&gt;6&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```vue

  &lt;div&gt;
    &lt;div&gt;
      &lt;div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;div&gt;
              &lt;div&gt;
                &lt;div&gt;
                  &lt;span&gt;Too deep!&lt;/span&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

```
  
  
```vue

  
    
      Title
    
    
      &lt;span&gt;Content&lt;/span&gt;
    
  

```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.vuejs.org/rules/max-template-depth.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue: max-template-depth&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Better Assertions in Tests&lt;/h3&gt;
&lt;p&gt;Use specific matchers for clearer test failures.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/__tests__/*&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;vitest/prefer-to-be&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vitest/prefer-to-have-length&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vitest/prefer-to-contain&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;vitest/prefer-mock-promise-shorthand&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
expect(value === null).toBe(true)
expect(arr.length).toBe(3)
expect(arr.includes(&apos;foo&apos;)).toBe(true)
```
  
  
```typescript
expect(value).toBeNull()
expect(arr).toHaveLength(3)
expect(arr).toContain(&apos;foo&apos;)
&lt;p&gt;// Also prefer mock shorthands&lt;br /&gt;
vi.fn().mockResolvedValue(‘data’)  // Instead of mockReturnValue(Promise.resolve(‘data’))&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

&amp;gt; [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)

---

### Test Structure Rules

Keep tests organized and readable.

```typescript
// eslint.config.ts
{
  files: [&apos;src/**/__tests__/*&apos;],
  rules: {
    &apos;vitest/consistent-test-it&apos;: [&apos;error&apos;, { fn: &apos;it&apos; }],
    &apos;vitest/prefer-hooks-on-top&apos;: &apos;error&apos;,
    &apos;vitest/prefer-hooks-in-order&apos;: &apos;error&apos;,
    &apos;vitest/no-duplicate-hooks&apos;: &apos;error&apos;,
    &apos;vitest/require-top-level-describe&apos;: &apos;error&apos;,
    &apos;vitest/max-nested-describe&apos;: [&apos;error&apos;, { max: 2 }],
    &apos;vitest/no-conditional-in-test&apos;: &apos;warn&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
test(&apos;works&apos;, () =&amp;gt; {})  // Inconsistent: test vs it
it(&apos;also works&apos;, () =&amp;gt; {})
&lt;p&gt;describe(‘feature’, () =&amp;gt; {&lt;br /&gt;
it(‘test 1’, () =&amp;gt; {})&lt;/p&gt;
&lt;p&gt;beforeEach(() =&amp;gt; {})  // Hook after test!&lt;/p&gt;
&lt;p&gt;describe(‘nested’, () =&amp;gt; {&lt;br /&gt;
describe(‘too deep’, () =&amp;gt; {&lt;br /&gt;
describe(‘way too deep’, () =&amp;gt; {})  // 3 levels!&lt;br /&gt;
})&lt;br /&gt;
})&lt;br /&gt;
})&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
  &amp;lt;Fragment slot=&quot;good&quot;&amp;gt;
```typescript
describe(&apos;feature&apos;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {})  // Hooks first, in order

  it(&apos;does something&apos;, () =&amp;gt; {})
  it(&apos;does another thing&apos;, () =&amp;gt; {})

  describe(&apos;edge cases&apos;, () =&amp;gt; {
    it(&apos;handles null&apos;, () =&amp;gt; {})
  })
})

// no-conditional-in-test prevents flaky tests
// Bad: if (data.length &amp;gt; 0) { expect(data[0]).toBeDefined() }
// Good: expect(data).toHaveLength(3); expect(data[0]).toBeDefined()
&lt;/code&gt;&lt;/pre&gt;
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/veritem/eslint-plugin-vitest&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vitest&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Prefer Vitest Locators in Tests&lt;/h3&gt;
&lt;p&gt;Use Vitest Browser locators instead of raw DOM queries.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  files&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;src/**/__tests__/**/*.{ts,spec.ts}&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;no-restricted-syntax&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      selector&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;CallExpression[callee.property.name=/^querySelector(All)?$/]&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Prefer page.getByRole(), page.getByText(), or page.getByTestId() over querySelector. Vitest locators are more resilient to DOM changes.&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
const button = container.querySelector(&apos;.submit-btn&apos;)
await button?.click()
```
  
  
```typescript
const button = page.getByRole(&apos;button&apos;, { name: &apos;Submit&apos; })
await button.click()
```
  

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://vitest.dev/guide/browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest Browser Mode&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;Unicorn Rules&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;eslint-plugin-unicorn&lt;/code&gt; package catches common mistakes and enforces modern JavaScript patterns.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint.config.ts&lt;/span&gt;
pluginUnicorn&lt;span&gt;.&lt;/span&gt;configs&lt;span&gt;.&lt;/span&gt;recommended&lt;span&gt;,&lt;/span&gt;

&lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;app/unicorn-overrides&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  rules&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// === Enable non-recommended rules that add value ===&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/better-regex&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;              &lt;span&gt;// Simplify regexes: /[0-9]/ → /\d/&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/custom-error-definition&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;  &lt;span&gt;// Correct Error subclassing&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-unused-properties&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;      &lt;span&gt;// Dead code detection&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/consistent-destructuring&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;warn&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;  &lt;span&gt;// Use destructured vars consistently&lt;/span&gt;

    &lt;span&gt;// === Disable rules that conflict with project conventions ===&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-null&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;                    &lt;span&gt;// We use null for database values&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/filename-case&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;              &lt;span&gt;// Vue uses PascalCase, tests use camelCase&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/prevent-abbreviations&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;      &lt;span&gt;// props, e, Db are fine&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-array-callback-reference&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// arr.filter(isValid) is fine&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-await-expression-member&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// (await fetch()).json() is fine&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-array-reduce&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;            &lt;span&gt;// reduce is useful for aggregations&lt;/span&gt;
    &lt;span&gt;&apos;unicorn/no-useless-undefined&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;off&apos;&lt;/span&gt;        &lt;span&gt;// mockResolvedValue(undefined) for TS&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// unicorn/better-regex&lt;/span&gt;
&lt;span&gt;// Bad:  /[0-9]/&lt;/span&gt;
&lt;span&gt;// Good: /\d/&lt;/span&gt;

&lt;span&gt;// unicorn/consistent-destructuring&lt;/span&gt;
&lt;span&gt;// Bad:&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; foo &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; object
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;object&lt;span&gt;.&lt;/span&gt;bar&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// Uses object.bar instead of destructuring&lt;/span&gt;

&lt;span&gt;// Good:&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; foo&lt;span&gt;,&lt;/span&gt; bar &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; object
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;bar&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sindresorhus/eslint-plugin-unicorn&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-unicorn&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Custom Local Rules&lt;/h2&gt;
&lt;p&gt;Sometimes you need rules that don’t exist. Write them yourself.&lt;/p&gt;
&lt;h3&gt;Composable Must Use Vue&lt;/h3&gt;
&lt;p&gt;A file named &lt;code&gt;use*.ts&lt;/code&gt; should import from Vue. If it doesn’t, it’s a utility, not a composable. For more on writing proper composables, see Vue Composables Style Guide: Lessons from VueUse’s Codebase.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint-local-rules/composable-must-use-vue.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;VALID_VUE_SOURCES&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;vue&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;@vueuse/core&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;vue-router&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;vue-i18n&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;VALID_PATH_PATTERNS&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^@\/stores\/&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;  &lt;span&gt;// Global state composables count too&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;isComposableFilename&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;filename&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^use[A-Z]&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;basename&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;filename&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;.ts&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; rule&lt;span&gt;:&lt;/span&gt; Rule&lt;span&gt;.&lt;/span&gt;RuleModule &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  meta&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      notAComposable&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;File &quot;{{filename}}&quot; does not import from Vue. Rename it or add Vue imports.&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;isComposableFilename&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;.&lt;/span&gt;filename&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;let&lt;/span&gt; hasVueImport &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;ImportDeclaration&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;VALID_VUE_SOURCES&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;has&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;source&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          hasVueImport &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&apos;Program:exit&apos;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;hasVueImport&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          context&lt;span&gt;.&lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; node&lt;span&gt;,&lt;/span&gt; messageId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;notAComposable&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
// src/composables/useFormatter.ts
export function useFormatter() {
  return {
    formatDate: (d: Date) =&amp;gt; d.toISOString()  // No Vue imports!
  }
}
```
  
  
```typescript
// src/lib/formatter.ts (renamed)
export function formatDate(d: Date) {
  return d.toISOString()
}
&lt;p&gt;// OR add Vue reactivity:&lt;br /&gt;
// src/composables/useFormatter.ts&lt;br /&gt;
export function useFormatter() {&lt;br /&gt;
const locale = ref(‘en-US’)&lt;br /&gt;
const formatter = computed(() =&amp;gt; new Intl.DateTimeFormat(locale.value))&lt;br /&gt;
return { formatter, locale }&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

---

### No Hardcoded Tailwind Colors

Hardcoded Tailwind colors (`bg-blue-500`) make theming impossible. Use semantic colors (`bg-primary`).

```typescript
// eslint-local-rules/no-hardcoded-colors.ts
// Status colors (red, amber, yellow, green, emerald) are ALLOWED for semantic states
const HARDCODED_COLORS = [&apos;slate&apos;, &apos;gray&apos;, &apos;zinc&apos;, &apos;blue&apos;, &apos;purple&apos;, &apos;pink&apos;, &apos;orange&apos;, &apos;indigo&apos;, &apos;violet&apos;]
const COLOR_UTILITIES = [&apos;bg&apos;, &apos;text&apos;, &apos;border&apos;, &apos;ring&apos;, &apos;fill&apos;, &apos;stroke&apos;]

const rule: Rule.RuleModule = {
  meta: {
    messages: {
      noHardcodedColor: &apos;Avoid &quot;{{color}}&quot;. Use semantic classes like bg-primary, text-foreground.&apos;
    }
  },
  create(context) {
    return {
      Literal(node) {
        if (typeof node.value !== &apos;string&apos;) return

        const matches = findHardcodedColors(node.value)
        for (const color of matches) {
          context.report({ node, messageId: &apos;noHardcodedColor&apos;, data: { color } })
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

  
```vue

  Click

```
  
  
```vue

  Click

```
  


Status colors (`red`, `amber`, `yellow`, `green`, `emerald`) are intentionally allowed for error/warning/success states. Only use these for semantic status indication, not general styling.

&lt;hr /&gt;
&lt;h3&gt;No let in describe Blocks&lt;/h3&gt;
&lt;p&gt;Mutable variables in test describe blocks create hidden state. Use setup functions instead.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint-local-rules/no-let-in-describe.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; rule&lt;span&gt;:&lt;/span&gt; Rule&lt;span&gt;.&lt;/span&gt;RuleModule &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  meta&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      noLetInDescribe&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Avoid `let` in describe blocks. Use setup functions instead.&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; describeDepth &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;CallExpression&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isDescribeCall&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; describeDepth&lt;span&gt;++&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&apos;CallExpression:exit&apos;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isDescribeCall&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; describeDepth&lt;span&gt;--&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;VariableDeclaration&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;describeDepth &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; node&lt;span&gt;.&lt;/span&gt;kind &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&apos;let&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          context&lt;span&gt;.&lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; node&lt;span&gt;,&lt;/span&gt; messageId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;noLetInDescribe&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
describe(&apos;Login&apos;, () =&amp;gt; {
  let user: User
&lt;p&gt;beforeEach(() =&amp;gt; {&lt;br /&gt;
user = createUser()  // Hidden mutation!&lt;br /&gt;
})&lt;/p&gt;
&lt;p&gt;it(‘works’, () =&amp;gt; {&lt;br /&gt;
expect(&lt;a href=&quot;http://user.name&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;user.name&lt;/a&gt;).toBe(‘test’)&lt;br /&gt;
})&lt;br /&gt;
})&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
  &amp;lt;Fragment slot=&quot;good&quot;&amp;gt;
```typescript
describe(&apos;Login&apos;, () =&amp;gt; {
  function setup() {
    return { user: createUser() }
  }

  it(&apos;works&apos;, () =&amp;gt; {
    const { user } = setup()
    expect(user.name).toBe(&apos;test&apos;)
  })
})
&lt;/code&gt;&lt;/pre&gt;
  

&lt;hr /&gt;
&lt;h3&gt;Extract Complex Conditions&lt;/h3&gt;
&lt;p&gt;Complex boolean expressions should have names. Extract them into variables.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// eslint-local-rules/extract-condition-variable.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;OPERATOR_THRESHOLD&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;  &lt;span&gt;// Conditions with 2+ logical operators need extraction&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; rule&lt;span&gt;:&lt;/span&gt; Rule&lt;span&gt;.&lt;/span&gt;RuleModule &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  meta&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      extractCondition&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Complex condition should be extracted into a named const.&apos;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;IfStatement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// Skip patterns that TypeScript needs inline for narrowing&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isEarlyExitGuard&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;consequent&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;  &lt;span&gt;// if (!x) return&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;hasOptionalChaining&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;test&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;      &lt;span&gt;// if (user?.name)&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;hasTruthyNarrowingPattern&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;test&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;// if (arr &amp;amp;&amp;amp; arr[0])&lt;/span&gt;

        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;countOperators&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;test&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;gt;=&lt;/span&gt; &lt;span&gt;OPERATOR_THRESHOLD&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          context&lt;span&gt;.&lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; node&lt;span&gt;:&lt;/span&gt; node&lt;span&gt;.&lt;/span&gt;test&lt;span&gt;,&lt;/span&gt; messageId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;extractCondition&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Smart Exceptions:&lt;/strong&gt; The rule skips several patterns that TypeScript needs inline for type narrowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Early exit guards: &lt;code&gt;if (!user) return&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Optional chaining: &lt;code&gt;if (user?.name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Truthy narrowing: &lt;code&gt;if (arr &amp;amp;&amp;amp; arr[0])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

  
```typescript
if (user.isActive &amp;amp;&amp;amp; user.role === &apos;admin&apos; &amp;amp;&amp;amp; !user.isBanned) {
  showAdminPanel()
}
```
  
  
```typescript
const canAccessAdminPanel = user.isActive &amp;amp;&amp;amp; user.role === &apos;admin&apos; &amp;amp;&amp;amp; !user.isBanned
&lt;p&gt;if (canAccessAdminPanel) {&lt;br /&gt;
showAdminPanel()&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

---

### Repository tryCatch Wrapper

Database calls can fail. Enforce wrapping them in `tryCatch()`.

```typescript
// eslint-local-rules/repository-trycatch.ts
// Matches pattern: get*Repository().method()
const REPO_PATTERN = /^get\w+Repository$/

const rule: Rule.RuleModule = {
  meta: {
    messages: {
      missingTryCatch: &apos;Repository calls must be wrapped with tryCatch().&apos;
    }
  },
  create(context) {
    return {
      AwaitExpression(node) {
        if (!isRepositoryMethodCall(node.argument)) return
        if (isWrappedInTryCatch(context, node)) return

        context.report({ node, messageId: &apos;missingTryCatch&apos; })
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

  
```typescript
const workouts = await getWorkoutRepository().findAll()  // Might throw!
```
  
  
```typescript
const [error, workouts] = await tryCatch(getWorkoutRepository().findAll())
&lt;p&gt;if (error) {&lt;br /&gt;
showError(‘Failed to load workouts’)&lt;br /&gt;
return&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;/Fragment&amp;gt;
&amp;lt;/CodeComparison&amp;gt;

&amp;lt;Alert type=&quot;note&quot;&amp;gt;
This rule matches the `get*Repository()` pattern. Ensure your repository factory functions follow this naming convention.
&amp;lt;/Alert&amp;gt;

---

## The Full Config

&amp;lt;Collapsible title=&quot;Complete eslint.config.ts example&quot;&amp;gt;

```typescript
export default defineConfigWithVueTs(
  { ignores: [&apos;**/dist/**&apos;, &apos;**/coverage/**&apos;, &apos;**/node_modules/**&apos;] },

  pluginVue.configs[&apos;flat/essential&apos;],
  vueTsConfigs.recommended,
  pluginUnicorn.configs.recommended,

  // Vue component rules
  {
    files: [&apos;src/**/*.vue&apos;],
    rules: {
      &apos;vue/multi-word-component-names&apos;: [&apos;error&apos;, { ignores: [&apos;App&apos;, &apos;Layout&apos;] }],
      &apos;vue/component-name-in-template-casing&apos;: [&apos;error&apos;, &apos;PascalCase&apos;],
      &apos;vue/prop-name-casing&apos;: [&apos;error&apos;, &apos;camelCase&apos;],
      &apos;vue/custom-event-name-casing&apos;: [&apos;error&apos;, &apos;kebab-case&apos;],
      &apos;vue/no-unused-properties&apos;: [&apos;error&apos;, { groups: [&apos;props&apos;, &apos;data&apos;, &apos;computed&apos;, &apos;methods&apos;] }],
      &apos;vue/no-unused-refs&apos;: &apos;error&apos;,
      &apos;vue/define-props-destructuring&apos;: &apos;error&apos;,
      &apos;vue/prefer-use-template-ref&apos;: &apos;error&apos;,
      &apos;vue/max-template-depth&apos;: [&apos;error&apos;, { maxDepth: 8 }],
    },
  },

  // TypeScript style guide
  {
    files: [&apos;src/**/*.{ts,vue}&apos;],
    rules: {
      &apos;complexity&apos;: [&apos;warn&apos;, { max: 10 }],
      &apos;no-nested-ternary&apos;: &apos;error&apos;,
      &apos;@typescript-eslint/consistent-type-assertions&apos;: [&apos;error&apos;, { assertionStyle: &apos;never&apos; }],
      &apos;no-restricted-syntax&apos;: [&apos;error&apos;,
        { selector: &apos;TSEnumDeclaration&apos;, message: &apos;Use literal unions instead of enums.&apos; },
        { selector: &apos;IfStatement &amp;gt; :not(IfStatement).alternate&apos;, message: &apos;Avoid else. Use early returns.&apos; },
        { selector: &apos;TryStatement&apos;, message: &apos;Use tryCatch() instead of try/catch.&apos; },
      ],
    },
  },

  // Feature boundaries
  {
    files: [&apos;src/**/*.{ts,vue}&apos;],
    plugins: { &apos;import-x&apos;: pluginImportX },
    rules: {
      &apos;import-x/no-restricted-paths&apos;: [&apos;error&apos;, {
        zones: [
          { target: &apos;./src/features/workout&apos;, from: &apos;./src/features&apos;, except: [&apos;./workout&apos;] },
          // ... other features
          { target: &apos;./src/features&apos;, from: &apos;./src/views&apos; },  // Unidirectional flow
        ]
      }],
    },
  },

  // i18n rules
  {
    files: [&apos;src/**/*.vue&apos;],
    plugins: { &apos;@intlify/vue-i18n&apos;: pluginVueI18n },
    rules: {
      &apos;@intlify/vue-i18n/no-raw-text&apos;: [&apos;error&apos;, { /* config */ }],
    },
  },

  // Prevent disabling i18n rules
  {
    files: [&apos;src/**/*.vue&apos;],
    plugins: { &apos;@eslint-community/eslint-comments&apos;: pluginEslintComments },
    rules: {
      &apos;@eslint-community/eslint-comments/no-restricted-disable&apos;: [&apos;error&apos;, &apos;@intlify/vue-i18n/*&apos;],
    },
  },

  // Vitest rules
  {
    files: [&apos;src/**/__tests__/*&apos;],
    ...pluginVitest.configs.recommended,
    rules: {
      &apos;vitest/consistent-test-it&apos;: [&apos;error&apos;, { fn: &apos;it&apos; }],
      &apos;vitest/prefer-hooks-on-top&apos;: &apos;error&apos;,
      &apos;vitest/prefer-hooks-in-order&apos;: &apos;error&apos;,
      &apos;vitest/no-duplicate-hooks&apos;: &apos;error&apos;,
      &apos;vitest/max-nested-describe&apos;: [&apos;error&apos;, { max: 2 }],
      &apos;vitest/no-conditional-in-test&apos;: &apos;warn&apos;,
    },
  },

  // Enforce test helpers
  {
    files: [&apos;src/**/__tests__/**/*.{ts,spec.ts}&apos;],
    rules: {
      &apos;no-restricted-imports&apos;: [&apos;error&apos;, {
        paths: [
          { name: &apos;vitest-browser-vue&apos;, importNames: [&apos;render&apos;], message: &apos;Use createTestApp()&apos; },
          { name: &apos;@vue/test-utils&apos;, importNames: [&apos;mount&apos;], message: &apos;Use createTestApp()&apos; },
        ]
      }],
    },
  },

  // Local rules
  {
    files: [&apos;src/**/*.{ts,vue}&apos;],
    plugins: { local: localRules },
    rules: {
      &apos;local/no-hardcoded-colors&apos;: &apos;error&apos;,
      &apos;local/composable-must-use-vue&apos;: &apos;error&apos;,
      &apos;local/repository-trycatch&apos;: &apos;error&apos;,
      &apos;local/extract-condition-variable&apos;: &apos;error&apos;,
      &apos;local/no-let-in-describe&apos;: &apos;error&apos;,
    },
  },

  // Disable rules handled by Oxlint
  ...pluginOxlint.buildFromOxlintConfigFile(&apos;./.oxlintrc.json&apos;),

  // pnpm catalog enforcement
  ...pnpmConfigs.recommended,

  skipFormatting,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complexity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limit function complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-nested-ternary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Readable conditionals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;consistent-type-assertions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No unsafe &lt;code&gt;as&lt;/code&gt; casts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-restricted-syntax&lt;/code&gt; (enums)&lt;/td&gt;
&lt;td&gt;Use unions over enums&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-restricted-syntax&lt;/code&gt; (else)&lt;/td&gt;
&lt;td&gt;Prefer early returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-restricted-syntax&lt;/code&gt; (routes)&lt;/td&gt;
&lt;td&gt;Use named routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;import-x/no-restricted-paths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feature isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vue/no-unused-*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dead code detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@intlify/vue-i18n/no-raw-text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;i18n compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-restricted-disable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No bypassing i18n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Must Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-restricted-imports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enforce test helpers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nice to Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vue/define-props-destructuring&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vue 3.5 patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nice to Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vue/max-template-depth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Template readability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nice to Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vitest/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Test consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nice to Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unicorn/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modern JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nice to Have&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pnpm/recommended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Catalog enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composable-must-use-vue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Composable validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-hardcoded-colors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Theming support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-let-in-describe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract-condition-variable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Readable conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;repository-trycatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Start with the must-haves. Add nice-to-haves when you’re ready. Write custom rules when nothing else fits.&lt;/p&gt;
&lt;p&gt;The combination of Oxlint for speed and ESLint for coverage gives you fast feedback during development and comprehensive checks in CI.&lt;/p&gt;

          </content:encoded><category>vue</category><category>typescript</category><category>tooling</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Next Level GitHub Copilot: Agents, Instructions &amp; Automation in VS Code</title><link>https://alexop.dev/posts/vs-code-copilot-workshop/</link><guid isPermaLink="true">https://alexop.dev/posts/vs-code-copilot-workshop/</guid><description>Workshop covering the transformation from LLM to Agent, context engineering, AGENTS.md, subagents, and skills in VS Code Copilot.</description><pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h1&gt;Next Level GitHub Copilot&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;http://Agents.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agents.md&lt;/a&gt; Subagents &amp;amp; Skills&lt;/p&gt;
&lt;p&gt;by Alexander Opalic&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Workshop Outline&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is an Agent? (LLM → Agent transformation)&lt;/li&gt;
&lt;li&gt;Context Engineering (the real skill)&lt;/li&gt;
&lt;li&gt;Back Pressure (core validation concept)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt; (open standard)&lt;/li&gt;
&lt;li&gt;Subagents (specialized invocation)&lt;/li&gt;
&lt;li&gt;Skills (portable workflows)&lt;/li&gt;
&lt;li&gt;Live Demo&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;
&lt;h2&gt;🙋 Who has used GitHub Copilot in VS Code?&lt;/h2&gt;
&lt;hr /&gt;
&lt;h2&gt;About me&lt;/h2&gt;
&lt;div&gt;
  &lt;h2&gt;Alex Opalic&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;🚀 7 years expierence as a full stack developer&lt;/li&gt;
&lt;li&gt;💼 Developer at Otto Payments&lt;/li&gt;
&lt;li&gt;🏡 Based in Geretsried (south of Munich, Bavaria)&lt;/li&gt;
&lt;li&gt;✍️ Blogger at alexop.dev&lt;/li&gt;
&lt;li&gt;🎤 Sharing &amp;amp; speaking about Vue, testing &amp;amp; GraphQL &amp;amp; AI&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h1&gt;What is an Agent?&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;The Transformation: LLM → Agent&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;At the beginning, an LLM is just a text generator&lt;/li&gt;
&lt;li&gt;One problem: the LLM didn’t have access to current news&lt;/li&gt;
&lt;li&gt;Solution: all providers gave the LLM access to tools&lt;/li&gt;
&lt;li&gt;With tools, the LLM can now interact with the world&lt;/li&gt;
&lt;li&gt;This is why an agent is an LLM + Tools + Agentic Loop&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;The Agentic Loop (nanocode)&lt;/h2&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;nanocode &lt;span&gt;|&lt;/span&gt; claude-opus-4-5 &lt;span&gt;|&lt;/span&gt; /Users/alexanderopalic/Projects/typescript/nanocode

────────────────────────────────────────────────────────────────────────────────
❯  create a simple typescript &lt;span&gt;file&lt;/span&gt; as a &lt;span&gt;sum&lt;/span&gt; &lt;span&gt;function&lt;/span&gt;
────────────────────────────────────────────────────────────────────────────────
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Starting with &lt;span&gt;1&lt;/span&gt; messages
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Got response, stop_reason: tool_use

⏺ Write&lt;span&gt;(&lt;/span&gt;src/sum.ts&lt;span&gt;)&lt;/span&gt;
  ⎿  ok
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Starting with &lt;span&gt;3&lt;/span&gt; messages
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Got response, stop_reason: end_turn

⏺ Created &lt;span&gt;&lt;span&gt;`&lt;/span&gt;src/sum.ts&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; with a simple &lt;span&gt;sum&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; that takes two numbers and returns their sum.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;~350 lines of TypeScript&lt;/strong&gt; to understand how Claude Code works.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Agentic Loop (Code)&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;agentLoop&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;messages&lt;span&gt;:&lt;/span&gt; Message&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; systemPrompt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Message&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;callApi&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;messages&lt;span&gt;,&lt;/span&gt; systemPrompt&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;printResponse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;response&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; toolResults &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;processToolCalls&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;response&lt;span&gt;.&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; newMessages &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;messages&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;assistant&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; content&lt;span&gt;:&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;content &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;toolResults&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; newMessages  &lt;span&gt;// No tools called, we&apos;re done&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;agentLoop&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;  &lt;span&gt;// Loop again with tool results&lt;/span&gt;
    &lt;span&gt;[&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;newMessages&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; content&lt;span&gt;:&lt;/span&gt; toolResults &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    systemPrompt
  &lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entire request → response → execute → loop cycle in ~15 lines.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Tool Registration&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;TOOLS&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;read&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Read file with line numbers&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;string&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; offset&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;number?&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; limit&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;number?&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    execute&lt;span&gt;:&lt;/span&gt; read
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;write&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Write content to file&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;string&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;string&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    execute&lt;span&gt;:&lt;/span&gt; write
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;bash&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Run shell command&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; cmd&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;string&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    execute&lt;span&gt;:&lt;/span&gt; bash
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;A Complete Tool Implementation&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;read&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;args&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; path &lt;span&gt;=&lt;/span&gt; args&lt;span&gt;.&lt;/span&gt;path &lt;span&gt;as&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; text &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; Bun&lt;span&gt;.&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; lines &lt;span&gt;=&lt;/span&gt; text&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;\n&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; offset &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;args&lt;span&gt;.&lt;/span&gt;offset &lt;span&gt;as&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; limit &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;args&lt;span&gt;.&lt;/span&gt;limit &lt;span&gt;as&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; lines&lt;span&gt;.&lt;/span&gt;length
  &lt;span&gt;return&lt;/span&gt; lines
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;offset&lt;span&gt;,&lt;/span&gt; offset &lt;span&gt;+&lt;/span&gt; limit&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;line&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;offset &lt;span&gt;+&lt;/span&gt; i &lt;span&gt;+&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;padStart&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;| &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;line&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;\n&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;VS Code Copilot Built-in Tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;⟨⟩ &lt;strong&gt;agent&lt;/strong&gt; — Delegate tasks to other agents&lt;/li&gt;
&lt;li&gt;ⓘ &lt;strong&gt;askQuestions&lt;/strong&gt; — Ask questions to clarify requirements&lt;/li&gt;
&lt;li&gt;✎ &lt;strong&gt;edit&lt;/strong&gt; — Edit files in your workspace&lt;/li&gt;
&lt;li&gt;▷ &lt;strong&gt;execute&lt;/strong&gt; — Execute code and applications&lt;/li&gt;
&lt;li&gt;⧉ &lt;strong&gt;read&lt;/strong&gt; — Read files in your workspace&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;search&lt;/strong&gt; — Search files in your workspace&lt;/li&gt;
&lt;li&gt;≡ &lt;strong&gt;todo&lt;/strong&gt; — Manage and track todo items&lt;/li&gt;
&lt;li&gt;✕ &lt;strong&gt;vscode&lt;/strong&gt; — Use VS Code features&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;web&lt;/strong&gt; — Fetch information from the web&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Context Engineering&lt;/h1&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;“Context engineering is the art and science of filling the context window with just the right information at each step of an agent’s trajectory.”&lt;/p&gt;
&lt;p&gt;— LangChain/Manus webinar&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Context Window Utilization&lt;/h2&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Three Long-Horizon Techniques&lt;/h2&gt;
&lt;p&gt;From &lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Anthropic’s guide&lt;/a&gt;:&lt;/p&gt;

1. **Compaction** — Summarize history, reset periodically
2. **Structured note-taking** — External memory systems
3. **Sub-agent architectures** — Distribute work across focused contexts

&lt;hr /&gt;
&lt;h1&gt;Back Pressure&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Back Pressure Matters&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Back pressure&lt;/strong&gt; = automated feedback that validates agent work&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without back pressure, &lt;strong&gt;you&lt;/strong&gt; become the validation layer&lt;/li&gt;
&lt;li&gt;Agents cannot self-correct if nothing tells them something is wrong&lt;/li&gt;
&lt;li&gt;With good back pressure, agents detect mistakes and iterate until correct&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;“If you’re directly responsible for checking each line is valid, that’s time taken away from higher-level goals.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Back Pressure Sources&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What It Validates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Types, interfaces, contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Syntax, imports, compilation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logic, behavior, regressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Style, patterns, best practices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Expressive type systems + good error messages = agents can self-correct.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;&lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt;&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;What is &lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; An open standard for agent-specific documentation&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Repository root (works in monorepos too)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who:&lt;/strong&gt; Works with Copilot, Claude, Cursor, Devin, 20+ agents&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“While &lt;a href=&quot;http://README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;README.md&lt;/a&gt; targets humans, &lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt; contains the extra context coding agents need.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt; Structure&lt;/h2&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; AGENTS.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Dev Environment&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; How to set up and navigate

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Build &amp;amp; Test Commands&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`pnpm install &amp;amp;&amp;amp; pnpm dev`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`pnpm test:unit`&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Code Style&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; TypeScript strict mode
&lt;span&gt;-&lt;/span&gt; Prefer composition over inheritance

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; PR Instructions&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Keep PRs small and focused
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; No required fields—use what helps your project.&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Before vs After: Progressive Disclosure&lt;/h2&gt;
&lt;h3&gt;❌ Bloated (847 lines)&lt;/h3&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; AGENTS.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; API Endpoints&lt;/span&gt;
[200 lines of docs...]
&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Testing Strategy&lt;/span&gt;
[150 lines of docs...]
&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Architecture&lt;/span&gt;
[300 lines of docs...]
&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Code Style&lt;/span&gt;
[100 lines of rules...]
&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Deployment&lt;/span&gt;
[97 lines of docs...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;40% context consumed before work starts&lt;/p&gt;
&lt;h3&gt;✅ Lean (58 lines)&lt;/h3&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; AGENTS.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Quick Start&lt;/span&gt;
pnpm install &amp;amp;&amp;amp; pnpm dev

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Docs Reference&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Doc &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; When to read &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;--------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; docs/api.md &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; API work &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; docs/testing.md &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tests &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; docs/arch.md &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Design &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docs loaded on-demand when needed&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;The /learn Skill&lt;/h2&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Learn from Conversation&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase 1: Deep Analysis&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; What patterns or approaches were discovered?
&lt;span&gt;-&lt;/span&gt; What gotchas or pitfalls were encountered?
&lt;span&gt;-&lt;/span&gt; What architecture decisions were made?

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase 2: Categorize &amp;amp; Locate&lt;/span&gt;
Read existing docs to find the best home.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase 3: Draft the Learning&lt;/span&gt;
Format to match existing doc style.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase 4: User Approval (BLOCKING)&lt;/span&gt;
Present changes, wait for explicit approval.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase 5: Save&lt;/span&gt;
After approval, save the learning.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Subagents&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Subagents in VS Code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How to invoke:&lt;/strong&gt;&lt;br /&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enable tools in Copilot Chat (hammer icon)&lt;/li&gt;
&lt;li&gt;Call explicitly with &lt;code&gt;#runSubagent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Or accept when Copilot suggests one&lt;br /&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Use Cases&lt;/h2&gt;

- Specialized searches (explore codebase, web, docs)
- Long-running tasks (data analysis, refactoring)
- TDD workflows (test generation, validation)
- Multi-step processes (research, summarize, act)

---
## Explore Subagent Flow
&lt;p&gt;&amp;lt;SubagentDiagram&lt;br /&gt;
task=“Find auth files”&lt;br /&gt;
files={[“Auth.tsx”, “auth.ts”, “authService.ts”]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Start&lt;/strong&gt; to see how the main agent delegates file search to a specialized Explore subagent.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Parallel Subagent Execution&lt;/h2&gt;
&lt;p&gt;&amp;lt;ParallelSubagentDiagram&lt;br /&gt;
task=“Research Vue 3 reactivity”&lt;br /&gt;
agents={[&lt;br /&gt;
{ name: “Web Agent”, icon: “🌐”, color: “#3b82f6”, domain: “Docs, GitHub”, findings: [“Official guide”, “RFC #123”, “GitHub issue”] },&lt;br /&gt;
{ name: “Community”, icon: “💬”, color: “#8b5cf6”, domain: “Reddit, SO”, findings: [“r/vuejs post”, “Top SO answer”, “Discord tip”] },&lt;br /&gt;
{ name: “Codebase”, icon: “📂”, color: “#10b981”, domain: “Project files”, findings: [“useAuth.ts”, “store.ts”, “api/client.ts”] }&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Start&lt;/strong&gt; to see the fan-out/fan-in pattern where multiple subagents search in parallel.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Subagent in nanocode: Fetch Tool&lt;/h2&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;nanocode &lt;span&gt;|&lt;/span&gt; claude-opus-4-5 &lt;span&gt;|&lt;/span&gt; /Users/alexanderopalic/Projects/typescript/nanocode

────────────────────────────────────────────────────────────────────────────────
❯ how does fc bayern played yesterday
────────────────────────────────────────────────────────────────────────────────
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Starting with &lt;span&gt;1&lt;/span&gt; messages
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Got response, stop_reason: tool_use

⏺ Fetch&lt;span&gt;(&lt;/span&gt;https://www.google.com/search?q&lt;span&gt;=&lt;/span&gt;fc+bayern+munich+m&lt;span&gt;)&lt;/span&gt;
  ⎿  I apologize, but I cannot access real-time sports results or &lt;span&gt;..&lt;/span&gt;. +7 lines
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Starting with &lt;span&gt;3&lt;/span&gt; messages
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Got response, stop_reason: tool_use

⏺ Fetch&lt;span&gt;(&lt;/span&gt;https://www.espn.com/soccer/team/_/id/132/bayern-m&lt;span&gt;)&lt;/span&gt;
  ⎿  Based on the information &lt;span&gt;in&lt;/span&gt; the image, Bayern Munich&apos;s &lt;span&gt;most&lt;/span&gt;  &lt;span&gt;..&lt;/span&gt;. +7 lines
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Starting with &lt;span&gt;5&lt;/span&gt; messages
&lt;span&gt;[&lt;/span&gt;agentLoop&lt;span&gt;]&lt;/span&gt; Got response, stop_reason: end_turn

⏺ FC Bayern Munich lost &lt;span&gt;2&lt;/span&gt;-1 to FC Augsburg &lt;span&gt;in&lt;/span&gt; the Bundesliga yesterday.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;Fetch tool&lt;/strong&gt; uses a subagent to summarize HTML responses before returning.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Skills&lt;/h1&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Real Skill: Plausible SEO Consultant&lt;/h2&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;.claude/skills/plausible-insights/
├── skill.md              &lt;span&gt;# Skill definition + quick start&lt;/span&gt;
├── scripts/              &lt;span&gt;# Automation scripts &lt;/span&gt;
│   └── fetch-data.ts    &lt;span&gt;# Fetch Plausible data CLI&lt;/span&gt;
└── references/           &lt;span&gt;# On-demand docs (progressive disclosure)&lt;/span&gt;
    ├── quick-ref.md      &lt;span&gt;# Common query patterns&lt;/span&gt;
    ├── api/
    │   ├── filters.md    &lt;span&gt;# Filter syntax&lt;/span&gt;
    │   └── errors.md     &lt;span&gt;# Error solutions&lt;/span&gt;
    └── seo/
        └── thresholds.md &lt;span&gt;# Interpretation guidelines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent reads &lt;code&gt;skill.md&lt;/code&gt; first. Reference docs load only when needed.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Skill in Action&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; “Why is my bounce rate so high on the Vue posts?”&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Description matches → &lt;a href=&quot;http://skill.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;skill.md&lt;/a&gt; loads (~500 tokens)&lt;/li&gt;
&lt;li&gt;Agent runs: &lt;code&gt;bun cli top-pages --range 7d --pattern &quot;/vue/&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Agent reads &lt;code&gt;references/seo/thresholds.md&lt;/code&gt; for interpretation&lt;/li&gt;
&lt;li&gt;Agent fetches actual pages with WebFetch&lt;/li&gt;
&lt;li&gt;Returns specific fixes based on real content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; Data shows symptoms. Content shows causes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;The Full Picture&lt;/h1&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;Live Demo&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;The demo uses &lt;code&gt;npx&lt;/code&gt; (bundled with Node.js) and Python. Install for your platform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac (Homebrew):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span&gt;install&lt;/span&gt; &lt;span&gt;node&lt;/span&gt; python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows (winget):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;winget &lt;span&gt;install&lt;/span&gt; OpenJS.NodeJS Python.Python.3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Or download from:&lt;/strong&gt; &lt;a href=&quot;https://nodejs.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;nodejs.org&lt;/a&gt; | &lt;a href=&quot;https://python.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;python.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;node&lt;/span&gt; &lt;span&gt;--version&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx &lt;span&gt;--version&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; python &lt;span&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Demo: Building a Skill&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enable Skills&lt;/strong&gt; in VS Code settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install skill-creator&lt;/strong&gt; via CLI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt&lt;/strong&gt; to generate a new skill&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;
&lt;h2&gt;Step 1: Enable Skills&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;VS Code Setting:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;chat.useAgentSkills&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or via UI: &lt;code&gt;Settings → Search &quot;agent skills&quot; → Enable&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Still in preview — enable in VS Code Insiders for latest features.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Create a new Skill&lt;/h2&gt;
&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; hello
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;use it everytime the user writes alex&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Hello SKill&lt;/span&gt;

if the user writes &quot;alex&quot;, respond with &quot;Hello, Alexander Opalic! How can I assist you today?&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Install skill-creator&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills &lt;span&gt;add&lt;/span&gt; https://github.com/anthropics/skills &lt;span&gt;--skill&lt;/span&gt; skill-creator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This adds the &lt;strong&gt;skill-creator&lt;/strong&gt; skill to your project — a skill that helps you create new skills.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project structure after install:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-project/
└── .github/
    └── skills/
        └── skill-creator/
            └── SKILL.md
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;◇  Source: https://github.com/anthropics/skills.git
│
◇  Repository cloned
│
◇  Found &lt;span&gt;17&lt;/span&gt; skills &lt;span&gt;(&lt;/span&gt;via Well-known Agent Skill Discovery&lt;span&gt;)&lt;/span&gt;
│
●  Selected &lt;span&gt;1&lt;/span&gt; skill: skill-creator
│
◇  Detected &lt;span&gt;3&lt;/span&gt; agents
│
◇  Install to
│  All agents &lt;span&gt;(&lt;/span&gt;Recommended&lt;span&gt;)&lt;/span&gt;
│
◇  Installation scope
│  Project
│
◇  Installation method
│  Symlink &lt;span&gt;(&lt;/span&gt;Recommended&lt;span&gt;)&lt;/span&gt;

│
◇  Installation Summary ──────────────────────────────╮
│                                                     │
│  ~/Projects/workshop/.agents/skills/skill-creator   │
│    symlink → Claude Code, GitHub Copilot, OpenCode  │
│                                                     │
├─────────────────────────────────────────────────────╯
│
◆  Proceed with installation?
│  ● Yes / ○ No
└
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Generate a New Skill&lt;/h2&gt;
&lt;p&gt;Important Skill name and folder name must match!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create a skill that will use https://alexop.dev/llms.txt
and will answer any question regarding Vue or AI.

The skill should fetch the content and use the
#runSubagent command. The subagent should do the
heavy work and then report back to the main agent.
name of the skill is vue-ai-assistant
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;→ &lt;strong&gt;skill-creator generates the &lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt; for us&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Gets Generated&lt;/h2&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; vue&lt;span&gt;-&lt;/span&gt;ai&lt;span&gt;-&lt;/span&gt;assistant
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Answer questions about Vue.js&lt;span&gt;,&lt;/span&gt; Nuxt&lt;span&gt;,&lt;/span&gt; and AI topics using Alexander Opalic&apos;s knowledge base. Use this skill when the user asks about Vue&lt;span&gt;,&lt;/span&gt; Vue 3&lt;span&gt;,&lt;/span&gt; Nuxt&lt;span&gt;,&lt;/span&gt; Nuxt 3&lt;span&gt;,&lt;/span&gt; Composition API&lt;span&gt;,&lt;/span&gt; Vue Router&lt;span&gt;,&lt;/span&gt; Pinia&lt;span&gt;,&lt;/span&gt; Vite&lt;span&gt;,&lt;/span&gt; AI&lt;span&gt;,&lt;/span&gt; machine learning&lt;span&gt;,&lt;/span&gt; LLMs&lt;span&gt;,&lt;/span&gt; or related frontend/AI topics. Triggers on questions like &quot;how do I use Vue&quot;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;explain Nuxt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;what&apos;s new in Vue 3&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;AI agent patterns&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; or any Vue/AI related query.&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Vue &amp;amp; AI Assistant&lt;/span&gt;

Answer questions about Vue.js ecosystem and AI topics by fetching knowledge from https://alexop.dev/llms.txt and delegating research to a subagent.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; MANDATORY Workflow&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;IMPORTANT: You MUST follow ALL steps below. Do NOT skip the subagent step. Do NOT answer directly after fetching - you MUST delegate to a subagent.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Fetch the knowledge base&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Use &lt;span&gt;`fetch_webpage`&lt;/span&gt; to retrieve content from &lt;span&gt;`https://alexop.dev/llms.txt`&lt;/span&gt;
&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;REQUIRED - Delegate to subagent&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Use &lt;span&gt;`runSubagent`&lt;/span&gt; with the fetched content and user&apos;s question. &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;This step is NOT optional.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Return the answer&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Present the subagent&apos;s findings to the user

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Implementation&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;You MUST execute ALL steps below. Skipping the subagent is a violation of this skill&apos;s requirements.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 1: Fetch Knowledge Base&lt;/span&gt;

Use the fetch_webpage tool:
&lt;span&gt;-&lt;/span&gt; URL: &lt;span&gt;`https://alexop.dev/llms.txt`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Query: The user&apos;s question about Vue or AI

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 2: Run Subagent with Context (MANDATORY)&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;You MUST call &lt;span&gt;`runSubagent`&lt;/span&gt; - do NOT answer the question yourself. The subagent handles the analysis and response.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

Use &lt;span&gt;`runSubagent`&lt;/span&gt; with a detailed prompt containing:

&lt;span&gt;1.&lt;/span&gt; The fetched content from llms.txt as the knowledge base
&lt;span&gt;2.&lt;/span&gt; The user&apos;s original question
&lt;span&gt;3.&lt;/span&gt; Instructions to:
   &lt;span&gt;-&lt;/span&gt; Analyze the knowledge base content thoroughly
   &lt;span&gt;-&lt;/span&gt; Find relevant information to answer the question
   &lt;span&gt;-&lt;/span&gt; Provide a clear, concise, and accurate answer
   &lt;span&gt;-&lt;/span&gt; Include code examples when relevant
   &lt;span&gt;-&lt;/span&gt; Cite specific sections from the knowledge base if applicable
   &lt;span&gt;-&lt;/span&gt; If the knowledge base doesn&apos;t contain the answer, use general knowledge but note this

Example subagent prompt:

You are a Vue.js and AI expert. Answer the following question using the provided knowledge base content.

KNOWLEDGE BASE CONTENT:
fetched_content

USER QUESTION:
user_question

Analyze thoroughly, provide code examples when relevant, and cite sources from the knowledge base.
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 3: Present Answer&lt;/span&gt;

Return the subagent&apos;s response to the user, formatted appropriately with code blocks and explanations.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Example&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;User asks&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: &quot;How do I use composables in Vue 3?&quot;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Execution&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;:
&lt;span&gt;1.&lt;/span&gt; Fetch https://alexop.dev/llms.txt
&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;MUST&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; call runSubagent with the content and question (do NOT skip this)
&lt;span&gt;3.&lt;/span&gt; Return the subagent&apos;s comprehensive answer about Vue 3 composables
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;Bonus: The askQuestions Tool&lt;/h2&gt;
&lt;p&gt;VS Code Copilot can &lt;strong&gt;ask clarifying questions&lt;/strong&gt; mid-task.&lt;/p&gt;
&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;help me to create a workout tracking app use the #askQuestions tool to find out how the tech specs should be
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;┌─────────────────────────────────────────────────────────────┐
│                     Platform &lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;/4&lt;span&gt;)&lt;/span&gt;                          │
├─────────────────────────────────────────────────────────────┤
│ What platform should the workout tracking app target?       │
├─────────────────────────────────────────────────────────────┤
│ ★ Web App  Browser-based PWA, accessible anywhere      &lt;span&gt;[&lt;/span&gt;✓&lt;span&gt;]&lt;/span&gt;  │
├─────────────────────────────────────────────────────────────┤
│   iOS Native  Swift/SwiftUI &lt;span&gt;for&lt;/span&gt; iPhone                      │
├─────────────────────────────────────────────────────────────┤
│   Android Native  Kotlin &lt;span&gt;for&lt;/span&gt; Android devices                │
├─────────────────────────────────────────────────────────────┤
│   Cross-Platform  React Native or Flutter &lt;span&gt;for&lt;/span&gt; iOS &lt;span&gt;&amp;amp;&lt;/span&gt; Android │
├─────────────────────────────────────────────────────────────┤
│   Desktop  Electron app &lt;span&gt;for&lt;/span&gt; Mac/Windows                     │
├─────────────────────────────────────────────────────────────┤
│ ✎ Other&lt;span&gt;..&lt;/span&gt;.  Enter custom answer                             │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Subagent Fan-Out Pattern&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Prompt for VS Code Insiders:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#runSubagent run 3 subagents that search the web
and tell me something interesting about Geretsried
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This demonstrates the &lt;strong&gt;fan-out/fan-in pattern&lt;/strong&gt; where multiple agents work in parallel.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Live Action: Excalidraw Skill&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Install the skill:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills &lt;span&gt;add&lt;/span&gt; https://github.com/softaworks/agent-toolkit &lt;span&gt;--skill&lt;/span&gt; excalidraw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the Excalidraw Extension in VS Code for best experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt to customize with brand colors:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Update the excalidraw skill to use these brand colors:

- Fill: rgb(33, 39, 55)
- Text: rgb(234, 237, 243)
- Accent: rgb(255, 107, 237)
- Card: rgb(52, 63, 96)
- Card Muted: rgb(138, 51, 123)
- Border: rgb(171, 75, 153)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;→ Agent modifies the skill’s &lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt; to include color instructions&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h2&gt;More Community Skills&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills &lt;span&gt;add&lt;/span&gt; https://github.com/anthropics/skills &lt;span&gt;--skill&lt;/span&gt; frontend-design
npx skills &lt;span&gt;add&lt;/span&gt; https://github.com/simonwong/agent-skills &lt;span&gt;--skill&lt;/span&gt; code-simplifier
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;frontend-design&lt;/strong&gt; — creates polished, production-grade UI components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;code-simplifier&lt;/strong&gt; — simplifies and refines code for clarity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Browse and discover skills at &lt;a href=&quot;https://agentskills.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;agentskills.io&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Key Takeaways&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agents = LLM + Tools + Loop&lt;/strong&gt; (nanocode shows this simply)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context is finite&lt;/strong&gt; — treat tokens as budget&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt;&lt;/strong&gt; — standardized project context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents&lt;/strong&gt; — specialized agents for complex tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; — portable workflows that load on demand&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;Thank You!&lt;/h1&gt;
&lt;p&gt;Questions?&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Resources&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/copilot/agents/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VS Code: Using Agents&lt;/a&gt; - Agent types and session management&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Anthropic: Effective Context Engineering&lt;/a&gt; - Context engineering guide&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=JepVi1tBNEE&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VS Code: Introducing Agent Skills&lt;/a&gt; - Agent Skills deep dive&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/copilot/guides/context-engineering-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VS Code: Context Engineering Guide&lt;/a&gt; - Microsoft’s context engineering workflow&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agents.md/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt; - Open standard for agent documentation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agentskills.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Agent Skills Spec&lt;/a&gt; - Open standard for portable agent skills&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexanderop/nanocode&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;nanocode&lt;/a&gt; - Minimal agent implementation in TypeScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.humanlayer.dev/blog/writing-a-good-claude-md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Writing a Good CLAUDE.md&lt;/a&gt; - Best practices for agent documentation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexanderop/claude-plausible-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Plausible SEO Skill&lt;/a&gt; - Skills deep dive with Plausible example&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://banay.me/dont-waste-your-backpressure/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Don’t Waste Your Back Pressure&lt;/a&gt; - Why automated feedback loops make agents more effective&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexanderop/workshop&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Workshop Solution&lt;/a&gt; - Complete code examples from this workshop&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexop.dev/prompts/claude/claude-learn-command/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Learn Prompt&lt;/a&gt; - Skill that helps agents learn from conversations&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;

          </content:encoded><category>vs-code</category><category>github-copilot</category><category>ai-agents</category><category>context-engineering</category><category>workshop</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>What&apos;s New in VS Code Copilot: January 2026 Update</title><link>https://alexop.dev/posts/whats-new-vscode-copilot-january-2026/</link><guid isPermaLink="true">https://alexop.dev/posts/whats-new-vscode-copilot-january-2026/</guid><description>Major updates to VS Code Copilot including parallel subagent execution, a new skills system, deeper Claude integration with extended thinking, terminal improvements with kitty keyboard protocol, and instruction files that now work everywhere.</description><pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;The past week has brought a wave of updates to VS Code’s Copilot experience, with major improvements to how agents work together, a new skills system, deeper Claude integration, and significant terminal enhancements. Here’s what you need to know—with concrete examples you can try today.&lt;/p&gt;
&lt;p&gt;For those who want to dive deeper into the implementation details, I’ve included links to the relevant GitHub pull requests and issues throughout this post.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Subagents Get Smarter (and Faster)&lt;/h2&gt;
&lt;p&gt;Two significant changes make subagents far more practical for complex workflows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href=&quot;https://github.com/microsoft/vscode/issues/274630&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #274630 - Parallel subagent execution&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Parallel Execution&lt;/h3&gt;
&lt;p&gt;Previously, if you kicked off multiple &lt;code&gt;runSubagent&lt;/code&gt; calls, they’d run one after another. Now they can run simultaneously when tasks are independent, dramatically reducing wait times for research and code review operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Research the best approaches for:
1. Rate limiting in our REST API
2. Caching strategies for our database queries
3. Error handling patterns for our microservices

Use a subagent for each topic and compile the findings.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With parallel execution, all three research subagents run concurrently instead of sequentially—cutting total wait time significantly.&lt;/p&gt;
&lt;figure&gt;
&lt;h3&gt;Fine-Grained Tool Access&lt;/h3&gt;
&lt;p&gt;You can now constrain which tools a subagent can access. This is critical for safety-conscious workflows where you want AI help without the risk of unintended changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating a custom agent with restricted tools:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a file at &lt;code&gt;.github/agents/github-researcher.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; github&lt;span&gt;-&lt;/span&gt;researcher
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Research agent with access to GitHub. Use for searching issues&lt;span&gt;,&lt;/span&gt;
             reading documentation&lt;span&gt;,&lt;/span&gt; and gathering information. Cannot edit files.
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;read&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;search&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;web&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;github/*&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;argument-hint&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; The research task to complete&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

You are a research assistant with read-only access to the codebase and GitHub.

Your capabilities:
&lt;span&gt;-&lt;/span&gt; Search and read files in the repository
&lt;span&gt;-&lt;/span&gt; Search GitHub issues and pull requests
&lt;span&gt;-&lt;/span&gt; Fetch web documentation

You cannot:
&lt;span&gt;-&lt;/span&gt; Edit or create files
&lt;span&gt;-&lt;/span&gt; Run terminal commands
&lt;span&gt;-&lt;/span&gt; Make commits

When researching, provide citations and links to sources.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can ask: &lt;em&gt;“Use a subagent to find all issues assigned to me about authentication and summarize them”&lt;/em&gt; — and the subagent will be limited to read-only operations.&lt;/p&gt;
&lt;p&gt;If you’ve used Claude Code’s subagent system, you’ll recognize this pattern—it’s similar to how Claude Code handles skills and subagents with tool restrictions.&lt;/p&gt;
&lt;h3&gt;Control Subagent Availability&lt;/h3&gt;
&lt;p&gt;Use the &lt;code&gt;infer&lt;/code&gt; attribute to control whether an agent can be used as a subagent:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dangerous&lt;span&gt;-&lt;/span&gt;deployer
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Handles production deployments
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;execute&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;edit&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;read&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;infer&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;  &lt;span&gt;# This agent cannot be auto-invoked as a subagent&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Skills Are Now a First-Class Feature&lt;/h2&gt;
&lt;p&gt;Skills are now &lt;strong&gt;enabled by default&lt;/strong&gt; for all users. They’re folders containing instructions and resources that Copilot loads on-demand when relevant to your task.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related PRs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286237&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #286237 - Custom agent improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286238&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #286238 - Skill lookup enhancements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode-copilot-chat/pull/3082&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #3082 - Implement agent using CustomAgentProvider API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Creating Your First Skill&lt;/h3&gt;
&lt;p&gt;Create a directory structure:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{ name: “.github”, children: [&lt;br /&gt;
{ name: “skills”, children: [&lt;br /&gt;
{ name: “webapp-testing”, children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt;”, comment: “// Instructions” },&lt;br /&gt;
{ name: “test-template.js”, comment: “// Example template” }&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;SKILL.md&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; webapp&lt;span&gt;-&lt;/span&gt;testing
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Guide for testing web applications using Playwright.
             Use this when asked to create or run browser&lt;span&gt;-&lt;/span&gt;based tests.&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Web Application Testing with Playwright&lt;/span&gt;

When creating tests for this project, follow these patterns:

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Test Structure&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Use &lt;span&gt;`describe`&lt;/span&gt; blocks for feature groupings
&lt;span&gt;-&lt;/span&gt; Use &lt;span&gt;`test`&lt;/span&gt; for individual test cases
&lt;span&gt;-&lt;/span&gt; Always include setup and teardown

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Assertions&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Prefer &lt;span&gt;`toBeVisible()`&lt;/span&gt; over &lt;span&gt;`toHaveCount(1)`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Use &lt;span&gt;`waitFor`&lt;/span&gt; for async operations
&lt;span&gt;-&lt;/span&gt; Include accessibility checks

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Example Template&lt;/span&gt;
Reference the &lt;span&gt;[&lt;span&gt;test template&lt;/span&gt;](&lt;span&gt;./test-template.js&lt;/span&gt;)&lt;/span&gt; for the standard structure.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Naming Convention&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Test files: &lt;span&gt;`*.spec.ts`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Test descriptions: &quot;should [expected behavior] when [condition]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you ask &lt;em&gt;“Write Playwright tests for the login form”&lt;/em&gt;, Copilot automatically loads this skill and follows your project’s testing conventions.&lt;/p&gt;
&lt;h3&gt;Loading Skills from Custom Locations&lt;/h3&gt;
&lt;p&gt;For teams sharing skills across repos, use the new setting:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;chat.agentSkillsLocations&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;&quot;.github/skills&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;~/shared-skills&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;/team/copilot-skills&quot;&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Extension-Contributed Skills&lt;/h3&gt;
&lt;p&gt;Extensions can now contribute skills via their &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;contributes&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;copilotSkills&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;docker-compose&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Helps create and debug Docker Compose configurations&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;path&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;./skills/docker-compose&quot;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or dynamically via the new API:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;vscode&lt;span&gt;.&lt;/span&gt;chat&lt;span&gt;.&lt;/span&gt;&lt;span&gt;registerSkill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;dynamic-skill&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;A skill registered at runtime&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;getInstructions&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Return context-aware instructions&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;generateInstructionsFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;context&lt;span&gt;.&lt;/span&gt;workspace&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Instruction Files Work Everywhere&lt;/h2&gt;
&lt;p&gt;Instruction files now apply to &lt;strong&gt;non-coding tasks&lt;/strong&gt; like code exploration, architecture explanation, and documentation. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287152&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287152&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; Your &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; was ignored when you asked &lt;em&gt;“Explain how authentication works in this codebase”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; Those instructions are now read for all codebase-related work.&lt;/p&gt;
&lt;p&gt;This aligns with the progressive disclosure approach where context is loaded on-demand rather than crammed into a single file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example &lt;code&gt;copilot-instructions.md&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Project Context&lt;/span&gt;

This is a microservices architecture with:
&lt;span&gt;-&lt;/span&gt; API Gateway (Node.js/Express)
&lt;span&gt;-&lt;/span&gt; Auth Service (Go)
&lt;span&gt;-&lt;/span&gt; User Service (Python/FastAPI)
&lt;span&gt;-&lt;/span&gt; Shared message queue (RabbitMQ)

When explaining code:
&lt;span&gt;-&lt;/span&gt; Always mention which service a file belongs to
&lt;span&gt;-&lt;/span&gt; Reference the architecture diagram at docs/architecture.md
&lt;span&gt;-&lt;/span&gt; Note any cross-service dependencies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;em&gt;“How does user registration work?”&lt;/em&gt; will include this context automatically.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Claude Code Gets Extended Thinking&lt;/h2&gt;
&lt;p&gt;The Claude Code integration now supports &lt;strong&gt;extended thinking&lt;/strong&gt;, showing Claude’s chain-of-thought reasoning in a collapsible section. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287658&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287658&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href=&quot;https://github.com/microsoft/vscode/issues/266962&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #266962 - Claude agent support&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287933&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287933 - Model picker support&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What It Looks Like&lt;/h3&gt;
&lt;p&gt;When you ask Claude to solve a complex problem, you’ll see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;▼ Thinking...
  Let me analyze the codebase structure first. I see there are
  three main modules: auth, api, and database. The user is asking
  about the authentication flow, so I should trace the request
  from the API gateway through to the auth service...

  The JWT validation happens in middleware/auth.ts, but the token
  generation is in services/auth/token.go. I need to explain how
  these connect via the shared Redis cache...

Here&apos;s how authentication works in your codebase:
[Final response]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configuration&lt;/h3&gt;
&lt;p&gt;Enable/disable thinking display in settings:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;github.copilot.chat.claude.showThinking&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Model Picker&lt;/h3&gt;
&lt;p&gt;You can now select which Claude model to use:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the Chat view&lt;/li&gt;
&lt;li&gt;Click the model selector dropdown&lt;/li&gt;
&lt;li&gt;Choose from available Claude models (Sonnet, Opus, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Different models offer different speed/capability tradeoffs—use faster models for simple tasks, more capable models for complex reasoning.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Terminal Gets Major Upgrades&lt;/h2&gt;
&lt;p&gt;The integrated terminal received significant keyboard handling improvements this release, with two new protocol implementations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related PRs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/pull/286897&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #286897 - xterm.js 6.1.0 with kitty keyboard and win32-input-mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286809&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #286809 - Kitty keyboard protocol support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286896&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Issue #286896 - Win32 input mode support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/xtermjs/xterm.js/pull/5600&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xterm.js PR #5600 - Implement kitty keyboard protocol&lt;/a&gt; (upstream)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Kitty Keyboard Protocol (CSI u)&lt;/h3&gt;
&lt;p&gt;VS Code’s terminal now supports the &lt;a href=&quot;https://sw.kovidgoyal.net/kitty/keyboard-protocol/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;kitty keyboard protocol&lt;/a&gt;, enabling more sophisticated keyboard input handling. This unlocks previously unavailable key combinations and provides better support for terminal applications that use this modern standard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This feature is &lt;strong&gt;disabled by default&lt;/strong&gt; as it’s experimental. Enable it in settings:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;terminal.integrated.enableKittyKeyboardProtocol&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The protocol improves handling of modifiers, key events, repeat detection, and escape sequences—particularly useful if you use tools like fish shell, neovim, or other terminal applications that support CSI u.&lt;/p&gt;
&lt;h3&gt;Win32 Input Mode&lt;/h3&gt;
&lt;p&gt;For Windows users, the terminal now supports win32-input-mode, improving keyboard handling compatibility with Windows console applications. VT sequences alone can’t send everything that Windows console programs expect (encoded as win32 INPUT_RECORDs), so this mode bridges that gap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Also disabled by default.&lt;/strong&gt; Enable with:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;terminal.integrated.enableWin32InputMode&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Terminal Command Output Streams Inline&lt;/h3&gt;
&lt;p&gt;When using Copilot in agent mode, terminal command output now streams inline inside the Chat view instead of requiring you to switch to the terminal panel. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/257468&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#257468&lt;/a&gt; The output auto-expands on command execution and collapses on success &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287664&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287664&lt;/a&gt;—keeping you focused on the conversation flow.&lt;/p&gt;
&lt;h3&gt;Terminal Timeout Parameter&lt;/h3&gt;
&lt;p&gt;The terminal tool now supports a timeout parameter to control how long commands run before timing out. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/286598&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286598&lt;/a&gt; This prevents unnecessary polling and gives you more control over long-running operations.&lt;/p&gt;
&lt;h3&gt;Terminal Command Sandboxing&lt;/h3&gt;
&lt;p&gt;Terminal command sandboxing is now available for &lt;strong&gt;macOS and Linux&lt;/strong&gt; &lt;a href=&quot;https://github.com/microsoft/vscode/issues/277286&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#277286&lt;/a&gt;, adding an extra layer of security when running commands through the terminal tool.&lt;/p&gt;
&lt;h3&gt;Syntax Highlighting in Confirmation Dialogs&lt;/h3&gt;
&lt;p&gt;The terminal tool now presents Python, Node.js, and Ruby commands with syntax highlighting in the confirmation dialog &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287772&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287772&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287773&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287773&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288360&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288360&lt;/a&gt;—making it easier to review commands before execution.&lt;/p&gt;
&lt;h3&gt;Expanded Auto-Approved Commands&lt;/h3&gt;
&lt;p&gt;More commands are now automatically approved for execution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dir&lt;/code&gt; in PowerShell &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288431&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288431&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sed -i&lt;/code&gt; when editing files within the workspace &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288318&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288318&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;od&lt;/code&gt;, &lt;code&gt;xxd&lt;/code&gt;, and safe &lt;code&gt;docker&lt;/code&gt; commands &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287652&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287652&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SGR 221/222 Escape Sequences&lt;/h3&gt;
&lt;p&gt;The terminal now supports SGR 221 and 222 escape sequences &lt;a href=&quot;https://github.com/microsoft/vscode/issues/286810&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286810&lt;/a&gt;, allowing independent control of bold and faint text attributes for more granular formatting.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;MCP Gets More Powerful&lt;/h2&gt;
&lt;p&gt;Model Context Protocol continues to evolve with significant new capabilities.&lt;/p&gt;
&lt;h3&gt;Dynamic Context Updates&lt;/h3&gt;
&lt;p&gt;MCP apps now support model context update methods, enabling servers to update the context model dynamically. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/289473&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#289473&lt;/a&gt; This means MCP servers can push new context to your chat sessions without requiring a refresh.&lt;/p&gt;
&lt;h3&gt;Custom Package Registries&lt;/h3&gt;
&lt;p&gt;Added support for &lt;code&gt;registryBaseUrl&lt;/code&gt; in MCP packages &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287549&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287549&lt;/a&gt;, allowing teams to use private package registries for their MCP servers.&lt;/p&gt;
&lt;h3&gt;Built-in MCP Apps Support&lt;/h3&gt;
&lt;p&gt;Built-in support for MCP Apps enables servers to provide custom UI for tool invocation. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/260218&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#260218&lt;/a&gt; This opens the door for richer, more interactive MCP experiences beyond simple text-based tools.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Quality of Life Improvements&lt;/h2&gt;
&lt;h3&gt;Codex Agent in Dropdown&lt;/h3&gt;
&lt;p&gt;The OpenAI Codex agent now appears directly in the agents dropdown &lt;a href=&quot;https://github.com/microsoft/vscode/issues/289040&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#289040&lt;/a&gt; for quick access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Agents ▼
├── Local Agent
├── Background Agent
├── Cloud Agent
└── Codex Agent       ← New!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;New MCP Server Command&lt;/h3&gt;
&lt;p&gt;A new &lt;code&gt;workbench.mcp.startServer&lt;/code&gt; command &lt;a href=&quot;https://github.com/microsoft/vscode/issues/283959&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283959&lt;/a&gt; lets you programmatically start specific or all MCP servers to discover their tools. This is useful for automation scenarios where you need to ensure servers are running before invoking their tools.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;/clear&lt;/code&gt; Command Archives Sessions&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;/clear&lt;/code&gt; command now archives the current session and starts a new one automatically &lt;a href=&quot;https://github.com/microsoft/vscode/issues/285854&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#285854&lt;/a&gt;—no more losing your chat history when you want a fresh start.&lt;/p&gt;
&lt;h3&gt;New Local Chat Command&lt;/h3&gt;
&lt;p&gt;A new “New Local Chat” command &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288467&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288467&lt;/a&gt; lets you start a local chat session quickly.&lt;/p&gt;
&lt;h3&gt;Chat Session Imports&lt;/h3&gt;
&lt;p&gt;You can now &lt;strong&gt;import&lt;/strong&gt; a chat session directly into the Chat view &lt;a href=&quot;https://github.com/microsoft/vscode/issues/283954&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283954&lt;/a&gt;, instead of only being able to open it in a new editor tab. This makes it easier to continue conversations from exported sessions.&lt;/p&gt;
&lt;h3&gt;Chat Session Exports with MCP Info&lt;/h3&gt;
&lt;p&gt;Exported sessions now include MCP server configuration &lt;a href=&quot;https://github.com/microsoft/vscode/issues/283945&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283945&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;session&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;messages&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;...&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;github&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;https://mcp.github.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;tools&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;search_issues&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;get_pr&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;list_repos&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes sessions reproducible—share them with teammates and they can recreate your exact setup.&lt;/p&gt;
&lt;h3&gt;Multi-Select in Sessions View&lt;/h3&gt;
&lt;p&gt;Select multiple chat sessions with &lt;code&gt;Cmd/Ctrl+Click&lt;/code&gt; &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288448&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288448&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Archive all selected&lt;/li&gt;
&lt;li&gt;Mark all as read&lt;/li&gt;
&lt;li&gt;Batch delete&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additional session management improvements include “Mark All Read”, “Archive All”, and “Unarchive All” actions in context menus &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288147&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288147&lt;/a&gt;, and increased locally persisted chat sessions &lt;a href=&quot;https://github.com/microsoft/vscode/issues/283123&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283123&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Resizable Sessions Sidebar&lt;/h3&gt;
&lt;p&gt;You can now resize the sessions sidebar in the Chat view by dragging the separator &lt;a href=&quot;https://github.com/microsoft/vscode/issues/281258&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#281258&lt;/a&gt;, similar to how terminal tabs work.&lt;/p&gt;
&lt;h3&gt;Extension Context Tooltips&lt;/h3&gt;
&lt;p&gt;Hover over extension-contributed context items to see additional information about what they provide. &lt;a href=&quot;https://github.com/microsoft/vscode/issues/280658&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#280658&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Accessible View Streams Thinking Content&lt;/h3&gt;
&lt;p&gt;The Accessible View now dynamically streams thinking content &lt;a href=&quot;https://github.com/microsoft/vscode/issues/289223&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#289223&lt;/a&gt;, making Claude’s chain-of-thought reasoning accessible to screen reader users in real-time.&lt;/p&gt;
&lt;h3&gt;Multi-Model Selection in Language Models Editor&lt;/h3&gt;
&lt;p&gt;Select multiple models in the Language Models editor and toggle their visibility at once &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287511&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287511&lt;/a&gt;. Enterprise and Business users also get access to the Manage Models action &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287814&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287814&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Editor &amp;amp; Language Improvements&lt;/h2&gt;
&lt;h3&gt;Improved Shebang Detection&lt;/h3&gt;
&lt;p&gt;VS Code now recognizes Deno, Bun, and other modern JavaScript runtimes &lt;a href=&quot;https://github.com/microsoft/vscode/issues/287819&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287819&lt;/a&gt; for better language detection when opening scripts.&lt;/p&gt;
&lt;h3&gt;Better Ghost Text Visibility&lt;/h3&gt;
&lt;p&gt;Improved visibility of ghost text in next edit suggestions &lt;a href=&quot;https://github.com/microsoft/vscode/issues/284517&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#284517&lt;/a&gt;, making it easier to distinguish AI suggestions from regular text.&lt;/p&gt;
&lt;h3&gt;Double-Click Selects Block Content&lt;/h3&gt;
&lt;p&gt;Double-clicking immediately after a curly brace or bracket now selects the content inside it &lt;a href=&quot;https://github.com/microsoft/vscode/issues/9123&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#9123&lt;/a&gt;—a small but impactful change for manipulating code blocks.&lt;/p&gt;
&lt;h3&gt;Match File Path Case Toggle&lt;/h3&gt;
&lt;p&gt;A new “Match File Path Case” toggle in the Search view’s “files to include” input &lt;a href=&quot;https://github.com/microsoft/vscode/issues/10633&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#10633&lt;/a&gt; lets you control whether file paths and glob patterns match case-sensitively.&lt;/p&gt;
&lt;h3&gt;Bracket Match Foreground Color&lt;/h3&gt;
&lt;p&gt;New &lt;code&gt;editorBracketMatch.foreground&lt;/code&gt; theme color &lt;a href=&quot;https://github.com/microsoft/vscode/issues/85775&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#85775&lt;/a&gt; enables customization of matched bracket text color.&lt;/p&gt;
&lt;h3&gt;Parallel Build Tasks&lt;/h3&gt;
&lt;p&gt;Dependent build tasks can now run in parallel &lt;a href=&quot;https://github.com/microsoft/vscode/issues/288439&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288439&lt;/a&gt;, improving build performance for projects with multiple independent compilation steps.&lt;/p&gt;
&lt;h3&gt;Git Delete File Command&lt;/h3&gt;
&lt;p&gt;A new “Git: Delete File” command &lt;a href=&quot;https://github.com/microsoft/vscode/issues/111767&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#111767&lt;/a&gt; performs &lt;code&gt;git rm&lt;/code&gt; on the current file directly from the command palette.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Try It Today&lt;/h2&gt;
&lt;p&gt;Here’s a quick workflow to test the new features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a custom agent&lt;/strong&gt; at &lt;code&gt;.github/agents/researcher.md&lt;/code&gt; with restricted tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a skill&lt;/strong&gt; at &lt;code&gt;.github/skills/my-skill/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask Copilot:&lt;/strong&gt; &lt;em&gt;“What skills and subagents do you have available?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test parallel execution:&lt;/strong&gt; &lt;em&gt;“Use subagents to research three different topics simultaneously”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable Claude thinking&lt;/strong&gt; and ask a complex architecture question&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Looking Ahead&lt;/h2&gt;
&lt;p&gt;These updates signal a clear direction: Copilot is evolving from a single-agent assistant into a &lt;strong&gt;coordinated multi-agent system&lt;/strong&gt;. The combination of parallel subagents, constrained tool access, and shareable skills creates a foundation for sophisticated automated workflows.&lt;/p&gt;
&lt;p&gt;If you’re interested in building your own agent systems, check out Building Your Own Coding Agent from Scratch for a hands-on guide to the underlying patterns.&lt;/p&gt;
&lt;p&gt;Key settings to know:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;chat.useAgentSkills&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;chat.agentSkillsLocations&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;.github/skills&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;chat.customAgentInSubagent.enabled&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;github.copilot.chat.claude.showThinking&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;terminal.integrated.enableKittyKeyboardProtocol&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;terminal.integrated.enableWin32InputMode&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ecosystem is about to get a lot more interesting.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related Pull Requests &amp;amp; Issues&lt;/h2&gt;
&lt;p&gt;For those who want to dig into the implementation details:&lt;/p&gt;
&lt;h3&gt;Agent &amp;amp; Skills&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/274630&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#274630 - Parallel subagent execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/280704&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#280704 - Agents define allowed subagents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288480&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288480 - Skills enabled by default&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288483&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288483 - Extension-contributed skills via manifest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288486&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288486 - Dynamic skills API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/282738&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#282738 - Skills from custom locations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Claude Integration&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287658&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287658 - Extended thinking support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287933&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287933 - Model picker for Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/266962&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#266962 - Claude agent support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Terminal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286809&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286809 - Kitty keyboard protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286896&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286896 - Win32 input mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286810&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286810 - SGR 221/222 escape sequences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/257468&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#257468 - Terminal output streams inline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287664&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287664 - Auto-expand/collapse terminal output&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/277286&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#277286 - Terminal sandboxing for macOS/Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286598&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286598 - Terminal timeout parameter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287772&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287772 - Python syntax highlighting in confirmations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/xtermjs/xterm.js/pull/5600&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xterm.js #5600 - Kitty keyboard protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MCP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/289473&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#289473 - Dynamic context updates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287549&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287549 - Custom package registries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/260218&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#260218 - Built-in MCP Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/283959&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283959 - startServer command&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/283945&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283945 - MCP info in session exports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Chat &amp;amp; Sessions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/285854&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#285854 - /clear archives sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288467&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288467 - New Local Chat command&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/283954&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283954 - Import chat sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288448&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288448 - Multi-select in sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/281258&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#281258 - Resizable sessions sidebar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/283123&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#283123 - Increased persisted sessions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/289223&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#289223 - Accessible View streams thinking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Editor &amp;amp; Other&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/287819&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#287819 - Improved shebang detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/284517&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#284517 - Ghost text visibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/9123&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#9123 - Double-click selects block content&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/10633&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#10633 - Match file path case toggle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/288439&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#288439 - Parallel build tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/111767&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#111767 - Git Delete File command&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Iteration Plan&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/vscode/issues/286040&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#286040 - January 2026 Iteration Plan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;These features are rolling out in VS Code Insiders (1.109) now, with stable release expected in early February. Note that some features like kitty keyboard protocol and win32-input-mode are disabled by default and require manual opt-in.&lt;/em&gt;&lt;/p&gt;

          &lt;/figure&gt;</content:encoded><category>ai</category><category>tooling</category><category>vscode</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Presentation Mode: Turn Your Blog Posts into Slides</title><link>https://alexop.dev/posts/presentation-mode-demo/</link><guid isPermaLink="true">https://alexop.dev/posts/presentation-mode-demo/</guid><description>A complete demo of presentation mode with v-click animations and drawing annotations. Press P to see keyboard navigation, incremental reveals, and press D to draw on slides!</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            
  Press **P** on your keyboard or click the floating button in the bottom-right corner to enter presentation mode. Use arrow keys to navigate between slides, and press **D** to draw annotations directly on slides!

&lt;h1&gt;Presentation Mode&lt;/h1&gt;
&lt;p&gt;Turn your blog posts into beautiful slides with incremental reveals&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Presentation Mode?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; You write a great blog post, then need to present it at a meetup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Old Solution:&lt;/strong&gt; Recreate everything in PowerPoint or Google Slides.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New Solution:&lt;/strong&gt; Just add &lt;code&gt;presentation: true&lt;/code&gt; to your frontmatter!&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;My Awesome Post&quot;&lt;/span&gt;
&lt;span&gt;presentation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Keyboard Shortcuts&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Toggle presentation mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;→&lt;/strong&gt; or &lt;strong&gt;Space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next click step, then next slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;←&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Previous click step, then previous slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1-9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jump to slide N (resets clicks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Home&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;First slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;End&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Last slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Toggle drawing mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;G&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Toggle grid overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Escape&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exit drawing → grid → presentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h1&gt;Drawing Annotations&lt;/h1&gt;
&lt;p&gt;Draw directly on slides with Excalidraw!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Try Drawing Mode&lt;/h2&gt;
&lt;p&gt;Press &lt;strong&gt;D&lt;/strong&gt; to toggle drawing mode. A toolbar will appear with these tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;↖ Selection&lt;/strong&gt; - Select and move drawings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;✏️ Freedraw&lt;/strong&gt; - Freehand drawing (press P)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;→ Arrow&lt;/strong&gt; - Draw arrows (press A)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;□ Rectangle&lt;/strong&gt; - Draw rectangles (press R)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;○ Ellipse&lt;/strong&gt; - Draw circles (press O)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T Text&lt;/strong&gt; - Add text annotations (press T)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;🧹 Eraser&lt;/strong&gt; - Erase drawings (press E)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h2&gt;Drawing Features&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Colors:&lt;/strong&gt; 6 preset colors - Red, Blue, Green, Yellow, White, Black&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stroke Widths:&lt;/strong&gt; 3 sizes - Thin (1px), Medium (2px), Thick (4px)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Persistence:&lt;/strong&gt; Drawings stay when you navigate between slides!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shortcuts:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt; - Clear current slide&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Shift+C&lt;/code&gt; - Clear all slides&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Escape&lt;/code&gt; - Exit drawing mode&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Use Cases&lt;/h2&gt;
&lt;p&gt;Why draw on slides during presentations?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Circle important code sections&lt;/li&gt;
&lt;li&gt;Draw arrows connecting concepts&lt;/li&gt;
&lt;li&gt;Add quick annotations for Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;Highlight key points in diagrams&lt;/li&gt;
&lt;li&gt;Sketch ideas during discussions&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h1&gt;V-Click Animations&lt;/h1&gt;
&lt;p&gt;Reveal content step-by-step with Slidev-style click animations&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sequential Reveals&lt;/h2&gt;
&lt;p&gt;Press &lt;strong&gt;→&lt;/strong&gt; to reveal each point:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; First, we define the problem clearly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Then, we explore possible solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Finally, we implement and test!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Building a List&lt;/h2&gt;
&lt;p&gt;Benefits of incremental reveals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeps audience focused on the current point&lt;/li&gt;
&lt;li&gt;Creates natural pacing for your talk&lt;/li&gt;
&lt;li&gt;Prevents information overload&lt;/li&gt;
&lt;li&gt;Makes complex topics digestible&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h2&gt;V-Click Syntax&lt;/h2&gt;
&lt;p&gt;In MDX files, use components to control reveals:&lt;/p&gt;
&lt;pre class=&quot;language-mdx&quot;&gt;&lt;code class=&quot;language-mdx&quot;&gt;
Content appears on click

&amp;lt;VClicks&amp;gt;
- Each list item
- Gets its own click
&amp;lt;/VClicks&amp;gt;

Explicit order (appears third)

This disappears on click
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Custom Components&lt;/h1&gt;
&lt;p&gt;Interactive React components work in slides!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Context Window Visualizer&lt;/h2&gt;
&lt;p&gt;This is a custom React component rendered inside a slide:&lt;/p&gt;
&lt;p&gt;Try typing messages to see the context fill up!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Code Blocks Work Perfectly&lt;/h2&gt;
&lt;p&gt;Here’s a Vue composable example with full syntax highlighting:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initial &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initial&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; double &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;*&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; double&lt;span&gt;,&lt;/span&gt; increment &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Magic Move: Code Evolution&lt;/h2&gt;
&lt;p&gt;Watch code transform with smooth animations (press &lt;strong&gt;→&lt;/strong&gt; to advance):&lt;/p&gt;
&lt;p&gt;&amp;lt;MagicMove&lt;br /&gt;
lang=“typescript”&lt;br /&gt;
steps={[&lt;br /&gt;
`const x = 1`,&lt;br /&gt;
`const x = 1&lt;br /&gt;
const y = 2`,&lt;br /&gt;
`const x = 1&lt;br /&gt;
const y = 2&lt;/p&gt;
&lt;p&gt;function add(a: number, b: number) {&lt;br /&gt;
return a + b&lt;br /&gt;
}&lt;code&gt;,     &lt;/code&gt;const x = 1&lt;br /&gt;
const y = 2&lt;/p&gt;
&lt;p&gt;function add(a: number, b: number) {&lt;br /&gt;
return a + b&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;const sum = add(x, y)&lt;br /&gt;
console.log(sum) // 3`&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Each arrow press animates the code to its next state!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mermaid Diagrams&lt;/h2&gt;
&lt;p&gt;Flowcharts render beautifully in slides:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    A&lt;span&gt;[Blog Post]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{presentation: true?}&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; C&lt;span&gt;[Show Toggle Button]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; D&lt;span&gt;[Normal Post]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Press P]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[Fullscreen Slides!]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Animated Diagrams&lt;/h2&gt;
&lt;p&gt;Interactive diagrams with self-contained animations:&lt;/p&gt;
&lt;p&gt;&amp;lt;SubagentDiagram&lt;br /&gt;
task=“Find auth files”&lt;br /&gt;
files={[“Auth.tsx”, “auth.ts”, “authService.ts”]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Start&lt;/strong&gt; to see the Explore subagent flow animation!&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Slide Layouts&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Available Layouts&lt;/h2&gt;
&lt;p&gt;This feature supports 9 different layout types:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard centered prose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Large title, full-bleed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;center&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fully centered content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;two-cols&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Two-column split&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image-left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Image 40%, content 60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image-right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Content 60%, image 40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full-bleed background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quote&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prominent blockquote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;section&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Section divider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iframe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Embedded website/demo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Iframe Layout&lt;/h2&gt;
&lt;p&gt;Embed live demos, CodePen, StackBlitz, or videos directly in slides:&lt;/p&gt;
&lt;p&gt;This is a live StackBlitz embed - fully interactive!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Layout Syntax&lt;/h2&gt;
&lt;p&gt;In &lt;strong&gt;.md files&lt;/strong&gt;, use HTML comments. In &lt;strong&gt;.mdx files&lt;/strong&gt;, use components:&lt;/p&gt;
&lt;pre class=&quot;language-mdx&quot;&gt;&lt;code class=&quot;language-mdx&quot;&gt;{/* MDX format */}

# My Title

---

{/* .md format */}
&amp;lt;!--slide:{&quot;layout&quot;:&quot;cover&quot;}--&amp;gt;
# My Title
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
Both file formats support layouts and v-clicks! For `.md` files use HTML comments, for `.mdx` files use the `Slide`, `VClick`, and `VClicks` components.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Layout Properties&lt;/h2&gt;
&lt;p&gt;All layouts accept these properties:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Layout name (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to image in /public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;backgroundSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS value (default: cover)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom CSS class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL for iframe layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accessibility title for iframe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Layout Animations&lt;/h2&gt;
&lt;p&gt;Each layout type has its own animation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layout&lt;/th&gt;
&lt;th&gt;Animation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;center&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;two-cols&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fade&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zoom&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quote&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fade&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;section&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fade&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iframe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fade&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h1&gt;Technical Details&lt;/h1&gt;
&lt;hr /&gt;
&lt;h2&gt;Architecture Overview&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TB
    &lt;span&gt;subgraph&lt;/span&gt; Frontend
        A&lt;span&gt;[PresentationToggle]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[PresentationMode]&lt;/span&gt;
        B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[PresentationSlide]&lt;/span&gt;
        B &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[PresentationControls]&lt;/span&gt;
        B &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[PresentationProgress]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Layouts
        C &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[SlideLayoutDefault]&lt;/span&gt;
        C &lt;span&gt;--&amp;gt;&lt;/span&gt; G&lt;span&gt;[SlideLayoutCover]&lt;/span&gt;
        C &lt;span&gt;--&amp;gt;&lt;/span&gt; H&lt;span&gt;[SlideLayoutTwoCols]&lt;/span&gt;
        C &lt;span&gt;--&amp;gt;&lt;/span&gt; I&lt;span&gt;[... more layouts]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Content
        J&lt;span&gt;[Blog Post]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; K&lt;span&gt;[Compiled HTML]&lt;/span&gt;
        K &lt;span&gt;--&amp;gt;&lt;/span&gt; L&lt;span&gt;[Parse slide comments]&lt;/span&gt;
        L &lt;span&gt;--&amp;gt;&lt;/span&gt; M&lt;span&gt;[SlideData Array]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    M &lt;span&gt;--&amp;gt;&lt;/span&gt; C
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Tips for Great Slides&lt;/h2&gt;
&lt;aside&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep slides focused&lt;/strong&gt; - One idea per slide&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use headings&lt;/strong&gt; - They become slide titles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leverage layouts&lt;/strong&gt; - Pick the right layout for content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use v-clicks&lt;/strong&gt; - Reveal complex info step-by-step&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test navigation&lt;/strong&gt; - Make sure flow makes sense&lt;/li&gt;
&lt;/ol&gt;
&lt;/aside&gt;
&lt;h3&gt;Content Length&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Short slides work best&lt;/li&gt;
&lt;li&gt;If content overflows, it scrolls within the slide&lt;/li&gt;
&lt;li&gt;But try to keep each slide digestible&lt;/li&gt;
&lt;li&gt;Use v-clicks to break up longer content&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h2&gt;Accessibility Features&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Focus trap&lt;/strong&gt; - Tab stays within the modal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ARIA live region&lt;/strong&gt; - Announces “Slide X of Y, Step N of M”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escape to exit&lt;/strong&gt; - Standard modal behavior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard navigation&lt;/strong&gt; - No mouse required&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme aware&lt;/strong&gt; - Respects light/dark mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced motion&lt;/strong&gt; - V-click respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;
&lt;h2&gt;Speaker Notes &amp;amp; Presenter View&lt;/h2&gt;
&lt;p&gt;Press &lt;strong&gt;N&lt;/strong&gt; to open the presenter view popup!&lt;/p&gt;
&lt;p&gt;The presenter view shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current and next slide previews&lt;/li&gt;
&lt;li&gt;Speaker notes with click-step highlighting&lt;/li&gt;
&lt;li&gt;A timer you can start/pause/reset&lt;/li&gt;
&lt;li&gt;Navigation controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;{“This is a speaker note! It’s only visible in the presenter view.\n[click] Now explain that the presenter view opens in a popup window.\n[click] Walk through each feature: previews, notes, timer, controls.\n[click] Mention keyboard shortcuts: T for timer, R to reset.”}&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Notes with Click Markers&lt;/h2&gt;
&lt;p&gt;Notes can reveal progressively with your click steps:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Define the problem&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Explore solutions&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Implement and ship!&lt;/p&gt;
&lt;p&gt;{“Always visible intro text for this slide.\n[click] Talk about why defining the problem clearly matters.\n[click] Mention 2-3 solution approaches you considered.\n[click] Emphasize shipping fast and iterating.”}&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Thank You!&lt;/h1&gt;
&lt;p&gt;Press &lt;strong&gt;Escape&lt;/strong&gt; to exit or continue with arrow keys&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Quick Reference&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;File format trade-offs:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Custom Components&lt;/th&gt;
&lt;th&gt;Layouts&lt;/th&gt;
&lt;th&gt;V-Click&lt;/th&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.mdx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;HTML comments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Press &lt;strong&gt;Escape&lt;/strong&gt; to exit presentation mode!&lt;/p&gt;

          </content:encoded><category>demo</category><category>presentation</category><category>feature</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Stop Bloating Your CLAUDE.md: Progressive Disclosure for AI Coding Tools</title><link>https://alexop.dev/posts/stop-bloating-your-claude-md-progressive-disclosure-ai-coding-tools/</link><guid isPermaLink="true">https://alexop.dev/posts/stop-bloating-your-claude-md-progressive-disclosure-ai-coding-tools/</guid><description>AI coding tools are stateless—every session starts fresh. The solution isn&apos;t cramming everything into CLAUDE.md, but building a layered context system where learnings accumulate in docs and specialized agents load on-demand.</description><pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Yesterday I spent an hour debugging a Nuxt Content gotcha with Claude. We figured it out together—you need to use &lt;code&gt;stem&lt;/code&gt; instead of &lt;code&gt;slug&lt;/code&gt; in page collection queries. Today? Claude made the same mistake. Yesterday’s session was gone.&lt;/p&gt;

The examples in this post come from my [Second Brain](https://second-brain-nuxt.vercel.app/)—a personal wiki built with Nuxt and Nuxt Content that uses Zettelkasten-style wiki-links for knowledge management. You can see the actual [CLAUDE.md file](https://github.com/alexanderop/second-brain-nuxt/blob/main/CLAUDE.md) on GitHub.

&lt;p&gt;That’s the constraint. &lt;strong&gt;Your context is just an array of tokens&lt;/strong&gt;—a sliding window that forgets everything the moment the conversation ends.[^1]&lt;/p&gt;

The percentages shown in these visualizations are illustrative examples—not real measurements. Actual system prompt overhead varies by tool version and configuration. The key insight is the relative proportions, not the exact numbers.

&lt;p&gt;There’s no hidden memory. No database of past conversations. Just this array, rebuilt fresh every session.&lt;/p&gt;
&lt;p&gt;Dex Horthy calls this “context engineering”—since LLMs are stateless, the only way to improve output is optimizing input.[^6] The array is all you have. Everything outside it doesn’t exist to the model.&lt;/p&gt;
&lt;p&gt;But that array has a size limit. Fill it with noise, and you’re working in what Dex calls the “dumb zone”—where performance degrades because irrelevant context competes for attention.&lt;/p&gt;
&lt;p&gt;Most developers respond to this by putting every lesson learned into their &lt;code&gt;CLAUDE.md&lt;/code&gt; file. I’ve seen files balloon to 2000 lines. Style guides, architectural decisions, war stories from that one bug that took three days to fix.&lt;/p&gt;
&lt;p&gt;This makes things worse.&lt;/p&gt;
&lt;h2&gt;Bloated &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; Makes Things Worse&lt;/h2&gt;
&lt;p&gt;When Claude makes a mistake, the instinct is to add a rule: “Never use &lt;code&gt;slug&lt;/code&gt; in page collection queries—use &lt;code&gt;stem&lt;/code&gt; instead.”&lt;/p&gt;
&lt;p&gt;Then another mistake, another rule. Then another.&lt;/p&gt;
&lt;p&gt;Before long, your &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; CLAUDE.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Project Overview&lt;/span&gt;
...50 lines...

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Code Style&lt;/span&gt;
...200 lines of formatting rules...

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Architecture Decisions&lt;/span&gt;
...150 lines of historical context...

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Gotchas&lt;/span&gt;
...300 lines of edge cases...

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Testing Conventions&lt;/span&gt;
...100 lines...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Half your context budget is gone before any work begins.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HumanLayer keeps their &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; under 60 lines.[^2] Frontier LLMs reliably follow 150-200 instructions—and Claude Code’s system prompt already uses about 50 of those.[^2]&lt;/p&gt;
&lt;p&gt;The math doesn’t work. You can’t stuff everything in one file.&lt;/p&gt;
&lt;h2&gt;Stop Writing Prose About Lint Rules&lt;/h2&gt;
&lt;p&gt;Why write two hundred lines about code style when one line handles it? I stopped putting anything a tool can enforce in &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;❌ &lt;strong&gt;Don’t write prose about style rules:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Code Style&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Use 2-space indentation
&lt;span&gt;-&lt;/span&gt; Prefer single quotes
&lt;span&gt;-&lt;/span&gt; Always add trailing commas
&lt;span&gt;-&lt;/span&gt; Maximum line length: 100 characters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Let ESLint handle it:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;extends&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;@nuxt/eslint-config&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rules are already there—you just don’t repeat them in prose:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// What @nuxt/eslint-config contains:&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;rules&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&apos;indent&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;quotes&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;single&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;comma-dangle&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;always-multiline&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&apos;max-len&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;error&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;code&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AI can run &lt;code&gt;pnpm lint:fix &amp;amp;&amp;amp; pnpm typecheck&lt;/code&gt; and know immediately if it violated a rule. No interpretation needed. No ambiguity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If a tool can enforce it, don’t write prose about it.&lt;/strong&gt; ESLint for style. TypeScript for types. Prettier for formatting. These rules are verifiable, not interpretable.&lt;/p&gt;
&lt;p&gt;Moss calls this &lt;em&gt;backpressure&lt;/em&gt;—automated feedback mechanisms that let agents self-correct.[^7] Without a linter, you waste your time typing messages like “you forgot to add the import” or “that should be a const, not let.” With backpressure, the agent runs the build, reads the error, and fixes itself. You remove yourself from trivial corrections and focus on higher-level decisions.&lt;/p&gt;
&lt;p&gt;My &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; now just says:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;Run &lt;span&gt;`pnpm lint:fix &amp;amp;&amp;amp; pnpm typecheck`&lt;/span&gt; after code changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line instead of two hundred. Or skip it entirely—use husky to run checks automatically on commit. This is especially useful for techniques like Ralph, where AI works autonomously through a queue of tasks.[^8]&lt;/p&gt;
&lt;h2&gt;The Gotchas ESLint Won’t Catch&lt;/h2&gt;
&lt;p&gt;ESLint won’t catch this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Nuxt Content v3 caches aggressively in &lt;code&gt;.data/&lt;/code&gt;. When you modify transformation logic in hooks, you must clear the cache to test changes.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or this:&lt;/p&gt;
&lt;p&gt;{/* &amp;gt; “Don’t mock &lt;code&gt;@nuxt/content/server&lt;/code&gt; internals in tests—it breaks when Nuxt Content updates. Extract pure logic to &lt;code&gt;server/utils/&lt;/code&gt; instead.” */}&lt;/p&gt;
&lt;p&gt;Or this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Wiki-links to data collections require path prefixes. Use &lt;code&gt;[[authors/john-doe]]&lt;/code&gt;, not &lt;code&gt;[[john-doe]]&lt;/code&gt;.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These are &lt;em&gt;gotchas&lt;/em&gt;—non-obvious behaviors that bite you once. The kind of thing you’d tell a new team member on their first day. They need documentation, but they don’t belong in &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The insight: &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; is for universal context. Gotchas are situational.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You don’t need the wiki-link prefix rule in every conversation—only when you’re writing content with author links. Loading it every time wastes tokens.&lt;/p&gt;
&lt;p&gt;So where do these gotchas go? And how do you capture them without breaking your flow?&lt;/p&gt;
&lt;h2&gt;My /learn Skill&lt;/h2&gt;
&lt;p&gt;My system: when I notice Claude struggling with something we’ve solved before, I run &lt;code&gt;/learn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a Claude Code skill I built (&lt;a href=&quot;https://alexop.dev//prompts/claude/claude-learn-command&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;see full prompt&lt;/a&gt;). It:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Analyzes the conversation for reusable, non-obvious insights&lt;/li&gt;
&lt;li&gt;Finds the right place in &lt;code&gt;/docs&lt;/code&gt; to save it (or proposes a new file)&lt;/li&gt;
&lt;li&gt;Asks for my approval before saving&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I end up with a growing knowledge base in my docs folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docs/
├── nuxt-content-gotchas.md    # 15 hard-won lessons
├── nuxt-component-gotchas.md  # Vue-specific pitfalls
├── testing-strategy.md        # When to use which test type
└── SYSTEM_KNOWLEDGE_MAP.md    # Architecture overview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; stays stable.&lt;/strong&gt; It just tells Claude where to look:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Further Reading&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;IMPORTANT:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Before starting any task, identify which docs below are relevant and read them first. Load the full context before making changes.

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`docs/nuxt-content-gotchas.md`&lt;/span&gt; - Nuxt Content v3 pitfalls
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`docs/testing-strategy.md`&lt;/span&gt; - Test layers and when to use each
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;IMPORTANT&lt;/strong&gt; instruction is critical—without it, Claude won’t automatically read these docs. With it, Claude identifies relevant docs before starting work: content queries trigger the gotchas doc, testing tasks trigger the testing strategy. Progressive disclosure—the right context at the right time.[^2]&lt;/p&gt;
&lt;p&gt;Another approach: build skills that load domain-specific gotchas automatically. A &lt;code&gt;nuxt-content&lt;/code&gt; skill that injects the gotchas doc whenever you’re working with content queries. In theory, this is cleaner—context loads without you thinking about it. In practice, I’ve found skills don’t always activate when expected. The trigger conditions can be fuzzy, and sometimes Claude just doesn’t invoke them. Vercel’s agent evals confirmed this: skills were never invoked in 56% of their test cases, producing zero improvement over baseline.[^9] The docs-based setup is more predictable: I know Claude will read what I point it to.&lt;/p&gt;
&lt;h2&gt;One Agent Per Domain&lt;/h2&gt;
&lt;p&gt;I take this further with custom agents. Each agent has its own documentation file that loads only when needed. If you’re new to how these customization layers work together, I wrote a detailed comparison of &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;, skills, and subagents.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.claude/agents/
├── nuxt-content-specialist.md   # Content queries, MDC, search
├── nuxt-ui-specialist.md        # Component styling, theming
├── vue-specialist.md            # Reactivity, composables
└── nuxt-specialist.md           # Routing, config, deployment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I’m debugging a content query, Claude loads the nuxt-content-specialist. When I’m styling a component, it loads nuxt-ui-specialist. The specialist agents know to fetch the latest documentation from official sources—they don’t rely on stale training data.&lt;/p&gt;
&lt;p&gt;This is why I don’t use MCPs like context7 for documentation. Agents can fetch llms.txt directly from official docs sites and find what they need. No tool definition bloat, no intermediate tokens—just a focused research task in its own context window. I wrote more about why I use custom research agents instead of MCPs.&lt;/p&gt;
&lt;p&gt;Skills work similarly—with &lt;code&gt;context:fork&lt;/code&gt;, they run in isolated contexts without polluting your main conversation. The agent has both the ability and motivation to read real documentation. No context7, no MCP overhead.&lt;/p&gt;
&lt;h2&gt;It Compounds&lt;/h2&gt;
&lt;p&gt;This system creates a feedback loop:&lt;/p&gt;
&lt;p&gt;Over time, my &lt;code&gt;/docs&lt;/code&gt; folder becomes a curated knowledge base of &lt;em&gt;exactly the things AI coding tools get wrong&lt;/em&gt; in my codebase. It’s like fine-tuning, but under my control.&lt;/p&gt;
&lt;p&gt;I got this idea from a pattern for self-improving skills where agents automatically analyze sessions and update themselves.[^5] I adapted it to use markdown documentation and a &lt;code&gt;/learn&lt;/code&gt; command instead—giving me explicit control over what gets captured and where it goes.&lt;/p&gt;
&lt;p&gt;An actual entry from my &lt;code&gt;nuxt-content-gotchas.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Page Collection Queries: Use `stem` Not `slug`&lt;/span&gt;

The &lt;span&gt;`slug`&lt;/span&gt; field doesn&apos;t exist in page-type collections.
Use &lt;span&gt;`stem`&lt;/span&gt; (file path without extension) instead:

// ❌ Fails: &quot;no such column: slug&quot;
queryCollection(&apos;content&apos;).select(&apos;slug&apos;, &apos;title&apos;).all()

// ✅ Works
queryCollection(&apos;content&apos;).select(&apos;stem&apos;, &apos;title&apos;).all()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude will never make this mistake again in my project. Not because I added it to &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;—but because when it’s working with content queries, it reads the gotchas doc first.&lt;/p&gt;
&lt;h2&gt;My 50-Line &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The structure:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; CLAUDE.md&lt;/span&gt;

Second Brain is a personal knowledge base using
Zettelkasten-style wiki-links.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Commands&lt;/span&gt;
pnpm dev          # Start dev server
pnpm lint:fix     # Auto-fix linting issues
pnpm typecheck    # Verify type safety

Run &lt;span&gt;`pnpm lint:fix &amp;amp;&amp;amp; pnpm typecheck`&lt;/span&gt; after code changes.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Stack&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Nuxt 4, @nuxt/content v3, @nuxt/ui v3

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Structure&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`app/`&lt;/span&gt; - Vue application
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`content/`&lt;/span&gt; - Markdown files
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`content.config.ts`&lt;/span&gt; - Collection schemas

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Further Reading&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;IMPORTANT:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Read relevant docs below before starting any task.

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`docs/nuxt-content-gotchas.md`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`docs/testing-strategy.md`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`docs/SYSTEM_KNOWLEDGE_MAP.md`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Universal context only. Everything else lives in docs, agents, or tooling.&lt;/p&gt;
&lt;h2&gt;Cross-Tool Compatibility&lt;/h2&gt;
&lt;p&gt;If you use multiple AI coding tools, you don’t need separate config files. VS Code Copilot and Cursor both support &lt;code&gt;agents.md&lt;/code&gt; for project-level instructions. You can symlink it to share the same configuration:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Create a symlink so all tools read the same file&lt;/span&gt;
&lt;span&gt;ln&lt;/span&gt; &lt;span&gt;-s&lt;/span&gt; CLAUDE.md agents.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your minimal, focused instructions work across Claude Code, Copilot, and Cursor. One source of truth, no drift between tools.&lt;/p&gt;
&lt;h2&gt;How This Played Out Last Week&lt;/h2&gt;
&lt;p&gt;Last week I was implementing semantic search. When Claude started working on content queries, it read &lt;code&gt;nuxt-content-gotchas.md&lt;/code&gt; first—as my &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; instructs. The stem/slug gotcha was already there.&lt;/p&gt;
&lt;p&gt;No mistake. No correction needed.&lt;/p&gt;
&lt;p&gt;But during the session, we discovered something new: &lt;code&gt;queryCollectionSearchSections&lt;/code&gt; returns IDs with a leading slash. Don’t add another slash when constructing URLs.&lt;/p&gt;
&lt;p&gt;I ran &lt;code&gt;/learn&lt;/code&gt;. Claude proposed:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Search Section IDs&lt;/span&gt;

Returns IDs with leading slash (&lt;span&gt;`/slug#section`&lt;/span&gt;).
Don&apos;t add another slash when constructing URLs.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Added. Next time I work on search, Claude will know.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;AI tools being stateless isn’t a bug to fight. It’s a design constraint—like limited screen real estate or slow network connections. Accept it, and you can build systems that work with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; minimal. Let tooling enforce what it can. Capture learnings as you go. Load context on demand.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One caveat: you can never be 100% sure agents will read your docs when they face issues. For tricky domains like Nuxt Content—where training data is sparse or outdated—I’ve learned to be explicit in my prompts. When I know I’m working on something with poor training coverage, I’ll add to the plan: “If you encounter issues with Nuxt Content APIs, read &lt;code&gt;docs/nuxt-content-gotchas.md&lt;/code&gt; first.” This nudge makes the difference between the agent guessing based on outdated patterns and actually consulting current knowledge.&lt;/p&gt;
&lt;p&gt;The AI forgets. Your documentation doesn’t.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;[^1]: LLMs have no memory between sessions—context is just tokens in a sliding window. See Factory’s analysis in &lt;a href=&quot;https://factory.ai/news/context-window-problem&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Context Window Problem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^2]: HumanLayer’s guide on &lt;a href=&quot;https://www.humanlayer.dev/blog/writing-a-good-claude-md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Writing a Good CLAUDE.md&lt;/a&gt; recommends keeping files under 60 lines and using progressive disclosure for detailed instructions.&lt;/p&gt;
&lt;p&gt;[^5]: Developers Digest, &lt;a href=&quot;https://www.youtube.com/watch?v=-4nUCaMNBR8&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Self-Improving Skills in Claude Code&lt;/a&gt;. A pattern for capturing learnings automatically: skills analyze sessions, extract corrections, and update themselves.&lt;/p&gt;
&lt;p&gt;[^6]: Dex Horthy, &lt;a href=&quot;https://www.youtube.com/watch?v=rmvDxxNubIg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;No Vibes Allowed: Solving Hard Problems in Complex Codebases&lt;/a&gt;. Dex is the founder of HumanLayer and creator of the Ralph technique for autonomous AI coding. His &lt;a href=&quot;https://www.humanlayer.dev/blog/12-factor-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;12 Factor Agents&lt;/a&gt; manifesto includes “Make Your Agent a Stateless Reducer” as Factor 12.&lt;/p&gt;
&lt;p&gt;[^7]: Moss, &lt;a href=&quot;https://banay.me/dont-waste-your-backpressure&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Don’t Waste Your Back Pressure&lt;/a&gt;. Backpressure—automated feedback from type systems, linters, and build tools—is what enables agents to work on longer-horizon tasks without constant human intervention.&lt;/p&gt;
&lt;p&gt;[^8]: Geoffrey Huntley, &lt;a href=&quot;https://ghuntley.com/ralph/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ralph&lt;/a&gt;. Ralph is a technique for autonomous AI coding where tasks are queued and executed without human intervention, making automated checks on commit essential.&lt;/p&gt;
&lt;p&gt;[^9]: Jude Gao, &lt;a href=&quot;https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md outperforms skills in our agent evals&lt;/a&gt;. Vercel’s evals found that a compressed docs index embedded directly in &lt;a href=&quot;http://AGENTS.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AGENTS.md&lt;/a&gt; achieved 100% pass rate, while skills maxed out at 79% even with explicit instructions—and performed no better than baseline when left to trigger naturally.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>ai-tools</category><category>developer-experience</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Built a Skill That Lets Me Talk to Claude&apos;s Conversation Memory</title><link>https://alexop.dev/posts/building-conversation-search-skill-claude-code/</link><guid isPermaLink="true">https://alexop.dev/posts/building-conversation-search-skill-claude-code/</guid><description>How I built a skill that lets Claude search its own conversation history, turning it into a persistent coding partner that remembers past solutions.</description><pubDate>Sat, 17 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;When I work with Claude Code on complex projects, I often remember discussing a problem or solution but can’t find it. “We fixed that EMFILE error last week, what was the solution?” or “What did we work on yesterday?”&lt;/p&gt;
&lt;p&gt;Claude Code stores every session locally. But Claude itself can’t search those files by default. So I built a skill that lets Claude search its own conversation history.&lt;/p&gt;
&lt;p&gt;This turns Claude into a persistent coding partner that actually remembers past solutions.&lt;/p&gt;
&lt;aside&gt;
If you&apos;re not familiar with how skills work in Claude Code, check out my guide to CLAUDE.md, skills, and subagents first.
&lt;/aside&gt;
&lt;aside&gt;
If you just want to check out the skill, find it here: [conversation-search skill](https://github.com/alexanderop/dotfiles/tree/main/claude/skills/conversation-search)
&lt;/aside&gt;
&lt;h2&gt;How Claude Code Stores Conversations&lt;/h2&gt;
&lt;p&gt;Every Claude Code session gets saved as a JSONL file in &lt;code&gt;~/.claude/projects/&lt;/code&gt;. The directory structure looks like this:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{ name: “.claude”, open: true, children: [&lt;br /&gt;
{ name: “projects”, open: true, children: [&lt;br /&gt;
{ name: “-Users-alex-Projects-myapp”, open: true,comment: “// encoded path”, children: [&lt;br /&gt;
{ name: “a1b2c3d4.jsonl”, comment: “// session file” , open: true},&lt;br /&gt;
{ name: “b2c3d4e5.jsonl” },&lt;br /&gt;
{ name: “c3d4e5f6.jsonl” },&lt;br /&gt;
{ name: “e5f6g7h8.jsonl” },&lt;br /&gt;
{ name: “agent-xyz123.jsonl”, comment: “// subagent session” }&lt;br /&gt;
]},&lt;br /&gt;
{ name: “-Users-alex-Projects-blog”, children: [&lt;br /&gt;
{ name: “i9j0k1l2.jsonl” }&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;The path encoding is simple: replace &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;-&lt;/code&gt; and prefix absolute paths with &lt;code&gt;-&lt;/code&gt;. So &lt;code&gt;/Users/alex/Projects/myapp&lt;/code&gt; becomes &lt;code&gt;-Users-alex-Projects-myapp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Each JSONL file contains one JSON object per line:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;2026-01-16T10:30:00Z&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;gitBranch&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;main&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Fix the EMFILE error&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;2026-01-16T10:30:15Z&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;content&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Let me investigate...&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;tool_use&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;name&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Bash&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;input&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;ulimit -n&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;summary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;summary&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Fixed EMFILE error by increasing file descriptor limit&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each entry includes the role, timestamp, git branch, message content, and tool uses. The &lt;code&gt;summary&lt;/code&gt; type appears when Claude generates a conversation summary.&lt;/p&gt;
&lt;h2&gt;The Skill Structure&lt;/h2&gt;
&lt;p&gt;The skill lives in &lt;code&gt;~/.claude/skills/conversation-search/&lt;/code&gt; with two files:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree open={true} tree={[&lt;br /&gt;
{ name: “conversation-search”, open: true, children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt;”, comment: “// trigger patterns and usage” },&lt;br /&gt;
{ name: “scripts”, open: true, children: [&lt;br /&gt;
{ name: “search_history.py”, comment: “// the search engine” }&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; file tells Claude when to activate this skill:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; conversation&lt;span&gt;-&lt;/span&gt;search
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Search past Claude Code conversation history. Use when asked to recall&lt;span&gt;,&lt;/span&gt;
  find&lt;span&gt;,&lt;/span&gt; or search for anything from previous conversations. Triggers include
  &quot;what did we do today&quot;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;how did we fix X&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;search history&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &quot;recall when we&quot;&lt;span&gt;...&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I ask “what did we do yesterday?”, Claude recognizes the trigger and knows to use this skill.&lt;/p&gt;
&lt;h2&gt;How the Python Script Works&lt;/h2&gt;
&lt;p&gt;The script has two modes: &lt;strong&gt;digest&lt;/strong&gt; for daily summaries and &lt;strong&gt;search&lt;/strong&gt; for finding specific solutions.&lt;/p&gt;
&lt;h3&gt;Data Structures&lt;/h3&gt;
&lt;p&gt;The script parses JSONL files into clean dataclasses:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span&gt;@dataclass&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;Message&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    uuid&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    parent_uuid&lt;span&gt;:&lt;/span&gt; Optional&lt;span&gt;[&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;  &lt;span&gt;# &apos;user&apos;, &apos;assistant&apos;&lt;/span&gt;
    content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    timestamp&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    tool_uses&lt;span&gt;:&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;
    tool_results&lt;span&gt;:&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;

&lt;span&gt;@dataclass&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;Conversation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    session_id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    file_path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    summary&lt;span&gt;:&lt;/span&gt; Optional&lt;span&gt;[&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;
    project_path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    git_branch&lt;span&gt;:&lt;/span&gt; Optional&lt;span&gt;[&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    timestamp&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;

&lt;span&gt;@dataclass&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;SearchResult&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    conversation&lt;span&gt;:&lt;/span&gt; Conversation
    score&lt;span&gt;:&lt;/span&gt; &lt;span&gt;float&lt;/span&gt;
    matched_messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;
    problem_excerpt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    solution_excerpt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;
    commands_run&lt;span&gt;:&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Relevance Scoring&lt;/h3&gt;
&lt;p&gt;The search algorithm tokenizes the query and content, then calculates relevance scores with weighted boosts:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;calculate_relevance_score&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;query&lt;span&gt;:&lt;/span&gt; &lt;span&gt;str&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; conversation&lt;span&gt;:&lt;/span&gt; Conversation&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;tuple&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    query_tokens &lt;span&gt;=&lt;/span&gt; tokenize&lt;span&gt;(&lt;/span&gt;query&lt;span&gt;)&lt;/span&gt;
    total_score &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0.0&lt;/span&gt;
    matched_messages &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

    &lt;span&gt;# Summary gets highest weight (3x)&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; conversation&lt;span&gt;.&lt;/span&gt;summary&lt;span&gt;:&lt;/span&gt;
        summary_tokens &lt;span&gt;=&lt;/span&gt; tokenize&lt;span&gt;(&lt;/span&gt;conversation&lt;span&gt;.&lt;/span&gt;summary&lt;span&gt;)&lt;/span&gt;
        summary_overlap &lt;span&gt;=&lt;/span&gt; &lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;query_tokens &lt;span&gt;&amp;amp;&lt;/span&gt; summary_tokens&lt;span&gt;)&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; &lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;query_tokens&lt;span&gt;)&lt;/span&gt;
        total_score &lt;span&gt;+=&lt;/span&gt; summary_overlap &lt;span&gt;*&lt;/span&gt; &lt;span&gt;3.0&lt;/span&gt;

    &lt;span&gt;# Check each message&lt;/span&gt;
    &lt;span&gt;for&lt;/span&gt; msg &lt;span&gt;in&lt;/span&gt; conversation&lt;span&gt;.&lt;/span&gt;messages&lt;span&gt;:&lt;/span&gt;
        msg_tokens &lt;span&gt;=&lt;/span&gt; tokenize&lt;span&gt;(&lt;/span&gt;msg&lt;span&gt;.&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;
        overlap &lt;span&gt;=&lt;/span&gt; &lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;query_tokens &lt;span&gt;&amp;amp;&lt;/span&gt; msg_tokens&lt;span&gt;)&lt;/span&gt;

        &lt;span&gt;if&lt;/span&gt; overlap &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
            msg_score &lt;span&gt;=&lt;/span&gt; overlap &lt;span&gt;/&lt;/span&gt; &lt;span&gt;len&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;query_tokens&lt;span&gt;)&lt;/span&gt;

            &lt;span&gt;# User messages get 1.5x boost (problem descriptions)&lt;/span&gt;
            &lt;span&gt;if&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;role &lt;span&gt;==&lt;/span&gt; &lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
                msg_score &lt;span&gt;*=&lt;/span&gt; &lt;span&gt;1.5&lt;/span&gt;

            &lt;span&gt;# Messages with tool uses get 1.3x boost (solutions)&lt;/span&gt;
            &lt;span&gt;if&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;tool_uses&lt;span&gt;:&lt;/span&gt;
                msg_score &lt;span&gt;*=&lt;/span&gt; &lt;span&gt;1.3&lt;/span&gt;

            total_score &lt;span&gt;+=&lt;/span&gt; msg_score
            matched_messages&lt;span&gt;.&lt;/span&gt;append&lt;span&gt;(&lt;/span&gt;msg&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; total_score&lt;span&gt;,&lt;/span&gt; matched_messages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The weighting makes sense: summaries are the most relevant since they capture the essence. User messages describe problems. Tool uses indicate actual solutions.&lt;/p&gt;
&lt;h3&gt;Date Filtering&lt;/h3&gt;
&lt;p&gt;The script supports filtering by date range:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Today&apos;s sessions only&lt;/span&gt;
python3 search_history.py &lt;span&gt;--today&lt;/span&gt; &lt;span&gt;&quot;newsletter&quot;&lt;/span&gt;

&lt;span&gt;# Yesterday&lt;/span&gt;
python3 search_history.py &lt;span&gt;--yesterday&lt;/span&gt; &lt;span&gt;&quot;bug fix&quot;&lt;/span&gt;

&lt;span&gt;# Last 7 days&lt;/span&gt;
python3 search_history.py &lt;span&gt;--days&lt;/span&gt; &lt;span&gt;7&lt;/span&gt; &lt;span&gt;&quot;refactor&quot;&lt;/span&gt;

&lt;span&gt;# Since a specific date&lt;/span&gt;
python3 search_history.py &lt;span&gt;--since&lt;/span&gt; &lt;span&gt;2026&lt;/span&gt;-01-01 &lt;span&gt;&quot;feature&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Extracting Useful Information&lt;/h3&gt;
&lt;p&gt;The script extracts practical information from each conversation:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;extract_bash_commands&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;conversation&lt;span&gt;:&lt;/span&gt; Conversation&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    &lt;span&gt;&quot;&quot;&quot;Extract Bash commands run during the conversation.&quot;&quot;&quot;&lt;/span&gt;
    commands &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;for&lt;/span&gt; msg &lt;span&gt;in&lt;/span&gt; conversation&lt;span&gt;.&lt;/span&gt;messages&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;for&lt;/span&gt; tool &lt;span&gt;in&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;tool_uses&lt;span&gt;:&lt;/span&gt;
            &lt;span&gt;if&lt;/span&gt; tool&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;==&lt;/span&gt; &lt;span&gt;&apos;Bash&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
                cmd &lt;span&gt;=&lt;/span&gt; tool&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;input&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;command&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
                &lt;span&gt;if&lt;/span&gt; cmd&lt;span&gt;:&lt;/span&gt;
                    commands&lt;span&gt;.&lt;/span&gt;append&lt;span&gt;(&lt;/span&gt;cmd&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; commands

&lt;span&gt;def&lt;/span&gt; &lt;span&gt;extract_files_touched&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;conversation&lt;span&gt;:&lt;/span&gt; Conversation&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;list&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    &lt;span&gt;&quot;&quot;&quot;Extract files that were read, written, or edited.&quot;&quot;&quot;&lt;/span&gt;
    files &lt;span&gt;=&lt;/span&gt; &lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;for&lt;/span&gt; msg &lt;span&gt;in&lt;/span&gt; conversation&lt;span&gt;.&lt;/span&gt;messages&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;for&lt;/span&gt; tool &lt;span&gt;in&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;tool_uses&lt;span&gt;:&lt;/span&gt;
            name &lt;span&gt;=&lt;/span&gt; tool&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            inp &lt;span&gt;=&lt;/span&gt; tool&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;input&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

            &lt;span&gt;if&lt;/span&gt; name &lt;span&gt;in&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Read&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;Write&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;Edit&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
                path &lt;span&gt;=&lt;/span&gt; inp&lt;span&gt;.&lt;/span&gt;get&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;file_path&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
                &lt;span&gt;if&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt;
                    files&lt;span&gt;.&lt;/span&gt;add&lt;span&gt;(&lt;/span&gt;Path&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;sorted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;files&lt;span&gt;)&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful for recreating solutions. If you found how you fixed something before, you can see exactly which commands you ran and which files you changed.&lt;/p&gt;
&lt;h2&gt;Using the Skill&lt;/h2&gt;
&lt;h3&gt;Daily Digest&lt;/h3&gt;
&lt;p&gt;Ask “what did we do yesterday?” and Claude runs the digest mode:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 search_history.py &lt;span&gt;--digest&lt;/span&gt; yesterday
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## January 16, 2026 - 32 sessions

### 1. Set Context Menu Feature Spec
   Session: `1498ff91`
   Branch: `fitnessFunctions`
   Files: set-context-menu.md, SetContextMenu.vue, SetContextMenuPO.ts
   Commands: 12 executed

### 2. Fix Pipeline: Missing i18n, Unused Exports
   Session: `23351e77`
   Branch: `fitnessFunctions`
   Files: de.json, en.json, claude-qa.yml
   Commands: 6 executed

### 3. Adding AI Coding Articles to Second Brain
   Session: `5c909423`
   Branch: `main`
   Files: article.md, dex-horthy.md, diagrams-guide.md
   Commands: 1 executed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is great for standup notes or just remembering what you worked on.&lt;/p&gt;
&lt;h3&gt;Keyword Search&lt;/h3&gt;
&lt;p&gt;Ask “how did we fix the EMFILE error?” and Claude searches for relevant sessions:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 search_history.py &lt;span&gt;&quot;EMFILE error&quot;&lt;/span&gt; &lt;span&gt;--days&lt;/span&gt; &lt;span&gt;14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;============================================================
Result #1 (Score: 4.25)
============================================================
Project: /Users/alex/Projects/fitness-app
Session: a1b2c3d4...
Branch: main
Date: 2026-01-10

PROBLEM:
Getting EMFILE error when running tests, too many open files

SOLUTION:
The issue was too many file watchers. Fixed by increasing the limit with
`ulimit -n 10240` and adding it to shell profile...

COMMANDS RUN (3 total):
  $ ulimit -n
  $ ulimit -n 10240
  $ echo &quot;ulimit -n 10240&quot; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can recreate the exact solution without remembering the details.&lt;/p&gt;
&lt;h3&gt;Project Filtering&lt;/h3&gt;
&lt;p&gt;You can narrow searches to a specific project:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 search_history.py &lt;span&gt;&quot;vitest config&quot;&lt;/span&gt; &lt;span&gt;--project&lt;/span&gt; ~/Projects/fitness-app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;Before this skill, I’d waste time re-solving problems I’d already solved. “I know we discussed this, but I can’t find it.” Now I just ask Claude.&lt;/p&gt;
&lt;p&gt;The benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No more re-solving problems&lt;/strong&gt; - Claude finds past solutions instantly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daily digests for standups&lt;/strong&gt; - “What did we work on yesterday?” gives a ready summary&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commands are preserved&lt;/strong&gt; - You can recreate exact solutions with the same commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-project search&lt;/strong&gt; - Find solutions from any project you’ve worked on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skill turns Claude from a stateless assistant into something closer to a persistent coding partner. It remembers what you’ve done together.&lt;/p&gt;

If you want to extend Claude Code with custom skills, check out my post on building a Claude Code plugin for packaging and sharing skills across projects.

          </content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><category>python</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>In Five Years, Developers Won&apos;t Write Code By Hand</title><link>https://alexop.dev/posts/developers-wont-write-code-by-hand/</link><guid isPermaLink="true">https://alexop.dev/posts/developers-wont-write-code-by-hand/</guid><description>Software development as translation work is dying. Software engineering—the strategic, architectural discipline—is more valuable than ever. The shift is already here.</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I haven’t written code by hand in months.&lt;/p&gt;
&lt;p&gt;This year alone, I built four complete projects using only Claude Code: a markdown editor, a Nuxt blog starter, a workout tracking app, and the Second Brain you might be reading this on. At work, I regularly one-shot entire issues without touching my keyboard for anything except prompts. Last week, I resolved a production incident using VS Code Copilot while barely glancing at the actual code.&lt;/p&gt;
&lt;p&gt;I’m not special. I’m just paying attention.&lt;/p&gt;
&lt;p&gt;And I’m not alone. Simon Willison[^1]—one of the most respected voices in the developer community—put it bluntly on the Oxide and Friends podcast:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I think the job of being paid money to type code into a computer will go the same way as punching punch cards […] I do not think anyone will be paid to just do the thing where you type the code. I think software engineering will still be an enormous career. I just think the software engineers won’t be spending multiple hours of their day in a text editor typing out syntax.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But here’s the part that matters:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The more time I spend on AI-assisted programming the less afraid I am for my job, because it turns out building software—especially at the rate it’s now possible to build—still requires enormous skill, experience and depth of understanding. The skills are changing though! Being able to read a detailed specification and transform it into lines of code is the thing that’s being automated away. What’s left is everything else, and the more time I spend working with coding agents the larger that “everything else” becomes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That “everything else” is the whole point.&lt;/p&gt;
&lt;h2&gt;The Shift Is Already Here&lt;/h2&gt;
&lt;p&gt;In five years, developers won’t write code by hand. This isn’t a prediction about some distant future—it’s a description of what’s already happening to anyone using the right tools.&lt;/p&gt;
&lt;p&gt;The reason most people don’t see it? Two things: skill gaps and companies failing to provide developers with modern tooling. Most developers are still typing every character. Most companies are still debating whether AI tools are “worth the license cost.” Meanwhile, the developers who figured this out are shipping at 10x the pace.&lt;/p&gt;
&lt;p&gt;The creator of Claude Code uses Claude Code to work on multiple features simultaneously. Techniques like Ralph[^2]—an automation framework that breaks work into discrete chunks—can literally rip through your entire backlog. This isn’t theoretical. It’s happening now, in production, at companies that stopped waiting for permission. (For a deep dive into how these tools actually work, see my guide to Claude Code’s architecture.)&lt;/p&gt;
&lt;h2&gt;The Great Distinction Nobody Talks About&lt;/h2&gt;
&lt;p&gt;Here’s what changes everything: understanding the difference between software &lt;em&gt;engineering&lt;/em&gt; and software &lt;em&gt;development&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software engineering&lt;/strong&gt; is designing systems. Architecture decisions. Test strategies. The guardrails that keep a codebase healthy over time. Knowing what to build and—more importantly—what not to build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software development&lt;/strong&gt; is writing the actual code. Translating specifications into syntax. Converting tickets into pull requests.&lt;/p&gt;
&lt;p&gt;One of these is a creative, strategic discipline. The other is translation work.&lt;/p&gt;
&lt;p&gt;Software development is dying. Software engineering is more valuable than ever.&lt;/p&gt;
&lt;p&gt;Kent Beck[^3] put it perfectly: 90% of traditional skills have lost their economic value because AI can replicate them efficiently. But the remaining 10%? They gain 1000x leverage through AI augmentation. The question you need to answer: which skills are your 10%?&lt;/p&gt;
&lt;h2&gt;Scrum Was a Workaround for Human Limitations&lt;/h2&gt;
&lt;p&gt;Think about why we created Scrum in the first place.&lt;/p&gt;
&lt;p&gt;We needed big teams with specialized roles because implementing features took forever. Frontend developers, backend developers, QA engineers, DevOps specialists—all coordinating through ceremonies and tickets because the bottleneck was literally typing characters into an editor.&lt;/p&gt;
&lt;p&gt;In the worst cases—an anti-pattern far too common—managers saw developers as “code monkeys” who converted tickets into code. The developer’s job was translation, not thinking.&lt;/p&gt;
&lt;p&gt;This made sense when coding was slow. When a feature took days or weeks to implement.&lt;/p&gt;
&lt;p&gt;Those days are over.&lt;/p&gt;
&lt;p&gt;When implementation takes days, you need ceremonies to coordinate. When implementation takes minutes, the coordination overhead becomes the bottleneck. Scrum isn’t dying because it was bad—it’s dying because the constraint it solved no longer exists.&lt;/p&gt;
&lt;h2&gt;The New Economics&lt;/h2&gt;
&lt;p&gt;The math has fundamentally changed.&lt;/p&gt;
&lt;p&gt;Prototypes are cheap now. What took a sprint takes an afternoon. Burke Holland[^4], a Microsoft developer advocate, built four substantial projects with AI—including Swift applications in a language he doesn’t know. His advice? “Make things. Stop waiting to have all the answers… you can make things faster than you ever thought possible.”&lt;/p&gt;
&lt;p&gt;I’ve watched product owners use Claude Code to generate their own prototypes, fix simple bugs, and submit pull requests. They’re not becoming developers—they’re just not waiting for developers anymore.&lt;/p&gt;
&lt;p&gt;Ralph-style automation lets you feed your backlog to an AI and get working code out the other end. Not perfect code. Not production-ready code on the first try. But functional code that’s 80% there, leaving humans to handle the remaining 20% that actually requires judgment.&lt;/p&gt;
&lt;p&gt;The developers who thrive aren’t writing more code. They’re orchestrating AI to write code for them, then applying their expertise to the parts that matter.&lt;/p&gt;
&lt;h2&gt;The Hard Part Was Never Coding&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable truth: coding was never the hard part. We just convinced ourselves it was because it took so much &lt;em&gt;time&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Lee Robinson[^5]—who went from Vercel to Cursor—built a Rust image compressor, a SvelteKit web app, and a hardware game without writing code by hand. His reflection? “Writing code was never really the bottleneck, especially for larger projects.” And: “It wasn’t about the code… It’s about building something great and something that I’m proud of.”&lt;/p&gt;
&lt;p&gt;The actually hard problems haven’t changed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understanding what customers need (not what they say they need)&lt;/li&gt;
&lt;li&gt;Writing specifications clear enough that anyone—human or AI—can implement them&lt;/li&gt;
&lt;li&gt;Knowing what to build and what to skip&lt;/li&gt;
&lt;li&gt;Making architectural decisions that won’t haunt you in two years&lt;/li&gt;
&lt;li&gt;Marketing, positioning, product sense&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are human problems. Creative problems. Strategic problems.&lt;/p&gt;
&lt;p&gt;Martin Fowler[^6] argues this is the biggest shift since assembly to high-level languages. But here’s what he gets right: AI lacks architectural judgment. It cannot distinguish good patterns from poor ones. It can write code all day, but it can’t tell you whether that code should exist.&lt;/p&gt;
&lt;p&gt;The value shifts from writing code to knowing what code to write and why.&lt;/p&gt;
&lt;h2&gt;“But AI Code Is Buggy”&lt;/h2&gt;
&lt;p&gt;Yes. And?&lt;/p&gt;
&lt;p&gt;The first high-level language compilers produced worse machine code than hand-written assembly. Early web frameworks were slower than hand-crafted HTML. Every abstraction layer introduces inefficiencies.&lt;/p&gt;
&lt;p&gt;We adopt them anyway because developer productivity matters more than perfect output. The question isn’t “is AI code flawless?”—it’s “is AI code good enough, fast enough, to change the economics?”&lt;/p&gt;
&lt;p&gt;The answer is yes. Today. Not in five years—today.&lt;/p&gt;
&lt;p&gt;I’m not arguing AI produces better code than expert developers. I’m arguing it produces acceptable code fast enough that the calculus changes. When you can generate ten implementations in the time it takes to write one, you can afford to throw away the bad ones.&lt;/p&gt;
&lt;p&gt;The skeptics are optimizing for the wrong variable. They’re measuring code quality when they should be measuring iteration speed.&lt;/p&gt;
&lt;h2&gt;How to Prepare&lt;/h2&gt;
&lt;p&gt;Geoffrey Huntley[^7] put it bluntly: “Software engineers who haven’t adopted or started exploring software assistants, are frankly not gonna make it.”&lt;/p&gt;
&lt;p&gt;But here’s the nuance he adds: “I suspect there’s not going to be mass-layoffs for software developers due to AI. Instead, what we will see is a natural attrition between those who invest in themselves right now and those who do not.”&lt;/p&gt;
&lt;p&gt;This isn’t about being replaced by AI. It’s about being outperformed by developers who use AI. The gap is already opening. (I wrote about this dynamic in The Age of the Generalist—high-agency builders thrive while passive specialists struggle.)&lt;/p&gt;
&lt;p&gt;If you want to thrive in this new world, here’s where to focus:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learn how LLMs actually work.&lt;/strong&gt; Not to build models—to orchestrate them. Understand context windows, token limits, prompt engineering. Know why your AI assistant suddenly “forgot” what you told it three messages ago. This isn’t optional knowledge anymore; it’s table stakes. (For practical starting points, see how I use LLMs in my daily work.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Study the architecture of AI agents.&lt;/strong&gt; The developers who can build custom agents for their specific workflows have superpowers the rest of the industry doesn’t understand yet. Resources like the 12 Factor Agents[^8] manifesto lay out the principles: small focused agents, deterministic control flow with strategic LLM decision points, and proper context window management. Learn what context engineering means. Understand why RAG exists. Build something that automates your own repetitive work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Double down on software engineering.&lt;/strong&gt; System design, architecture patterns, testing strategies—these skills become more valuable, not less. When anyone can generate code, the people who know what code to generate become invaluable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stop optimizing for code output.&lt;/strong&gt; Start optimizing for clarity of thought, quality of specifications, and speed of iteration. Your value isn’t in the characters you type; it’s in the decisions you make.&lt;/p&gt;
&lt;h2&gt;The Paradox&lt;/h2&gt;
&lt;p&gt;Here’s what I find fascinating: I don’t think we’ll ever get AI that matches human agency and creativity. The models might plateau. They might not get dramatically “smarter” than they are today.&lt;/p&gt;
&lt;p&gt;It doesn’t matter.&lt;/p&gt;
&lt;p&gt;Even with current capabilities, as tooling improves, we’re witnessing the biggest transformation in software development history. The change isn’t coming from AI replacing human thinking—it’s coming from AI eliminating the translation layer between human thinking and working software.&lt;/p&gt;
&lt;p&gt;You don’t need AGI to automate code. You just need models that are good enough at translation, combined with humans who are good enough at specification.&lt;/p&gt;
&lt;p&gt;We have both. Right now.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The question isn’t whether this shift will happen. It’s whether you’ll be ready when your company finally notices.&lt;/p&gt;
&lt;p&gt;[^1]: Simon Willison, &lt;a href=&quot;https://simonwillison.net/2026/Jan/8/llm-predictions-for-2026&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LLM Predictions for 2026&lt;/a&gt;&lt;br /&gt;
[^2]: &lt;a href=&quot;https://github.com/snarktank/ralph&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ralph&lt;/a&gt; - Automation framework for AI-driven development&lt;br /&gt;
[^3]: Kent Beck, &lt;a href=&quot;https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;90% of My Skills Are Now Worth $0&lt;/a&gt;&lt;br /&gt;
[^4]: Burke Holland, &lt;a href=&quot;https://burkeholland.github.io/posts/opus-4-5-change-everything/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Opus 4.5 is Going to Change Everything&lt;/a&gt;&lt;br /&gt;
[^5]: Lee Robinson, &lt;a href=&quot;https://www.youtube.com/watch?v=UrNLVip0hSA&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AI codes better than me. Now what?&lt;/a&gt;&lt;br /&gt;
[^6]: Martin Fowler, &lt;a href=&quot;https://www.youtube.com/watch?v=CQmI4XKTa0U&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How AI Will Change Software Engineering&lt;/a&gt;&lt;br /&gt;
[^7]: Geoffrey Huntley, &lt;a href=&quot;https://ghuntley.com/ngmi/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;What do I mean by some software devs are “ngmi”?&lt;/a&gt;&lt;br /&gt;
[^8]: &lt;a href=&quot;https://www.humanlayer.dev/blog/12-factor-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;12 Factor Agents&lt;/a&gt; - Principles for production-grade AI agents&lt;/p&gt;

          </content:encoded><category>ai</category><category>software-engineering</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Mutation Testing with AI Agents When Stryker Doesn&apos;t Work</title><link>https://alexop.dev/posts/mutation-testing-ai-agents-vitest-browser-mode/</link><guid isPermaLink="true">https://alexop.dev/posts/mutation-testing-ai-agents-vitest-browser-mode/</guid><description>When Stryker doesn&apos;t support your test stack, AI agents can execute mutation testing manually. A practical approach for Vitest browser mode and Playwright.</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;The Coverage Lie&lt;/h2&gt;
&lt;p&gt;Code coverage lies. A test that exercises a line doesn’t mean it verifies that line does the right thing:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 100% coverage - would still pass if add() returned 999&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;adds numbers&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mutation testing flips the question. Instead of asking “did tests run this code?”, it asks &lt;strong&gt;“if I break this code, do tests fail?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using our &lt;code&gt;add&lt;/code&gt; example, a mutation tester would:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Original&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Mutated: swap + for -&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; a &lt;span&gt;-&lt;/span&gt; b  &lt;span&gt;// &amp;lt;-- bug introduced&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now run the test. &lt;code&gt;add(2, 2)&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt; instead of &lt;code&gt;4&lt;/code&gt;. Does the test fail? No—it never checked the result. &lt;strong&gt;The mutant survives.&lt;/strong&gt; Your test has a gap.&lt;/p&gt;
&lt;p&gt;The process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Mutate&lt;/strong&gt;: Introduce a small bug (change &lt;code&gt;&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;gt;=&lt;/code&gt;, swap &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; for &lt;code&gt;||&lt;/code&gt;, delete a line)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run tests&lt;/strong&gt;: Execute your test suite against the mutated code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate&lt;/strong&gt;: If tests pass with the bug, your tests are weak. If tests fail, they caught it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A mutation that tests fail to catch is a “surviving mutant”—proof of a test gap.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;When Stryker Works: The Gold Standard&lt;/h2&gt;
&lt;p&gt;When your test stack supports it, automated mutation testing with Stryker is the way to go. It’s fast, deterministic, generates HTML reports, and runs in CI pipelines. This is especially valuable when you have pure functions with high test coverage but want to verify test quality.&lt;/p&gt;
&lt;p&gt;Here’s what it looks like in practice:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;pnpm&lt;/span&gt; test:mutation
&lt;span&gt;# or: stryker run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INFO ProjectReader Found 7 of 2947 file(s) to be mutated.
INFO Instrumenter Instrumented 7 source file(s) with 394 mutant(s)
INFO DryRunExecutor Initial test run succeeded. Ran 184 tests in 0 seconds.

Mutation testing  [====================] 100% | 394/394 Mutants tested
(35 survived, 0 timed out)

--------------|---------|----------|----------|----------|
File          |  % score | # killed | # survived | # no cov |
--------------|---------|----------|----------|----------|
All files     |   90.86 |      358 |         35 |        1 |
 backlinks.ts |   96.30 |       26 |          1 |        0 |
 callouts.ts  |   93.94 |       62 |          4 |        0 |
 graph.ts     |   91.55 |       65 |          6 |        0 |
 mentions.ts  |   91.30 |       63 |          5 |        1 |
 minimark.ts  |   82.61 |       76 |         16 |        0 |
 text.ts      |  100.00 |       34 |          0 |        0 |
 wikilinks.ts |   91.43 |       32 |          3 |        0 |
--------------|---------|----------|----------|----------|

INFO MutationTestExecutor Done in 36 seconds.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;394 mutants tested across 7 files in 36 seconds. The report shows exactly which files have weak spots—&lt;code&gt;minimark.ts&lt;/code&gt; at 82.61% needs attention, while &lt;code&gt;text.ts&lt;/code&gt; is solid at 100%.&lt;/p&gt;
&lt;p&gt;Stryker also generates an interactive HTML report where you can drill into each surviving mutant and see exactly what code change your tests failed to catch.&lt;/p&gt;

  If your stack supports Stryker (standard Vitest in Node mode, Jest, Mocha), use it. Deterministic tooling in your CI pipeline beats manual approaches every time. The AI agent technique in this post is for when Stryker isn&apos;t an option.

&lt;hr /&gt;
&lt;h2&gt;The Vitest Browser Mode Problem&lt;/h2&gt;
&lt;p&gt;But what if Stryker doesn’t support your stack? Stryker doesn’t work with Vitest’s browser mode. Their instrumentation assumes Node.js execution, but browser mode runs tests in actual Chromium via Playwright.&lt;/p&gt;
&lt;p&gt;My setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Framework&lt;/strong&gt;: Vitest 4 with &lt;code&gt;browser.enabled: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider&lt;/strong&gt;: Playwright (Chromium)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test style&lt;/strong&gt;: Integration tests with real DOM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My testing strategy relies heavily on Vitest browser mode for realistic user flow testing. Stryker’s mutation coverage reports? Not an option. And switching to Node-based testing would mean losing the browser-specific behavior I’m actually testing.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;AI Agents as Manual Mutation Testers&lt;/h2&gt;
&lt;p&gt;The mutation testing algorithm is simple enough that an AI coding agent can execute it manually. Claude Code can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read your source code&lt;/li&gt;
&lt;li&gt;Apply mutations systematically&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pnpm test --run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Record whether tests passed or failed&lt;/li&gt;
&lt;li&gt;Restore the original code&lt;/li&gt;
&lt;li&gt;Report surviving mutants with suggested fixes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I adapted a Claude Code skill originally created by &lt;a href=&quot;https://www.linkedin.com/posts/paul-hammond-bb5b78251_mutation-testing-is-typically-expensive-but-activity-7414719212071473152-_xTm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Paul Hammond&lt;/a&gt; that codifies this workflow.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    &lt;span&gt;subgraph&lt;/span&gt; Agent&lt;span&gt;[&quot;AI Agent Workflow&quot;]&lt;/span&gt;
        A&lt;span&gt;[Read source file]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Identify mutation targets]&lt;/span&gt;
        B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Apply single mutation]&lt;/span&gt;
        C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Run test suite]&lt;/span&gt;
        D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;{Tests fail?}&lt;/span&gt;
        E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; F&lt;span&gt;[Mutant KILLED]&lt;/span&gt;
        E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; G&lt;span&gt;[Mutant SURVIVED]&lt;/span&gt;
        F &lt;span&gt;--&amp;gt;&lt;/span&gt; H&lt;span&gt;[Restore original code]&lt;/span&gt;
        G &lt;span&gt;--&amp;gt;&lt;/span&gt; H
        H &lt;span&gt;--&amp;gt;&lt;/span&gt; I&lt;span&gt;{More mutations?}&lt;/span&gt;
        I &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; C
        I &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; J&lt;span&gt;[Generate report]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Results&lt;span&gt;[&quot;Report Output&quot;]&lt;/span&gt;
        J &lt;span&gt;--&amp;gt;&lt;/span&gt; K&lt;span&gt;[Killed mutants: Tests caught the bug]&lt;/span&gt;
        J &lt;span&gt;--&amp;gt;&lt;/span&gt; L&lt;span&gt;[Survived mutants: Test gaps found]&lt;/span&gt;
        L &lt;span&gt;--&amp;gt;&lt;/span&gt; M&lt;span&gt;[Suggested fixes for each gap]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;style&lt;/span&gt; G &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#f96&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#333&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; F &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#6f9&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#333&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; L &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#f96&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#333&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; K &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#6f9&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#333&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Mutation Testing Skill&lt;/h3&gt;
&lt;p&gt;The skill defines mutation operators in priority order:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 1 - Boundaries&lt;/strong&gt; (most likely to survive):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Mutate To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Priority 2 - Boolean Logic&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Mutate To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;||&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;||&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!condition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;condition&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Priority 3 - Return Values&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Mutate To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;return x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;return null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;return true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;return false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early return&lt;/td&gt;
&lt;td&gt;Remove it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Priority 4 - Statement Removal&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Mutate To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;array.push(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;await save(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;emit(&apos;event&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The agent applies each mutation one at a time, runs tests, records results, and restores the original code immediately.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Real Example: Settings Feature&lt;/h2&gt;
&lt;p&gt;I ran this against my settings feature. The integration tests looked comprehensive—theme toggling, language switching, unit preferences. Code coverage would show high percentages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results: 38% mutation score&lt;/strong&gt; (5 killed, 8 survived out of 13 mutations)&lt;/p&gt;
&lt;p&gt;Here’s what the AI agent found:&lt;/p&gt;
&lt;h3&gt;Surviving Mutant #1: Volume Boundary Not Tested&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Original (stores/settings.ts:65)&lt;/span&gt;
Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;volume&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Mutation: Change 0.5 to 0.4&lt;/span&gt;
Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;volume&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0.4&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Result: Tests PASSED -&amp;gt; Mutant SURVIVED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My tests never verified the minimum volume constraint. A bug changing the minimum from 50% to 40% would ship undetected.&lt;/p&gt;
&lt;h3&gt;Surviving Mutant #2: Theme DOM Class Not Verified&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Original (composables/useTheme.ts:26)&lt;/span&gt;
newMode &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&apos;dark&apos;&lt;/span&gt;

&lt;span&gt;// Mutation: Negate the condition&lt;/span&gt;
newMode &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&apos;dark&apos;&lt;/span&gt;

&lt;span&gt;// Result: Tests PASSED -&amp;gt; Mutant SURVIVED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My test checked that clicking the toggle changed the stored preference. It never verified that &lt;code&gt;document.documentElement.classList&lt;/code&gt; actually received the &lt;code&gt;dark&lt;/code&gt; class. The UI could break while tests pass.&lt;/p&gt;
&lt;h3&gt;Surviving Mutant #3: Error Handling Path Untested&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Original (stores/settings.ts:28)&lt;/span&gt;
&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;

&lt;span&gt;// Mutation: Negate the condition&lt;/span&gt;
&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;

&lt;span&gt;// Result: Tests PASSED -&amp;gt; Mutant SURVIVED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No test exercised the error handling branch. A bug that inverted error handling would go unnoticed.&lt;/p&gt;
&lt;h3&gt;The Fixes&lt;/h3&gt;
&lt;p&gt;The agent suggested specific tests for each surviving mutant:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Fix for Mutant #1: Boundary test&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;volume slider has minimum value constraint of 50%&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; volumeSlider &lt;span&gt;=&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;timer-sound-volume-slider&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; expect&lt;span&gt;.&lt;/span&gt;&lt;span&gt;poll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; el &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; volumeSlider&lt;span&gt;.&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; el&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAttribute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;min&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;0.5&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Fix for Mutant #2: DOM verification&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;adds dark class to html element when dark mode enabled&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; themeToggle &lt;span&gt;=&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme-toggle&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;themeToggle&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;await&lt;/span&gt; expect&lt;span&gt;.&lt;/span&gt;&lt;span&gt;poll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
    document&lt;span&gt;.&lt;/span&gt;documentElement&lt;span&gt;.&lt;/span&gt;classList&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;dark&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;How to Set This Up&lt;/h2&gt;
&lt;h3&gt;Step 1: Create the Skill&lt;/h3&gt;
&lt;p&gt;Save this as &lt;code&gt;.claude/skills/mutation-testing/SKILL.md&lt;/code&gt;:&lt;/p&gt;

Full Mutation Testing Skill (click to expand)
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; mutation&lt;span&gt;-&lt;/span&gt;testing
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;
  Mutation testing patterns for verifying test effectiveness. Use when analyzing branch code
  to find weak or missing tests. Triggers: &quot;mutation testing&quot;, &quot;test effectiveness&quot;,
  &quot;would tests catch this bug&quot;, &quot;weak tests&quot;, &quot;are my tests good enough&quot;, &quot;surviving mutants&quot;.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Mutation Testing&lt;/span&gt;

Mutation testing answers: &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;&quot;Would my tests catch this bug?&quot;&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; by actually introducing bugs and running tests.

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Execution Workflow&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;CRITICAL&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: This skill actually mutates code and runs tests. Follow this exact process:

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 1: Identify Target Code&lt;/span&gt;

git diff main...HEAD --name-only | grep -E &apos;\.(ts|js|tsx|jsx|vue)&apos; | grep -v &apos;\.test\.&apos; | grep -v &apos;\.spec\.&apos;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 2: For Each Function to Test&lt;/span&gt;

Execute this loop for each mutation:

&lt;span&gt;1.&lt;/span&gt; READ the original file and note exact content
&lt;span&gt;2.&lt;/span&gt; APPLY one mutation (edit the code)
&lt;span&gt;3.&lt;/span&gt; RUN tests: pnpm test --run (or specific test file)
&lt;span&gt;4.&lt;/span&gt; RECORD result: KILLED (test failed) or SURVIVED (test passed)
&lt;span&gt;5.&lt;/span&gt; RESTORE original code immediately
&lt;span&gt;6.&lt;/span&gt; Repeat for next mutation

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Step 3: Report Results&lt;/span&gt;

After all mutations, provide a summary table:

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutation &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Location &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Result &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Action Needed &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;--------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;---------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;`&lt;/span&gt; → &lt;span&gt;`&amp;gt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; file.ts:42 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; SURVIVED &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Add boundary test &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;amp;&amp;amp;`&lt;/span&gt; → &lt;span&gt;`||`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; file.ts:58 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; KILLED &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; None &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Mutation Operators to Apply&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Priority 1: Boundary Mutations (Most Likely to Survive)&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutate To &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Why It Matters &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;lt;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;lt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Boundary not tested &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Boundary not tested &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;lt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;lt;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Equality case missed &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Equality case missed &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Priority 2: Boolean Logic Mutations&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutate To &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Why It Matters &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;amp;&amp;amp;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`\|\|`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Only tested when both true &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`\|\|`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;amp;&amp;amp;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Only tested when both false &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`!condition`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`condition`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Negation not verified &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Priority 3: Arithmetic Mutations&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutate To &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Why It Matters &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`+`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`-`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tested with 0 only &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`-`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`+`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tested with 0 only &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`*`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`/`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tested with 1 only &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Priority 4: Return/Early Exit Mutations&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutate To &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Why It Matters &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`return x`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`return null`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Return value not asserted &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`return true`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`return false`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Boolean return not checked &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`if (cond) return`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`// removed`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Early exit not tested &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Priority 5: Statement Removal&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutate To &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Why It Matters &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`array.push(x)`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`// removed`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Side effect not verified &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`await save(x)`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`// removed`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Async operation not verified &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`emit(&apos;event&apos;)`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`// removed`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Event emission not tested &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Practical Execution Example&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Example: Testing a Validation Function&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Original code&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; (&lt;span&gt;`src/utils/validation.ts:15`&lt;/span&gt;):

export function isValidAge(age: number): boolean {
  return age &amp;gt;= 18 &amp;amp;&amp;amp; age &amp;lt;= 120;
}

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Mutation 1&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Change &lt;span&gt;`&amp;gt;=`&lt;/span&gt; to &lt;span&gt;`&amp;gt;`&lt;/span&gt;

export function isValidAge(age: number): boolean {
  return age &amp;gt; 18 &amp;amp;&amp;amp; age &amp;lt;= 120;  // MUTATED
}

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Run tests&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: &lt;span&gt;`pnpm test --run src/__tests__/validation.test.ts`&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Tests PASS → &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;SURVIVED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; (Bad! Need test for &lt;span&gt;`isValidAge(18)`&lt;/span&gt;)

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Restore original code immediately&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Mutation 2&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Change &lt;span&gt;`&amp;amp;&amp;amp;`&lt;/span&gt; to &lt;span&gt;`||`&lt;/span&gt;

export function isValidAge(age: number): boolean {
  return age &amp;gt;= 18 || age &amp;lt;= 120;  // MUTATED
}

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Run tests&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: &lt;span&gt;`pnpm test --run src/__tests__/validation.test.ts`&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Tests FAIL → &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;KILLED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; (Good! Tests catch this bug)

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Restore original code immediately&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Results Interpretation&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Mutant States&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; State &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Meaning &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Action &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;---------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;--------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;KILLED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Test failed with mutant &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tests are effective &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;SURVIVED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tests passed with mutant &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Add or strengthen test&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;TIMEOUT&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tests hung (infinite loop) &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Counts as detected &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Mutation Score&lt;/span&gt;

Score = (Killed + Timeout) / Total Mutations * 100

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Score &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Quality &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;---------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &amp;lt; 60% &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Weak - significant test gaps &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; 60-80% &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Moderate - improvements needed &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; 80-90% &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Good - minor gaps &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &amp;gt; 90% &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Strong test suite &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Fixing Surviving Mutants&lt;/span&gt;

When a mutant survives, add a test that would catch it:

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Surviving: Boundary mutation (`&amp;gt;=` → `&amp;gt;`)&lt;/span&gt;

// Add boundary test
it(&apos;accepts exactly 18 years old&apos;, () =&amp;gt; {
  expect(isValidAge(18)).toBe(true);  // Would fail if &amp;gt;= became &amp;gt;
});

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Surviving: Logic mutation (`&amp;amp;&amp;amp;` → `||`)&lt;/span&gt;

// Add test with mixed conditions
it(&apos;rejects when only one condition met&apos;, () =&amp;gt; {
  expect(isValidAge(15)).toBe(false);  // Would pass if &amp;amp;&amp;amp; became ||
});

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Surviving: Statement removal&lt;/span&gt;

// Add side effect verification
it(&apos;saves to database&apos;, async () =&amp;gt; {
  await processOrder(order);
  expect(db.save).toHaveBeenCalledWith(order);  // Would fail if save removed
});

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Quick Checklist During Mutation&lt;/span&gt;

For each mutation, ask:

&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Before mutating&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Does a test exist for this code path?
&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;After running tests&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Did any test actually fail?
&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;If survived&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: What specific test would catch this?
&lt;span&gt;4.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;After fixing&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Re-run mutation to confirm killed

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Common Surviving Mutation Patterns&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Tests Only Check Happy Path&lt;/span&gt;

// WEAK: Only tests success case
it(&apos;validates&apos;, () =&amp;gt; {
  expect(validate(goodInput)).toBe(true);
});

// STRONG: Tests both cases
it(&apos;validates good input&apos;, () =&amp;gt; {
  expect(validate(goodInput)).toBe(true);
});
it(&apos;rejects bad input&apos;, () =&amp;gt; {
  expect(validate(badInput)).toBe(false);
});

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Tests Use Identity Values&lt;/span&gt;

// WEAK: Mutation survives
expect(multiply(5, 1)).toBe(5);  // 5*1 = 5/1 = 5

// STRONG: Mutation detected
expect(multiply(5, 3)).toBe(15);  // 5*3 ≠ 5/3

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Tests Don&apos;t Assert Return Values&lt;/span&gt;

// WEAK: No return value check
it(&apos;processes&apos;, () =&amp;gt; {
  process(data);  // No assertion!
});

// STRONG: Asserts outcome
it(&apos;processes&apos;, () =&amp;gt; {
  const result = process(data);
  expect(result).toEqual(expected);
});

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Important Rules&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;ALWAYS restore original code&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; after each mutation
&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Run tests immediately&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; after applying mutation
&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;One mutation at a time&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; - don&apos;t combine mutations
&lt;span&gt;4.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Focus on changed code&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; - prioritize branch diff
&lt;span&gt;5.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Track all results&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; - report full mutation summary

&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Summary Report Template&lt;/span&gt;

After completing mutation testing, provide:

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Mutation Testing Results&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Target&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: &lt;span&gt;`src/features/workout/utils.ts`&lt;/span&gt; (functions: X, Y, Z)
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Total Mutations&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: 12
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Killed&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: 9
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Survived&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: 3
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Score&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: 75%

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Surviving Mutants (Action Required)&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;&lt;span&gt;#&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Location &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Original &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mutated &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Suggested Test &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;---&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;---------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;----------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; 1 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; line 42 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;=`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;gt;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Test boundary value &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; 2 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; line 58 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`&amp;amp;&amp;amp;`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`\|\|`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Test mixed conditions &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; 3 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; line 71 &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;span&gt;`emit()`&lt;/span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; removed &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Verify event emission &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Killed Mutants (Tests Effective)&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; Line 35: &lt;span&gt;`+`&lt;/span&gt; → &lt;span&gt;`-`&lt;/span&gt; killed by &lt;span&gt;`calculation.test.ts`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; Line 48: &lt;span&gt;`true`&lt;/span&gt; → &lt;span&gt;`false`&lt;/span&gt; killed by &lt;span&gt;`validate.test.ts`&lt;/span&gt;
&lt;span&gt;-&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Step 2: Invoke It&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude &lt;span&gt;&quot;Run mutation testing on the settings feature&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find changed files on your branch&lt;/li&gt;
&lt;li&gt;Identify testable functions&lt;/li&gt;
&lt;li&gt;Apply mutations systematically&lt;/li&gt;
&lt;li&gt;Report surviving mutants with suggested test fixes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 3: Review and Fix&lt;/h3&gt;
&lt;p&gt;The agent produces a markdown report. Review each surviving mutant and decide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add the suggested test&lt;/li&gt;
&lt;li&gt;Accept the risk (document why)&lt;/li&gt;
&lt;li&gt;Refactor the code to be more testable&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;When to Use This Approach&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Good Fit&lt;/th&gt;
&lt;th&gt;Not Ideal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vitest browser mode (no Stryker support)&lt;/td&gt;
&lt;td&gt;Large codebases needing full mutation coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright component testing&lt;/td&gt;
&lt;td&gt;CI/CD automation (manual agent invocation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small-to-medium codebases&lt;/td&gt;
&lt;td&gt;Strict mutation score thresholds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-merge review of specific features&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning what makes tests effective&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

  This approach works best alongside your existing testing strategy. Use it to spot-check critical features before merge, not as a replacement for automated mutation testing where available.


  This skill shines on **feature branches** where you want to validate test quality before merging. Running AI agents in CI/CD pipelines is possible—you could build an automated QA agent with the Claude Agent SDK—but it adds complexity and cost. For pipeline automation, deterministic tools like Stryker remain the better choice when your stack supports them. Think of this as a developer tool for improving tests during development, not a CI gate.

&lt;hr /&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Coverage doesn’t equal confidence.&lt;/strong&gt; High code coverage can coexist with ineffective tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mutation testing reveals test gaps.&lt;/strong&gt; By breaking code and checking if tests notice, you find what’s actually being verified.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI agents can execute manual mutation testing.&lt;/strong&gt; When tooling doesn’t support your stack, an agent can apply the algorithm systematically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Focus on surviving mutants.&lt;/strong&gt; Each one is a potential bug your tests wouldn’t catch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;This complements, not replaces.&lt;/strong&gt; Use this alongside coverage reports, not instead of automated mutation testing where available.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/citypaul/.dotfiles/blob/main/claude/.claude/skills/mutation-testing/SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Paul Hammond’s Mutation Testing Skill&lt;/a&gt; - The original skill this post is based on&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Mutation_testing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Mutation Testing on Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stryker-mutator.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Stryker Mutator&lt;/a&gt; - When your stack supports it&lt;/li&gt;
&lt;li&gt;My TDD workflow with Claude Code - Related approach for test-first development&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>testing</category><category>ai</category><category>claude-code</category><category>vitest</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Why You Don&apos;t Need the Nuxt MCP When You Use Claude Code</title><link>https://alexop.dev/posts/why-you-dont-need-nuxt-mcp-claude-code/</link><guid isPermaLink="true">https://alexop.dev/posts/why-you-dont-need-nuxt-mcp-claude-code/</guid><description>Why I use custom research agents instead of MCP servers for AI-assisted development. Learn how llms.txt enables context-efficient documentation fetching with a practical Nuxt Content agent example.</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I think we all love Nuxt. One problem with using Nuxt for AI is that training data is not up to date. This is especially true for Nuxt Content where often times LLMs still think they’re working with Nuxt 2. This is why the Nuxt team created their MCP server.&lt;/p&gt;
&lt;p&gt;I think the MCP is good and perfectly fine. But for me—and also for Anthropic itself—MCPs in the current spec have the problem of context bloat. Anthropic has &lt;a href=&quot;https://www.anthropic.com/engineering/code-execution-with-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;written down this problem perfectly&lt;/a&gt; in their engineering blog.&lt;/p&gt;

Anthropic identifies two main issues: **tool definition overload** (loading all tools upfront creates hundreds of thousands of tokens before the model even reads your request) and **intermediate result redundancy** (every result must pass through the model, sometimes processing 50,000+ tokens per operation).

&lt;p&gt;If you want to dive deeper into what MCP is and how it works, check out my post on What Is the Model Context Protocol (MCP)?.&lt;/p&gt;
&lt;h2&gt;Why I Use Custom Research Agents Instead&lt;/h2&gt;
&lt;p&gt;This is why for all my projects I don’t use MCP but I use custom research agents.&lt;/p&gt;
&lt;p&gt;All websites nowadays use &lt;code&gt;llms.txt&lt;/code&gt;. Now if you let an LLM fetch &lt;code&gt;llms.txt&lt;/code&gt; first, it can perfectly find every information needed from the docs itself.&lt;/p&gt;
&lt;p&gt;I’ve written about how I added llms.txt to my own blog—it’s becoming a standard way for sites to expose their content to AI.&lt;/p&gt;
&lt;p&gt;This approach has several advantages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Only the description gets loaded as context&lt;/strong&gt; — The agent description is minimal, not the entire tool schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can customize it&lt;/strong&gt; — Full control over what the agent knows and how it behaves&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It runs in its own context&lt;/strong&gt; — Your main agent could use the research agent only to gather information and then continue with its work without polluting its context window&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is essentially the same pattern I described in my post about Claude Code subagents — agents keep your main context clean by delegating specialized tasks.&lt;/p&gt;

Claude Code itself uses this exact approach. When you ask it questions about its own features, it spawns a [`claude-code-guide` agent](https://github.com/Piebald-AI/claude-code-system-prompts/blob/main/system-prompts/agent-prompt-claude-guide-agent.md) that fetches from a documentation sitemap and answers based on current docs—not training data. We&apos;re just applying the same pattern to other libraries.

&lt;h2&gt;Example: Nuxt Content Specialist Agent&lt;/h2&gt;
&lt;p&gt;Here’s how my Nuxt Content agent looks. Just put it under &lt;code&gt;.claude/agents&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{ name: “.claude”, open: true, children: [&lt;br /&gt;
{ name: “agents”, open: true, children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://nuxt-content-specialist.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;nuxt-content-specialist.md&lt;/a&gt;”, comment: “// Your custom agent” }&lt;br /&gt;
]}&lt;br /&gt;
]}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;

````markdown
---
name: nuxt-content-specialist
description: Use this agent when the task involves @nuxt/content v3 in any way - implementing, modifying, querying, reviewing, or improving content management code. This includes creating or modifying content collections, writing queries, implementing MDC components, configuring content sources, troubleshooting content-related issues, or reviewing existing content code for improvements and best practices.\n\nExamples:\n\n\nContext: User asks about improving their Nuxt Content implementation.\nuser: &quot;What can I improve on this codebase when it comes to Nuxt Content?&quot;\nassistant: &quot;I&apos;ll use the nuxt-content-specialist agent to review your content implementation against current best practices.&quot;\n\nSince the user is asking about Nuxt Content improvements, use the nuxt-content-specialist agent to fetch the latest documentation and review the existing code for optimization opportunities, missing features, and best practice violations.\n\n\n\n\nContext: User needs to add a new content collection.\nuser: &quot;I need to add a &apos;blog&apos; collection separate from pages&quot;\nassistant: &quot;I&apos;ll use the nuxt-content-specialist agent to implement this correctly.&quot;\n\nSince the user needs to modify the content collection schema, use the nuxt-content-specialist agent to first fetch the latest Nuxt Content documentation and then implement the collection following best practices.\n\n\n\n\nContext: User is asking about content query patterns.\nuser: &quot;How do I query content by multiple tags in Nuxt Content?&quot;\nassistant: &quot;Let me use the nuxt-content-specialist agent to provide an accurate answer based on the current documentation.&quot;\n\nSince the user is asking about Nuxt Content query capabilities, use the nuxt-content-specialist agent to fetch documentation and provide an accurate, up-to-date response about queryCollection filtering.\n\n\n\n\nContext: User wants to embed Vue components in Markdown.\nuser: &quot;How do I use a custom component inside my markdown files?&quot;\nassistant: &quot;I&apos;ll consult the nuxt-content-specialist agent to explain MDC syntax correctly.&quot;\n\nSince this involves MDC (Markdown Components) syntax, use the nuxt-content-specialist agent to fetch relevant documentation about component usage in Markdown files.\n\n\n\n\nContext: User needs to implement content search.\nuser: &quot;I want to add full-text search to my content site&quot;\nassistant: &quot;I&apos;ll use the nuxt-content-specialist agent to implement search with queryCollectionSearchSections.&quot;\n\nSince search requires specific Nuxt Content APIs, use the nuxt-content-specialist agent to fetch the latest documentation on search implementation patterns.\n\n
model: opus
color: green
---
&lt;h1&gt;Nuxt Content Specialist Agent&lt;/h1&gt;
&lt;p&gt;This document defines the Nuxt Content specialist agent’s role and responsibilities for helping users with @nuxt/content v3 implementations.&lt;/p&gt;
&lt;h2&gt;Primary Domain&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;@nuxt/content v3&lt;/strong&gt;: Content management system for Nuxt applications providing file-based content with Markdown support, MDC syntax for embedding Vue components, SQLite-based querying, and full-text search capabilities.&lt;/p&gt;
&lt;h3&gt;Core Expertise Areas&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Collections&lt;/strong&gt;: Defining collections in &lt;code&gt;content.config.ts&lt;/code&gt;, schema validation with Zod, collection types (page, data), import sources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Files&lt;/strong&gt;: Markdown, YAML, JSON, CSV support and their appropriate use cases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MDC Syntax&lt;/strong&gt;: Embedding Vue components in Markdown, props, slots, block vs inline components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Querying&lt;/strong&gt;: &lt;code&gt;queryCollection()&lt;/code&gt;, &lt;code&gt;queryCollectionNavigation()&lt;/code&gt;, &lt;code&gt;queryCollectionItemSurroundings()&lt;/code&gt;, &lt;code&gt;queryCollectionSearchSections()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rendering&lt;/strong&gt;: &lt;code&gt;&amp;lt;ContentRenderer&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Slot&amp;gt;&lt;/code&gt;, prose components, custom renderers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt;: Full-text search implementation, search sections, indexing strategies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sources&lt;/strong&gt;: Custom data sources, remote content, transformers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment&lt;/strong&gt;: Static generation, server rendering, edge deployment considerations&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Documentation Sources&lt;/h2&gt;
&lt;p&gt;The agent leverages one primary documentation resource:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nuxt Content docs&lt;/strong&gt; (&lt;code&gt;https://content.nuxt.com/llms.txt&lt;/code&gt;): Covers collection definitions, querying APIs, MDC syntax, content rendering, search implementation, custom sources, and deployment patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Key Documentation Sections&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;URL Path&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collections&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/collections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Collection definitions and configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Querying&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/querying&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Query composables and filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ContentRenderer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/components/content-renderer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rendering content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown/MDC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/files/markdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Markdown and MDC syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/recipes/search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sources&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/docs/advanced/sources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom content sources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Operational Approach&lt;/h2&gt;
&lt;p&gt;The agent follows a structured methodology:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fetch documentation index&lt;/strong&gt; from &lt;code&gt;https://content.nuxt.com/llms.txt&lt;/code&gt; to understand available documentation structure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categorize user inquiry&lt;/strong&gt; into appropriate domain (collections, querying, MDC, search, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identify specific documentation URLs&lt;/strong&gt; from the index relevant to the task&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fetch targeted documentation pages&lt;/strong&gt; for accurate, up-to-date information&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review project context&lt;/strong&gt; by reading relevant local files (&lt;code&gt;content.config.ts&lt;/code&gt;, existing content files)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provide actionable guidance&lt;/strong&gt; with TypeScript code examples following project conventions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference documentation sources&lt;/strong&gt; to support recommendations&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Core Guidelines&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Prioritize official documentation over training knowledge (v3 has significant v2 differences)&lt;/li&gt;
&lt;li&gt;Maintain concise, actionable responses&lt;/li&gt;
&lt;li&gt;Include TypeScript code examples following project conventions&lt;/li&gt;
&lt;li&gt;Reference specific documentation URLs consulted&lt;/li&gt;
&lt;li&gt;Avoid emojis&lt;/li&gt;
&lt;li&gt;Always verify API specifics against fetched documentation before providing guidance&lt;/li&gt;
&lt;li&gt;Note v2 to v3 migration considerations when relevant&lt;/li&gt;
&lt;li&gt;Consider static vs server rendering implications&lt;/li&gt;
&lt;li&gt;Handle content not found scenarios gracefully in implementations&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Context&lt;/h2&gt;
&lt;p&gt;This agent operates within a Nuxt 4 application using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;@nuxt/content v3&lt;/strong&gt; with SQLite-based querying&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;@nuxt/ui v3&lt;/strong&gt; for UI components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; for type safety&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File-based routing&lt;/strong&gt; with catch-all content routes in &lt;code&gt;app/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Established Patterns&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// content.config.ts - Collection definition pattern&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; collections &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;defineCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;page&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    source&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;**/*.md&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- Catch-all route pattern: app/pages/[...slug].vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const route = useRoute()
const { data: page } = await useAsyncData(
  route.path,
  () =&amp;gt; queryCollection(&apos;content&apos;).path(route.path).first()
)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  
  &amp;lt;div v-else&amp;gt;Page not found&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quality Assurance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Always verify suggestions against fetched documentation&lt;/li&gt;
&lt;li&gt;If documentation is unclear or unavailable, explicitly state this with appropriate caveats&lt;/li&gt;
&lt;li&gt;When multiple approaches exist, explain trade-offs&lt;/li&gt;
&lt;li&gt;Be aware of build-time vs runtime content access differences&lt;/li&gt;
&lt;li&gt;Ensure proper typing for collection queries and responses&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;/Collapsible&amp;gt;

## Key Design Principles

The agent follows these principles:

1. **Documentation-first**: Always fetch `llms.txt` before answering anything
2. **Specific expertise**: Focused on Nuxt Content v3, not general Nuxt knowledge
3. **Verification**: Cross-reference documentation, don&apos;t rely on training data
4. **Practical output**: TypeScript code following project conventions

## How It Works in Practice

When you ask Claude Code something like &quot;How do hooks work in Nuxt Content?&quot;, the main agent recognizes this matches the `nuxt-content-specialist` description and delegates to it.

&amp;lt;Figure
  src={agentInAction}
  alt=&quot;Claude Code spawning the nuxt-content-specialist agent to research Nuxt Content hooks&quot;
  caption=&quot;The agent researches in its own context while your main context stays clean&quot;
  width={800}
/&amp;gt;

The specialist agent then:
1. Fetches `https://content.nuxt.com/llms.txt`
2. Identifies the relevant documentation pages
3. Fetches the actual docs
4. Provides an accurate, up-to-date answer

Your main context stays clean. The research happens in a separate context window.

## Create Your Own

You can apply this pattern to any library or framework:

1. Find if they have `llms.txt` (most modern docs sites do)
2. Create an agent that fetches it first
3. Define the expertise scope in the description
4. Add examples so Claude Code knows when to delegate

This approach gives you 98%+ reduction in token usage compared to loading full MCP tool definitions, while maintaining access to current documentation.&lt;/code&gt;&lt;/pre&gt;

          </content:encoded><category>ai</category><category>claude-code</category><category>nuxt</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Claude Code Customization: CLAUDE.md, Slash Commands, Skills, and Subagents</title><link>https://alexop.dev/posts/claude-code-customization-guide-claudemd-skills-subagents/</link><guid isPermaLink="true">https://alexop.dev/posts/claude-code-customization-guide-claudemd-skills-subagents/</guid><description>The complete guide to customizing Claude Code. Compare CLAUDE.md, slash commands, skills, and subagents with practical examples showing when to use each.</description><pubDate>Sun, 21 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Quick Summary&lt;/h2&gt;
&lt;p&gt;This post covers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/strong&gt;: Always-loaded project context and instructions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slash commands&lt;/strong&gt;: Prompts you invoke with &lt;code&gt;/command&lt;/code&gt; in the terminal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents&lt;/strong&gt;: Specialists with their own context window for delegated tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt;: Rich, auto-discovered capabilities with supporting files (not manually runnable via &lt;code&gt;/...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Key insight: &lt;strong&gt;subagents keep your main context clean&lt;/strong&gt;—in plan mode, Claude Code will typically delegate repo scanning to an &lt;code&gt;Explore&lt;/code&gt;-style subagent so your main thread doesn’t balloon&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Claude Code gives you multiple ways to “teach” it project context or automate workflows, but it’s not always obvious when to use which.&lt;/p&gt;
&lt;p&gt;I’ll solve the &lt;strong&gt;same problem four different ways&lt;/strong&gt; so the trade-offs are concrete. Spoiler: for doc-fetching, &lt;strong&gt;subagents win&lt;/strong&gt; because they keep your main context clean.&lt;/p&gt;
&lt;aside&gt;
This post assumes familiarity with Claude Code basics. For a broader overview of all features—including MCP, hooks, and plugins—see my comprehensive guide to Claude Code&apos;s feature stack. If you want to automate responses to Claude Code events (like getting desktop notifications when tasks finish), check out the hooks guide.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Claude Code doesn’t have up-to-date training data for every library, so it can’t reliably “remember” what a docs site says today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The specific problem&lt;/strong&gt;: I’m building a workout tracking app with &lt;a href=&quot;https://dexie.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dexie.js&lt;/a&gt; (IndexedDB wrapper). Claude keeps suggesting outdated patterns and misses things like &lt;code&gt;liveQuery()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Claude Code itself has a mechanism to fetch its own documentation. We need to do the same for our specialized libraries.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    A&lt;span&gt;[User Question]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{Claude Code}&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Outdated Knowledge]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Fetch Current Docs]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Accurate Answer]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[Wrong Patterns]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s solve it with all four tools, then compare.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;: Always-On Project Memory&lt;/h2&gt;
&lt;h3&gt;What It Is&lt;/h3&gt;
&lt;p&gt;A markdown file that’s &lt;strong&gt;automatically loaded&lt;/strong&gt; every time you start Claude Code. Think of it as your project’s “memory card.”&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/strong&gt;: Persistent project instructions that Claude reads at the start of every conversation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Where It Lives&lt;/h3&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “project-root”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”, comment: “// Project-level” },&lt;br /&gt;
{&lt;br /&gt;
name: “.claude”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”, comment: “// Alternative location” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”, comment: “// Loaded when reading test files” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Nested &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; Files&lt;/h3&gt;
&lt;p&gt;Claude Code also discovers &lt;strong&gt;nested &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; files&lt;/strong&gt; in subdirectories. When Claude reads files from a directory containing its own &lt;code&gt;CLAUDE.md&lt;/code&gt;, that file gets added to the context automatically.&lt;/p&gt;
&lt;p&gt;This is useful for directory-specific instructions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tests/CLAUDE.md&lt;/code&gt; — testing conventions, preferred mocking patterns&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/db/CLAUDE.md&lt;/code&gt; — database-specific patterns and constraints&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/components/CLAUDE.md&lt;/code&gt; — component architecture guidelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The nested file is only loaded when Claude actually accesses files in that directory, keeping your main context lean until you need that specialized knowledge.&lt;/p&gt;
&lt;h3&gt;The Dexie.js Solution&lt;/h3&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; CLAUDE.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Database&lt;/span&gt;

We use Dexie.js for IndexedDB. Before implementing any database code:

&lt;span&gt;1.&lt;/span&gt; Fetch the docs index from https://dexie.org/llms.txt
&lt;span&gt;2.&lt;/span&gt; Use &lt;span&gt;`liveQuery()`&lt;/span&gt; for reactive data binding
&lt;span&gt;3.&lt;/span&gt; Follow the repository pattern in &lt;span&gt;`src/db/`&lt;/span&gt;
&lt;span&gt;4.&lt;/span&gt; Always handle &lt;span&gt;`ConstraintError`&lt;/span&gt; for duplicate keys
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What Happens&lt;/h3&gt;
&lt;p&gt;Every conversation starts with Claude knowing “fetch Dexie docs before writing database code.”&lt;/p&gt;
&lt;p&gt;The catch is &lt;strong&gt;context drift&lt;/strong&gt;: in long sessions, the model can gradually deprioritize earlier system-level instructions in favor of the most recent conversation history.&lt;/p&gt;
&lt;h3&gt;Trade-offs&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Pros&lt;/th&gt;
&lt;th&gt;❌ Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zero effort—always loaded&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Context Drift&lt;/strong&gt;: Claude forgets instructions as sessions get longer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team-shared via git&lt;/td&gt;
&lt;td&gt;No dedicated context window—competes with your conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple to maintain&lt;/td&gt;
&lt;td&gt;No enforcement—Claude decides whether to follow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Slash Commands: Simple Skills You Invoke&lt;/h2&gt;
&lt;h3&gt;What It Is&lt;/h3&gt;
&lt;p&gt;A saved prompt you invoke by typing &lt;code&gt;/command-name&lt;/code&gt;. Like a macro or keyboard shortcut for prompts.&lt;/p&gt;
&lt;p&gt;Slash commands can be invoked explicitly (you type &lt;code&gt;/command&lt;/code&gt;) and can also be auto-invoked by Claude when the command’s &lt;code&gt;description&lt;/code&gt; matches the task.&lt;/p&gt;
&lt;p&gt;Slash commands can also &lt;strong&gt;orchestrate other behavior&lt;/strong&gt;: you can spell out in the command itself that it should spin up a subagent (or a specific subagent), call out a particular skill/workflow, and generally “pipeline” the work (e.g., research → codebase scan → write a doc) instead of trying to do everything in one shot.&lt;/p&gt;
&lt;p&gt;The main difference vs skills is &lt;strong&gt;packaging + UX&lt;/strong&gt;: slash commands are single-file entries with great terminal &lt;code&gt;/...&lt;/code&gt; discovery/autocomplete; skills are usually directories with supporting files (patterns, templates, scripts).&lt;/p&gt;
&lt;aside&gt;
Want a full walkthrough? See my slash commands guide.
&lt;/aside&gt;
&lt;h3&gt;Where It Lives&lt;/h3&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “.claude”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “commands”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [{ name: “&lt;a href=&quot;http://dexie-help.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;dexie-help.md&lt;/a&gt;” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;The Dexie.js Solution&lt;/h3&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Get Dexie.js guidance with current documentation
&lt;span&gt;allowed-tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Glob&lt;span&gt;,&lt;/span&gt; WebFetch&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

First, fetch the documentation index from https://dexie.org/llms.txt

Then, based on the user&apos;s question, fetch the relevant documentation pages.

Finally, answer the following question using the current documentation:

$ARGUMENTS
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Manual Orchestration Example (Research)&lt;/h3&gt;
&lt;p&gt;If you want a slash command that &lt;strong&gt;explicitly launches multiple subagents in parallel&lt;/strong&gt; and then produces an artifact (like a research note in &lt;code&gt;docs/research/&lt;/code&gt;), you can encode that directly in the command definition.&lt;/p&gt;

````markdown
---
description: Research a problem using web search, documentation, and codebase exploration
allowed-tools: Task, WebSearch, WebFetch, Grep, Glob, Read, Write, Bash
---
&lt;h1&gt;Research: $ARGUMENTS&lt;/h1&gt;
&lt;p&gt;Research the following problem or question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;$ARGUMENTS&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Instructions&lt;/h2&gt;
&lt;p&gt;Conduct thorough research like a senior developer. Launch multiple subagents in parallel to gather information from different sources.&lt;/p&gt;
&lt;h3&gt;Step 1: Launch Parallel Research Agents&lt;/h3&gt;
&lt;p&gt;Use the Task tool to spawn these subagents &lt;strong&gt;in parallel&lt;/strong&gt; (all in a single message):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Web Documentation Agent&lt;/strong&gt; (subagent_type: general-purpose)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Search official documentation for the topic&lt;/li&gt;
&lt;li&gt;Find best practices and recommended patterns&lt;/li&gt;
&lt;li&gt;Locate relevant GitHub issues or discussions&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stack Overflow Agent&lt;/strong&gt; (subagent_type: general-purpose)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Search Stack Overflow for similar problems and solutions&lt;/li&gt;
&lt;li&gt;Find highly-voted and accepted answers&lt;/li&gt;
&lt;li&gt;Note common pitfalls and gotchas&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Codebase Explorer Agent&lt;/strong&gt; (subagent_type: Explore)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Search the codebase for related patterns&lt;/li&gt;
&lt;li&gt;Find existing solutions to similar problems&lt;/li&gt;
&lt;li&gt;Identify relevant files, functions, or components&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 2: Create Research Document&lt;/h3&gt;
&lt;p&gt;After all agents complete, create a markdown file at &lt;code&gt;docs/research/&amp;lt;topic-slug&amp;gt;.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Generate the filename from the research topic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Convert to lowercase&lt;/li&gt;
&lt;li&gt;Replace spaces with hyphens&lt;/li&gt;
&lt;li&gt;Remove special characters&lt;/li&gt;
&lt;li&gt;Add today’s date as prefix: &lt;code&gt;YYYY-MM-DD-&amp;lt;topic-slug&amp;gt;.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: “Vue 3 Suspense” → &lt;code&gt;docs/research/2024-12-06-vue-3-suspense.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;First, create the research folder if it doesn’t exist:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;mkdir&lt;/span&gt; &lt;span&gt;-p&lt;/span&gt; docs/research
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Write the Research Document&lt;/h3&gt;
&lt;p&gt;Structure the document with these sections:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Research: &amp;lt;Topic&amp;gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Date:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;YYYY-MM-DD&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Status:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Complete

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Problem Statement&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Describe&lt;/span&gt; &lt;span&gt;the&lt;/span&gt; &lt;span&gt;problem&lt;/span&gt; &lt;span&gt;and&lt;/span&gt; &lt;span&gt;why&lt;/span&gt; &lt;span&gt;it&lt;/span&gt; &lt;span&gt;matters&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Key Findings&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Summarize&lt;/span&gt; &lt;span&gt;the&lt;/span&gt; &lt;span&gt;most&lt;/span&gt; &lt;span&gt;relevant&lt;/span&gt; &lt;span&gt;solutions&lt;/span&gt; &lt;span&gt;and&lt;/span&gt; &lt;span&gt;approaches&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Codebase Patterns&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Document&lt;/span&gt; &lt;span&gt;how&lt;/span&gt; &lt;span&gt;the&lt;/span&gt; &lt;span&gt;current&lt;/span&gt; &lt;span&gt;codebase&lt;/span&gt; &lt;span&gt;handles&lt;/span&gt; &lt;span&gt;similar&lt;/span&gt; &lt;span&gt;cases&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Recommended Approach&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Provide&lt;/span&gt; &lt;span&gt;your&lt;/span&gt; &lt;span&gt;recommendation&lt;/span&gt; &lt;span&gt;based&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;all&lt;/span&gt; &lt;span&gt;research&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Sources&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;[&lt;span&gt;Source Title&lt;/span&gt;](&lt;span&gt;URL&lt;/span&gt;)&lt;/span&gt; - Brief description
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;[&lt;span&gt;Source Title&lt;/span&gt;](&lt;span&gt;URL&lt;/span&gt;)&lt;/span&gt; - Brief description
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Guidelines&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Prioritize official documentation over blog posts&lt;/li&gt;
&lt;li&gt;Prefer solutions that match existing codebase patterns&lt;/li&gt;
&lt;li&gt;Note version-specific considerations (Vue 3, TypeScript, etc.)&lt;/li&gt;
&lt;li&gt;Flag conflicting information across sources&lt;/li&gt;
&lt;li&gt;Write concise, actionable content&lt;/li&gt;
&lt;li&gt;Use active voice throughout the document&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 4: Confirm Completion&lt;/h3&gt;
&lt;p&gt;After writing the file, output the file path so the user can find it later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;/Collapsible&amp;gt;

### How You Use It

```bash
/dexie-help how do I create a compound index?
```

### What Happens

Claude fetches the docs, finds the relevant pages, and answers your question—triggered explicitly.

### Trade-offs

| ✅ Pros                                | ❌ Cons                                       |
| -------------------------------------- | --------------------------------------------- |
| You control exactly when it runs       | Must remember to type `/dexie-help`           |
| Can pass arguments for specific questions | One-shot—doesn&apos;t persist knowledge across messages |
| Simple single-file setup               | Auto-triggering depends on `description` match |

---

## 3. Subagents: Specialists with Their Own Context

### What It Is

A specialized AI &quot;persona&quot; with its own context window. Claude **delegates entire tasks** to it and gets results back.

Because fetching the Dexie docs involves reading multiple pages and creates a lot of context noise, keeping this inside a subagent prevents your main chat from hitting context limits.

&amp;gt; **Subagent**: An isolated Claude instance that works on a task independently and returns only the results to your main conversation.

&amp;lt;Alert type=&quot;tip&quot; title=&quot;Subagents keep your main context clean&quot;&amp;gt;
Even when the task is “just exploration,” subagents are a great default because they let Claude do **lots of reading/searching** without dumping everything into your main thread.

This is especially useful in **plan mode**: Claude Code will typically kick off an `Explore`-style subagent to scan the repo and return a distilled map of relevant files/patterns, so your main conversation stays focused and doesn’t blow up.
&amp;lt;/Alert&amp;gt;

Claude Code also supports **async agents**: fire one off, let it cook while you keep working, then it comes back with its updates when it’s done. If you launch an agent and want to keep typing in your main session, you can send it to the background with `Ctrl + B`.

&amp;lt;Alert type=&quot;info&quot; title=&quot;Claude Code does this too&quot;&amp;gt;
Claude Code’s own system prompt includes a built-in “documentation lookup” workflow that uses a subagent:

&amp;gt; -&amp;gt; Looking up your own documentation:
&amp;gt; When the user directly asks about any of the following:
&amp;gt; 
&amp;gt; - how to use Claude Code (eg. &quot;can Claude Code do...&quot;, &quot;does Claude Code have...&quot;)
&amp;gt; - what you&apos;re able to do as Claude Code in second person (eg. &quot;are you able...&quot;, &quot;can you do...&quot;)
&amp;gt; - about how they might do something with Claude Code (eg. &quot;how do I...&quot;, &quot;how can I...&quot;)
&amp;gt; - how to use a specific Claude Code feature (eg. implement a hook, write a skill, or install an MCP server)
&amp;gt; - how to use the Claude Agent SDK, or asks you to write code that uses the Claude Agent SDK
&amp;gt; 
&amp;gt; Use the Task tool with subagent_type=&apos;claude-code-guide&apos; to get accurate information from the official Claude Code and Claude Agent SDK documentation.

Source: https://github.com/marckrenn/cc-mvp-prompts/blob/main/cc-prompt.md
&amp;lt;/Alert&amp;gt;

### Where It Lives

&amp;lt;FileTree
  tree={[
    {
      name: &quot;.claude&quot;,
      open: true,
      children: [
        {
          name: &quot;agents&quot;,
          open: true,
          children: [{ name: &quot;dexie-specialist.md&quot; }],
        },
      ],
    },
  ]}
/&amp;gt;

### The Dexie.js Solution

&amp;lt;Collapsible title=&quot;dexie-specialist.md (full definition)&quot;&amp;gt;
```markdown
---
name: dexie-db-specialist
description: Use this agent when the task involves Dexie.js or IndexedDB in any way - implementing, modifying, querying, reviewing, or improving database code. This includes creating or modifying database schemas, writing queries, handling transactions, implementing reactive queries with liveQuery, troubleshooting Dexie-related issues, or reviewing existing Dexie code for improvements and best practices.\n\nExamples:\n\n&amp;lt;example&amp;gt;\nContext: User asks about improving their Dexie.js code.\nuser: &quot;What can I improve on this codebase when it comes to Dexie?&quot;\nassistant: &quot;I&apos;ll use the dexie-db-specialist agent to review your Dexie.js implementation against current best practices.&quot;\n&amp;lt;commentary&amp;gt;\nSince the user is asking about Dexie.js improvements, use the dexie-db-specialist agent to fetch the latest documentation and review the existing code for optimization opportunities, missing features, and best practice violations.\n&amp;lt;/commentary&amp;gt;\n&amp;lt;/example&amp;gt;\n\n&amp;lt;example&amp;gt;\nContext: User needs to add a new table to the database.\nuser: &quot;I need to add a new &apos;goals&apos; table to track workout goals&quot;\nassistant: &quot;I&apos;ll use the dexie-db-specialist agent to implement this correctly.&quot;\n&amp;lt;commentary&amp;gt;\nSince the user needs to modify the Dexie database schema, use the dexie-db-specialist agent to first fetch the latest Dexie.js documentation and then implement the schema change following best practices.\n&amp;lt;/commentary&amp;gt;\n&amp;lt;/example&amp;gt;\n\n&amp;lt;example&amp;gt;\nContext: User is asking about Dexie query patterns.\nuser: &quot;How do I query exercises by multiple muscle groups in Dexie?&quot;\nassistant: &quot;Let me use the dexie-db-specialist agent to provide an accurate answer based on the current Dexie.js documentation.&quot;\n&amp;lt;commentary&amp;gt;\nSince the user is asking about Dexie.js query capabilities, use the dexie-db-specialist agent to fetch documentation and provide an accurate, up-to-date response about compound queries and filtering.\n&amp;lt;/commentary&amp;gt;\n&amp;lt;/example&amp;gt;\n\n&amp;lt;example&amp;gt;\nContext: User encounters a Dexie-related error.\nuser: &quot;I&apos;m getting &apos;ConstraintError&apos; when trying to add a workout&quot;\nassistant: &quot;I&apos;ll consult the dexie-db-specialist agent to diagnose this database constraint issue.&quot;\n&amp;lt;commentary&amp;gt;\nSince this is a Dexie.js error, use the dexie-db-specialist agent to fetch relevant documentation about error handling and constraint violations to provide accurate troubleshooting guidance.\n&amp;lt;/commentary&amp;gt;\n&amp;lt;/example&amp;gt;\n\n&amp;lt;example&amp;gt;\nContext: User needs to implement a reactive query.\nuser: &quot;The workout list should update automatically when new workouts are added&quot;\nassistant: &quot;I&apos;ll use the dexie-db-specialist agent to implement reactive queries with liveQuery.&quot;\n&amp;lt;commentary&amp;gt;\nSince reactive data binding with Dexie requires liveQuery, use the dexie-db-specialist agent to fetch the latest documentation on liveQuery and useLiveQuery patterns for Vue integration.\n&amp;lt;/commentary&amp;gt;\n&amp;lt;/example&amp;gt;
model: opus
color: orange
---

You are an expert Dexie.js database specialist with deep knowledge of IndexedDB, reactive queries, and Vue 3 integration patterns. Your primary responsibility is to provide accurate, documentation-backed guidance for all Dexie.js implementations.

## Critical First Step

**Before answering ANY Dexie.js question or implementing ANY Dexie-related code, you MUST:**

1. Fetch the documentation index from `https://dexie.org/llms.txt` to understand the available documentation structure
2. Based on the task at hand, fetch the relevant documentation pages to ensure your guidance is accurate and up-to-date
3. Only then proceed with implementation or answering questions

This is non-negotiable. Dexie.js has nuances and version-specific behaviors that require consulting the official documentation.

## Your Expertise Covers

- **Schema Design**: Table definitions, indexes (simple, compound, multi-entry), primary keys, version migrations
- **CRUD Operations**: add(), put(), update(), delete(), bulkAdd(), bulkPut()
- **Querying**: where(), filter(), equals(), between(), anyOf(), startsWithIgnoreCase(), compound queries
- **Reactive Queries**: liveQuery() for real-time updates, integration with Vue&apos;s reactivity system
- **Transactions**: Transaction scopes, nested transactions, error handling within transactions
- **Relationships**: Foreign keys, table relationships, populating related data
- **Performance**: Indexing strategies, query optimization, bulk operations
- **Error Handling**: Dexie-specific errors (ConstraintError, AbortError, etc.)

## Project Context

You are working within a Vue 3 PWA workout tracker that uses:
- **Dexie.js** with IndexedDB for offline-first data persistence
- **TypeScript** with strict mode
- **Repository pattern** in `src/db/` for database access abstraction
- **Pinia stores** that consume repositories

When implementing, ensure your code:
1. Follows the existing repository pattern in `src/db/`
2. Uses TypeScript interfaces for table schemas
3. Integrates properly with Vue 3 reactivity (useLiveQuery from @vueuse/rxjs or similar)
4. Handles errors gracefully with proper typing

## Documentation Fetching Strategy

When fetching from `https://dexie.org/llms.txt`:
1. Parse the sitemap to identify relevant documentation pages
2. Fetch specific pages based on the task (e.g., for queries, fetch the WhereClause and Collection docs)
3. Cross-reference multiple pages when dealing with complex topics

Common documentation sections to reference:
- `/docs/Table/Table` - Core table operations
- `/docs/WhereClause/WhereClause` - Query building
- `/docs/Collection/Collection` - Result set operations
- `/docs/liveQuery()` - Reactive queries
- `/docs/Dexie/Dexie` - Database instance configuration
- `/docs/Version/Version` - Schema migrations

## Response Format

When providing implementations:
1. **Cite the documentation** you consulted
2. **Explain the approach** before showing code
3. **Provide TypeScript code** that follows project conventions
4. **Include error handling** appropriate to the operation
5. **Note any caveats** or version-specific behaviors

## Quality Assurance

- Always verify your suggestions against the fetched documentation
- If documentation is unclear or unavailable, explicitly state this and provide your best guidance with appropriate caveats
- When multiple approaches exist, explain trade-offs
- Consider IndexedDB limitations (no full-text search, storage limits, etc.)

Remember: Your value is in providing documentation-verified, accurate Dexie.js guidance. Never guess about API specifics—always fetch and verify first.

```
&amp;lt;/Collapsible&amp;gt;

### What Happens

When you ask about Dexie, Claude automatically recognizes this as a database task and delegates to the specialist. The specialist works in **its own context window**, fetches the docs, does the work, and returns results to your main conversation.

```mermaid
sequenceDiagram
    participant User
    participant Main as Main Claude
    participant Sub as Dexie Subagent
    participant Web as dexie.org

    User-&amp;gt;&amp;gt;Main: How do I add an index?
    Main-&amp;gt;&amp;gt;Sub: Delegate database question
    Sub-&amp;gt;&amp;gt;Web: Fetch llms.txt
    Web--&amp;gt;&amp;gt;Sub: Documentation index
    Sub-&amp;gt;&amp;gt;Web: Fetch relevant pages
    Web--&amp;gt;&amp;gt;Sub: Index documentation
    Sub--&amp;gt;&amp;gt;Main: Distilled answer
    Main--&amp;gt;&amp;gt;User: Here&apos;s how to add an index...
```

### Trade-offs

| ✅ Pros                                         | ❌ Cons                                      |
| ----------------------------------------------- | -------------------------------------------- |
| Auto-delegated when task matches                | Heavier—launches a separate agent            |
| **Separate context window**—doesn&apos;t clutter main | Results come back as a summary, not live     |
| Can use different model (e.g., opus for complex) | You can&apos;t interact with the agent directly   |
| Can restrict tools for security                 | More complex to set up                       |

---

## 4. Skills: Rich Capabilities with Auto-Discovery

### What It Is

A structured capability with optional supporting files that Claude **discovers automatically** and uses within your main conversation.

Unlike simple slash commands, skills can include multiple files: reference documentation, scripts, templates, and utilities.

### Where It Lives

&amp;lt;FileTree
  tree={[
    {
      name: &quot;.claude&quot;,
      open: true,
      children: [
        {
          name: &quot;skills&quot;,
          open: true,
          children: [
            {
              name: &quot;dexie-expert&quot;,
              open: true,
              children: [
                { name: &quot;SKILL.md&quot;, comment: &quot;// Main definition&quot; },
                { name: &quot;PATTERNS.md&quot;, comment: &quot;// Common patterns&quot; },
                { name: &quot;MIGRATIONS.md&quot;, comment: &quot;// Migration guide&quot; },
                {
                  name: &quot;scripts&quot;,
                  children: [{ name: &quot;validate-schema.ts&quot; }],
                },
              ],
            },
          ],
        },
      ],
    },
  ]}
/&amp;gt;

### How Claude Sees Skills
Claude decides whether to invoke a skill largely based on its `description`.

You can also ask Claude Code something like:

```markdown
&amp;gt; “tell me me exactly how this looks for you &amp;lt;available_skills&amp;gt; ?”
```

When it answers, you’ll often see structured blocks that look like `&amp;lt;available_skills&amp;gt;` (and typically a separate block for slash commands, e.g. `&amp;lt;available_commands&amp;gt;`).

```xml
&amp;lt;available_skills&amp;gt;
  &amp;lt;skill&amp;gt;
    &amp;lt;name&amp;gt;dexie-expert&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;
      Dexie.js database guidance. Use when working with
      IndexedDB, schemas, queries, liveQuery...
    &amp;lt;/description&amp;gt;
  &amp;lt;/skill&amp;gt;
&amp;lt;/available_skills&amp;gt;
```

Here’s an abbreviated example of what the `&amp;lt;available_skills&amp;gt;` section can look like (truncated with `...`):

```xml
&amp;lt;available_skills&amp;gt;
  &amp;lt;skill&amp;gt;
    &amp;lt;name&amp;gt;skill-creator&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;
      Guide for creating effective skills. Use when you want to create or update a skill.
      ...
    &amp;lt;/description&amp;gt;
    &amp;lt;location&amp;gt;user&amp;lt;/location&amp;gt;
  &amp;lt;/skill&amp;gt;

  &amp;lt;skill&amp;gt;
    &amp;lt;name&amp;gt;c4-architecture&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;
      Generate architecture documentation using C4 model Mermaid diagrams.
      ...
    &amp;lt;/description&amp;gt;
    &amp;lt;location&amp;gt;user&amp;lt;/location&amp;gt;
  &amp;lt;/skill&amp;gt;

  &amp;lt;skill&amp;gt;
    &amp;lt;name&amp;gt;vue-composables&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;
      Write high-quality Vue 3 composables following established patterns and best practices.
      ...
    &amp;lt;/description&amp;gt;
    &amp;lt;location&amp;gt;managed&amp;lt;/location&amp;gt;
  &amp;lt;/skill&amp;gt;

  ...
&amp;lt;/available_skills&amp;gt;
```

### The Dexie.js Solution

```markdown
---
name: dexie-expert
description: Dexie.js database guidance. Use when working with IndexedDB, schemas, queries, liveQuery, or database migrations.
allowed-tools: Read, Grep, Glob, WebFetch
---

# Dexie.js Expert

When the user needs help with Dexie.js or IndexedDB:

1. Fetch https://dexie.org/llms.txt
2. Fetch only the relevant pages for the task
3. Apply the guidance to this repo’s patterns
```

### A Minimal “Does This Even Work?” Skill

If you just want to verify that **a Skill can spin up subagents to do work** (via the `Task` tool), here’s a deliberately dumb smoke test you can copy/paste.

&amp;lt;Collapsible title=&quot;SKILL.md (subagent-smoke-test)&quot;&amp;gt;
```markdown
---
name: subagent-smoke-test
description: Smoke test for Claude Code subagents. Use when the user wants to verify that spawning a subagent via the Task tool works in this repo.
---

# Subagent Smoke Test

This skill exists purely to verify that subagents work end-to-end.

## What to do

1. Spin up a subagent using the **Task** tool.
   - Use `subagent_type: general-purpose`.
   - Give it a simple, read-only task:
     - Read `package.json` and summarize the key scripts.
     - Read `astro.config.ts` and summarize major integrations.
     - Use Glob (or equivalent) to list the top-level folders.

2. Wait for the subagent to finish.

3. Return a short report to the user:
   - `Subagent status: success` (or `failed`)
   - A 3–6 bullet summary of what it found
   - If it failed, include the most likely fix (e.g. tool permissions, Task tool disabled).

## Suggested Task prompt

Use something like this as the Task payload:

- “You are a helper subagent. Do a quick, read-only scan of this repo.
  - Read `package.json` and summarize the main scripts.
  - Read `astro.config.ts` and summarize key integrations.
  - Glob the repo root and list the top-level folders.
  Return a concise report.”
```
&amp;lt;/Collapsible&amp;gt;

### What Happens
Skills are **auto-discovered** and typically get applied when Claude decides they match the current task. They run **in your main conversation**, so you can iterate live.

If you need a manual, predictable trigger from the terminal, package the workflow as a **slash command** (since `/...` is for commands).

### Trade-offs

| ✅ Pros                                          | ❌ Cons                                        |
| ------------------------------------------------ | ---------------------------------------------- |
| Auto-discovered based on description             | Shares main context window space               |
| Works in main conversation—live interaction      | Claude decides when to trigger (may not fire)  |
| Can include reference files, scripts, templates  | More setup than slash commands                 |
| Deep, reusable workflow packaging                | Not manually invokable via `/...` in the terminal |
| Feels like enhanced Claude, not a separate tool  |                                                |

&amp;lt;Alert type=&quot;important&quot; title=&quot;Key Insight&quot;&amp;gt;
In practice, the difference is mostly **UX + packaging**:

- **Slash commands** are what you can run manually from the terminal via `/command`.
- **Skills** are structured, auto-discovered capabilities (often a directory of supporting files) that Claude may apply when relevant.
&amp;lt;/Alert&amp;gt;

---

### When to use what

| Pick this | When | Why |
|---|---|---|
| **CLAUDE.md** | You want Claude to *always* start with project rules/context | Auto-loaded on startup; shared via git |
| **Slash command** | You want an explicit one-shot workflow you run on demand | Discoverable via `/...`, can take arguments |
| **Subagent** | The task is research-heavy (lots of reading/searching/synthesis) | Uses a separate context window; returns a distilled result |
| **Skill** | You want a rich workflow that Claude can auto-apply when it recognizes the task | Packaged capability (often with supporting files) |

### How they relate

| Mechanism | Runs in main conversation | Separate context window | Can spawn subagents | Can use skills | Manually runnable via `/...` |
|---|---:|---:|---:|---:|---:|
| **CLAUDE.md** | ✅ | ❌ | ❌ | ❌ | ❌ |
| **Slash command** | ✅ | ❌ | ✅ (by instructing `Task`) | ✅ (indirectly; Claude may apply skills) | ✅ |
| **Skill** | ✅ | ❌ | ✅ (if `Task` is allowed) | ✅ (Claude may apply multiple skills) | ❌ |
| **Subagent** | ❌ | ✅ | ⚠️ Possible (depends on allowed tools, e.g. `Bash(claude:*)`) | ✅ (if configured via `skills:`) | ⚠️ Usually delegated |

## Conclusion

- Use **subagents** (especially `Explore` in plan mode) to keep your main context small and focused.
- Use **slash commands** when you want an explicit, repeatable terminal entry point.
- Use **skills** when you want Claude to auto-apply a richer workflow (often with supporting files).
- Use **CLAUDE.md** for short, always-true project conventions and standards.&lt;/code&gt;&lt;/pre&gt;

          </content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Customize Your Claude Code Status Line</title><link>https://alexop.dev/posts/customize_claude_code_status_line/</link><guid isPermaLink="true">https://alexop.dev/posts/customize_claude_code_status_line/</guid><description>Learn how to display model name, context usage, and cost directly in your terminal while using Claude Code. A step-by-step guide to creating custom status line scripts.</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Ever glanced at Claude Code and wondered which model you’re actually using? Or how much of the context window you’ve burned through? By default, this information is hidden away—but you can surface it right in your terminal.&lt;/p&gt;
&lt;p&gt;A custom status line shows you what matters at a glance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Opus] Context: 12%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells you the active model and context usage without interrupting your flow. Let me show you how to set it up.&lt;/p&gt;
&lt;h2&gt;How the status line works&lt;/h2&gt;
&lt;p&gt;Claude Code pipes JSON data to your status line script via stdin. Your script processes that data and outputs whatever text you want displayed.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[Claude Code]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|JSON via stdin|&lt;/span&gt; B&lt;span&gt;[Your Script]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Formatted text|&lt;/span&gt; C&lt;span&gt;[Terminal Status Line]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The JSON contains everything you’d want to know: model info, token counts, costs, and workspace details.&lt;/p&gt;
&lt;h2&gt;Step 1: Create the status line script&lt;/h2&gt;
&lt;p&gt;Create a new file at &lt;code&gt;~/.claude/statusline.sh&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;
&lt;span&gt;input&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;MODEL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.model.display_name&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;INPUT_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.total_input_tokens&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;OUTPUT_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.total_output_tokens&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;CONTEXT_SIZE&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.context_window_size&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;TOTAL_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$((&lt;/span&gt;INPUT_TOKENS &lt;span&gt;+&lt;/span&gt; OUTPUT_TOKENS&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;PERCENT_USED&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$((&lt;/span&gt;TOTAL_TOKENS &lt;span&gt;*&lt;/span&gt; &lt;span&gt;100&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; CONTEXT_SIZE&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;[&lt;span&gt;$MODEL&lt;/span&gt;] Context: &lt;span&gt;${PERCENT_USED}&lt;/span&gt;%&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script reads JSON from stdin, extracts the fields we care about using &lt;code&gt;jq&lt;/code&gt;, calculates the percentage, and outputs the formatted string.&lt;/p&gt;
&lt;h2&gt;Step 2: Make it executable&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;chmod&lt;/span&gt; +x ~/.claude/statusline.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Configure Claude Code&lt;/h2&gt;
&lt;p&gt;Add the status line configuration to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;statusLine&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;~/.claude/statusline.sh&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you already have settings in this file, add the &lt;code&gt;statusLine&lt;/code&gt; block alongside your existing configuration.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “.claude”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://statusline.sh&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;statusline.sh&lt;/a&gt;”, comment: “Your custom script” },&lt;br /&gt;
{ name: “settings.json”, comment: “Configuration file” }&lt;br /&gt;
]&lt;br /&gt;
}&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;h2&gt;Step 4: Restart Claude Code&lt;/h2&gt;
&lt;p&gt;Close and reopen Claude Code. Your new status line should appear.&lt;/p&gt;
&lt;h2&gt;Available variables&lt;/h2&gt;
&lt;p&gt;The script receives JSON with these fields:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model.id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full model ID (e.g., &lt;code&gt;claude-opus-4-5-20251101&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model.display_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Short name (e.g., &lt;code&gt;Opus&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context_window.total_input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Input tokens used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context_window.total_output_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output tokens used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context_window.context_window_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Max context size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cost.total_cost_usd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session cost in USD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cost.total_duration_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workspace.current_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Adding cost tracking&lt;/h2&gt;
&lt;p&gt;Want to see how much your session is costing? Extend the script:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;
&lt;span&gt;input&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;MODEL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.model.display_name&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;INPUT_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.total_input_tokens&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;OUTPUT_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.total_output_tokens&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;CONTEXT_SIZE&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.context_window.context_window_size&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;COST&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$input&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.cost.total_cost_usd&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;TOTAL_TOKENS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$((&lt;/span&gt;INPUT_TOKENS &lt;span&gt;+&lt;/span&gt; OUTPUT_TOKENS&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;PERCENT_USED&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&lt;span&gt;$((&lt;/span&gt;TOTAL_TOKENS &lt;span&gt;*&lt;/span&gt; &lt;span&gt;100&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; CONTEXT_SIZE&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;printf&lt;/span&gt; &lt;span&gt;&quot;[%s] Context: %d%% | $%.2f&quot;&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$MODEL&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$PERCENT_USED&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$COST&lt;/span&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you’ll see something like: &lt;code&gt;[Opus] Context: 12% | $0.45&lt;/code&gt;&lt;/p&gt;
&lt;aside&gt;
  Use the `/statusline` slash command for a guided setup. Just type `/statusline show the model name and context usage percentage` and Claude Code creates the configuration automatically.
&lt;/aside&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Status line not showing?&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check that &lt;code&gt;jq&lt;/code&gt; is installed: &lt;code&gt;brew install jq&lt;/code&gt; (macOS) or &lt;code&gt;apt install jq&lt;/code&gt; (Linux)&lt;/li&gt;
&lt;li&gt;Verify the script is executable: &lt;code&gt;ls -la ~/.claude/statusline.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Restart Claude Code after making changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Test your script manually:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&apos;{&quot;model&quot;:{&quot;display_name&quot;:&quot;Opus&quot;},&quot;context_window&quot;:{&quot;total_input_tokens&quot;:1000,&quot;total_output_tokens&quot;:500,&quot;context_window_size&quot;:200000}}&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; ~/.claude/statusline.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Should output: &lt;code&gt;[Opus] Context: 0%&lt;/code&gt;&lt;/p&gt;

  The status line script requires `jq` for JSON parsing. If you don&apos;t have it installed, the script will fail silently.

&lt;h2&gt;Taking it further&lt;/h2&gt;
&lt;p&gt;The status line is one piece of the Claude Code customization puzzle. Once you’re comfortable with scripts like this, explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notification hooks to get desktop alerts when Claude needs input&lt;/li&gt;
&lt;li&gt;Slash commands to automate repetitive tasks&lt;/li&gt;
&lt;li&gt;The full Claude Code feature stack for MCP, skills, and subagents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The status line script pattern—reading JSON from stdin and outputting formatted text—is the same foundation that powers many of Claude Code’s extensibility features.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Vue 3 Testing Pyramid: A Practical Guide with Vitest Browser Mode</title><link>https://alexop.dev/posts/vue3_testing_pyramid_vitest_browser_mode/</link><guid isPermaLink="true">https://alexop.dev/posts/vue3_testing_pyramid_vitest_browser_mode/</guid><description>Learn a practical testing strategy for Vue 3 applications using composable unit tests, Vitest browser mode integration tests, and visual regression testing.</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Quick Summary&lt;/h2&gt;
&lt;p&gt;This post covers a practical testing approach for Vue 3 applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Composable unit tests for fast logic verification&lt;/li&gt;
&lt;li&gt;Integration tests with Vitest browser mode for realistic user flows&lt;/li&gt;
&lt;li&gt;Accessibility and visual tests for critical screen checks&lt;/li&gt;
&lt;li&gt;Simplified data factories to manage test data easily&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I’m building a workout tracking PWA with Vue 3, and I needed confidence that my changes work. Not the “I clicked around and it seems fine” kind of confidence, but the “I can refactor this and know immediately if I broke something” kind.&lt;/p&gt;
&lt;p&gt;Here’s the thing: I don’t write much code myself anymore. AI tools handle most of the implementation. I describe what I want, review the changes, and guide the direction—but the actual keystrokes? That’s the AI. This workflow is incredibly productive, but it comes with a catch: I need a robust safety net.&lt;/p&gt;
&lt;p&gt;When an AI writes code, tests become even more critical. They serve three purposes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Catch bugs&lt;/strong&gt; before users do&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable refactoring&lt;/strong&gt; — change code freely knowing tests will catch regressions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document behavior&lt;/strong&gt; — tests act as a “user manual” for your code&lt;/li&gt;
&lt;/ol&gt;

  Tests are just one part of your safety net. **Linting** (ESLint) catches code style issues and potential bugs statically. **Type checking** (TypeScript) catches type errors at compile time. Run all three—lint, type check, and tests—before every commit.

&lt;h2&gt;Before We Start: A Mini Glossary&lt;/h2&gt;
&lt;p&gt;Testing has a lot of jargon. Here’s a cheat sheet to keep handy as you read:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unit Test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Testing a tiny, isolated piece of code (like a single function) to ensure it returns the right value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration Test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Testing how multiple pieces work together (e.g., clicking a button and seeing a database update)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Regression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A bug where a feature that used to work stops working after you change something else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A fake version of a complex tool (like faking an API call) so you can test without relying on the internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Assertion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A line of code that checks if a result matches your expectation (e.g., &lt;code&gt;expect(2 + 2).toBe(4)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A11y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short for “Accessibility” (there are 11 letters between A and y)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Your Architecture Shapes Your Testing Strategy&lt;/h2&gt;
&lt;p&gt;Your testing strategy reflects your frontend architecture. They’re not independent choices.&lt;/p&gt;
&lt;p&gt;If you write &lt;strong&gt;monolithic components&lt;/strong&gt; (huge files with logic and UI mixed), testing is a nightmare. If you use &lt;strong&gt;composables&lt;/strong&gt; (extracting logic into separate files), testing becomes straightforward.&lt;/p&gt;
&lt;h3&gt;Bad vs. Good Architecture&lt;/h3&gt;
&lt;h4&gt;The Monolith (Hard to Test)&lt;/h4&gt;
&lt;p&gt;To test the timer logic here, you have to mount the whole component, find the button, click it, and wait for the UI to update. It’s slow and fragile.&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
// Logic is trapped inside the component!
const time = ref(0)
const start = () =&amp;gt; setInterval(() =&amp;gt; time.value++, 1000)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;start&quot;&amp;gt;{{ time }}&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The Composable (Easy to Test)&lt;/h4&gt;
&lt;p&gt;Here, the logic lives in a plain TypeScript file. We can test &lt;code&gt;useTimer&lt;/code&gt; without ever looking at a Vue component or HTML.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// useTimer.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTimer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; time &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;start&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; time&lt;span&gt;,&lt;/span&gt; start &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My strategy relies on this “composable-first” approach. However, for the UI itself, we use integration tests. These tests don’t care about your code structure; they test behavior through the UI, just like a user would.&lt;/p&gt;
&lt;aside&gt;
  For a deep dive into testing composables specifically, check out How to Test Vue Composables.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;The Testing Pyramid&lt;/h2&gt;
&lt;p&gt;My approach inverts the traditional pyramid. &lt;strong&gt;Integration tests make up ~70%&lt;/strong&gt; of my test suite because Vitest browser mode makes them fast and reliable. Composable unit tests cover ~20% for pure logic, and the remaining ~10% goes to accessibility and visual regression tests.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Environment: Browser Mode vs JSDOM&lt;/h2&gt;
&lt;p&gt;In the past, most Vue tests ran in JSDOM. Now, I recommend &lt;strong&gt;Vitest Browser Mode&lt;/strong&gt; with &lt;code&gt;vitest-browser-vue&lt;/code&gt;. Here’s why:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;JSDOM (Old Standard)&lt;/th&gt;
&lt;th&gt;Vitest Browser Mode (New Standard)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What is it?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A simulation of a browser running in Node.js (Fake)&lt;/td&gt;
&lt;td&gt;A real instance of Chrome/Firefox running your tests (Real)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good for logic, bad for layout/CSS&lt;/td&gt;
&lt;td&gt;100% accurate — it’s a real browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hard. You stare at console logs&lt;/td&gt;
&lt;td&gt;Easy. You can watch the test click buttons on your screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Surprisingly slow (see benchmarks below)&lt;/td&gt;
&lt;td&gt;Often faster due to native browser APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires Testing Library for DOM queries&lt;/td&gt;
&lt;td&gt;Built-in &lt;code&gt;page&lt;/code&gt; object with Playwright-like locators&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Real-World Performance Comparison&lt;/h3&gt;
&lt;p&gt;A common misconception is that browser mode is slower. In my testing with the same test suite, &lt;strong&gt;browser mode was actually 4x faster&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Vitest Browser Mode (Chromium)&lt;/th&gt;
&lt;th&gt;Vitest Unit Mode (JSDOM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.59s 🚀&lt;/td&gt;
&lt;td&gt;53.72s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test Files&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;82 (78 passed)&lt;/td&gt;
&lt;td&gt;82 (78 passed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.48s&lt;/td&gt;
&lt;td&gt;53ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Import Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19.84s&lt;/td&gt;
&lt;td&gt;7.98s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test Execution Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29.48s&lt;/td&gt;
&lt;td&gt;40.53s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;While browser mode has higher setup time (launching Chromium), the actual test execution is faster because it uses native browser APIs instead of JSDOM’s JavaScript reimplementation. The total duration speaks for itself.&lt;/p&gt;
&lt;aside&gt;
  Vitest browser mode handles everything in one command. The browser launches, components render, and tests run. It&apos;s much simpler for AI assistants (and humans) to manage than setting up complex End-to-End servers.
&lt;/aside&gt;
&lt;h3&gt;Setting Up Vitest Browser Mode&lt;/h3&gt;
&lt;p&gt;Vitest 4.0+ requires a browser provider package. Install the dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; &lt;span&gt;-D&lt;/span&gt; vitest @vitest/browser-playwright vitest-browser-vue playwright
&lt;/code&gt;&lt;/pre&gt;

  You can use `@vitest/browser-playwright` (recommended) or `@vitest/browser-webdriverio`. Playwright offers the best developer experience with automatic browser downloads.

&lt;h3&gt;No More Testing Library&lt;/h3&gt;
&lt;p&gt;With Vitest browser mode, you don’t need &lt;code&gt;@testing-library/vue&lt;/code&gt; anymore. The &lt;code&gt;page&lt;/code&gt; object from &lt;code&gt;vitest/browser&lt;/code&gt; provides Playwright-like locators that are more powerful and consistent:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Instead of screen.getByRole(), use page.getByRole()&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; button &lt;span&gt;=&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;button&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;button&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Layer 1: Composable Unit Tests&lt;/h2&gt;
&lt;p&gt;Composables are just functions. You test them by calling them and checking the result.&lt;/p&gt;
&lt;h3&gt;A Simple Composable Test&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;useDialogState&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;starts closed&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// 1. Run the code&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isOpen &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useDialogState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;// 2. Assert the result&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isOpen&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;opens when requested&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isOpen&lt;span&gt;,&lt;/span&gt; open &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useDialogState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isOpen&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No HTML, no mounting, no complexity. Just functions and values.&lt;/p&gt;
&lt;aside&gt;
  For comprehensive patterns including async composables, lifecycle hooks, and dependency injection, see How to Test Vue Composables.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Managing Test Data with Factories&lt;/h2&gt;
&lt;p&gt;When writing tests, you constantly need data. For example, to test a “Profile Page,” you need a “User.”&lt;/p&gt;
&lt;p&gt;Beginners often copy-paste the same big object into every single test file. This is messy and hard to maintain. If you add a new required field (like &lt;code&gt;phoneNumber&lt;/code&gt;) to your User, you have to go back and fix every single test.&lt;/p&gt;
&lt;p&gt;The solution is the &lt;strong&gt;Factory Pattern&lt;/strong&gt;. Think of it like ordering a pizza: there’s a “standard” pizza (Cheese &amp;amp; Tomato), and you only specify the changes you want (“…but add pepperoni”).&lt;/p&gt;
&lt;h3&gt;The Problem: Hard-coded Data&lt;/h3&gt;
&lt;p&gt;Without factories, your tests look like this. Notice how much noise there is just to test one specific thing:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// ❌ BAD: Repeating data everywhere&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shows admin badge&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;1&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;John Doe&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    email&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;john@example.com&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;admin&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// This is the only line we actually care about!&lt;/span&gt;
    isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    createdAt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;2023-01-01&apos;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// ... test logic ...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Solution: A Simple Factory Function&lt;/h3&gt;
&lt;p&gt;A factory is just a plain TypeScript function. It holds the “Standard Pizza” defaults and lets you overwrite specific slices using the spread operator (&lt;code&gt;...&lt;/code&gt;).&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// factories/userFactory.ts&lt;/span&gt;

&lt;span&gt;// 1. Define the shape of your data&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;User&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;user&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&apos;admin&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 2. Define your &quot;Standard Pizza&quot; (Sensible Defaults)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; defaultUser&lt;span&gt;:&lt;/span&gt; User &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;123&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Test User&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 3. The Factory Function&lt;/span&gt;
&lt;span&gt;// It takes &quot;overrides&quot; (partial data) and merges them on top of the defaults&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;overrides&lt;span&gt;:&lt;/span&gt; Partial&lt;span&gt;&amp;lt;&lt;/span&gt;User&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; User &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;defaultUser&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// Start with defaults&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;overrides    &lt;span&gt;// Apply your specific changes&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using It in Tests&lt;/h3&gt;
&lt;p&gt;Now your tests focus purely on what matters for that specific scenario:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// ✅ GOOD: Clean and focused&lt;/span&gt;

&lt;span&gt;// Scenario 1: I just need ANY user, I don&apos;t care about details&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; basicUser &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Result: { id: &apos;123&apos;, name: &apos;Test User&apos;, role: &apos;user&apos;, ... }&lt;/span&gt;

&lt;span&gt;// Scenario 2: I specifically need an ADMIN&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; admin &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;admin&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Result: { id: &apos;123&apos;, name: &apos;Test User&apos;, role: &apos;admin&apos;, ... }&lt;/span&gt;

&lt;span&gt;// Scenario 3: I need an INACTIVE user&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; bannedUser &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Result: { id: &apos;123&apos;, name: &apos;Test User&apos;, isActive: false, ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern keeps your tests readable and makes refactoring easy. If you add a new field to &lt;code&gt;User&lt;/code&gt; later, you only update the &lt;code&gt;defaultUser&lt;/code&gt; object in one place.&lt;/p&gt;
&lt;h3&gt;Factories Work at Every Layer&lt;/h3&gt;
&lt;p&gt;The beauty of factories is that they work for &lt;strong&gt;both&lt;/strong&gt; unit tests and integration tests:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// ✅ Unit Test: Testing a composable&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;formats user display name&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Jane Doe&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;admin&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; displayName &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useUserProfile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;displayName&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Jane Doe (Admin)&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// ✅ Integration Test: Testing a rendered component&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;shows admin badge in profile&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; admin &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;admin&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;renderProfilePage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; user&lt;span&gt;:&lt;/span&gt; admin &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; expect&lt;span&gt;.&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Admin&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; Factories handle &lt;strong&gt;data&lt;/strong&gt;. They don’t care whether you’re testing a function or a full page—they just give you clean, predictable objects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;aside&gt;
  For my workout tracking PWA, I use factories like `createWorkout()`, `createExercise()`, and `createSet()`. The pattern scales nicely—start simple and add complexity only when your data relationships demand it.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Layer 2: Integration Tests&lt;/h2&gt;
&lt;p&gt;Integration tests verify complete user flows. They render the app, click buttons, and check if the right things appear on screen.&lt;/p&gt;

  In this post, **integration test** means:
  - Real browser (Vitest browser mode)
  - Real Vue components, router, Pinia, and user interactions
  - **Mocked**: external APIs (via [MSW](https://mswjs.io/)), browser storage (IndexedDB), third-party services
&lt;p&gt;&lt;strong&gt;E2E test&lt;/strong&gt; means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real browser&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero mocking&lt;/strong&gt;—full stack (frontend + backend + database)&lt;/li&gt;
&lt;li&gt;Tests exactly how a user interacts with the production system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;: In my workout tracker, I mock IndexedDB but test real Vue components and user flows—that’s an integration test. For an e-commerce site, you’d mock the product API and payment gateway via MSW, but test the real checkout flow. If you spin up your actual backend and database, that’s E2E.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Component Tests vs. Integration Tests&lt;/h3&gt;
&lt;p&gt;Vitest browser mode supports two approaches:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What you render&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Component test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single component (&lt;code&gt;render(MyButton)&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Testing component behavior in isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full app (&lt;code&gt;render(App)&lt;/code&gt; with router, store)&lt;/td&gt;
&lt;td&gt;Testing complete user flows across multiple components&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Component tests&lt;/strong&gt; are faster and more focused—great for testing a single component’s props, events, and states.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration tests&lt;/strong&gt; render your entire &lt;code&gt;App.vue&lt;/code&gt; with router and Pinia. The user can navigate between pages, fill forms, and see how components work together. This is where you catch bugs that only appear when components interact.&lt;/p&gt;
&lt;p&gt;For most Vue apps, I recommend focusing on &lt;strong&gt;integration tests&lt;/strong&gt;. They give you more confidence because they test what users actually experience.&lt;/p&gt;
&lt;h3&gt;The “Test App” Helper&lt;/h3&gt;
&lt;p&gt;To make testing easier, I use a helper function called &lt;code&gt;createTestApp&lt;/code&gt;. It sets up your Router, Pinia (state), and renders your app using &lt;code&gt;vitest-browser-vue&lt;/code&gt; so you don’t have to repeat it in every file.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// helpers/createTestApp.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;createTestApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// ... setup router, pinia, render app ...&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    router&lt;span&gt;,&lt;/span&gt;       &lt;span&gt;// The navigation system&lt;/span&gt;
    cleanup       &lt;span&gt;// A function to tidy up after the test&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

```typescript
// helpers/createTestApp.ts
import {
  CommonPO,
  BuilderPO,
  ActiveWorkoutPO,
  QueuePO,
  BenchmarksPO,
  BenchmarkFormPO,
  BenchmarkDetailPO,
} from &apos;./pages&apos;
&lt;p&gt;type CreateTestAppOptions = {&lt;br /&gt;
initialRoute?: string&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;type TestApp = {&lt;br /&gt;
router: Router&lt;br /&gt;
container: Element&lt;br /&gt;
// Page Objects&lt;br /&gt;
common: CommonPO&lt;br /&gt;
builder: BuilderPO&lt;br /&gt;
workout: ActiveWorkoutPO&lt;br /&gt;
queue: QueuePO&lt;br /&gt;
benchmarks: BenchmarksPO&lt;br /&gt;
benchmarkForm: BenchmarkFormPO&lt;br /&gt;
benchmarkDetail: BenchmarkDetailPO&lt;br /&gt;
// Raw query methods (use page.getBy* for new code)&lt;br /&gt;
getByRole: typeof page.getByRole&lt;br /&gt;
getByText: typeof page.getByText&lt;br /&gt;
getByTestId: typeof page.getByTestId&lt;br /&gt;
queryByRole: typeof page.getByRole&lt;br /&gt;
queryByText: typeof page.getByText&lt;br /&gt;
findByRole: typeof page.getByRole&lt;br /&gt;
findByText: typeof page.getByText&lt;br /&gt;
// Helpers&lt;br /&gt;
navigateTo: (to: RouteLocationRaw) =&amp;gt; Promise&lt;br /&gt;
cleanup: () =&amp;gt; void&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;export async function createTestApp(options: CreateTestAppOptions = {}): Promise {&lt;br /&gt;
const { initialRoute = ‘/’ } = options&lt;/p&gt;
&lt;p&gt;const pinia = createPinia()&lt;br /&gt;
const router = createRouter({&lt;br /&gt;
history: createMemoryHistory(),&lt;br /&gt;
routes,&lt;br /&gt;
})&lt;/p&gt;
&lt;p&gt;if (initialRoute !== ‘/’) {&lt;br /&gt;
router.push(initialRoute)&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;// Preload English messages for tests&lt;br /&gt;
i18n.global.setLocaleMessage(‘en’, en)&lt;br /&gt;
i18n.global.locale.value = ‘en’&lt;/p&gt;
&lt;p&gt;const screen = render(App, {&lt;br /&gt;
global: {&lt;br /&gt;
plugins: [router, pinia, i18n],&lt;br /&gt;
},&lt;br /&gt;
})&lt;/p&gt;
&lt;p&gt;await router.isReady()&lt;/p&gt;
&lt;p&gt;// Flush Vue’s async operations to ensure onMounted fires&lt;br /&gt;
await flushPromises()&lt;/p&gt;
&lt;p&gt;// Wait for app initialization to complete (exercises seeding and loading)&lt;br /&gt;
const exercisesStore = useExercisesStore(pinia)&lt;br /&gt;
await expect&lt;br /&gt;
.poll(() =&amp;gt; exercisesStore.customExercises.length, { timeout: 5000 })&lt;br /&gt;
.toBeGreaterThan(0)&lt;/p&gt;
&lt;p&gt;// Create context for page objects&lt;br /&gt;
const context =&lt;/p&gt;
&lt;p&gt;// Instantiate page objects&lt;br /&gt;
const common = new CommonPO(context)&lt;br /&gt;
const builder = new BuilderPO(context, common)&lt;br /&gt;
const workout = new ActiveWorkoutPO(context, common)&lt;br /&gt;
const queue = new QueuePO(context, common)&lt;br /&gt;
const benchmarks = new BenchmarksPO(context, common)&lt;br /&gt;
const benchmarkForm = new BenchmarkFormPO(context, common)&lt;br /&gt;
const benchmarkDetail = new BenchmarkDetailPO(context, common)&lt;/p&gt;
&lt;p&gt;// Simple navigation helper&lt;br /&gt;
async function navigateTo(to: RouteLocationRaw) {&lt;br /&gt;
await router.push(to)&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;// vitest-browser-vue cleans up before tests automatically&lt;br /&gt;
// This is kept for backward compatibility with test structure&lt;br /&gt;
function cleanup() {&lt;br /&gt;
screen.unmount()&lt;br /&gt;
}&lt;/p&gt;
&lt;p&gt;return {&lt;br /&gt;
router,&lt;br /&gt;
container: screen.container,&lt;br /&gt;
// Page Objects&lt;br /&gt;
common,&lt;br /&gt;
builder,&lt;br /&gt;
workout,&lt;br /&gt;
queue,&lt;br /&gt;
benchmarks,&lt;br /&gt;
benchmarkForm,&lt;br /&gt;
benchmarkDetail,&lt;br /&gt;
// Raw query methods - use page locators (return Locators, not HTMLElements)&lt;br /&gt;
getByRole: page.getByRole.bind(page),&lt;br /&gt;
getByText: page.getByText.bind(page),&lt;br /&gt;
getByTestId: page.getByTestId.bind(page),&lt;br /&gt;
queryByRole: page.getByRole.bind(page),&lt;br /&gt;
queryByText: page.getByText.bind(page),&lt;br /&gt;
findByRole: page.getByRole.bind(page),&lt;br /&gt;
findByText: page.getByText.bind(page),&lt;br /&gt;
// Helpers&lt;br /&gt;
navigateTo,&lt;br /&gt;
cleanup,&lt;br /&gt;
}&lt;br /&gt;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;/Collapsible&amp;gt;

&amp;lt;Alert type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
  This isn&apos;t a library you download. It&apos;s a helper file you write once for your project to handle the setup boilerplate.
&amp;lt;/Alert&amp;gt;

### A Real Integration Test

Notice how we use `getByRole` to find elements—this ensures our app is accessible:

```typescript
it(&apos;completes a set&apos;, async () =&amp;gt; {
  await createTestApp()

  // 1. Find the &quot;Start&quot; button and click it
  await userEvent.click(page.getByRole(&apos;button&apos;, { name: /start/i }))

  // 2. Type &quot;100&quot; into the weight input
  const weightInput = page.getByRole(&apos;spinbutton&apos;, { name: /weight/i })
  await userEvent.type(weightInput, &apos;100&apos;)

  // 3. Click &quot;Complete&quot;
  await userEvent.click(page.getByRole(&apos;button&apos;, { name: /complete/i }))

  // 4. Wait for the success message
  await expect.element(page.getByText(&apos;Set Completed&apos;)).toBeVisible()
})
&lt;/code&gt;&lt;/pre&gt;

  Always prefer `getByRole()` over `getByTestId()` or CSS selectors. When you use `getByRole(&apos;button&apos;, { name: /submit/i })`, you&apos;re asserting that:
&lt;ol&gt;
&lt;li&gt;The element has the correct ARIA role (it’s actually a button)&lt;/li&gt;
&lt;li&gt;The element has an accessible name (screen readers can announce it)&lt;/li&gt;
&lt;li&gt;The element is visible and interactive&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your test can’t find an element by role, that’s a signal your UI has an accessibility problem—fix the component, not the test. Reserve &lt;code&gt;getByTestId&lt;/code&gt; only for elements that truly have no semantic meaning.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Page Objects: Handling DOM Interaction&lt;/h3&gt;
&lt;p&gt;As your test suite grows, you’ll notice repetitive DOM queries everywhere. &lt;strong&gt;Page Objects&lt;/strong&gt; solve this by encapsulating all DOM interactions for a specific page or component.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Difference:&lt;/strong&gt; Factories handle &lt;strong&gt;data&lt;/strong&gt; (creating test objects). Page Objects handle &lt;strong&gt;DOM interaction&lt;/strong&gt; (clicking, typing, querying elements). They complement each other.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// pages/WorkoutPage.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;WorkoutPage&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Queries - finding elements&lt;/span&gt;
  &lt;span&gt;get&lt;/span&gt; &lt;span&gt;startButton&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;button&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;get&lt;/span&gt; &lt;span&gt;weightInput&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;spinbutton&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;weight&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Actions - user interactions&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;start&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;startButton&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;setWeight&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clear&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;weightInput&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;weightInput&lt;span&gt;,&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;completeSet&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;button&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;complete&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your tests read like plain English:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;completes a set with weight&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; workoutPage &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;WorkoutPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;await&lt;/span&gt; workoutPage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; workoutPage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setWeight&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; workoutPage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;completeSet&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;await&lt;/span&gt; expect&lt;span&gt;.&lt;/span&gt;&lt;span&gt;element&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Set Completed&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Helper&lt;/th&gt;
&lt;th&gt;Handles&lt;/th&gt;
&lt;th&gt;Used In&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Factories&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test data (objects, entities)&lt;/td&gt;
&lt;td&gt;Unit tests, Integration tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Page Objects&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM interaction (clicks, queries)&lt;/td&gt;
&lt;td&gt;Integration tests only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;aside&gt;
  Don&apos;t create Page Objects upfront. Write your first few tests with inline queries. When you notice the same `getByRole` patterns repeating across 3+ tests, extract them into a Page Object.
&lt;/aside&gt;
&lt;aside&gt;
  I use a [Claude Code command to refactor tests into Page Objects](/prompts/claude/claude-refactor-page-object-command) automatically. Point it at a test file and it extracts repeated queries into a clean page object factory.
&lt;/aside&gt;
&lt;p&gt;This approach aligns with black box testing principles—testing behavior rather than implementation details.&lt;/p&gt;
&lt;p&gt;Stop White Box Testing Vue Components&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Layer 3: Accessibility and Visual Tests&lt;/h2&gt;
&lt;p&gt;These are the “cherries on top” of your pyramid.&lt;/p&gt;
&lt;h3&gt;Accessibility (A11y)&lt;/h3&gt;
&lt;p&gt;We use a tool called &lt;strong&gt;axe-core&lt;/strong&gt;. It scans your rendered HTML for common violations (like low contrast text or missing labels).&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;has no accessibility violations&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; container &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;createTestApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;// This one line checks for dozens of common a11y bugs!&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;assertNoViolations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;container&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  For a complete setup guide with jest-axe, see How to Improve Accessibility with Testing Library and jest-axe. For general Vue accessibility best practices, check out Vue Accessibility Blueprint: 8 Steps.
&lt;/aside&gt;
&lt;h3&gt;Visual Regression&lt;/h3&gt;
&lt;p&gt;This takes a screenshot of your component and compares it to a “golden” version saved on your computer. If a pixel changes, the test fails.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;matches the design&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByTestId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;app&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMatchScreenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;settings-page.png&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this sparingly. Visual tests are brittle (even a font rendering update can break them), so only use them for critical screens.&lt;/p&gt;
&lt;aside&gt;
  For a complete setup guide on visual regression testing with Vitest browser mode, see How to Do Visual Regression Testing in Vue with Vitest.
&lt;/aside&gt;
&lt;h3&gt;Testing Your Core UI Library&lt;/h3&gt;
&lt;p&gt;There’s one place where visual regression and accessibility tests shine: &lt;strong&gt;your base component library&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you’re building your own UI components (BaseButton, DatePicker, Modal, Input), these components should be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dumb&lt;/strong&gt; — no business logic, just presentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusable&lt;/strong&gt; — used across your entire app&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt; — rarely change once built&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes them perfect candidates for visual and accessibility testing:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// BaseButton.visual.spec.ts&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;BaseButton&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;renders all variants correctly&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ButtonStory&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// A component showing all button states&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;page&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMatchScreenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;button-variants.png&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;has no accessibility violations&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; container &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;BaseButton&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; label&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Click me&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;assertNoViolations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;container&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each base component, test:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Type&lt;/th&gt;
&lt;th&gt;What to Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All variants (primary, secondary, disabled, loading)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A11y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Focus states, ARIA attributes, color contrast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keyboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tab navigation, Enter/Space activation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;aside&gt;
  If you use a pre-built library like **shadcn/ui**, **Vuetify**, or **PrimeVue**, skip this. Those libraries already handle visual consistency and accessibility. Focus your testing efforts on your business logic and user flows instead.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Not End-to-End (E2E) Tests?&lt;/h2&gt;
&lt;p&gt;You might hear people say, “Just use Cypress or Playwright for everything!”&lt;/p&gt;
&lt;p&gt;E2E tests mean &lt;strong&gt;zero mocking&lt;/strong&gt;—you run your real backend and database. They test your entire stack: Frontend + Backend + Database.&lt;/p&gt;
&lt;p&gt;For a new developer or a solo project, this is painful because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s slow&lt;/li&gt;
&lt;li&gt;It breaks easily (if the backend API is down, your frontend tests fail)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Alternative: Mocking&lt;/h3&gt;
&lt;p&gt;Instead, we use &lt;strong&gt;MSW (Mock Service Worker)&lt;/strong&gt;. It intercepts network requests and returns fake data immediately. This makes your integration tests fast and stable. You don’t need a running backend to test your frontend.&lt;/p&gt;

  **The less you mock, the better your tests.** Every mock is a lie you&apos;re telling your test suite. Mock only what you can&apos;t control:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;External APIs&lt;/strong&gt; (network calls to third-party services)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System boundaries&lt;/strong&gt; (time, random numbers, file system)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paid services&lt;/strong&gt; (payment gateways, SMS providers)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Never mock your own code just to make tests easier. If a component is hard to test without mocking internal modules, that’s a sign your architecture needs refactoring—not more mocks.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

  In large corporate teams, you might use &quot;Contract Testing&quot; to ensure your mocks match the real API. For now, don&apos;t worry about it. Focus on getting your integration and unit tests running smoothly.

&lt;hr /&gt;
&lt;h2&gt;Comparison: Testing Approaches&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;Flakiness&lt;/th&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unit Tests (Composables)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Very fast&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;~20%&lt;/td&gt;
&lt;td&gt;Logic validation, utility functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration Tests (Browser)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🚀 Fast&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~70%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User flows, component interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A11y Tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🚀 Fast&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;~5%&lt;/td&gt;
&lt;td&gt;Critical screens, forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual Regression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🐢 Slow&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;~5%&lt;/td&gt;
&lt;td&gt;Design system components&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Summary: Your Next Steps&lt;/h2&gt;
&lt;p&gt;Don’t try to implement the whole pyramid today. Start with what matters most.&lt;/p&gt;
&lt;h3&gt;Step 1: Identify What Can Never Fail&lt;/h3&gt;
&lt;p&gt;Ask yourself: &lt;em&gt;“What flows in my app would be catastrophic if they broke?”&lt;/em&gt; For an e-commerce site, that’s checkout. For a banking app, that’s transfers. For my workout tracker, it’s completing a set.&lt;/p&gt;
&lt;p&gt;Write integration tests for these critical paths first using Vitest browser mode. Even 3-5 tests covering your core flows provide massive confidence.&lt;/p&gt;
&lt;h3&gt;Step 2: Set Up the Infrastructure&lt;/h3&gt;
&lt;p&gt;Get Vitest browser mode running with a simple &lt;code&gt;createTestApp&lt;/code&gt; helper. Once you can render your app and click a button in a test, you have the foundation for everything else.&lt;/p&gt;
&lt;h3&gt;Step 3: Write Tickets with Testable Acceptance Criteria&lt;/h3&gt;
&lt;p&gt;Good tickets have Gherkin-style acceptance criteria that read like tests:&lt;/p&gt;
&lt;pre class=&quot;language-gherkin&quot;&gt;&lt;code class=&quot;language-gherkin&quot;&gt;Given I am on the workout page
When I tap &lt;span&gt;&quot;Complete Set&quot;&lt;/span&gt;
Then I should see &lt;span&gt;&quot;Set Completed&quot;&lt;/span&gt; confirmation
And the set should be saved to history
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These ACs translate directly into integration tests. Now you can practice TDD: write the test from the AC first, watch it fail, then implement the feature.&lt;/p&gt;
&lt;h3&gt;Step 4: Extract Patterns as You Go&lt;/h3&gt;
&lt;p&gt;Don’t create factories or page objects upfront. Write a few tests with inline data and queries. When you notice repetition, extract it. This way, your abstractions solve real problems instead of imagined ones.&lt;/p&gt;
&lt;p&gt;For guidance on writing clear, maintainable test names, check out Frontend Testing Guide: 10 Essential Rules for Naming Tests.&lt;/p&gt;
&lt;aside&gt;
  Want to see this testing setup in a real project? Check out my [Workout Tracker PWA on GitHub](https://github.com/alexanderop/workoutTracker). It includes the `createTestApp` helper, page objects, factories, and integration tests using Vitest browser mode.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Bonus: Performance Testing in CI&lt;/h2&gt;
&lt;p&gt;While not part of the traditional testing pyramid, &lt;strong&gt;performance budgets&lt;/strong&gt; catch regressions before they reach production. I run Lighthouse CI on every build to enforce thresholds for performance, accessibility, and best practices.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;# .github/workflows/ci.yml&lt;/span&gt;
&lt;span&gt;performance-budget&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;needs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; build
  &lt;span&gt;runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ubuntu&lt;span&gt;-&lt;/span&gt;latest
  &lt;span&gt;timeout-minutes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;10&lt;/span&gt;
  &lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Checkout code
      &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; actions/checkout@v4.2.2

    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Setup pnpm
      &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; pnpm/action&lt;span&gt;-&lt;/span&gt;setup@v4.1.0

    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Setup Node.js
      &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; actions/setup&lt;span&gt;-&lt;/span&gt;node@v4.4.0
      &lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;node-version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; $&lt;span&gt;{&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; env.NODE_VERSION &lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Restore node_modules
      &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; actions/cache/restore@v4.2.3
      &lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;path&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; node_modules
        &lt;span&gt;key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; node&lt;span&gt;-&lt;/span&gt;modules&lt;span&gt;-&lt;/span&gt;$&lt;span&gt;{&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; runner.os &lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;$&lt;span&gt;{&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; hashFiles(&apos;pnpm&lt;span&gt;-&lt;/span&gt;lock.yaml&apos;) &lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Download build artifacts
      &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; actions/download&lt;span&gt;-&lt;/span&gt;artifact@v6.0.0
      &lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dist
        &lt;span&gt;path&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dist

    &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Run Lighthouse CI
      &lt;span&gt;run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; pnpm lhci autorun
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  - Performance regressions (bundle size bloat, slow renders)
  - Accessibility violations (missing labels, low contrast)
  - SEO issues (missing meta tags, non-crawlable links)
  - Best practice violations (HTTP/2, image optimization)
&lt;p&gt;Configure thresholds in &lt;code&gt;lighthouserc.js&lt;/code&gt; to fail the build when scores drop below acceptable levels.&lt;/p&gt;
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Beyond the Pyramid: AI-Powered QA&lt;/h2&gt;
&lt;p&gt;There’s a new layer emerging that doesn’t fit neatly into the traditional pyramid: &lt;strong&gt;AI-driven testing&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What if you could have an AI test your app the way a real QA engineer would? Not following scripts, but actually exploring your UI, trying edge cases, and writing bug reports?&lt;/p&gt;
&lt;p&gt;I’ve been experimenting with exactly this approach. Using Claude Code combined with Playwright’s browser automation, I built an AI QA engineer that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests my app through the browser like a real user&lt;/li&gt;
&lt;li&gt;Tries unexpected inputs and edge cases automatically&lt;/li&gt;
&lt;li&gt;Runs on every pull request via GitHub Actions&lt;/li&gt;
&lt;li&gt;Posts detailed bug reports with screenshots directly to my PRs&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    PR&lt;span&gt;[Open PR]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; GH&lt;span&gt;[GitHub Actions]&lt;/span&gt;
    GH &lt;span&gt;--&amp;gt;&lt;/span&gt; AI&lt;span&gt;[Claude Code + Playwright]&lt;/span&gt;
    AI &lt;span&gt;--&amp;gt;&lt;/span&gt; Test&lt;span&gt;[Browser Testing]&lt;/span&gt;
    Test &lt;span&gt;--&amp;gt;&lt;/span&gt; Report&lt;span&gt;[QA Report on PR]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn’t a replacement for the testing pyramid—it’s a complement. Your unit and integration tests catch regressions deterministically. AI QA excels at exploratory testing and finding bugs that scripted tests would never think to check.&lt;/p&gt;
&lt;aside&gt;
  I wrote a complete guide on setting this up: Building an AI QA Engineer with Claude Code and Playwright MCP. It covers the GitHub Actions workflow, prompt engineering for effective testing, and how to get bug reports posted automatically to your PRs.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest Browser Mode Guide&lt;/a&gt; - The official docs are excellent&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vitest-dev/vitest-browser-vue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;vitest-browser-vue&lt;/a&gt; - Vue rendering for Vitest browser mode&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vitest-dev/vitest/tree/main/examples&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;vitest-examples on GitHub&lt;/a&gt; - “Hello World” setup examples&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><category>testing</category><category>vitest</category><category>typescript</category><category>accessibility</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building an AI QA Engineer with Claude Code and Playwright MCP</title><link>https://alexop.dev/posts/building_ai_qa_engineer_claude_code_playwright/</link><guid isPermaLink="true">https://alexop.dev/posts/building_ai_qa_engineer_claude_code_playwright/</guid><description>Learn how to build an automated QA engineer using Claude Code and Playwright MCP that tests your web app like a real user, runs on every pull request, and writes detailed bug reports.</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Quick Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Build an AI-powered QA engineer that tests your app through the browser like a real user&lt;/li&gt;
&lt;li&gt;Use Claude Code with Playwright MCP to automate browser interactions&lt;/li&gt;
&lt;li&gt;Run automated QA on every pull request via GitHub Actions&lt;/li&gt;
&lt;li&gt;Get detailed bug reports with screenshots posted directly to your PRs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Manual testing gets old fast. Clicking through your app after every change, checking if forms still work, making sure nothing breaks on mobile—it’s tedious work that most developers avoid.&lt;/p&gt;
&lt;p&gt;So I built an AI that does it for me.&lt;/p&gt;
&lt;p&gt;Meet &lt;strong&gt;Quinn&lt;/strong&gt;, my automated QA engineer. Quinn tests my app like a real person would. It clicks buttons. It fills forms with weird inputs. It resizes the browser to check mobile layouts. And it writes detailed bug reports.&lt;/p&gt;
&lt;p&gt;The best part? Quinn runs automatically every time I open a pull request.&lt;/p&gt;
&lt;h2&gt;The secret sauce: Claude Code + Playwright&lt;/h2&gt;
&lt;p&gt;Two tools make this possible:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; is Anthropic’s coding assistant. It can run commands, create files, and—here’s the magic—control a web browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Playwright&lt;/strong&gt; is a browser automation tool. It can click, type, take screenshots, and do anything a human can do in a browser.&lt;/p&gt;
&lt;p&gt;When you combine them through the Model Context Protocol (MCP), Claude can literally browse your app like a real user.&lt;/p&gt;
&lt;aside&gt;
  MCP (Model Context Protocol) standardizes how AI tools connect to external services. Think of it as USB-C for AI—one universal way to connect tools like Playwright, databases, or APIs to any LLM. Learn more in my MCP deep dive.
&lt;/aside&gt;
&lt;h2&gt;Step 1: Give Claude a personality&lt;/h2&gt;
&lt;p&gt;I didn’t want a boring test robot. I wanted a QA engineer with opinions.&lt;/p&gt;
&lt;p&gt;So I created a prompt file that gives Claude a backstory:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; QA Engineer Identity&lt;/span&gt;

You are &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Quinn&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;, a veteran QA engineer with 12 years
of experience breaking software. You&apos;ve seen it all -
apps that crash on empty input, forms that lose data,
buttons that do nothing.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Your Philosophy&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Trust nothing.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Developers say it works? Prove it.
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Users are creative.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; They&apos;ll do things no one anticipated.
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Edge cases are where bugs hide.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; The happy path is boring.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn’t just for fun. The personality makes Claude test more thoroughly. Quinn doesn’t just check if buttons work—Quinn tries to &lt;em&gt;break&lt;/em&gt; things.&lt;/p&gt;
&lt;p&gt;I also gave Quinn strict rules:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Non-Negotiable Rules&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;UI ONLY.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; You interact through the browser like a
   real user. You cannot read source code.

&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;SCREENSHOT BUGS.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Every bug gets a screenshot.

&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;CONTINUE AFTER BUGS.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Finding a bug is not the end.
   Document it, then KEEP TESTING.

&lt;span&gt;4.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;MOBILE MATTERS.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; Always test mobile viewport (375x667).
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Create the GitHub Action&lt;/h2&gt;
&lt;p&gt;GitHub Actions are like little robots that run tasks for you. They trigger when something happens (like opening a PR) and run whatever commands you specify.&lt;/p&gt;
&lt;p&gt;Here’s the core of the workflow:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Claude QA

&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    &lt;span&gt;types&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;labeled&lt;span&gt;]&lt;/span&gt;

&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;qa&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
    &lt;span&gt;runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ubuntu&lt;span&gt;-&lt;/span&gt;latest

    &lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Checkout code
        &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Start my app
        &lt;span&gt;run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;
          pnpm dev &amp;amp;
          # Wait for server to be ready
          sleep 10&lt;/span&gt;

      &lt;span&gt;-&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Run Claude QA
        &lt;span&gt;uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; anthropics/claude&lt;span&gt;-&lt;/span&gt;code&lt;span&gt;-&lt;/span&gt;action@v1
        &lt;span&gt;with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
          &lt;span&gt;prompt&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; $&lt;span&gt;{&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; steps.load&lt;span&gt;-&lt;/span&gt;prompts.outputs.prompt &lt;span&gt;}&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
          &lt;span&gt;claude_args&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;
            --mcp-config &apos;{&quot;mcpServers&quot;:{&quot;playwright&quot;:{
              &quot;command&quot;:&quot;npx&quot;,
              &quot;args&quot;:[&quot;@playwright/mcp@latest&quot;,&quot;--headless&quot;]
            }}}&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me break this down:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Trigger&lt;/strong&gt;: The workflow runs when you add a label to a PR (like &lt;code&gt;qa-verify&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start the app&lt;/strong&gt;: Launch your dev server so Claude has something to test&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run Claude&lt;/strong&gt;: Use Anthropic’s official GitHub Action with Playwright MCP connected&lt;/li&gt;
&lt;/ol&gt;

  The `--headless` flag runs the browser without a visible window. This is required for CI environments like GitHub Actions where there&apos;s no display.

&lt;h2&gt;Step 3: Tell Claude what to test&lt;/h2&gt;
&lt;p&gt;For each PR, I want Claude to verify the actual changes. So I pass in the PR description:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; PR Verification Testing&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;PR #32&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Improve set editing and fix playlist overflow

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Your Mission&lt;/span&gt;

This PR claims to implement something. Your job is to:
&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Verify&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; the claimed changes actually work
&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Break&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; them with edge cases
&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Ensure&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; no regressions in related features

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Test This PR&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; Can users edit ANY set during active workout?
&lt;span&gt;-&lt;/span&gt; Do completed sets stay editable?
&lt;span&gt;-&lt;/span&gt; Do long exercise names truncate properly?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude reads this, understands what changed, and tests specifically for those features.&lt;/p&gt;
&lt;h2&gt;What Quinn actually does&lt;/h2&gt;
&lt;p&gt;Here’s a real example from my workout tracker. I opened a PR that said “allow editing any set during a workout.”&lt;/p&gt;
&lt;aside&gt;
  You can view the [actual GitHub Actions run](https://github.com/alexanderop/workoutTracker/actions/runs/20197464088) for this PR. The workflow completed in about 7 minutes and generated a QA report artifact.
&lt;/aside&gt;
&lt;p&gt;Quinn went to work:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[Start Workout]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Add Exercise]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Fill Set Data]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Mark Complete]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;{Test Edit Feature}&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Happy Path|&lt;/span&gt; F&lt;span&gt;[Change Weight ✓]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Edge Case|&lt;/span&gt; G&lt;span&gt;[Enter -50]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Edge Case|&lt;/span&gt; H&lt;span&gt;[Enter 999999]&lt;/span&gt;
    F &lt;span&gt;--&amp;gt;&lt;/span&gt; I&lt;span&gt;[Mobile Test]&lt;/span&gt;
    G &lt;span&gt;--&amp;gt;&lt;/span&gt; I
    H &lt;span&gt;--&amp;gt;&lt;/span&gt; I
    I &lt;span&gt;--&amp;gt;&lt;/span&gt; J&lt;span&gt;[Long Name Test]&lt;/span&gt;
    J &lt;span&gt;--&amp;gt;&lt;/span&gt; K&lt;span&gt;[Generate Report]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The report&lt;/h2&gt;
&lt;p&gt;Quinn generates a full QA report in Markdown:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; QA Verification Report&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;PR&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: #32 - Improve set editing
&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Tester&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Quinn (Claude QA)

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Executive Summary&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;APPROVED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; - All claimed features work as described.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Requirements Verification&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Requirement &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Status &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; How Tested &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;-------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;--------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;------------&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Edit any set &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PASS &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Changed weight after marking complete &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Long names truncate &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PASS &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Added 27-character exercise name &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Mobile layout &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; PASS &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; Tested at 375x667 viewport &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Bugs Found&lt;/span&gt;

None

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Verdict&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;APPROVED&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; - Ready to merge.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This report gets posted automatically as a comment on your PR. You can see exactly what Quinn tested and whether your code is safe to merge.&lt;/p&gt;
&lt;h2&gt;The toolbox&lt;/h2&gt;
&lt;p&gt;Quinn only gets access to browser tools—no code access:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;allowedTools &quot;
  mcp__playwright__browser_navigate&lt;span&gt;,&lt;/span&gt;
  mcp__playwright__browser_click&lt;span&gt;,&lt;/span&gt;
  mcp__playwright__browser_type&lt;span&gt;,&lt;/span&gt;
  mcp__playwright__browser_take_screenshot&lt;span&gt;,&lt;/span&gt;
  mcp__playwright__browser_resize&lt;span&gt;,&lt;/span&gt;
  Write
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps things realistic. A real QA engineer tests through the UI, not by reading code. Quinn does the same.&lt;/p&gt;
&lt;aside&gt;
  Limiting Claude to browser-only tools prevents it from &quot;cheating&quot; by reading your source code. This forces truly black-box testing—the same way real users experience your app.
&lt;/aside&gt;
&lt;h2&gt;Why this works&lt;/h2&gt;
&lt;p&gt;Three reasons this approach beats traditional testing:&lt;/p&gt;
&lt;h3&gt;It tests like a human&lt;/h3&gt;
&lt;p&gt;Unit tests check if functions return the right values. Quinn checks if users can actually accomplish their goals.&lt;/p&gt;
&lt;h3&gt;It’s flexible&lt;/h3&gt;
&lt;p&gt;You don’t write test scripts that break when you change a button’s text. Quinn understands intent and adapts.&lt;/p&gt;
&lt;h3&gt;It finds unexpected bugs&lt;/h3&gt;
&lt;p&gt;Quinn tries things you wouldn’t think to try. Negative numbers? Extremely long inputs? Clicking the same button five times fast? Quinn tests all of it.&lt;/p&gt;
&lt;h2&gt;Comparison: AI QA vs traditional testing&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Unit Tests&lt;/th&gt;
&lt;th&gt;E2E Scripts&lt;/th&gt;
&lt;th&gt;AI QA (Quinn)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tests user flows&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles UI changes&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finds edge cases&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Want to build your own AI QA engineer? Here’s what you need:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Get Claude Code access&lt;/strong&gt; — Sign up at Anthropic and get an API token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create your QA prompt&lt;/strong&gt; — Give Claude a personality and testing philosophy&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up the GitHub Action&lt;/strong&gt; — Use &lt;code&gt;anthropics/claude-code-action&lt;/code&gt; with Playwright MCP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write a verification template&lt;/strong&gt; — Tell Claude what to test for each PR&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;
  The complete GitHub Actions workflow with explore/verify modes, focus areas, and automatic PR comments is available as a [GitHub Gist](https://gist.github.com/alexanderop/464a7a228653e4df27179b9c806b2065). Use it as a starting point for your own QA automation.
&lt;/aside&gt;

  If you&apos;re new to Claude Code, check out my comprehensive guide to Claude Code features covering MCP, Skills, Hooks, and more. You can also set up desktop notifications via hooks so you know the moment Claude finishes a task locally.

&lt;h2&gt;A word of caution&lt;/h2&gt;
&lt;p&gt;This approach is experimental. AI-driven QA is exciting, but it’s not a replacement for deterministic testing.&lt;/p&gt;
&lt;p&gt;A solid testing foundation still matters more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; catch regressions instantly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration tests&lt;/strong&gt; verify your components work together&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E2E tests&lt;/strong&gt; with Playwright or Cypress give you reproducible, reliable checks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI QA works best as a &lt;em&gt;complement&lt;/em&gt; to these, not a replacement. Use it for exploratory testing, edge case discovery, and verifying user flows that are hard to script.&lt;/p&gt;

  Claude Code in GitHub Actions isn&apos;t limited to QA. The same pattern works for:
  - **SEO audits** — Check meta tags, heading structure, Core Web Vitals
  - **Accessibility testing** — Verify ARIA labels, keyboard navigation, color contrast
  - **Content review** — Validate links, check for broken images, lint prose
  - **Visual regression** — Compare screenshots across deployments
&lt;p&gt;Any task where you’d open a browser and manually check something can be automated this way.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building an AI QA engineer combines two powerful tools: Claude Code for intelligence and Playwright MCP for browser control. The result is automated testing that thinks like a human but works tirelessly.&lt;/p&gt;
&lt;p&gt;It’s still early days for this approach. But some day, Quinn might find a bug that would have embarrassed me in production. On that day, this whole experiment will have paid for itself.&lt;/p&gt;
&lt;h2&gt;Additional resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/alexanderop/464a7a228653e4df27179b9c806b2065&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Full GitHub Actions Workflow&lt;/a&gt; — Complete QA workflow with explore/verify modes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code-action&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Anthropic Claude Code Action&lt;/a&gt; — Official GitHub Action&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/playwright-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Playwright MCP&lt;/a&gt; — Browser automation for Claude&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub Actions Documentation&lt;/a&gt; — Workflow automation basics&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>ai</category><category>testing</category><category>claude-code</category><category>automation</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Vue Composables Style Guide: Lessons from VueUse&apos;s Codebase</title><link>https://alexop.dev/posts/vueuse_composables_style_guide/</link><guid isPermaLink="true">https://alexop.dev/posts/vueuse_composables_style_guide/</guid><description>A practical guide for writing production-quality Vue 3 composables, distilled from studying VueUse&apos;s patterns for SSR safety, cleanup, and TypeScript.</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I was studying VueUse’s codebase to understand how they structure their composables. VueUse has become the de facto standard library for Vue utilities, and I wanted to understand the patterns that make their composables so reliable. After diving deep into their source code, I distilled the key patterns into this style guide.&lt;/p&gt;
&lt;p&gt;Whether you’re building your own composable library or just want to write better code, these patterns will help you create maintainable, type-safe, and SSR-compatible composition utilities.&lt;/p&gt;
&lt;p&gt;If you’re new to Vue composables, I recommend starting with my earlier post Mastering Vue 3 Composables: A Comprehensive Style Guide, which covers many of the same patterns from a beginner-friendly perspective.&lt;/p&gt;
&lt;h2&gt;Quick Summary&lt;/h2&gt;
&lt;p&gt;This guide covers patterns for writing production-quality Vue 3 composables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project structure&lt;/strong&gt; and naming conventions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ref type selection&lt;/strong&gt; (shallowRef vs ref)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible inputs&lt;/strong&gt; with &lt;code&gt;MaybeRefOrGetter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR safety&lt;/strong&gt; patterns for server-side rendering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleanup and memory management&lt;/strong&gt; with auto-cleanup utilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controllable composables&lt;/strong&gt; (pausable, stoppable patterns)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript best practices&lt;/strong&gt; for full type inference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing strategies&lt;/strong&gt; - see How to Test Vue Composables&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Getting Started&lt;/h2&gt;
&lt;h3&gt;What Makes a Good Composable?&lt;/h3&gt;
&lt;p&gt;A well-designed composable should be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Focused&lt;/strong&gt;: Does one thing well&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;/strong&gt;: Accepts refs, getters, or plain values&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe&lt;/strong&gt;: Works in SSR, handles cleanup automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typed&lt;/strong&gt;: Full TypeScript support with inference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testable&lt;/strong&gt;: Easy to unit test in isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Minimal Example&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;decrement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;--&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;reset&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; increment&lt;span&gt;,&lt;/span&gt; decrement&lt;span&gt;,&lt;/span&gt; reset &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simple example already demonstrates several VueUse patterns: using &lt;code&gt;shallowRef&lt;/code&gt; for primitives, accepting &lt;code&gt;MaybeRefOrGetter&lt;/code&gt; for flexible inputs, and returning an object with reactive state and methods.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Project Structure&lt;/h2&gt;
&lt;h3&gt;Recommended Layout&lt;/h3&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “src/composables”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “useCounter”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “index.ts”, comment: “Implementation” },&lt;br /&gt;
{ name: “index.test.ts”, comment: “Tests” },&lt;br /&gt;
{ name: “types.ts”, comment: “Types (optional)” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “useFetch”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “index.ts” },&lt;br /&gt;
{ name: “index.test.ts” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “utils”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “ssr.ts”, comment: “SSR utilities” },&lt;br /&gt;
{ name: “types.ts”, comment: “Shared types” },&lt;br /&gt;
{ name: “cleanup.ts”, comment: “Cleanup utilities” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “index.ts”, comment: “Public exports” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Export Pattern&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/composables/index.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; useCounter &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&apos;./useCounter&apos;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; useFetch &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&apos;./useFetch&apos;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; UseCounterReturn&lt;span&gt;,&lt;/span&gt; UseCounterOptions &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&apos;./useCounter&apos;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; UseFetchReturn&lt;span&gt;,&lt;/span&gt; UseFetchOptions &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&apos;./useFetch&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
Use named exports only. Never use default exports for composables. This ensures better tree-shaking and clearer imports.
&lt;/aside&gt;
&lt;p&gt;For more on project organization, check out How to Structure Vue Projects.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Naming Conventions&lt;/h2&gt;
&lt;h3&gt;Function Names&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;use&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard composables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useMouse&lt;/code&gt;, &lt;code&gt;useStorage&lt;/code&gt;, &lt;code&gt;useFetch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Factory functions that return composables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createSharedState&lt;/code&gt;, &lt;code&gt;createEventHook&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Event listener composables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onClickOutside&lt;/code&gt;, &lt;code&gt;onKeyPress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;try&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safe operations that may fail silently&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tryOnMounted&lt;/code&gt;, &lt;code&gt;tryOnCleanup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Type Names&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Options: Use{Name}Options&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseStorageOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  deep&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
  listenToChanges&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Return type: Use{Name}Return&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseStorageReturn&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  data&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;set&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;remove&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Inferred type shorthand&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;UseStorageReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; useStorage&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Choosing the Right Ref Type&lt;/h2&gt;
&lt;p&gt;This is one of the most important decisions when writing composables. VueUse consistently follows this pattern:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    A&lt;span&gt;[What type of data?]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{Primitive?}&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; C&lt;span&gt;[shallowRef]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; D&lt;span&gt;{Will you mutate nested properties?}&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; E&lt;span&gt;[ref - deep reactivity]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No - replace whole object|&lt;/span&gt; F&lt;span&gt;[shallowRef]&lt;/span&gt;

    &lt;span&gt;style&lt;/span&gt; C &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#22c55e&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; E &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#3b82f6&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; F &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#22c55e&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;shallowRef - For Primitives and Replaced Objects&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Primitives&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; isActive &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; name &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Objects that get replaced, not mutated&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;User &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Response &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Usage: Replace the whole object&lt;/span&gt;
user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;John&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;30&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;  &lt;span&gt;// Triggers reactivity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ref - For Deep Mutations&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Objects with nested mutations&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; form &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  user&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; email&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  settings&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; theme&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;light&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Usage: Mutate nested properties&lt;/span&gt;
form&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;user&lt;span&gt;.&lt;/span&gt;name &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&apos;John&apos;&lt;/span&gt;  &lt;span&gt;// Triggers reactivity&lt;/span&gt;
form&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;settings&lt;span&gt;.&lt;/span&gt;theme &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&apos;dark&apos;&lt;/span&gt;  &lt;span&gt;// Triggers reactivity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Let Users Choose&lt;/h3&gt;
&lt;p&gt;For composables storing user data, let them decide:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseStateOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Use shallow reactivity for better performance with large objects
   * @default false
   */&lt;/span&gt;
  shallow&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; options&lt;span&gt;:&lt;/span&gt; UseStateOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; shallow &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; state &lt;span&gt;=&lt;/span&gt; shallow
    &lt;span&gt;?&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;:&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; state &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Flexible Inputs&lt;/h2&gt;
&lt;h3&gt;Accept Refs, Getters, or Plain Values&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;MaybeRefOrGetter&amp;lt;T&amp;gt;&lt;/code&gt; to make your composables flexible:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;title&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// toValue() handles all input types:&lt;/span&gt;
  &lt;span&gt;// - Plain value: &apos;Hello&apos; → &apos;Hello&apos;&lt;/span&gt;
  &lt;span&gt;// - Ref: ref(&apos;Hello&apos;) → &apos;Hello&apos;&lt;/span&gt;
  &lt;span&gt;// - Getter: () =&amp;gt; &apos;Hello&apos; → &apos;Hello&apos;&lt;/span&gt;

  &lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    document&lt;span&gt;.&lt;/span&gt;title &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;title&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// All of these work:&lt;/span&gt;
&lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Static Title&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Reactive Title&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Page &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;currentPage&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;useTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userName&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;s Profile&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reactive Configuration&lt;/h3&gt;
&lt;p&gt;For options that should be reactive:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseIntervalOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  interval&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
  immediate&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UseIntervalOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; interval &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; immediate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;// Watch the interval for changes&lt;/span&gt;
  &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;interval&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;ms&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;ms &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;callback&lt;span&gt;,&lt;/span&gt; ms&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; immediate &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Interval can change reactively&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; delay &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;useInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;tick&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; interval&lt;span&gt;:&lt;/span&gt; delay &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
delay&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;500&lt;/span&gt;  &lt;span&gt;// Interval updates automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Designing Options&lt;/h2&gt;
&lt;h3&gt;Structure&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseStorageOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Storage type to use
   * @default &apos;local&apos;
   */&lt;/span&gt;
  storage&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;local&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&apos;session&apos;&lt;/span&gt;

  &lt;span&gt;/**
   * Custom serializer for complex data
   * @default JSON.stringify/parse
   */&lt;/span&gt;
  serializer&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;read&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;raw&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;
    &lt;span&gt;write&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;/**
   * Sync across browser tabs
   * @default true
   */&lt;/span&gt;
  listenToStorageChanges&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;

  &lt;span&gt;/**
   * Called when an error occurs
   */&lt;/span&gt;
  onError&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;:&lt;/span&gt; Error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rules for Options&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Document every option&lt;/strong&gt; with JSDoc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provide sensible defaults&lt;/strong&gt; using &lt;code&gt;@default&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use callbacks&lt;/strong&gt; for events (&lt;code&gt;onError&lt;/code&gt;, &lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onChange&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group related options&lt;/strong&gt; in nested objects if complex&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Extending Base Interfaces&lt;/h3&gt;
&lt;p&gt;Create reusable option interfaces:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/composables/utils/types.ts&lt;/span&gt;

&lt;span&gt;/** Options for composables that use window */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ConfigurableWindow&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Custom window instance (useful for iframes or testing)
   * @default window
   */&lt;/span&gt;
  window&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Window
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/** Options for composables that use document */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ConfigurableDocument&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Custom document instance
   * @default document
   */&lt;/span&gt;
  document&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Document
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Usage in composables&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseEventListenerOptions&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;ConfigurableWindow&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  capture&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
  passive&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Return Values&lt;/h2&gt;
&lt;h3&gt;Object Return (Recommended for Multiple Values)&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseMouseReturn&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Current X position */&lt;/span&gt;
  x&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;/** Current Y position */&lt;/span&gt;
  y&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;/** Source of the last event */&lt;/span&gt;
  sourceType&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;mouse&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&apos;touch&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useMouse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; UseMouseReturn &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; x &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; y &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; sourceType &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;mouse&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&apos;touch&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;// ... implementation&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;x&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    y&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;y&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    sourceType&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sourceType&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Single Ref Return (For Simple Composables)&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useOnline&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isOnline &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;navigator&lt;span&gt;.&lt;/span&gt;onLine&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;// ... implementation&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isOnline&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tuple Return (When Destructuring Order Matters)&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useToggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  initialValue &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; state &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;toggle&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    state&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; value &lt;span&gt;??&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;state&lt;span&gt;.&lt;/span&gt;value
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;state&lt;span&gt;,&lt;/span&gt; toggle&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Usage&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;isOpen&lt;span&gt;,&lt;/span&gt; toggleOpen&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useToggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Making Composables Awaitable&lt;/h3&gt;
&lt;p&gt;For async composables, implement &lt;code&gt;PromiseLike&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useFetch&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;url&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isLoading &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;execute&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;url&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      data&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; e &lt;span&gt;as&lt;/span&gt; Error
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; shell &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; data&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;,&lt;/span&gt; execute &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;shell&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;// Make it awaitable&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TResult&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      onFulfilled&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; shell&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; TResult
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TResult&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isLoading&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;loading&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;loading&lt;span&gt;)&lt;/span&gt; &lt;span&gt;resolve&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;onFulfilled&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;shell&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; TResult&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Can be used both ways:&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; data&lt;span&gt;,&lt;/span&gt; isLoading &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/api/users&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// Or awaited:&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; data &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;useFetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;/api/users&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// Data is ready&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8. SSR Safety&lt;/h2&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Browser APIs (&lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt;) don’t exist on the server. Accessing them during SSR causes errors.&lt;/p&gt;
&lt;p&gt;For a deep dive into this topic, see How VueUse Solves SSR Window Errors.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; B as Browser
    &lt;span&gt;participant&lt;/span&gt; S as Server &lt;span&gt;(Node.js)&lt;/span&gt;
    B&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; Request page
    S&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; Run Vue code &lt;span&gt;(no window!)&lt;/span&gt;
    &lt;span&gt;Note over&lt;/span&gt; S&lt;span&gt;:&lt;/span&gt; window, document undefined
    S&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Send HTML
    B&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Hydrate app &lt;span&gt;(has window)&lt;/span&gt;
    &lt;span&gt;Note over&lt;/span&gt; B&lt;span&gt;:&lt;/span&gt; Browser APIs available
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Solution: Create SSR Utilities&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/composables/utils/ssr.ts&lt;/span&gt;

&lt;span&gt;/**
 * Check if code is running in browser
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; isClient &lt;span&gt;=&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; window &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&apos;undefined&apos;&lt;/span&gt;

&lt;span&gt;/**
 * Check if code is running on server
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; isServer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;isClient

&lt;span&gt;/**
 * Safe window reference (undefined on server)
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultWindow &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; window &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;

&lt;span&gt;/**
 * Safe document reference (undefined on server)
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultDocument &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; document &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;

&lt;span&gt;/**
 * Safe localStorage reference (undefined on server)
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultStorage &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; localStorage &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using SSR Utilities&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseWindowSizeOptions&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;ConfigurableWindow&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  initialWidth&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;
  initialHeight&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useWindowSize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;options&lt;span&gt;:&lt;/span&gt; UseWindowSizeOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    window &lt;span&gt;=&lt;/span&gt; defaultWindow&lt;span&gt;,&lt;/span&gt;
    initialWidth &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Infinity&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    initialHeight &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Infinity&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; width &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialWidth&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; height &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialHeight&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;update&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Guard: Only run if window exists&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;window&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      width&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;innerWidth
      height&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;innerHeight
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Only set up listeners on client&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;window&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;resize&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; update&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;onUnmounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;resize&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; update&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; width&lt;span&gt;,&lt;/span&gt; height &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Feature Detection&lt;/h3&gt;
&lt;p&gt;Create a utility to safely check for browser features:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useSupported&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isSupported &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;onMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isSupported&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;check&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isSupported&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Usage&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useClipboard&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isSupported &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useSupported&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; navigator &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;&apos;clipboard&apos;&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; navigator
  &lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;copy&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;isSupported&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Clipboard API not supported&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;// ... implementation&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isSupported&lt;span&gt;,&lt;/span&gt; copy &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;9. Cleanup and Memory Management&lt;/h2&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Event listeners, timers, and observers must be cleaned up to prevent memory leaks.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[Composable Created]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Register Resources]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Listeners, Timers, etc.]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;{Component Unmounted?}&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; E&lt;span&gt;[Cleanup Required]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; C
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[Remove Listeners]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; G&lt;span&gt;[Clear Timers]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; H&lt;span&gt;[Disconnect Observers]&lt;/span&gt;

    &lt;span&gt;style&lt;/span&gt; E &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#ef4444&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; F &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#22c55e&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; G &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#22c55e&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
    &lt;span&gt;style&lt;/span&gt; H &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#22c55e&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#fff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Solution: Auto-Cleanup Utility&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/composables/utils/cleanup.ts&lt;/span&gt;
&lt;span&gt;/**
 * Register a cleanup function that runs when the scope is disposed.
 * Safe to call outside of component context.
 *
 * @returns true if cleanup was registered, false otherwise
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;getCurrentScope&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;onScopeDispose&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fn&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Safe onMounted that doesn&apos;t error outside component context
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;tryOnMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;getCurrentScope&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;onMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fn&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using Auto-Cleanup&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;callback&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; ms&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; timer&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; setInterval&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;start&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;stop&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;callback&lt;span&gt;,&lt;/span&gt; ms&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;stop&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt;
      timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;start&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;// Automatically stops when component unmounts&lt;/span&gt;
  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;stop&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; start&lt;span&gt;,&lt;/span&gt; stop &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Event Listener Composable with Auto-Cleanup&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useEventListener&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; WindowEventMap&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  event&lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;handler&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;event&lt;span&gt;:&lt;/span&gt; WindowEventMap&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; AddEventListenerOptions &lt;span&gt;&amp;amp;&lt;/span&gt; ConfigurableWindow
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; window &lt;span&gt;=&lt;/span&gt; defaultWindow&lt;span&gt;,&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;listenerOptions &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options &lt;span&gt;??&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;let&lt;/span&gt; &lt;span&gt;cleanup&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;window&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;event&lt;span&gt;,&lt;/span&gt; handler&lt;span&gt;,&lt;/span&gt; listenerOptions&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;cleanup&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;event&lt;span&gt;,&lt;/span&gt; handler&lt;span&gt;,&lt;/span&gt; listenerOptions&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;cleanup&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; cleanup
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;10. Controllable Composables&lt;/h2&gt;
&lt;h3&gt;Pausable Pattern&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;stateDiagram-v2&lt;/span&gt;
    &lt;span&gt;[*]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; Active&lt;span&gt;:&lt;/span&gt; immediate=true
    &lt;span&gt;[*]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; Paused&lt;span&gt;:&lt;/span&gt; immediate=false
    Active &lt;span&gt;--&amp;gt;&lt;/span&gt; Paused&lt;span&gt;:&lt;/span&gt; pause&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    Paused &lt;span&gt;--&amp;gt;&lt;/span&gt; Active&lt;span&gt;:&lt;/span&gt; resume&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    Active &lt;span&gt;--&amp;gt;&lt;/span&gt; &lt;span&gt;[*]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; cleanup
    Paused &lt;span&gt;--&amp;gt;&lt;/span&gt; &lt;span&gt;[*]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; cleanup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For composables that can be paused and resumed:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Pausable&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Whether the composable is currently active */&lt;/span&gt;
  isActive&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;/** Pause the composable */&lt;/span&gt;
  &lt;span&gt;pause&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;/** Resume the composable */&lt;/span&gt;
  &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseIntervalOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Start immediately */&lt;/span&gt;
  immediate&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
  &lt;span&gt;/** Call callback immediately when starting */&lt;/span&gt;
  immediateCallback&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useIntervalFn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  interval&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UseIntervalOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Pausable &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; immediateCallback &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; isActive &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; timer&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; setInterval&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt;
      timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;pause&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isActive&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; ms &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;interval&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;ms &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;

    isActive&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;immediateCallback&lt;span&gt;)&lt;/span&gt; &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;callback&lt;span&gt;,&lt;/span&gt; ms&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;immediate&lt;span&gt;)&lt;/span&gt; &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;pause&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isActive&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    pause&lt;span&gt;,&lt;/span&gt;
    resume&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Usage&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isActive&lt;span&gt;,&lt;/span&gt; pause&lt;span&gt;,&lt;/span&gt; resume &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useIntervalFn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;tick&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;pause&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;   &lt;span&gt;// Stop ticking&lt;/span&gt;
&lt;span&gt;resume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// Start again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Stoppable Pattern&lt;/h3&gt;
&lt;p&gt;For one-way stopping (e.g., timeouts, one-time operations):&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Stoppable&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Whether the operation is pending */&lt;/span&gt;
  isPending&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;/** Stop the operation */&lt;/span&gt;
  &lt;span&gt;stop&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTimeoutFn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  interval&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Stoppable &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; isPending &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; timer&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; setTimeout&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;stop&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isPending&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt;
      timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;start&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;stop&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    isPending&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isPending&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
      timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
      &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;interval&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;immediate&lt;span&gt;)&lt;/span&gt; &lt;span&gt;start&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;stop&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isPending&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isPending&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    stop&lt;span&gt;,&lt;/span&gt;
    start&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;11. Error Handling&lt;/h2&gt;
&lt;h3&gt;Graceful Degradation&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useGeolocation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isSupported &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useSupported&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; navigator &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;&apos;geolocation&apos;&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; navigator
  &lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; coords &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;GeolocationCoordinates &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;GeolocationPositionError &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;isSupported&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;

    navigator&lt;span&gt;.&lt;/span&gt;geolocation&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCurrentPosition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;(&lt;/span&gt;position&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        coords&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; position&lt;span&gt;.&lt;/span&gt;coords
        error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; err
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;isSupported&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isSupported&lt;span&gt;,&lt;/span&gt;
    coords&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;,&lt;/span&gt;
    update&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error Callbacks&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseAsyncStateOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Called on success */&lt;/span&gt;
  onSuccess&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;data&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;/** Called on error */&lt;/span&gt;
  onError&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;/**
   * Whether to throw errors
   * @default false
   */&lt;/span&gt;
  throwError&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useAsyncState&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;promise&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  initialState&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UseAsyncStateOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; onSuccess&lt;span&gt;,&lt;/span&gt; onError&lt;span&gt;,&lt;/span&gt; throwError &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; state &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialState&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isLoading &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;promise&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      state&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; data
      onSuccess&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;data&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; e
      onError&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;throwError&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; e
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; state&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;,&lt;/span&gt; execute &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;13. TypeScript Best Practices&lt;/h2&gt;
&lt;h3&gt;Generic Type Inference&lt;/h3&gt;
&lt;p&gt;Let TypeScript infer types when possible:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Type T is inferred from defaultValue&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useStorage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; defaultValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// ...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Usage - types are inferred&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; name &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;name&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;John&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;     &lt;span&gt;// Ref&amp;lt;string&amp;gt;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;count&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;        &lt;span&gt;// Ref&amp;lt;number&amp;gt;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;  &lt;span&gt;// Ref&amp;lt;{ id: number }&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Function Overloads&lt;/h3&gt;
&lt;p&gt;Use overloads for different call signatures:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Overload 1: Window events&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useEventListener&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; WindowEventMap&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  event&lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;handler&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;:&lt;/span&gt; WindowEventMap&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;

&lt;span&gt;// Overload 2: Element events&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useEventListener&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; HTMLElementEventMap&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  target&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;HTMLElement &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  event&lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;handler&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;:&lt;/span&gt; HTMLElementEventMap&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;

&lt;span&gt;// Implementation&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;args&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// ... implementation handles all cases&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional Return Types&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// If passed a ref, return just the toggle function&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useToggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  value&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;

&lt;span&gt;// If passed a plain value, return tuple&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useToggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  initialValue&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

&lt;span&gt;// Implementation&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useToggle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  initialValue&lt;span&gt;:&lt;/span&gt; MaybeRef&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; valueIsRef &lt;span&gt;=&lt;/span&gt; &lt;span&gt;isRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; state &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;toggle&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    state&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; value &lt;span&gt;??&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;state&lt;span&gt;.&lt;/span&gt;value
    &lt;span&gt;return&lt;/span&gt; state&lt;span&gt;.&lt;/span&gt;value
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;valueIsRef&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; toggle
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;state&lt;span&gt;,&lt;/span&gt; toggle&lt;span&gt;]&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;const&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;14. Testing&lt;/h2&gt;
&lt;p&gt;For comprehensive testing strategies including basic test structure, testing with timers, and testing async composables, see How to Test Vue Composables.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;15. Documentation&lt;/h2&gt;
&lt;h3&gt;JSDoc Comments&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;/**
 * Reactive mouse position
 *
 * @param options - Configuration options
 * @returns Reactive mouse coordinates and source type
 *
 * @example
 * ```ts
 * const { x, y } = useMouse()
 *
 * watchEffect(() =&amp;gt; {
 *   console.log(`Mouse at ${x.value}, ${y.value}`)
 * })
 * ```
 *
 * @see https://your-docs.com/composables/use-mouse
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useMouse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;options&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; UseMouseOptions&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; UseMouseReturn &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// ...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Document Every Option&lt;/h3&gt;
&lt;p&gt;Every option should have a JSDoc comment with a &lt;code&gt;@default&lt;/code&gt; tag:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseStorageOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Storage type to use
   * @default &apos;local&apos;
   */&lt;/span&gt;
  storage&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;local&apos;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&apos;session&apos;&lt;/span&gt;

  &lt;span&gt;/**
   * Whether to sync across browser tabs
   * @default true
   */&lt;/span&gt;
  listenToStorageChanges&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;16. Templates&lt;/h2&gt;
&lt;h3&gt;Basic Composable Template&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseXxxOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Option description
   * @default &apos;defaultValue&apos;
   */&lt;/span&gt;
  someOption&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseXxxReturn&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/** Description of value */&lt;/span&gt;
  value&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;/** Description of action */&lt;/span&gt;
  &lt;span&gt;doSomething&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Short description of what this composable does
 *
 * @param param - Description of parameter
 * @param options - Configuration options
 */&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useXxx&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  param&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UseXxxOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; UseXxxReturn &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; someOption &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&apos;defaultValue&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;doSomething&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; paramValue &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;param&lt;span&gt;)&lt;/span&gt;
    value&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Cleanup if needed&lt;/span&gt;
  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// cleanup logic&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    doSomething&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Async Composable Template&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseAsyncXxxOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Execute immediately
   * @default true
   */&lt;/span&gt;
  immediate&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
  &lt;span&gt;/**
   * Called on success
   */&lt;/span&gt;
  onSuccess&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;data&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;/**
   * Called on error
   */&lt;/span&gt;
  onError&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;:&lt;/span&gt; Error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UseAsyncXxxReturn&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  data&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  error&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  isLoading&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
  onSuccess&lt;span&gt;:&lt;/span&gt; EventHook&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;on&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  onError&lt;span&gt;:&lt;/span&gt; EventHook&lt;span&gt;&amp;lt;&lt;/span&gt;Error&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;on&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useAsyncXxx&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;fetcher&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UseAsyncXxxOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; UseAsyncXxxReturn&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; onSuccess&lt;span&gt;,&lt;/span&gt; onError &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isLoading &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; successHook &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;createEventHook&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; errorHook &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;createEventHook&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Error&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetcher&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      data&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; result
      onSuccess&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;)&lt;/span&gt;
      successHook&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; err &lt;span&gt;=&lt;/span&gt; e &lt;span&gt;as&lt;/span&gt; Error
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; err
      onError&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;
      errorHook&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;immediate&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    data&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;data&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isLoading&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    execute&lt;span&gt;,&lt;/span&gt;
    onSuccess&lt;span&gt;:&lt;/span&gt; successHook&lt;span&gt;.&lt;/span&gt;on&lt;span&gt;,&lt;/span&gt;
    onError&lt;span&gt;:&lt;/span&gt; errorHook&lt;span&gt;.&lt;/span&gt;on&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pausable Composable Template&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Pausable&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  isActive&lt;span&gt;:&lt;/span&gt; Readonly&lt;span&gt;&amp;lt;&lt;/span&gt;Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;pause&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
  &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UsePausableXxxOptions&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Start immediately
   * @default true
   */&lt;/span&gt;
  immediate&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;usePausableXxx&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;callback&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  interval&lt;span&gt;:&lt;/span&gt; MaybeRefOrGetter&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; UsePausableXxxOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Pausable &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; immediate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options

  &lt;span&gt;const&lt;/span&gt; isActive &lt;span&gt;=&lt;/span&gt; &lt;span&gt;shallowRef&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; timer&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; setInterval&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer&lt;span&gt;)&lt;/span&gt;
      timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;pause&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isActive&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;
    &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; ms &lt;span&gt;=&lt;/span&gt; &lt;span&gt;toValue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;interval&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;ms &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;

    isActive&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    &lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    timer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;callback&lt;span&gt;,&lt;/span&gt; ms&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;immediate&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;resume&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;tryOnCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;pause&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isActive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isActive&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    pause&lt;span&gt;,&lt;/span&gt;
    resume&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Quick Reference Checklist&lt;/h2&gt;
&lt;p&gt;Use this checklist when creating new composables:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Named export (no default)&lt;/li&gt;
&lt;li&gt;[ ] Explicit return type interface&lt;/li&gt;
&lt;li&gt;[ ] JSDoc with &lt;code&gt;@param&lt;/code&gt;, &lt;code&gt;@returns&lt;/code&gt;, &lt;code&gt;@example&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reactivity&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;shallowRef&lt;/code&gt; for primitives&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;ref&lt;/code&gt; only when deep mutations needed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;MaybeRefOrGetter&lt;/code&gt; for flexible inputs&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;toValue()&lt;/code&gt; to unwrap inputs&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;readonly()&lt;/code&gt; for exposed refs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Safety&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Guard browser APIs (&lt;code&gt;if (window)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Auto-cleanup with &lt;code&gt;tryOnCleanup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] Feature detection for optional APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Generic type inference where possible&lt;/li&gt;
&lt;li&gt;[ ] Overloads for multiple signatures&lt;/li&gt;
&lt;li&gt;[ ] Strict types, no &lt;code&gt;any&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Unit tests for all functionality&lt;/li&gt;
&lt;li&gt;[ ] Edge cases (null, undefined, empty)&lt;/li&gt;
&lt;li&gt;[ ] Cleanup verification&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/extras/composition-api-faq.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Composition API Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/extras/reactivity-in-depth.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Reactivity in Depth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vueuse.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VueUse&lt;/a&gt; - Collection of Vue composables (the source of these patterns)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest&lt;/a&gt; - Testing framework for Vue&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;These patterns represent the accumulated wisdom from VueUse’s codebase. Apply them consistently to build maintainable, type-safe, and production-ready Vue composables.&lt;/p&gt;

          </content:encoded><category>vue</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Forcing Claude Code to TDD: An Agentic Red-Green-Refactor Loop</title><link>https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/</guid><description>Build a custom TDD workflow with Claude Code using skills and subagents that enforce Red-Green-Refactor discipline for your Vue projects.</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I rely on Claude Code, but it has a structural limitation: it defaults to implementation-first. It writes the “Happy Path,” ignoring edge cases. When I try to force TDD in a single context window, the implementation “bleeds” into the test logic (context pollution). This article documents a multi-agent system using Claude’s “Skills” and “Hooks” that enforces a strict Red-Green-Refactor cycle.&lt;/p&gt;

  While this article uses Vue as an example, the TDD principles and Claude Code workflow apply to any technology. Whether you&apos;re working with React, Angular, Svelte, or even backend languages like Python, Go, or Rust—the Red-Green-Refactor cycle and subagent orchestration work the same way.

&lt;h2&gt;The Problem with AI-Assisted TDD&lt;/h2&gt;
&lt;p&gt;When I ask Claude to “implement feature X,” it writes the implementation first. Every time. TDD flips this—you write the test first, watch it fail, then write minimal code to make it pass.&lt;/p&gt;
&lt;p&gt;I needed a way to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Force test-first&lt;/strong&gt; — No implementation before a failing test exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep phases focused&lt;/strong&gt; — The test writer shouldn’t think about implementation details&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ensure refactoring happens&lt;/strong&gt; — Easy to skip when the feature already works&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Skills + Subagents&lt;/h2&gt;
&lt;p&gt;Claude Code supports two features I hadn’t explored until recently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; (&lt;code&gt;.claude/skills/&lt;/code&gt;): High-level workflows that orchestrate complex tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents&lt;/strong&gt; (&lt;code&gt;.claude/agents/&lt;/code&gt;): Specialized workers that handle specific jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might wonder: why use subagents at all? Skills alone could handle the TDD workflow. But there’s a catch—context pollution.&lt;/p&gt;
&lt;h3&gt;The Context Pollution Problem&lt;/h3&gt;

When everything runs in one context window, **the LLM cannot truly follow TDD**. The test writer&apos;s detailed analysis bleeds into the implementer&apos;s thinking. The implementer&apos;s code exploration pollutes the refactorer&apos;s evaluation. Each phase drags along baggage from the others.
&lt;p&gt;This isn’t just messy—it fundamentally breaks TDD. The whole point of writing the test first is that &lt;strong&gt;you don’t know the implementation yet&lt;/strong&gt;. But if the same context sees both phases, the LLM subconsciously designs tests around the implementation it’s already planning. It “cheats” without meaning to.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subagents solve this architectural limitation.&lt;/strong&gt; Each phase runs in complete isolation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;test writer&lt;/strong&gt; focuses purely on test design—it has no idea how the feature will be implemented&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;implementer&lt;/strong&gt; sees only the failing test—it can’t be influenced by test-writing decisions&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;refactorer&lt;/strong&gt; evaluates clean implementation code—it starts fresh without implementation baggage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each agent starts with exactly the context it needs and nothing more. This isn’t just organization—it’s the only way to get genuine test-first development from an LLM.&lt;/p&gt;
&lt;p&gt;Combining skills with subagents gave me exactly what I needed:&lt;/p&gt;
&lt;h2&gt;The TDD Skill&lt;/h2&gt;
&lt;p&gt;The orchestrating skill lives at &lt;code&gt;.claude/skills/tdd-integration/skill.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; tdd&lt;span&gt;-&lt;/span&gt;integration
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Enforce Test&lt;span&gt;-&lt;/span&gt;Driven Development with strict Red&lt;span&gt;-&lt;/span&gt;Green&lt;span&gt;-&lt;/span&gt;Refactor cycle using integration tests. Auto&lt;span&gt;-&lt;/span&gt;triggers when implementing new features or functionality. Trigger phrases include &quot;implement&quot;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;add feature&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;build&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;create functionality&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; or any request to add new behavior. Does NOT trigger for bug fixes&lt;span&gt;,&lt;/span&gt; documentation&lt;span&gt;,&lt;/span&gt; or configuration changes.&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; TDD Integration Testing&lt;/span&gt;

Enforce strict Test-Driven Development using the Red-Green-Refactor cycle with dedicated subagents.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Mandatory Workflow&lt;/span&gt;

Every new feature MUST follow this strict 3-phase cycle. Do NOT skip phases.

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Phase 1: RED - Write Failing Test&lt;/span&gt;

🔴 RED PHASE: Delegating to tdd-test-writer...

Invoke the &lt;span&gt;`tdd-test-writer`&lt;/span&gt; subagent with:
&lt;span&gt;-&lt;/span&gt; Feature requirement from user request
&lt;span&gt;-&lt;/span&gt; Expected behavior to test

The subagent returns:
&lt;span&gt;-&lt;/span&gt; Test file path
&lt;span&gt;-&lt;/span&gt; Failure output confirming test fails
&lt;span&gt;-&lt;/span&gt; Summary of what the test verifies

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Do NOT proceed to Green phase until test failure is confirmed.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Phase 2: GREEN - Make It Pass&lt;/span&gt;

🟢 GREEN PHASE: Delegating to tdd-implementer...

Invoke the &lt;span&gt;`tdd-implementer`&lt;/span&gt; subagent with:
&lt;span&gt;-&lt;/span&gt; Test file path from RED phase
&lt;span&gt;-&lt;/span&gt; Feature requirement context

The subagent returns:
&lt;span&gt;-&lt;/span&gt; Files modified
&lt;span&gt;-&lt;/span&gt; Success output confirming test passes
&lt;span&gt;-&lt;/span&gt; Implementation summary

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Do NOT proceed to Refactor phase until test passes.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;###&lt;/span&gt; Phase 3: REFACTOR - Improve&lt;/span&gt;

🔵 REFACTOR PHASE: Delegating to tdd-refactorer...

Invoke the &lt;span&gt;`tdd-refactorer`&lt;/span&gt; subagent with:
&lt;span&gt;-&lt;/span&gt; Test file path
&lt;span&gt;-&lt;/span&gt; Implementation files from GREEN phase

The subagent returns either:
&lt;span&gt;-&lt;/span&gt; Changes made + test success output, OR
&lt;span&gt;-&lt;/span&gt; &quot;No refactoring needed&quot; with reasoning

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Cycle complete when refactor phase returns.&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Multiple Features&lt;/span&gt;

Complete the full cycle for EACH feature before starting the next:

Feature 1: 🔴 → 🟢 → 🔵 ✓
Feature 2: 🔴 → 🟢 → 🔵 ✓
Feature 3: 🔴 → 🟢 → 🔵 ✓

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Phase Violations&lt;/span&gt;

Never:
&lt;span&gt;-&lt;/span&gt; Write implementation before the test
&lt;span&gt;-&lt;/span&gt; Proceed to Green without seeing Red fail
&lt;span&gt;-&lt;/span&gt; Skip Refactor evaluation
&lt;span&gt;-&lt;/span&gt; Start a new feature before completing the current cycle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; field contains trigger phrases so Claude activates this skill automatically when I ask to implement something. Each phase has explicit “Do NOT proceed until…” gates—Claude needs clear boundaries. The 🔴🟢🔵 emojis make tracking progress easy in the output.&lt;/p&gt;
&lt;h2&gt;The Test Writer Agent (RED Phase)&lt;/h2&gt;
&lt;p&gt;At &lt;code&gt;.claude/agents/tdd-test-writer.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; tdd&lt;span&gt;-&lt;/span&gt;test&lt;span&gt;-&lt;/span&gt;writer
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Write failing integration tests for TDD RED phase. Use when implementing new features with TDD. Returns only after verifying test FAILS.
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Glob&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Write&lt;span&gt;,&lt;/span&gt; Edit&lt;span&gt;,&lt;/span&gt; Bash
&lt;span&gt;skills&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; vue&lt;span&gt;-&lt;/span&gt;integration&lt;span&gt;-&lt;/span&gt;testing&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; TDD Test Writer (RED Phase)&lt;/span&gt;

Write a failing integration test that verifies the requested feature behavior.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Process&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; Understand the feature requirement from the prompt
&lt;span&gt;2.&lt;/span&gt; Write an integration test in &lt;span&gt;`src/__tests__/integration/`&lt;/span&gt;
&lt;span&gt;3.&lt;/span&gt; Run &lt;span&gt;`pnpm test:unit &amp;lt;test-file&amp;gt;`&lt;/span&gt; to verify it fails
&lt;span&gt;4.&lt;/span&gt; Return the test file path and failure output

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Test Structure&lt;/span&gt;

typescript
describe(&apos;Feature Name&apos;, () =&amp;gt; {
  afterEach(async () =&amp;gt; {
    resetWorkout()
    await resetDatabase()
    document.body.innerHTML = &apos;&apos;
  })

  it(&apos;describes the user journey&apos;, async () =&amp;gt; {
    const app = await createTestApp()

&lt;span&gt;    // Act: User interactions
    await app.user.click(app.getByRole(&apos;button&apos;, { name: /action/i }))&lt;/span&gt;

&lt;span&gt;    // Assert: Verify outcomes
    expect(app.router.currentRoute.value.path).toBe(&apos;/expected&apos;)&lt;/span&gt;

&lt;span&gt;    app.cleanup()&lt;/span&gt;
  })
})

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Requirements&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; Test must describe user behavior, not implementation details
&lt;span&gt;-&lt;/span&gt; Use &lt;span&gt;`createTestApp()`&lt;/span&gt; for full app integration
&lt;span&gt;-&lt;/span&gt; Use Testing Library queries (&lt;span&gt;`getByRole`&lt;/span&gt;, &lt;span&gt;`getByText`&lt;/span&gt;)
&lt;span&gt;-&lt;/span&gt; Test MUST fail when run - verify before returning

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Return Format&lt;/span&gt;

Return:
&lt;span&gt;-&lt;/span&gt; Test file path
&lt;span&gt;-&lt;/span&gt; Failure output showing the test fails
&lt;span&gt;-&lt;/span&gt; Brief summary of what the test verifies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I limited the tools to only what’s needed for writing and running tests. The &lt;code&gt;skills&lt;/code&gt; field pulls in my &lt;code&gt;vue-integration-testing&lt;/code&gt; skill for project-specific context. And the explicit return format ensures clean handoffs between phases.&lt;/p&gt;

  This skill defines how I want tests written: using jsdom with Vue Test Utils, writing BDD-style tests that describe user behavior, and avoiding mocks wherever possible. I don&apos;t see much value in unit tests that mock everything—they often just verify implementation details rather than actual functionality. Integration tests that exercise real code paths catch more bugs.

&lt;h2&gt;The Implementer Agent (GREEN Phase)&lt;/h2&gt;
&lt;p&gt;At &lt;code&gt;.claude/agents/tdd-implementer.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; tdd&lt;span&gt;-&lt;/span&gt;implementer
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Implement minimal code to pass failing tests for TDD GREEN phase. Write only what the test requires. Returns only after verifying test PASSES.
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Glob&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Write&lt;span&gt;,&lt;/span&gt; Edit&lt;span&gt;,&lt;/span&gt; Bash&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; TDD Implementer (GREEN Phase)&lt;/span&gt;

Implement the minimal code needed to make the failing test pass.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Process&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; Read the failing test to understand what behavior it expects
&lt;span&gt;2.&lt;/span&gt; Identify the files that need changes
&lt;span&gt;3.&lt;/span&gt; Write the minimal implementation to pass the test
&lt;span&gt;4.&lt;/span&gt; Run &lt;span&gt;`pnpm test:unit &amp;lt;test-file&amp;gt;`&lt;/span&gt; to verify it passes
&lt;span&gt;5.&lt;/span&gt; Return implementation summary and success output

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Principles&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Minimal&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Write only what the test requires
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;No extras&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: No additional features, no &quot;nice to haves&quot;
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Test-driven&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: If the test passes, the implementation is complete
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Fix implementation, not tests&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: If the test fails, fix your code

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Return Format&lt;/span&gt;

Return:
&lt;span&gt;-&lt;/span&gt; Files modified with brief description of changes
&lt;span&gt;-&lt;/span&gt; Test success output
&lt;span&gt;-&lt;/span&gt; Summary of the implementation
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Refactorer Agent (REFACTOR Phase)&lt;/h2&gt;
&lt;p&gt;At &lt;code&gt;.claude/agents/tdd-refactorer.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; tdd&lt;span&gt;-&lt;/span&gt;refactorer
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Evaluate and refactor code after TDD GREEN phase. Improve code quality while keeping tests passing. Returns evaluation with changes made or &quot;no refactoring needed&quot; with reasoning.
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Glob&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Write&lt;span&gt;,&lt;/span&gt; Edit&lt;span&gt;,&lt;/span&gt; Bash
&lt;span&gt;skills&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; vue&lt;span&gt;-&lt;/span&gt;composables&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; TDD Refactorer (REFACTOR Phase)&lt;/span&gt;

Evaluate the implementation for refactoring opportunities and apply improvements while keeping tests green.

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Process&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; Read the implementation and test files
&lt;span&gt;2.&lt;/span&gt; Evaluate against refactoring checklist
&lt;span&gt;3.&lt;/span&gt; Apply improvements if beneficial
&lt;span&gt;4.&lt;/span&gt; Run &lt;span&gt;`pnpm test:unit &amp;lt;test-file&amp;gt;`&lt;/span&gt; to verify tests still pass
&lt;span&gt;5.&lt;/span&gt; Return summary of changes or &quot;no refactoring needed&quot;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Refactoring Checklist&lt;/span&gt;

Evaluate these opportunities:

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Extract composable&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Reusable logic that could benefit other components
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Simplify conditionals&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Complex if/else chains that could be clearer
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Improve naming&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Variables or functions with unclear names
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Remove duplication&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Repeated code patterns
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Thin components&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Business logic that should move to composables

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Decision Criteria&lt;/span&gt;

Refactor when:
&lt;span&gt;-&lt;/span&gt; Code has clear duplication
&lt;span&gt;-&lt;/span&gt; Logic is reusable elsewhere
&lt;span&gt;-&lt;/span&gt; Naming obscures intent
&lt;span&gt;-&lt;/span&gt; Component contains business logic

Skip refactoring when:
&lt;span&gt;-&lt;/span&gt; Code is already clean and simple
&lt;span&gt;-&lt;/span&gt; Changes would be over-engineering
&lt;span&gt;-&lt;/span&gt; Implementation is minimal and focused

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Return Format&lt;/span&gt;

If changes made:
&lt;span&gt;-&lt;/span&gt; Files modified with brief description
&lt;span&gt;-&lt;/span&gt; Test success output confirming tests pass
&lt;span&gt;-&lt;/span&gt; Summary of improvements

If no changes:
&lt;span&gt;-&lt;/span&gt; &quot;No refactoring needed&quot;
&lt;span&gt;-&lt;/span&gt; Brief reasoning (e.g., &quot;Implementation is minimal and focused&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This agent has a &lt;strong&gt;decision framework&lt;/strong&gt; for whether to refactor. Sometimes “no refactoring needed” is the right answer. The &lt;code&gt;skills&lt;/code&gt; field references my &lt;code&gt;vue-composables&lt;/code&gt; skill so it knows my project’s patterns for extracting reusable logic.&lt;/p&gt;
&lt;h2&gt;Real Example: Adding Workout Detail View&lt;/h2&gt;
&lt;p&gt;Here’s what this looks like in practice. My request:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“When a user is on the Workouts page, they should be able to click on a past workout and see a detail view of what exercises and sets they completed.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The workflow executes like this:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; U as User
    &lt;span&gt;participant&lt;/span&gt; S as TDD Skill
    &lt;span&gt;participant&lt;/span&gt; TW as Test Writer
    &lt;span&gt;participant&lt;/span&gt; I as Implementer
    &lt;span&gt;participant&lt;/span&gt; R as Refactorer

    U&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Add workout detail view&quot;&lt;/span&gt;
    S&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;TW&lt;span&gt;:&lt;/span&gt; Feature requirement
    TW&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;TW&lt;span&gt;:&lt;/span&gt; Write test
    TW&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;TW&lt;span&gt;:&lt;/span&gt; Run test
    TW&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; ❌ Test fails
    S&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;I&lt;span&gt;:&lt;/span&gt; Test file path
    I&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;I&lt;span&gt;:&lt;/span&gt; Write minimal code
    I&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;I&lt;span&gt;:&lt;/span&gt; Run test
    I&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; ✅ Test passes
    S&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;R&lt;span&gt;:&lt;/span&gt; Implementation files
    R&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;R&lt;span&gt;:&lt;/span&gt; Evaluate code
    R&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;R&lt;span&gt;:&lt;/span&gt; Extract composable
    R&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;R&lt;span&gt;:&lt;/span&gt; Run test
    R&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; ✅ Improvements applied
    S&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;U&lt;span&gt;:&lt;/span&gt; 🔴→🟢→🔵 Complete
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🔴 RED Phase&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tdd-test-writer&lt;/code&gt; produced:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/__tests__/integration/workout-detail.spec.ts&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Workout History Detail View&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;afterEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// cleanup&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;navigates to detail view when clicking a completed workout&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Arrange: Create a completed workout&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; completedWorkout &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;generateId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Push Day&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      exercises&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;Bench Press&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        sets&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; kg&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;100&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; reps&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;10&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      completedAt&lt;span&gt;:&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;workouts&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;completedWorkout&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;// Act: Navigate and click&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; app &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;createTestApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; app&lt;span&gt;.&lt;/span&gt;user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;button&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;workouts&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; app&lt;span&gt;.&lt;/span&gt;user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Push Day&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;// Assert&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;app&lt;span&gt;.&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/workouts/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;completedWorkout&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Bench Press&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeDefined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;100&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeDefined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

    app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test failed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AssertionError: expected &apos;/workouts&apos; to be &apos;/workouts/d747077d-...&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exactly what I wanted—a clear failure pointing to missing functionality.&lt;/p&gt;
&lt;h3&gt;🟢 GREEN Phase&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tdd-implementer&lt;/code&gt; created:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WorkoutDetailView.vue&lt;/code&gt; — New detail view component&lt;/li&gt;
&lt;li&gt;Modified &lt;code&gt;TheWorkoutsView.vue&lt;/code&gt; — Added click handlers&lt;/li&gt;
&lt;li&gt;Modified &lt;code&gt;router/index.ts&lt;/code&gt; — Added &lt;code&gt;/workouts/:id&lt;/code&gt; route&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Test passed. Minimal implementation, just enough to satisfy the assertions.&lt;/p&gt;
&lt;h3&gt;🔵 REFACTOR Phase&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tdd-refactorer&lt;/code&gt; evaluated the code and made improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Extracted &lt;code&gt;useWorkoutDetail&lt;/code&gt; composable&lt;/strong&gt; — Reusable data fetching with discriminated union states&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Created shared formatters&lt;/strong&gt; — Pulled &lt;code&gt;formatDuration&lt;/code&gt; and &lt;code&gt;formatDate&lt;/code&gt; into &lt;code&gt;lib/formatters.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Added accessibility&lt;/strong&gt; — Keyboard navigation for clickable cards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All tests still passed. The cycle completed.&lt;/p&gt;
&lt;h2&gt;The Test Helper&lt;/h2&gt;
&lt;p&gt;A crucial piece making all this work is my &lt;code&gt;createTestApp()&lt;/code&gt; helper:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// src/__tests__/helpers/createTestApp.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;createTestApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TestApp&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; pinia &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createPinia&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    history&lt;span&gt;:&lt;/span&gt; &lt;span&gt;createMemoryHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    routes&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;App&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    global&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;router&lt;span&gt;,&lt;/span&gt; pinia&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;await&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isReady&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    router&lt;span&gt;,&lt;/span&gt;
    user&lt;span&gt;:&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    getByRole&lt;span&gt;:&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;getByRole&lt;span&gt;,&lt;/span&gt;
    getByText&lt;span&gt;:&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;getByText&lt;span&gt;,&lt;/span&gt;
    findByText&lt;span&gt;:&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;findByText&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;waitForRoute&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;pattern&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;waitFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;pattern&lt;span&gt;.&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Route mismatch&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;cleanup&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;.&lt;/span&gt;innerHTML &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&apos;&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives agents a consistent API for rendering the full app and simulating user interactions. They don’t need to figure out how to set up Vue, Pinia, and Vue Router each time—they just call &lt;code&gt;createTestApp()&lt;/code&gt; and start writing assertions.&lt;/p&gt;
&lt;h2&gt;Hooks for Consistent Skill Activation&lt;/h2&gt;
&lt;p&gt;Even with well-written skills, Claude sometimes skipped evaluation and jumped straight to implementation. I tracked this informally—skill activation happened maybe 20% of the time.&lt;/p&gt;
&lt;p&gt;I found a great solution in &lt;a href=&quot;https://scottspence.com/posts/how-to-make-claude-code-skills-activate-reliably&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Scott Spence’s post on making skills activate reliably&lt;/a&gt;. He tested 200+ prompts across different hook configurations and found that a “forced eval” approach—making Claude explicitly evaluate each skill before proceeding—jumped activation from ~20% to ~84%.&lt;/p&gt;
&lt;p&gt;The fix: &lt;strong&gt;hooks&lt;/strong&gt;. Claude Code runs hooks at specific lifecycle points, and I used &lt;code&gt;UserPromptSubmit&lt;/code&gt; to inject a reminder before every response.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;UserPromptSubmit&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;npx tsx \&quot;$CLAUDE_PROJECT_DIR/.claude/hooks/user-prompt-skill-eval.ts\&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;timeout&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;5&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hook script at &lt;code&gt;.claude/hooks/user-prompt-skill-eval.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;#!/usr/bin/env npx tsx&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;utf-8&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// consume stdin&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; instruction &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;
INSTRUCTION: MANDATORY SKILL ACTIVATION SEQUENCE

Step 1 - EVALUATE:
For each skill in &amp;lt;available_skills&amp;gt;, state: [skill-name] - YES/NO - [reason]

Step 2 - ACTIVATE:
IF any skills are YES → Use Skill(skill-name) tool for EACH relevant skill NOW
IF no skills are YES → State &quot;No skills needed&quot; and proceed

Step 3 - IMPLEMENT:
Only after Step 2 is complete, proceed with implementation.

CRITICAL: You MUST call Skill() tool in Step 2. Do NOT skip to implementation.
&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;

  stdout&lt;span&gt;.&lt;/span&gt;&lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;instruction&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  With this hook, skill activation jumped from ~20% to ~84%. Now when I say &quot;implement the workout detail view,&quot; the TDD skill triggers automatically.

&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Claude Code’s default behavior produces implementation-first code with minimal test coverage. Without constraints, it optimizes for “working code” rather than “tested code.”&lt;/p&gt;
&lt;p&gt;The system described here addresses this through architectural separation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hooks&lt;/strong&gt; inject evaluation logic before every prompt, increasing skill activation from ~20% to ~84%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; define explicit phase gates that block progression until each TDD step completes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents&lt;/strong&gt; enforce context isolation—the test writer cannot see implementation plans, so tests reflect actual requirements rather than anticipated code structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The setup cost is ~2 hours of configuration. After that, each feature request automatically follows the Red-Green-Refactor cycle without manual enforcement.&lt;/p&gt;

          </content:encoded><category>ai</category><category>vue</category><category>testing</category><category>claude-code</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Claude Code Notifications: Get Alerts When Tasks Finish (Hooks Setup)</title><link>https://alexop.dev/posts/claude-code-notification-hooks/</link><guid isPermaLink="true">https://alexop.dev/posts/claude-code-notification-hooks/</guid><description>How to set up Claude Code notifications using hooks. Get desktop alerts when Claude finishes a task, needs your input, or requests permission, instead of watching the terminal.</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;You’re deep in your work. Claude Code is running, doing its thing. You check back five minutes later. Still waiting. Ten minutes later? Still waiting.&lt;/p&gt;
&lt;p&gt;Wouldn’t it be nice to know &lt;em&gt;when&lt;/em&gt; Claude actually needs you?&lt;/p&gt;
&lt;p&gt;This is where hooks come in. Claude Code runs hooks at specific points in its workflow. You can tap into those hooks to send yourself desktop notifications—so you never miss an important moment.&lt;/p&gt;
&lt;p&gt;But here’s the thing—if you’ve never used hooks before, they might sound abstract. Let me break it down.&lt;/p&gt;
&lt;h2&gt;What Are Hooks?&lt;/h2&gt;
&lt;p&gt;Hooks are commands that run at specific points in Claude Code’s lifecycle. They let you respond to events without constantly watching the CLI. Instead of polling, you get notified.&lt;/p&gt;
&lt;p&gt;Claude Code provides two notification hooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;permission_prompt&lt;/code&gt;&lt;/strong&gt; - Claude needs your permission to do something&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;idle_prompt&lt;/code&gt;&lt;/strong&gt; - Claude is waiting for your input&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of them like webhooks, but for your local machine. Claude Code fires an event, you can respond.&lt;/p&gt;
&lt;h2&gt;Setting Up Desktop Notifications&lt;/h2&gt;
&lt;p&gt;Now let’s get this working. It’s straightforward—just two pieces: a configuration file and a notification script.&lt;/p&gt;
&lt;p&gt;Start by creating a &lt;code&gt;.claude/hooks&lt;/code&gt; directory in your project. Then add the hook configuration to &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;Notification&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;permission_prompt|idle_prompt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;npx tsx \&quot;$CLAUDE_PROJECT_DIR/.claude/hooks/notification-desktop.ts\&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;timeout&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;5&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Claude Code: “When you hit a &lt;code&gt;permission_prompt&lt;/code&gt; or &lt;code&gt;idle_prompt&lt;/code&gt;, run this command.” The &lt;code&gt;timeout: 5&lt;/code&gt; means the hook has 5 seconds to complete before Claude moves on.&lt;/p&gt;
&lt;p&gt;You can place this in two locations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt; - Project-specific (checked into git, shared with team)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/settings.json&lt;/code&gt; - Global user settings (personal machine only)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use project-specific settings for team hooks, global settings for personal notifications. The &lt;code&gt;$CLAUDE_PROJECT_DIR&lt;/code&gt; is an environment variable Claude Code provides—it expands to your project root automatically.&lt;/p&gt;
&lt;p&gt;Here’s what your project structure should look like:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree tree={[&lt;br /&gt;
{ name: ‘your-project’, children: [&lt;br /&gt;
{ name: ‘.claude’, children: [&lt;br /&gt;
{ name: ‘settings.json’, comment: ‘Hook configuration’ },&lt;br /&gt;
{ name: ‘hooks’, children: [&lt;br /&gt;
{ name: ‘notification-desktop.ts’, comment: ‘Notification script’ }&lt;br /&gt;
], open: true }&lt;br /&gt;
], open: true },&lt;br /&gt;
{ name: ‘src’, comment: ‘…’ },&lt;br /&gt;
{ name: ‘package.json’, comment: ‘…’ }&lt;br /&gt;
], open: true }&lt;br /&gt;
]} /&amp;gt;&lt;/p&gt;
&lt;h2&gt;The Notification Script&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;.claude/hooks/notification-desktop.ts&lt;/code&gt;. This script handles sending the actual notifications:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;#!/usr/bin/env npx tsx&lt;/span&gt;
&lt;span&gt;/* eslint-disable node/prefer-global/process */&lt;/span&gt;
&lt;span&gt;/**
 * Claude Code Notification Hook - Desktop Alerts
 *
 * Sends system notifications when Claude needs attention:
 * - Permission prompts
 * - Idle prompts (waiting for input)
 */&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;readStdin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;utf-8&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;sendMacNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Escape special characters for AppleScript&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; escapedTitle &lt;span&gt;=&lt;/span&gt; title&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;\\&quot;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; escapedMessage &lt;span&gt;=&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;\\&quot;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; script &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;display notification &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;escapedMessage&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot; with title &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;escapedTitle&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot; sound name &quot;Ping&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;

  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;execSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;osascript -e &apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;script&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; stdio&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&apos;ignore&apos;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Notification failed, ignore silently&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; rawInput &lt;span&gt;=&lt;/span&gt; &lt;span&gt;readStdin&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;let&lt;/span&gt; parsedInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;
  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    parsedInput &lt;span&gt;=&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;rawInput&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    process&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; input &lt;span&gt;=&lt;/span&gt; parsedInput &lt;span&gt;as&lt;/span&gt; NotificationHookInput

  &lt;span&gt;const&lt;/span&gt; notificationType &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;input &lt;span&gt;as&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; notification_type&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;notification_type
  &lt;span&gt;const&lt;/span&gt; message &lt;span&gt;=&lt;/span&gt; input&lt;span&gt;.&lt;/span&gt;message

  &lt;span&gt;switch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;notificationType&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&apos;permission_prompt&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;sendMacNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Claude Code - Permission Required&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; message &lt;span&gt;||&lt;/span&gt; &lt;span&gt;&apos;Claude needs your permission to continue&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;break&lt;/span&gt;
    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&apos;idle_prompt&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;sendMacNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Claude Code - Waiting&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; message &lt;span&gt;||&lt;/span&gt; &lt;span&gt;&apos;Claude is waiting for your input&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;break&lt;/span&gt;
    &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;// Don&apos;t notify for other types&lt;/span&gt;
      &lt;span&gt;break&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  process&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;When This Really Shines&lt;/h2&gt;
&lt;p&gt;You’ve set up the basics. Now here’s where it becomes powerful.&lt;/p&gt;
&lt;p&gt;This notification system is especially useful when you’re doing deep focus work and Claude Code runs a long operation. You don’t have to check your terminal every few seconds. Permission prompts that need immediate action? They hit you with a different sound. Idle waits while you’ve stepped away? A gentle reminder pulls you back.&lt;/p&gt;
&lt;p&gt;The key is this: the notification comes exactly when you need to be engaged. No sooner, no later.&lt;/p&gt;
&lt;p&gt;Want to go deeper? You can even build custom plugins that use hooks across different projects for more powerful automation.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hooks transform Claude Code from a tool you watch into a tool that watches for you.&lt;br /&gt;
The setup takes maybe five minutes. Copy the configuration, create the script, adjust the sounds to your preference. After that, you’re done. No more context switching. No more glancing at the terminal every few seconds wondering if Claude needs you. The notification arrives exactly when it matters.&lt;br /&gt;
That’s the real power here. It’s not about automating notifications. It’s about reclaiming your focus—letting Claude Code work while you work, and pulling your attention back only when it’s needed. Set it up once, and you’ve unlocked a better way to collaborate with AI.&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>notifications</category><category>hooks</category><category>automation</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Speed Up Your Claude Code Experience with Slash Commands</title><link>https://alexop.dev/posts/claude-code-slash-commands-guide/</link><guid isPermaLink="true">https://alexop.dev/posts/claude-code-slash-commands-guide/</guid><description>Learn how to transform Claude Code from a chatbot into a deterministic engine using Slash Commands. This guide covers the technical setup and a complete &apos;Full Circle&apos; workflow that automates your entire feature lifecycle.</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I was wasting time. Every commit message, every branch name, every PR description. I typed the same things over and over.&lt;br /&gt;
Then I discovered Slash Commands in Claude Code. Now I type &lt;code&gt;/commit&lt;/code&gt; and it writes the message for me. &lt;code&gt;/branch &quot;add dark mode&quot;&lt;/code&gt; and it creates &lt;code&gt;feat/add-dark-mode&lt;/code&gt;. &lt;code&gt;/pr&lt;/code&gt; and it generates a full PR description from my commits.&lt;br /&gt;
This post shows you how to build the same workflow. I’ll cover how Slash Commands work, then we’ll build a complete system that automates your entire git lifecycle.&lt;/p&gt;
&lt;aside&gt;
Slash Commands are just one piece of the Claude Code puzzle. For the full picture—including MCP, hooks, subagents, and skills—see my comprehensive guide to Claude Code&apos;s feature stack.
&lt;/aside&gt;

You need Git and the GitHub CLI (`gh`). Install `gh` with `brew install gh` on macOS or check [cli.github.com](https://cli.github.com). Run `gh auth login` to authenticate.
&lt;p&gt;Without &lt;code&gt;gh&lt;/code&gt;, commands like &lt;code&gt;/pr&lt;/code&gt; and &lt;code&gt;/fix-pipeline&lt;/code&gt; will not work.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Two things you need to know&lt;/h2&gt;
&lt;p&gt;Before we build the workflow, you need to understand two features.&lt;/p&gt;
&lt;h3&gt;Bash command execution&lt;/h3&gt;
&lt;p&gt;Write &lt;code&gt;!git status&lt;/code&gt; inside a command file. Claude runs the command first, captures the output, and injects it into the prompt. The AI sees the result before it starts thinking.&lt;/p&gt;
&lt;p&gt;This is how &lt;code&gt;/commit&lt;/code&gt; knows what you changed. It runs &lt;code&gt;!git diff&lt;/code&gt; automatically. See the &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/slash-commands#bash-command-execution&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;official documentation&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h3&gt;Model selection&lt;/h3&gt;
&lt;p&gt;You don’t need a powerful model to fix a missing semicolon.&lt;br /&gt;
Claude Code lets you pick the model in the frontmatter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sonnet&lt;/code&gt; — for complex reasoning (default)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;haiku&lt;/code&gt; — fast and cheap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add &lt;code&gt;model: haiku&lt;/code&gt; and commands run almost instantly.&lt;/p&gt;
&lt;h2&gt;Command structure&lt;/h2&gt;
&lt;p&gt;Slash commands are Markdown files stored in &lt;code&gt;.claude/commands/&lt;/code&gt; (project-level) or &lt;code&gt;~/.claude/commands/&lt;/code&gt; (personal). The filename becomes the command name: &lt;code&gt;commit.md&lt;/code&gt; becomes &lt;code&gt;/commit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “my-project”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “.claude”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “settings.json”, comment: “project settings” },&lt;br /&gt;
{&lt;br /&gt;
name: “commands”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://branch.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;branch.md&lt;/a&gt;”, comment: “/branch” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://commit.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;commit.md&lt;/a&gt;”, comment: “/commit” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://pr.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pr.md&lt;/a&gt;”, comment: “/pr” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://lint.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;lint.md&lt;/a&gt;”, comment: “/lint” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “src” },&lt;br /&gt;
{ name: “package.json” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Here is a complete example:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Create a git commit with a conventional message
&lt;span&gt;allowed-tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Bash(git add&lt;span&gt;:&lt;/span&gt;&lt;span&gt;*)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Bash(git commit&lt;span&gt;:&lt;/span&gt;&lt;span&gt;*)&lt;/span&gt;
&lt;span&gt;argument-hint&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;message&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;model&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; haiku&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; Commit Changes&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;git_diff&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
!&lt;span&gt;`git diff --cached`&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;git_diff&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

Create a commit message following Conventional Commits.
If $ARGUMENTS is provided, use it as the commit message.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Frontmatter options&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Brief description shown in &lt;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First line of prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tools the command can use&lt;/td&gt;
&lt;td&gt;Inherits from conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model to use (&lt;code&gt;sonnet&lt;/code&gt;, &lt;code&gt;haiku&lt;/code&gt;, or full model ID)&lt;/td&gt;
&lt;td&gt;Inherits from conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;argument-hint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows expected arguments in autocomplete&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Arguments&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;$ARGUMENTS&lt;/code&gt; to capture everything passed to the command:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;Create a branch named: $ARGUMENTS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For multiple arguments, use positional parameters &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, etc:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;argument-hint&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;pr&lt;span&gt;-&lt;/span&gt;number&lt;span&gt;]&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;priority&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

Review PR #$1 with priority $2.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;File references&lt;/h3&gt;
&lt;p&gt;Include file contents with the &lt;code&gt;@&lt;/code&gt; prefix:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;Review the implementation in @src/utils/helpers.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The workflow&lt;/h2&gt;
&lt;p&gt;I replaced my manual git rituals with custom commands. They live in &lt;code&gt;.claude/commands/&lt;/code&gt;. Here is how I drive a feature from start to merge.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Development Workflow
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;flowchart&lt;/span&gt; LR
    &lt;span&gt;%% Initial Setup&lt;/span&gt;
    Start&lt;span&gt;((Start))&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; Branch&lt;span&gt;[&quot;/branch&quot;]&lt;/span&gt;
    Branch &lt;span&gt;--&amp;gt;&lt;/span&gt; Code&lt;span&gt;[Write Code]&lt;/span&gt;

    &lt;span&gt;%% Local Iteration Loop&lt;/span&gt;
    Code &lt;span&gt;--&amp;gt;&lt;/span&gt; Lint&lt;span&gt;[&quot;/lint&amp;lt;br/&amp;gt;(Haiku)&quot;]&lt;/span&gt;
    Lint &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;Auto-fix&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Lint
    Lint &lt;span&gt;--&amp;gt;&lt;/span&gt; Test&lt;span&gt;[&quot;/vitest&amp;lt;br/&amp;gt;(Haiku)&quot;]&lt;/span&gt;
    Test &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;Fix Failure&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Test
    
    &lt;span&gt;%% Deployment Flow&lt;/span&gt;
    Test &lt;span&gt;--&amp;gt;&lt;/span&gt; Push&lt;span&gt;[&quot;/push&quot;]&lt;/span&gt;
    Push &lt;span&gt;--&amp;gt;&lt;/span&gt; PR&lt;span&gt;[&quot;/pr&quot;]&lt;/span&gt;
    PR &lt;span&gt;--&amp;gt;&lt;/span&gt; CI&lt;span&gt;{CI Pass?}&lt;/span&gt;

    &lt;span&gt;%% CI Debugging Loop&lt;/span&gt;
    CI &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;No&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Fix[&lt;span&gt;&quot;/fix-pipeline&amp;lt;br/&amp;gt;(Sonnet)&quot;&lt;/span&gt;]
    Fix &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;Fix &amp;amp; Push&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; CI

    &lt;span&gt;%% Final Review &amp;amp; Merge&lt;/span&gt;
    CI &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;Yes&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Review&lt;span&gt;[&quot;/review-coderabbit&quot;]&lt;/span&gt;
    Review &lt;span&gt;--&amp;gt;&lt;/span&gt; Merge&lt;span&gt;[&quot;/merge-to-main&quot;]&lt;/span&gt;
    Merge &lt;span&gt;--&amp;gt;&lt;/span&gt; Done&lt;span&gt;((Done))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/branch — start a task&lt;/h3&gt;

I type `/branch &quot;implement dark mode toggle&quot;` and Claude checks out main, pulls latest, and creates `feat/dark-mode-toggle`. No more thinking about naming conventions.

&lt;h3&gt;/lint — fix before commit&lt;/h3&gt;

I type `/lint`. It runs the linter with auto-fix, and if errors remain, Claude fixes them. Uses Haiku for speed—runs in about 20 seconds.

&lt;h3&gt;/vitest — run unit tests&lt;/h3&gt;

I type `/vitest`. It runs the test suite and fixes any failures. The prompt tells Claude to fix the code, not the test—implementation should match expected behavior.

&lt;h3&gt;/commit — save your work&lt;/h3&gt;

I type `/commit`. Claude analyzes the diff, generates a Conventional Commit message, and commits. It looks at recent commits to match your project&apos;s style.

&lt;h3&gt;/push — commit and push in one step&lt;/h3&gt;

I type `/push`. It stages everything, generates a commit message, commits, and pushes. My most-used command—one word and the code is on GitHub.

&lt;h3&gt;/fix-pipeline — fix failing CI tests&lt;/h3&gt;

I type `/fix-pipeline`. It fetches the failed logs via `gh`, analyzes the error, and fixes it. Uses Sonnet because debugging requires reasoning. The prompt includes guardrails—Claude must read the actual error before proposing fixes.

&lt;h3&gt;/pr — create a pull request&lt;/h3&gt;

I type `/pr`. It analyzes all commits on the branch, generates a PR title and description, and opens it via `gh pr create`. Checks if a PR already exists first.

&lt;h3&gt;/review-coderabbit — address review comments&lt;/h3&gt;

I type `/review-coderabbit`. It fetches CodeRabbit&apos;s comments via GraphQL, verifies each suggestion against the codebase, implements valid fixes or pushes back with reasoning, and resolves every thread. AI reviewers aren&apos;t always right—the prompt ensures Claude verifies before acting.

&lt;h3&gt;/merge-to-main — finish the task&lt;/h3&gt;

I type `/merge-to-main`. It squash merges the PR, deletes the branch, and pulls main. Done.

&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;By moving your process into &lt;code&gt;.claude/commands/&lt;/code&gt;, you are building a system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bash command execution injects real-time context&lt;/li&gt;
&lt;li&gt;Model selection balances speed vs reasoning&lt;/li&gt;
&lt;li&gt;The workflow automates branching, linting, committing, CI debugging, PRs, and merging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Define the process once. Claude executes it every time.&lt;/p&gt;
&lt;p&gt;Want to extend Claude Code even further? Connect external tools via MCP (Model Context Protocol) or package your commands into a shareable plugin.&lt;/p&gt;
&lt;p&gt;I don’t think about naming conventions, commit messages, or PR descriptions anymore. The commands handle it.&lt;/p&gt;

You can skip the interactive prompt entirely with `claude -p`. Add aliases to your `.zshrc` or `.bashrc`:
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;alias&lt;/span&gt; &lt;span&gt;clint&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;claude -p &apos;/lint&apos;&quot;&lt;/span&gt;
&lt;span&gt;alias&lt;/span&gt; &lt;span&gt;cpush&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;claude -p &apos;/push&apos;&quot;&lt;/span&gt;
&lt;span&gt;alias&lt;/span&gt; &lt;span&gt;ccommit&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;claude -p &apos;/commit&apos;&quot;&lt;/span&gt;
&lt;span&gt;alias&lt;/span&gt; &lt;span&gt;cbranch&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;claude -p &apos;/branch&apos;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;clint&lt;/code&gt; runs the lint command without opening the interactive session. The &lt;code&gt;-p&lt;/code&gt; flag passes the prompt directly—Claude executes and exits. Two steps become one keystroke.&lt;br /&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

          </content:encoded><category>ai</category><category>claude-code</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Understanding Claude Code&apos;s Full Stack: MCP, Skills, Subagents, and Hooks Explained</title><link>https://alexop.dev/posts/understanding-claude-code-full-stack/</link><guid isPermaLink="true">https://alexop.dev/posts/understanding-claude-code-full-stack/</guid><description>A practical guide to Claude Code&apos;s features: MCP, CLAUDE.md, slash commands, subagents, hooks, plugins, skills, and scheduled tasks. Updated April 2026 with deferred tool loading, worktree isolation, agent teams, and more.</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I used Claude Code for months as a glorified autocomplete. Quick edits. Boilerplate. The vibe coding tool.&lt;/p&gt;
&lt;p&gt;Then I dug into MCP servers, slash commands, plugins, skills, hooks, subagents, and &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; files. Claude Code is a framework for orchestrating AI agents, and most people use one or two features without seeing how they stack together. (It also runs in more places now: CLI, VS Code, JetBrains, a standalone desktop app, a web app at &lt;a href=&quot;http://claude.ai/code&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;claude.ai/code&lt;/a&gt;, and iOS.)&lt;/p&gt;
&lt;p&gt;This guide explains each concept &lt;strong&gt;in the order they build on each other&lt;/strong&gt;, from external connections to automatic behaviors. (New to using LLMs for development? Start with my overview of how I use LLMs for context.)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude Code is, with hindsight, poorly named. It’s not purely a coding tool: it’s a tool for general computer automation. Anything you can achieve by typing commands into a computer is something that can now be automated by Claude Code. It’s best described as a general agent. Skills make this a whole lot more obvious and explicit.&lt;/p&gt;
&lt;p&gt;— Simon Willison, &lt;a href=&quot;https://simonwillison.net/2025/Oct/16/claude-skills/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Skills are awesome, maybe a bigger deal than MCP&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;TLDR&lt;br /&gt;
items={[&lt;br /&gt;
“&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; files give Claude project memory and context”,&lt;br /&gt;
“Skills are the unified extensibility model — commands and skills are now one thing”,&lt;br /&gt;
“Subagents handle parallel work in isolated contexts”,&lt;br /&gt;
“Hooks react to lifecycle events without manual triggering”,&lt;br /&gt;
“Plugins bundle commands, hooks, and skills for sharing”,&lt;br /&gt;
“MCP connects external tools through a universal protocol”,&lt;br /&gt;
“Skills activate based on task context”,&lt;br /&gt;
“Scheduled tasks run Claude on a cron schedule without you present”,&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;The feature stack&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; — the foundation for connecting external tools and data sources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code core features&lt;/strong&gt; — project memory, skills, subagents, and hooks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt; — shareable packages that bundle skills, hooks, and metadata&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; — the unified extensibility model (replaces the old command/skill split)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled Tasks&lt;/strong&gt; — cloud-based triggers that run Claude on a cron schedule&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
&lt;hr /&gt;
&lt;h2&gt;1) Model Context Protocol (MCP) — connecting external systems&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    actor User
    &lt;span&gt;participant&lt;/span&gt; Claude
    &lt;span&gt;participant&lt;/span&gt; MCPServer

    User&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Claude&lt;span&gt;:&lt;/span&gt; /mcp connect github
    Claude&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;MCPServer&lt;span&gt;:&lt;/span&gt; Authenticate + request capabilities
    MCPServer&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Claude&lt;span&gt;:&lt;/span&gt; Return tools/resources/prompts
    Claude&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;User&lt;span&gt;:&lt;/span&gt; Display /mcp__github__* commands
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; The Model Context Protocol connects Claude Code to external tools and data sources. Think universal adapter for GitHub, databases, APIs, and other systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Connect an MCP server, get access to its tools, resources, and prompts as slash commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# Install a server&lt;/span&gt;
claude mcp &lt;span&gt;add&lt;/span&gt; playwright npx @playwright/mcp@latest

&lt;span&gt;# Use it&lt;/span&gt;
/mcp__playwright__create-test &lt;span&gt;[&lt;/span&gt;args&lt;span&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  Claude Code no longer loads full MCP tool schemas at startup. It loads tool names only, then fetches the full schema on demand via `ToolSearch`. If you run 50+ MCP tools, this cuts context overhead by an order of magnitude. Monitor remaining usage with `/context`.

&lt;p&gt;&lt;strong&gt;Remote servers.&lt;/strong&gt; The MCP spec added streamable HTTP transport (replacing SSE) and OAuth 2.1 with PKCE for authentication. You can now connect to remote MCP servers without running a local stdio process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The gotcha.&lt;/strong&gt; MCP servers expose their own tools. They don’t inherit Claude’s Read, Write, or Bash unless you provide them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-world example.&lt;/strong&gt; Want to see MCP in action? Check out how to build an AI QA engineer with Playwright MCP that tests your app like a real user.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2) Claude Code core features&lt;/h2&gt;
&lt;h3&gt;2.1) Project memory with &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Markdown files Claude loads at startup. They give Claude memory about your project’s conventions, architecture, and patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Files merge hierarchically from enterprise → user (&lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;) → project (&lt;code&gt;./CLAUDE.md&lt;/code&gt;). When you reference &lt;code&gt;@components/Button.vue&lt;/code&gt;, Claude also reads &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; from that directory and its parents.&lt;/p&gt;
&lt;p&gt;Two additions since launch: &lt;code&gt;CLAUDE.local.md&lt;/code&gt; files sit alongside &lt;code&gt;CLAUDE.md&lt;/code&gt; but are gitignored, so you can add personal preferences without affecting the team. And &lt;code&gt;~/.claude/rules/&lt;/code&gt; lets you split global instructions into separate files instead of one large &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example structure for a Vue app:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “my-vue-app”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”,&lt;br /&gt;
comment: “Project-wide conventions, tech stack, build commands”,&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”,&lt;br /&gt;
comment: “Component patterns, naming conventions, prop types”,&lt;br /&gt;
},&lt;br /&gt;
{ name: “Button.vue” },&lt;br /&gt;
{ name: “Card.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “pages”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;”,&lt;br /&gt;
comment: “Routing patterns, page structure, data fetching”,&lt;br /&gt;
},&lt;br /&gt;
{ name: “Home.vue” },&lt;br /&gt;
{ name: “About.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;When you work on &lt;code&gt;src/components/Button.vue&lt;/code&gt;, Claude loads context from:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enterprise &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; (if configured)&lt;/li&gt;
&lt;li&gt;User &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; (personal preferences)&lt;/li&gt;
&lt;li&gt;Project root &lt;code&gt;CLAUDE.md&lt;/code&gt; (project-wide info)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/components/CLAUDE.md&lt;/code&gt; (component-specific patterns)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What goes in.&lt;/strong&gt; Common commands, coding standards, architectural patterns. Keep it concise — reference guide, not documentation. Need help creating your own? Check out this &lt;a href=&quot;https://alexop.dev//prompts/claude/claude-create-md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md creation guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here’s my blog’s &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;#&lt;/span&gt; CLAUDE.md&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Project Overview&lt;/span&gt;

Alexander Opalic&apos;s personal blog built on AstroPaper - Astro-based blog theme with TypeScript, React, TailwindCSS.

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Tech Stack&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;: Astro 5, TypeScript, React, TailwindCSS, Shiki, FuseJS, Playwright

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Development Commands&lt;/span&gt;

&lt;span&gt;&lt;span&gt;```&lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt; run dev              &lt;span&gt;# Build + Pagefind + dev server (localhost:4321)&lt;/span&gt;
&lt;span&gt;npm&lt;/span&gt; run build            &lt;span&gt;# Production build&lt;/span&gt;
&lt;span&gt;npm&lt;/span&gt; run lint             &lt;span&gt;# ESLint for .astro, .ts, .tsx&lt;/span&gt;
---&lt;/span&gt;
&lt;span&gt;```&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2) Skills — the unified extensibility model&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    Skill&lt;span&gt;[&quot;SKILL.md&quot;]&lt;/span&gt;
    Auto&lt;span&gt;[&quot;Claude auto-invokes&amp;lt;br/&amp;gt;(description matches task)&quot;]&lt;/span&gt;
    Manual&lt;span&gt;[&quot;User invokes&amp;lt;br/&amp;gt;(/skill-name args)&quot;]&lt;/span&gt;
    Fork&lt;span&gt;{&quot;context: fork?&quot;}&lt;/span&gt;
    Main&lt;span&gt;[&quot;Run in main context&quot;]&lt;/span&gt;
    Sub&lt;span&gt;[&quot;Run in subagent&quot;]&lt;/span&gt;

    Skill &lt;span&gt;--&amp;gt;&lt;/span&gt; Auto
    Skill &lt;span&gt;--&amp;gt;&lt;/span&gt; Manual
    Auto &lt;span&gt;--&amp;gt;&lt;/span&gt; Fork
    Manual &lt;span&gt;--&amp;gt;&lt;/span&gt; Fork
    Fork &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|no|&lt;/span&gt; Main
    Fork &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|yes|&lt;/span&gt; Sub
&lt;/code&gt;&lt;/pre&gt;

  As of 2026, slash commands and skills are unified. Files in `.claude/commands/` still work, but the recommended approach is `.claude/skills/`. Every skill gets a `/slash-command` interface. Frontmatter controls whether Claude can auto-invoke it, whether users see it in the `/` menu, and whether it runs in a subagent.

&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; Folders with a &lt;code&gt;SKILL.md&lt;/code&gt; file (plus optional helper scripts) that define reusable behaviors. Frontmatter controls invocation: Claude can auto-invoke based on task context, you can trigger manually with &lt;code&gt;/skill-name&lt;/code&gt;, or both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key frontmatter fields:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display name, becomes &lt;code&gt;/slash-command&lt;/code&gt;. Lowercase, hyphens, max 64 chars. Defaults to directory name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the skill does. Claude uses this to decide auto-invocation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disable-model-invocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;true&lt;/code&gt; to prevent Claude from auto-invoking (deploy, commit).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-invocable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;false&lt;/code&gt; to hide from &lt;code&gt;/&lt;/code&gt; menu (background knowledge only).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tools Claude can use without asking.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set to &lt;code&gt;fork&lt;/code&gt; to run in an isolated subagent context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subagent type when &lt;code&gt;context: fork&lt;/code&gt; is set (&lt;code&gt;Explore&lt;/code&gt;, &lt;code&gt;Plan&lt;/code&gt;, &lt;code&gt;general-purpose&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model override (&lt;code&gt;haiku&lt;/code&gt;, &lt;code&gt;sonnet&lt;/code&gt;, &lt;code&gt;opus&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;argument-hint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hint for expected arguments, shown in autocomplete.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Glob patterns limiting when skill auto-loads (e.g., &lt;code&gt;src/**/*.ts&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Argument substitution:&lt;/strong&gt; &lt;code&gt;$ARGUMENTS&lt;/code&gt; for all args, &lt;code&gt;$0&lt;/code&gt;, &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt; for positional, &lt;code&gt;${CLAUDE_SKILL_DIR}&lt;/code&gt; for the skill’s directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@file&lt;/code&gt; syntax to inline code&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allowed-tools: Bash(...)&lt;/code&gt; for pre-execution scripts&lt;/li&gt;
&lt;li&gt;

  XML-tagged prompts

for reliable outputs
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use.&lt;/strong&gt; Repeatable workflows, domain expertise, automated enforcement. For a complete example of a git workflow built with skills, see my Slash Commands Guide. Want to create your own? Use this &lt;a href=&quot;https://alexop.dev//prompts/claude/claude-create-skill&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;skill creation guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: a deploy skill that only you can trigger:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; deploy
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Deploy the application to production
&lt;span&gt;disable-model-invocation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;span&gt;allowed-tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Bash(npm&lt;span&gt;:&lt;/span&gt;&lt;span&gt;*)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Bash(git&lt;span&gt;:&lt;/span&gt;&lt;span&gt;*)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

Deploy to production:
&lt;span&gt;1.&lt;/span&gt; Run tests
&lt;span&gt;2.&lt;/span&gt; Build
&lt;span&gt;3.&lt;/span&gt; Push to deployment target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates &lt;code&gt;/deploy&lt;/code&gt; that &lt;strong&gt;only you can invoke&lt;/strong&gt; — Claude cannot auto-trigger it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: a skill that runs in a subagent:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; deep&lt;span&gt;-&lt;/span&gt;research
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Research a topic thoroughly
&lt;span&gt;context&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; fork
&lt;span&gt;agent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Explore
&lt;span&gt;allowed-tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Glob&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

Research $ARGUMENTS thoroughly. Find relevant files, read and analyze them, summarize findings.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When invoked, this spawns a separate context window. Your main conversation stays clean.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2.3) Subagents — specialized AI personalities for delegation&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; Main
    &lt;span&gt;participant&lt;/span&gt; SubA
    &lt;span&gt;participant&lt;/span&gt; SubB

    Main&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;SubA&lt;span&gt;:&lt;/span&gt; task&lt;span&gt;:&lt;/span&gt; security analysis
    Main&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;SubB&lt;span&gt;:&lt;/span&gt; task&lt;span&gt;:&lt;/span&gt; test generation
    &lt;span&gt;par&lt;/span&gt; Parallel execution
        SubA&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Main&lt;span&gt;:&lt;/span&gt; results
        SubB&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Main&lt;span&gt;:&lt;/span&gt; results
    &lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; Pre-configured AI personalities with specific expertise areas. Each subagent has its own system prompt, allowed tools, and separate context window. Claude delegates to them when a task matches their expertise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why use them.&lt;/strong&gt; Keep your main conversation clean while offloading specialized work. Each subagent works in its own context window, preventing token bloat. Run multiple subagents in parallel for concurrent analysis.&lt;/p&gt;

  Subagents prevent &quot;context poisoning&quot; — when detailed implementation work
  clutters your main conversation. Use subagents for deep dives (security
  audits, test generation, refactoring) that would otherwise fill your primary
  context with noise.

&lt;p&gt;&lt;strong&gt;Example structure:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; security&lt;span&gt;-&lt;/span&gt;auditor
&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Analyzes code for security vulnerabilities
&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Read&lt;span&gt;,&lt;/span&gt; Grep&lt;span&gt;,&lt;/span&gt; Bash &lt;span&gt;# Controls what this personality can access&lt;/span&gt;
&lt;span&gt;model&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; sonnet &lt;span&gt;# Optional: sonnet, opus, haiku, inherit&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

You are a security-focused code auditor.

Identify vulnerabilities (XSS, SQL injection, CSRF, etc.)
Check dependencies and packages
Verify auth/authorization
Review data validation

Provide severity levels: Critical, High, Medium, Low.
Focus on OWASP Top 10.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The system prompt shapes the subagent’s behavior. The &lt;code&gt;description&lt;/code&gt; tells Claude when to delegate. The &lt;code&gt;tools&lt;/code&gt; field restricts access.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best practices:&lt;/strong&gt; One expertise area per subagent. Minimal tool access. Use &lt;code&gt;haiku&lt;/code&gt; for simple tasks, &lt;code&gt;sonnet&lt;/code&gt; for complex analysis. Run independent work in parallel. Need a template? Check out this &lt;a href=&quot;https://alexop.dev//prompts/claude/claude-create-agent&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;subagent creation guide&lt;/a&gt;.&lt;/p&gt;

  Set `isolation: worktree` to give a subagent its own git worktree. Each subagent works on an isolated copy of the repo, so multiple subagents can edit files in parallel without conflicts. The worktree is cleaned up if no changes are made; if the subagent produces changes, you get the worktree path and branch back.

&lt;p&gt;Claude also ships three built-in subagent types you can use without writing a config file: &lt;strong&gt;Explore&lt;/strong&gt; (fast, read-only, uses Haiku), &lt;strong&gt;Plan&lt;/strong&gt; (research and architecture, read-only), and &lt;strong&gt;General-purpose&lt;/strong&gt; (full tool access).&lt;/p&gt;

  Subagents report to a parent. Agent teams are different: multiple Claude sessions that coordinate as peers, messaging each other and sharing tasks. Think of subagents as delegation and agent teams as collaboration.

&lt;hr /&gt;
&lt;h3&gt;2.4) Hooks — automatic event-driven actions&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    Event&lt;span&gt;[Lifecycle Event]&lt;/span&gt;
    HookA&lt;span&gt;[Hook 1]&lt;/span&gt;
    HookB&lt;span&gt;[Hook 2]&lt;/span&gt;
    HookC&lt;span&gt;[Hook 3]&lt;/span&gt;

    Event &lt;span&gt;--&amp;gt;&lt;/span&gt; HookA
    Event &lt;span&gt;--&amp;gt;&lt;/span&gt; HookB
    Event &lt;span&gt;--&amp;gt;&lt;/span&gt; HookC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; JSON-configured handlers in &lt;code&gt;.claude/settings.json&lt;/code&gt; that trigger on lifecycle events. No manual invocation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Available events&lt;/strong&gt; (expanded from the original 7):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool lifecycle:&lt;/strong&gt; &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;PostToolUse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session lifecycle:&lt;/strong&gt; &lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;SubagentStart&lt;/code&gt;, &lt;code&gt;SubagentStop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task lifecycle:&lt;/strong&gt; &lt;code&gt;TaskCreated&lt;/code&gt;, &lt;code&gt;TaskCompleted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment:&lt;/strong&gt; &lt;code&gt;CwdChanged&lt;/code&gt;, &lt;code&gt;FileChanged&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permissions:&lt;/strong&gt; &lt;code&gt;PermissionDenied&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; &lt;code&gt;PreCompact&lt;/code&gt;, &lt;code&gt;PostCompact&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User input:&lt;/strong&gt; &lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;Notification&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Handler types:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Command:&lt;/strong&gt; Run shell commands (fast, predictable)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; Let Claude decide with the LLM (flexible, context-aware)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP:&lt;/strong&gt; POST JSON to an external service with auth headers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async:&lt;/strong&gt; Add &lt;code&gt;&quot;async&quot;: true&lt;/code&gt; to run a hook in the background without blocking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PreToolUse hooks can also return &lt;code&gt;updatedInput&lt;/code&gt; to modify tool arguments before execution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Auto-lint after file edits.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;PostToolUse&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;&quot;matcher&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Edit|Write&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;hooks&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;\&quot;$CLAUDE_PROJECT_DIR\&quot;/.claude/hooks/run-oxlint.sh&quot;&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span&gt;file_path&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;span&gt;&lt;span&gt;$(&lt;/span&gt;jq &lt;span&gt;-r&lt;/span&gt; &lt;span&gt;&apos;.tool_input.file_path // &quot;&quot;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;[&lt;/span&gt; &lt;span&gt;&quot;&lt;span&gt;$file_path&lt;/span&gt;&quot;&lt;/span&gt; &lt;span&gt;=~&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;.&lt;span&gt;(&lt;/span&gt;js&lt;span&gt;|&lt;/span&gt;jsx&lt;span&gt;|&lt;/span&gt;ts&lt;span&gt;|&lt;/span&gt;tsx&lt;span&gt;|&lt;/span&gt;vue&lt;span&gt;)&lt;/span&gt;$ &lt;span&gt;]&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;then&lt;/span&gt;
  &lt;span&gt;pnpm&lt;/span&gt; lint:fast
&lt;span&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Common uses:&lt;/strong&gt; Auto-format after edits, require approval for bash commands, validate writes, initialize sessions. For a practical example, see how to set up desktop notifications when Claude needs your attention. Want to build your own? Use this &lt;a href=&quot;https://alexop.dev//prompts/claude/claude-create-hook&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;hook creation guide&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3) Plugins — shareable, packaged configurations&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;classDiagram&lt;/span&gt;
    &lt;span&gt;class&lt;/span&gt; Plugin &lt;span&gt;{&lt;/span&gt;
      name
      version
      author
    &lt;span&gt;}&lt;/span&gt;
    Plugin &lt;span&gt;--&amp;gt;&lt;/span&gt; Skills
    Plugin &lt;span&gt;--&amp;gt;&lt;/span&gt; Hooks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; Distributable bundles of skills, hooks, and metadata. Share your setup with teammates or install pre-built configurations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic structure:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “my-plugin”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “.claude-plugin”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “plugin.json”, comment: “Manifest: name, version, author” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “skills”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “greet”, children: [{ name: “&lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt;” }] },&lt;br /&gt;
{ name: “my-skill”, children: [{ name: “&lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt;” }] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “hooks”, children: [{ name: “hooks.json” }] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to use.&lt;/strong&gt; Share team configurations, package domain workflows, distribute opinionated patterns, install community tooling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; Install a plugin, get instant access. Hooks combine, skills appear in autocomplete and activate on context match. Ready to build your own? Check out this &lt;a href=&quot;https://alexop.dev//prompts/claude/claude-create-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;plugin creation guide&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4) Skills deep dive — where to put them and how they compose&lt;/h2&gt;
&lt;p&gt;Skills were covered in section 2.2. This section focuses on discovery, placement, and composition patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How Claude discovers skills.&lt;/strong&gt; When you give Claude a task, it reviews available skill descriptions to find relevant ones. If a skill’s &lt;code&gt;description&lt;/code&gt; field matches the task context, Claude loads the full instructions. You don’t need to invoke auto-invoked skills by name — Claude finds them.&lt;/p&gt;

  Check out the [official Anthropic skills
  repository](https://github.com/anthropics/skills) for ready-to-use examples.

&lt;blockquote&gt;
&lt;p&gt;Claude Skills are awesome, maybe a bigger deal than MCP&lt;/p&gt;
&lt;p&gt;— Simon Willison, &lt;a href=&quot;https://simonwillison.net/2025/Oct/16/claude-skills/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Skills are awesome, maybe a bigger deal than MCP&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

Want rigorous, spec-driven development? Check out [obra&apos;s superpowers](https://github.com/obra/superpowers) — a comprehensive skills library that enforces systematic workflows.
&lt;p&gt;&lt;strong&gt;What it provides:&lt;/strong&gt; TDD workflows (RED-GREEN-REFACTOR), systematic debugging, code review processes, git worktree management, and brainstorming frameworks. Each skill pushes you toward verification-based development instead of “trust me, it works.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The philosophy:&lt;/strong&gt; Test before implementation. Verify with evidence. Debug systematically through four phases. Plan before coding. No shortcuts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; You want Claude to be more disciplined about development practices, especially for production code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to put them:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.claude/skills/&lt;/code&gt; — personal, all projects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.claude/skills/&lt;/code&gt; — project-specific&lt;/li&gt;
&lt;li&gt;Inside plugins — distributable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Skills vs &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;.&lt;/strong&gt; Skills are modular chunks of a &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; file. Instead of reviewing a massive document on every task, Claude loads specific skill instructions only when the task matches. Better context efficiency, same automatic behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Controlling invocation with frontmatter.&lt;/strong&gt; The old command/skill split is gone. A single &lt;a href=&quot;http://SKILL.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SKILL.md&lt;/a&gt; file with different frontmatter covers every use case:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Frontmatter&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto-invoke only (old “skill”)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;description:&lt;/code&gt; set, defaults&lt;/td&gt;
&lt;td&gt;Claude invokes when relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual only (old “command”)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;disable-model-invocation: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only runs when you type &lt;code&gt;/name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background knowledge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user-invocable: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude reads it, users don’t see it in &lt;code&gt;/&lt;/code&gt; menu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both auto and manual&lt;/td&gt;
&lt;td&gt;&lt;code&gt;description:&lt;/code&gt; set, &lt;code&gt;user-invocable: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude can invoke it, you can too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolated execution&lt;/td&gt;
&lt;td&gt;&lt;code&gt;context: fork&lt;/code&gt;, &lt;code&gt;agent: Explore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs in a subagent with its own context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

Files in `.claude/commands/` still work and appear as `/slash-commands`. But new work should go in `.claude/skills/`. The skill format supports everything commands did, plus auto-invocation, `context: fork`, `paths` filtering, and model overrides.

&lt;hr /&gt;
&lt;h2&gt;5) Scheduled Tasks and Triggers — running Claude without you&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; Cloud-based cron jobs that run Claude Code on Anthropic’s infrastructure. You define a schedule and a prompt; Claude executes it on the cron interval, with full access to your project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to set one up.&lt;/strong&gt; Use the &lt;code&gt;/schedule&lt;/code&gt; command or &lt;code&gt;claude trigger create&lt;/code&gt; from the CLI. Define the cron expression and the task prompt:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude trigger create &lt;span&gt;--schedule&lt;/span&gt; &lt;span&gt;&quot;0 9 * * 1&quot;&lt;/span&gt; &lt;span&gt;--prompt&lt;/span&gt; &lt;span&gt;&quot;Review open PRs and summarize status&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;In-session polling.&lt;/strong&gt; For shorter-lived recurring work, &lt;code&gt;/loop 5m /your-command&lt;/code&gt; runs a slash command on an interval inside your current session.&lt;/p&gt;

  Channels let MCP servers push messages into a Claude session. You can connect Telegram, Discord, webhooks, or other services so external events trigger Claude&apos;s attention. Still a research preview as of April 2026.

&lt;hr /&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;How these features work together in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Memory (&lt;code&gt;CLAUDE.md&lt;/code&gt;)&lt;/strong&gt; — Establish project context and conventions that Claude always knows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; — Define reusable behaviors, from manual workflows to auto-invoked expertise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents&lt;/strong&gt; — Offload parallel or isolated work to specialized agents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hooks&lt;/strong&gt; — Enforce rules and automate repetitive actions at key lifecycle events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt; — Package and distribute your entire setup to others&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt; — Connect external systems and make their capabilities available as commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled Tasks&lt;/strong&gt; — Run Claude on a cron schedule for recurring work&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Example: A Task-Based Development Workflow&lt;/h3&gt;
&lt;p&gt;A real-world workflow that combines multiple features:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setup phase:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; contains implementation standards (“don’t commit until I approve”, “write tests first”)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/load-context&lt;/code&gt; skill (&lt;code&gt;disable-model-invocation: true&lt;/code&gt;) initializes new chats with project state&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update-documentation&lt;/code&gt; skill (auto-invoked) activates after implementations&lt;/li&gt;
&lt;li&gt;Hook triggers linting after file edits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Planning phase (Chat 1):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main agent plans bug fix or new feature&lt;/li&gt;
&lt;li&gt;Outputs detailed task file with approach&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Implementation phase (Chat 2):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start fresh context with &lt;code&gt;/load-context&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Feed in the plan from Chat 1&lt;/li&gt;
&lt;li&gt;Implementation subagent executes the plan&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update-documentation&lt;/code&gt; skill updates docs on its own&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/resolve-task&lt;/code&gt; skill marks task complete&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; Main context stays focused on planning. Heavy implementation work happens in isolated context. Skills handle documentation. Hooks enforce quality standards.&lt;/p&gt;
&lt;h2&gt;Decision guide: choosing the right tool&lt;/h2&gt;

  🎉 **Huge thanks to [@thewiredbear](https://github.com/thewiredbear)** for creating the [Claude Code Driver](https://github.com/thewiredbear/Claude_Code_Driver/) repository! This community-driven collection includes examples, templates, and resources based on this guide. Perfect for getting started quickly or finding inspiration for your own Claude Code setup. Check it out and contribute your own patterns!


  For a comprehensive visual guide to all Claude Code features, check out the
  [Awesome Claude Code Cheat Sheet](https://awesomeclaude.ai/code-cheatsheet).


  Want model name, context usage, and cost displayed in your terminal? See how to customize your Claude Code status line.

&lt;aside&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt; to define lasting project context — architecture, conventions, and patterns Claude should always remember. Best for: static knowledge that rarely changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Skills&lt;/strong&gt; for reusable workflows and expertise. Set &lt;code&gt;disable-model-invocation: true&lt;/code&gt; for manual-only triggers, leave it off for auto-invocation, or use &lt;code&gt;context: fork&lt;/code&gt; to run in a subagent. Best for: everything from deploy scripts to style enforcement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Subagents&lt;/strong&gt; when you need parallel execution or want to isolate heavy computational work. Best for: preventing context pollution, specialized deep dives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Hooks&lt;/strong&gt; to enforce standards or react to specific events. Best for: quality gates, actions tied to tool usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Plugins&lt;/strong&gt; to package and share complete configurations across teams or projects. Best for: team standardization, distributing opinionated setups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use MCP&lt;/strong&gt; to integrate external systems and expose their capabilities as native commands. Best for: connecting databases, APIs, third-party tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use Scheduled Tasks&lt;/strong&gt; for recurring work that should run without you present. Best for: nightly audits, weekly reports, periodic maintenance.&lt;/li&gt;
&lt;/ul&gt;
&lt;/aside&gt;
&lt;h3&gt;Feature comparison&lt;/h3&gt;

  This comparison table is adapted from [IndyDevDan&apos;s video &quot;I finally CRACKED
  Claude Agent Skills&quot;](https://www.youtube.com/watch?v=kFpLzCVLA20&amp;amp;t=1027s).

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;th&gt;Subagent&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Triggered By&lt;/td&gt;
&lt;td&gt;Agent, Engineer, or Both&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context Efficiency&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context Persistence&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelizable&lt;/td&gt;
&lt;td&gt;✅ (context: fork)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specializable&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharable&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modularity&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool Permissions&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can Use Prompts&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can Use Skills&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Kind of&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can Use MCP Servers&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can Use Subagents&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Real-world examples&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Best Tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;“Always use Pinia for state management in Vue apps”&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistent context that applies to all conversations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate standardized commit messages&lt;/td&gt;
&lt;td&gt;Skill (&lt;code&gt;disable-model-invocation: true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Explicit action you trigger when ready to commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check Jira tickets and analyze security simultaneously&lt;/td&gt;
&lt;td&gt;Subagents&lt;/td&gt;
&lt;td&gt;Parallel execution with isolated contexts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run linter after every file edit&lt;/td&gt;
&lt;td&gt;Hook&lt;/td&gt;
&lt;td&gt;Automatic reaction to lifecycle event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share your team’s Vue testing patterns&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;Distributable package with commands + skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query PostgreSQL database for reports&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;External system integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run automated SEO audits with browser testing&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;External system integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect style guide violations during any edit&lt;/td&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;Automatic behavior based on task context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create React components from templates&lt;/td&gt;
&lt;td&gt;Skill (&lt;code&gt;disable-model-invocation: true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Manual workflow with repeatable structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;“Never use &lt;code&gt;any&lt;/code&gt; type in TypeScript”&lt;/td&gt;
&lt;td&gt;Hook&lt;/td&gt;
&lt;td&gt;Automatic enforcement after code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-format code on save&lt;/td&gt;
&lt;td&gt;Hook&lt;/td&gt;
&lt;td&gt;Event-driven automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect to GitHub for issue management&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;External API integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run comprehensive test suite in parallel&lt;/td&gt;
&lt;td&gt;Subagent&lt;/td&gt;
&lt;td&gt;Isolated, resource-intensive work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy to staging environment&lt;/td&gt;
&lt;td&gt;Skill (&lt;code&gt;disable-model-invocation: true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Manual trigger with safeguards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce TDD workflow automatically&lt;/td&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;Context-aware automatic behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initialize new projects with team standards&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;Shareable, complete configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run nightly dependency audit&lt;/td&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;Recurring task on a cron schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summarize open PRs every Monday morning&lt;/td&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;Scheduled report without manual invocation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

          &lt;/figure&gt;</content:encoded><category>claude-code</category><category>ai</category><category>mcp</category><category>productivity</category><category>tooling</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building My First Claude Code Plugin</title><link>https://alexop.dev/posts/building-my-first-claude-code-plugin/</link><guid isPermaLink="true">https://alexop.dev/posts/building-my-first-claude-code-plugin/</guid><description>How I built a Claude Code plugin to generate skills, agents, commands, and more—and stopped copy-pasting boilerplate.</description><pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate><content:encoded>
            
            
  If you&apos;re unfamiliar with Claude Code or want to understand the full ecosystem (MCP, Skills, Subagents, Hooks, and Plugins), check out my{&quot; &quot;}
  
    comprehensive guide to Claude Code&apos;s full stack
  
  {&quot; &quot;}first. This post assumes you know the basics.

&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I’ve been using Claude Code for a while now. It’s been my daily driver for development work, alongside other AI tools in my workflow.&lt;/p&gt;
&lt;p&gt;But here’s the thing—over the last few months, I stopped paying attention to what Anthropic was shipping. Skills? Didn’t look into them. Plugins? No idea they existed.&lt;/p&gt;
&lt;p&gt;Today I caught up. And I discovered something I’d been missing: plugins.&lt;/p&gt;
&lt;p&gt;The idea clicked immediately. Everything I’d been building locally—custom commands, agents, configurations—was stuck in &lt;code&gt;.claude/&lt;/code&gt; folders per project. Plugins change that. You can package it up and share it across projects. Give Claude Code new abilities anywhere.&lt;/p&gt;
&lt;p&gt;That’s when I decided to build one. A plugin that generates slash commands, skills, agents, and everything else I kept creating manually.&lt;/p&gt;
&lt;aside&gt;
The plugin is open source and ready to use. Check out the repository on GitHub to see the implementation or install it right away.
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/claude-code-builder&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
&lt;aside&gt;
Plugins extend Claude Code with custom commands, agents, hooks, Skills, and MCP servers through the plugin system. They let you package up functionality and share it across projects and teams.
&lt;p&gt;Plugins can contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slash commands&lt;/strong&gt; – Custom workflows you trigger explicitly (like &lt;code&gt;/analyze-deps&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; – Abilities Claude automatically uses when relevant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents&lt;/strong&gt; – Specialized sub-agents for focused tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hooks&lt;/strong&gt; – Event handlers that run on tool use, prompt submit, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For complete technical specifications and the official guide, see the &lt;a href=&quot;https://code.claude.com/docs/en/plugins&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Code Plugins documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2&gt;The Manual Workflow Was Painful&lt;/h2&gt;
&lt;p&gt;Before the plugin, creating a new command looked like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Search the docs for the right frontmatter format&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;.claude/commands/my-command.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy-paste a template&lt;/li&gt;
&lt;li&gt;Fill in the blanks&lt;/li&gt;
&lt;li&gt;Hope you got the structure right&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Repeat for agents. Repeat for skills. Repeat for hooks.&lt;/p&gt;
&lt;p&gt;10 minutes on boilerplate. 5 minutes on actual logic.&lt;/p&gt;
&lt;p&gt;Same problem every time: too much manual work for something that should be instant.&lt;/p&gt;
&lt;h2&gt;The Solution: Claude Code Builder&lt;/h2&gt;
&lt;p&gt;I fixed this by building a plugin that generates everything for me.&lt;/p&gt;
&lt;p&gt;Here’s what it includes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-skill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate model-invoked skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create specialized sub-agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-command&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add custom slash commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-hook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configure event-driven hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate &lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt; files for project context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-output-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create custom output styles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Package your setup as a plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each command handles the structure, frontmatter, and best practices. I just provide the name and description.&lt;/p&gt;
&lt;h2&gt;The Plugin Structure&lt;/h2&gt;
&lt;p&gt;Here’s the structure:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “.claude-plugin”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “marketplace.json”, comment: “Marketplace manifest” },&lt;br /&gt;
{ name: “plugin.json”, comment: “Plugin metadata” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “.gitignore” },&lt;br /&gt;
{ name: “LICENSE” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;README.md&lt;/a&gt;” },&lt;br /&gt;
{&lt;br /&gt;
name: “commands”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-agent.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-agent.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-command.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-command.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-hook.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-hook.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-md.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-md.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-output-style.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-output-style.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-plugin.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-plugin.md&lt;/a&gt;” },&lt;br /&gt;
{ name: “&lt;a href=&quot;http://create-skill.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;create-skill.md&lt;/a&gt;” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Command Files: Where the Magic Happens&lt;/h2&gt;
&lt;p&gt;Each command is a markdown file with frontmatter. Here’s the &lt;code&gt;/create-skill&lt;/code&gt; command as an example:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Generate a new Claude Skill with proper structure and YAML frontmatter
&lt;span&gt;argument-hint&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;skill&lt;span&gt;-&lt;/span&gt;name&lt;span&gt;]&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;description&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;#&lt;/span&gt; /create-skill&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Purpose&lt;/span&gt;

Generate a new Claude Skill with proper structure and YAML frontmatter using official documentation as reference

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Contract&lt;/span&gt;

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Inputs:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`$1`&lt;/span&gt; — SKILL_NAME (lowercase, kebab-case, max 64 characters)
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`$2`&lt;/span&gt; — DESCRIPTION (what the skill does and when to use it, max 1024 characters)
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`--personal`&lt;/span&gt; — create in ~/.claude/skills/ (default)
&lt;span&gt;-&lt;/span&gt; &lt;span&gt;`--project`&lt;/span&gt; — create in .claude/skills/

&lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Outputs:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt; &lt;span&gt;`STATUS=&amp;lt;CREATED|EXISTS|FAIL&amp;gt; PATH=&amp;lt;path&amp;gt;`&lt;/span&gt;

&lt;span&gt;&lt;span&gt;##&lt;/span&gt; Instructions&lt;/span&gt;

&lt;span&gt;1.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Validate inputs:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
   &lt;span&gt;-&lt;/span&gt; Skill name: lowercase letters, numbers, hyphens only
   &lt;span&gt;-&lt;/span&gt; Description: non-empty, max 1024 characters

&lt;span&gt;2.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Determine target directory:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
   &lt;span&gt;-&lt;/span&gt; Personal (default): &lt;span&gt;`~/.claude/skills/{{SKILL_NAME}}/`&lt;/span&gt;
   &lt;span&gt;-&lt;/span&gt; Project: &lt;span&gt;`.claude/skills/{{SKILL_NAME}}/`&lt;/span&gt;

&lt;span&gt;3.&lt;/span&gt; &lt;span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Generate SKILL.md using this template:&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;/span&gt;
   [template content here...]
&lt;/code&gt;&lt;/pre&gt;

  Commands are just instructions for Claude. Write them like you&apos;re teaching a
  junior developer the exact steps to follow. Good{&quot; &quot;}
  
    prompt engineering principles
  {&quot; &quot;}
  apply here too.

&lt;p&gt;Here’s what the plugin generates when you run a command:&lt;/p&gt;
&lt;figure&gt;
&lt;h2&gt;Publishing to GitHub&lt;/h2&gt;
&lt;p&gt;Once I had it working locally, publishing was straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Push to GitHub&lt;/li&gt;
&lt;li&gt;Users add the marketplace: &lt;code&gt;/plugin marketplace add alexanderop/claude-code-builder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Users install: &lt;code&gt;/plugin install claude-code-builder@claude-code-builder&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No npm, no build step. Just GitHub.&lt;/p&gt;
&lt;h2&gt;Try It Yourself&lt;/h2&gt;
&lt;p&gt;Ready to stop copy-pasting Claude Code boilerplate?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Install the plugin&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/plugin &lt;span&gt;install&lt;/span&gt; claude-code-builder@claude-code-builder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Verify installation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Check that the plugin is loaded:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see &lt;code&gt;claude-code-builder&lt;/code&gt; in the list.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Use the new commands&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You now have access to seven new commands. Try creating your first skill:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/create-skill commit-helper &lt;span&gt;&quot;Generate clear commit messages; use when committing&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;p&gt;That’s it. You’re now equipped to generate skills, agents, commands, and more—without touching the docs.&lt;/p&gt;
&lt;h2&gt;What’s Next?&lt;/h2&gt;
&lt;p&gt;I’m using this daily. Every time I think “I wish Claude could…”, I run &lt;code&gt;/create-skill&lt;/code&gt; instead of Googling docs.&lt;/p&gt;
&lt;p&gt;Right now, I’m focused on workflow optimization: building Vue applications faster with Claude Code.&lt;/p&gt;
&lt;p&gt;The question I’m exploring: How do I teach Claude Code to write good Vue applications?&lt;/p&gt;
&lt;p&gt;I’m working on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Skills that encode Vue best practices&lt;/li&gt;
&lt;li&gt;Commands for common Vue patterns (composables, stores, components)&lt;/li&gt;
&lt;li&gt;Custom agents that understand Vue architecture decisions&lt;/li&gt;
&lt;li&gt;

  MCP server integrations

for external tools
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not just about speed. It’s about teaching Claude Code the way I think about development.&lt;/p&gt;
&lt;p&gt;Building tools that build tools. That’s where it gets fun.&lt;/p&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>claude-code</category><category>ai</category><category>tooling</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building a Modular Monolith with Nuxt Layers: A Practical Guide</title><link>https://alexop.dev/posts/nuxt-layers-modular-monolith/</link><guid isPermaLink="true">https://alexop.dev/posts/nuxt-layers-modular-monolith/</guid><description>Learn how to build scalable applications using Nuxt Layers to enforce clean architecture boundaries without the complexity of microservices.</description><pubDate>Sun, 02 Nov 2025 10:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I once worked on a project that wanted to build an e-commerce website with Nuxt that could be used by multiple countries. The architecture was a nightmare: they had a base repository, and then they would merge the base repo into country-specific code. This was before Nuxt Layers existed, back in the Nuxt 2 days, and managing this was incredibly painful. Every merge brought conflicts, and maintaining consistency across countries was a constant struggle.&lt;/p&gt;
&lt;p&gt;Now with Nuxt Layers, we finally have a much better solution for this exact use case. But in this blog post, we’re going to explore something even more powerful: using Nuxt Layers to build a &lt;strong&gt;modular monolith architecture&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I recently built a simple example e-commerce application to explore this pattern in depth, and I want to share what I learned. By the end of this post, you’ll understand how to structure your Nuxt applications with clean boundaries and enforced separation of concerns, without the complexity of microservices or the pain of repository merging strategies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full project repository&lt;/strong&gt;: &lt;a href=&quot;https://github.com/alexanderop/nuxt-layer-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/alexanderop/nuxt-layer-example&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is Part 4 of my &lt;a href=&quot;https://alexop.dev//posts/how-to-structure-vue-projects/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to Structure Vue Projects&lt;/a&gt;. If you’re choosing between architecture patterns, start there first.&lt;/p&gt;
&lt;h2&gt;The Problem: When Flat Architecture Stops Scaling&lt;/h2&gt;
&lt;p&gt;Most projects start the same way. You create a new Nuxt project, organize files into &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;composables/&lt;/code&gt;, and &lt;code&gt;stores/&lt;/code&gt; folders, and everything feels clean and organized. This works beautifully at first.&lt;/p&gt;
&lt;p&gt;Then your application grows. You add a product catalog, then a shopping cart, then user profiles, then an admin panel. Suddenly your &lt;code&gt;components/&lt;/code&gt; folder has 50+ files. Your stores reference each other in complex ways you didn’t plan for. A seemingly innocent change to the cart accidentally breaks the product listing page.&lt;/p&gt;
&lt;p&gt;I’ve been there, and I’m sure you have too.&lt;/p&gt;
&lt;p&gt;The core problem is simple: &lt;strong&gt;flat architectures have no boundaries&lt;/strong&gt;. Nothing prevents your cart component from directly importing from your products store. Nothing stops circular dependencies. You can import anything from anywhere, and this freedom becomes a liability as your codebase grows.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “app”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “ProductCard.vue” },&lt;br /&gt;
{ name: “CartButton.vue” },&lt;br /&gt;
{ name: “CartItem.vue” },&lt;br /&gt;
{ name: “FilterBar.vue” },&lt;br /&gt;
{ name: “…”, comment: “50+ more files” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “composables”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [{ name: “…”, comment: “everything mixed together” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “stores”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “products.ts” },&lt;br /&gt;
{ name: “cart.ts” },&lt;br /&gt;
{ name: “…”, comment: “tightly coupled” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;When I first encountered this problem, I considered micro frontends. While &lt;a href=&quot;https://alexop.dev//posts/how-to-build-microfrontends-with-module-federation-and-vue/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to build Microfrontends with Module Federation and Vue&lt;/a&gt; solve similar problems, Nuxt Layers offers better developer experience for monorepos. I wanted clean boundaries without the operational complexity of deploying and maintaining separate services.&lt;/p&gt;
&lt;p&gt;That’s when I discovered Nuxt Layers.&lt;/p&gt;
&lt;h2&gt;What Are Nuxt Layers?&lt;/h2&gt;
&lt;p&gt;Before diving into the implementation, let me explain what Nuxt Layers actually are and why they solve our problem.&lt;/p&gt;
&lt;p&gt;Nuxt Layers let you split your application into independent, reusable modules. Think of each layer as a mini Nuxt application with its own components, composables, pages, and stores. Each layer lives in its own folder with its own &lt;code&gt;nuxt.config.ts&lt;/code&gt; file.&lt;/p&gt;
&lt;aside&gt;
  For comprehensive documentation on Nuxt Layers, visit the [official Nuxt
  Layers guide](https://nuxt.com/docs/4.x/guide/going-further/layers).
&lt;/aside&gt;
&lt;p&gt;You compose these layers together using the &lt;code&gt;extends&lt;/code&gt; keyword in your main configuration:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineNuxtConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;extends&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;&quot;./layers/shared&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// Local folder&lt;/span&gt;
    &lt;span&gt;&quot;./layers/products&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;./layers/cart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  Layers aren&apos;t limited to local folders. You can also extend from npm packages
  (`@your-org/ui-layer`) or git repositories (`github:your-org/shared-layer`).
  For remote sources to work as valid layers, they must contain a
  `nuxt.config.ts` file in their repository. This makes layers incredibly
  powerful for code reuse across projects.
&lt;/aside&gt;
&lt;p&gt;When you extend layers, Nuxt merges their configurations and makes their code available to your application. All extended layers become accessible through auto-generated TypeScript paths (like &lt;code&gt;#layers/products/...&lt;/code&gt;), and their components, composables, and utilities are automatically imported.&lt;/p&gt;
&lt;p&gt;Here’s the important part: &lt;strong&gt;by default, there’s no compile-time enforcement preventing cross-layer imports&lt;/strong&gt;. If your app extends both the products and cart layers, the cart layer can technically import from products at runtime—even if cart doesn’t extend products directly. This is where ESLint enforcement becomes crucial, which I’ll cover later.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[App Root&amp;lt;br/&amp;gt;extends all layers]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Products Layer&amp;lt;br/&amp;gt;extends shared]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Cart Layer&amp;lt;br/&amp;gt;extends shared]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Shared Layer&amp;lt;br/&amp;gt;extends nothing]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; D
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building an E-commerce Application with Layers&lt;/h2&gt;
&lt;p&gt;Let me show you how I structured a real e-commerce application using this pattern. I created three layers, each with a specific purpose:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared Layer&lt;/strong&gt;: The foundation. This layer provides UI components (like badges and buttons), utility functions (currency formatting, storage helpers), and nothing else. No business logic lives here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Products Layer&lt;/strong&gt;: Everything related to browsing and viewing products. Product schemas, the product store, catalog pages, and filter components all live here. Crucially, this layer knows nothing about shopping carts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cart Layer&lt;/strong&gt;: Everything related to managing a shopping cart. The cart store, localStorage persistence, and cart UI components. This layer knows nothing about product catalogs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your Project Root&lt;/strong&gt;: The orchestrator. This is not a separate layer—it’s your main application that extends all the layers. This is where you create pages that combine features from multiple layers (like a product listing page with “add to cart” functionality).&lt;/p&gt;
&lt;p&gt;Here’s the folder structure:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “layers”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “shared”,&lt;br /&gt;
comment: “Foundation (no dependencies)”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “base”,&lt;br /&gt;
children: [{ name: “BaseBadge.vue” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “utils”,&lt;br /&gt;
children: [{ name: “currency.ts” }, { name: “storage.ts” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “products”,&lt;br /&gt;
comment: “Product feature (depends on shared only)”,&lt;br /&gt;
open: false,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “ProductCard.vue” },&lt;br /&gt;
{ name: “ProductFilters.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “stores”,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “products”,&lt;br /&gt;
children: [{ name: “useProductsStore.ts” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “schemas”,&lt;br /&gt;
children: [{ name: “product.schema.ts” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “cart”,&lt;br /&gt;
comment: “Cart feature (depends on shared only)”,&lt;br /&gt;
open: false,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “CartSummary.vue” },&lt;br /&gt;
{ name: “CartItemCard.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “stores”,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “cart”,&lt;br /&gt;
children: [{ name: “useCartStore.ts” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Notice how the products and cart layers never import from each other. They are completely independent features. This is the core principle that makes this pattern work.&lt;/p&gt;
&lt;h2&gt;The Difference: Before and After&lt;/h2&gt;
&lt;p&gt;Let me show you the contrast between a traditional approach and the layered approach.&lt;/p&gt;
&lt;h3&gt;Without Layers: Tight Coupling&lt;/h3&gt;
&lt;p&gt;In a traditional flat structure, your product component might directly import the cart store:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// ❌ Tight coupling in flat architecture
const cart = useCartStore();
const products = useProductsStore();

function addToCart(productId: string) {
  const product = products.getById(productId);
  cart.addItem(product);
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates hidden dependencies. The products feature now depends on the cart feature. You cannot use products without including cart. You cannot understand one without reading the other. Testing becomes harder because everything is coupled.&lt;/p&gt;
&lt;h3&gt;With Layers: Clear Boundaries&lt;/h3&gt;
&lt;p&gt;With layers, the product component has no idea that carts exist:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- layers/products/components/ProductCard.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const props = defineProps&amp;lt;{
  product: Product;
}&amp;gt;();

const emit = defineEmits&amp;lt;{
  select: [productId: string];
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;UCard&amp;gt;
    &amp;lt;h3&amp;gt;{{ product.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;{{ product.price }}&amp;lt;/p&amp;gt;
    &amp;lt;UButton @click=&quot;emit(&apos;select&apos;, product.id)&quot;&amp;gt; View Details &amp;lt;/UButton&amp;gt;
  &amp;lt;/UCard&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The product component simply emits an event. The parent page (living in your project root) connects products to cart:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- pages/index.vue (in your project root) --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const products = useProductsStore();
const cart = useCartStore();

function handleProductSelect(productId: string) {
  const product = products.getById(productId);
  if (product) {
    cart.addItem(product);
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;ProductCard
      v-for=&quot;product in products.items&quot;
      :key=&quot;product.id&quot;
      :product=&quot;product&quot;
      @select=&quot;handleProductSelect&quot;
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your project root acts as the orchestrator. It knows about both products and cart, but the features themselves stay completely independent.&lt;/p&gt;
&lt;aside&gt;
  This pattern follows the dependency inversion principle. High-level modules
  (the app) depend on low-level modules (features), but features don&apos;t depend on
  each other. Changes to one feature won&apos;t cascade to others.
&lt;/aside&gt;
&lt;h2&gt;How Features Communicate&lt;/h2&gt;
&lt;p&gt;When a page needs functionality from multiple layers, your project root orchestrates the interaction. I like to think of this pattern as similar to micro frontends with an app shell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature layers&lt;/strong&gt; are independent workers. Each does one job well. They expose simple interfaces (stores, components, composables) but have no knowledge of each other.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your project root&lt;/strong&gt; is the manager. It knows all the workers. When a task needs multiple workers, your project root coordinates them.&lt;/p&gt;
&lt;p&gt;Here’s a sequence diagram showing how this works:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; User
    &lt;span&gt;participant&lt;/span&gt; Page &lt;span&gt;(Project Root)&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; Products Layer
    &lt;span&gt;participant&lt;/span&gt; Cart Layer

    User&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Page &lt;span&gt;(Project Root)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Click product
    Page &lt;span&gt;(Project Root)&lt;/span&gt;-&amp;gt;&amp;gt;Products Layer&lt;span&gt;:&lt;/span&gt; Get product data
    Products Layer&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Page &lt;span&gt;(Project Root)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Return product
    Page &lt;span&gt;(Project Root)&lt;/span&gt;-&amp;gt;&amp;gt;Cart Layer&lt;span&gt;:&lt;/span&gt; Add to cart
    Cart Layer&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Page &lt;span&gt;(Project Root)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Cart updated
    Page &lt;span&gt;(Project Root)&lt;/span&gt;-&amp;gt;&amp;gt;User&lt;span&gt;:&lt;/span&gt; Show confirmation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me show you a real example from the cart page. It needs to display cart items (from the cart layer) with product details (from the products layer):&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- pages/cart.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const cart = useCartStore();
const products = useProductsStore();

// App layer combines data from both features
const enrichedItems = computed(() =&amp;gt; {
  return cart.items.map(cartItem =&amp;gt; {
    const product = products.getById(cartItem.productId);
    return {
      ...cartItem,
      productDetails: product,
    };
  });
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Your Cart&amp;lt;/h1&amp;gt;
    &amp;lt;CartItemCard
      v-for=&quot;item in enrichedItems&quot;
      :key=&quot;item.id&quot;
      :item=&quot;item&quot;
      @remove=&quot;cart.removeItem&quot;
      @update-quantity=&quot;cart.updateQuantity&quot;
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your project root queries both stores and combines the data. Neither feature layer knows about the other. This keeps your features loosely coupled and incredibly easy to test in isolation.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TB
    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Project Root (Orchestrator)&quot;&lt;/span&gt;
        Page&lt;span&gt;[Cart Page]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Independent Features&quot;&lt;/span&gt;
        Cart&lt;span&gt;[Cart Layer&amp;lt;br/&amp;gt;Cart items &amp;amp; logic]&lt;/span&gt;
        Products&lt;span&gt;[Products Layer&amp;lt;br/&amp;gt;Product data]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    Page &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|reads|&lt;/span&gt; Cart
    Page &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|reads|&lt;/span&gt; Products
    Page &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|combines|&lt;/span&gt; Combined&lt;span&gt;[Combined View]&lt;/span&gt;

    Cart &lt;span&gt;-.-&amp;gt;&lt;/span&gt;&lt;span&gt;|never imports|&lt;/span&gt; Products
    Products &lt;span&gt;-.-&amp;gt;&lt;/span&gt;&lt;span&gt;|never imports|&lt;/span&gt; Cart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enforcing Boundaries with ESLint&lt;/h2&gt;
&lt;p&gt;Now here’s something important I discovered while working with this pattern. Nuxt provides basic boundary enforcement through TypeScript: if you try to import from a layer not in your &lt;code&gt;extends&lt;/code&gt; array, your build fails. This is good, but it’s not enough.&lt;/p&gt;
&lt;p&gt;The problem is this: if your main config extends both products and cart, nothing prevents the cart layer from importing from products. Technically both layers are available at runtime. This creates the exact coupling we’re trying to avoid.&lt;/p&gt;
&lt;p&gt;I needed stricter enforcement. So I built a custom ESLint plugin called &lt;code&gt;eslint-plugin-nuxt-layers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This plugin enforces two critical rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No cross-feature imports&lt;/strong&gt;: Cart cannot import from products (or vice versa)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No upward imports&lt;/strong&gt;: Feature layers cannot import from the app layer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The plugin detects which layer a file belongs to based on its path, then validates all imports against the allowed dependencies.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;// ❌ This fails linting&lt;/span&gt;
&lt;span&gt;// In layers/cart/stores/cart/useCartStore.ts&lt;/span&gt;
&lt;span&gt;// Error: cart layer cannot import from products layer&lt;/span&gt;

&lt;span&gt;// ✅ This passes linting (in layers/cart/)&lt;/span&gt;
&lt;span&gt;// OK: cart layer can import from shared layer&lt;/span&gt;

&lt;span&gt;// ✅ This also passes linting (in your project root)&lt;/span&gt;
&lt;span&gt;// OK: project root can import from any layer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
I&apos;ve published this ESLint plugin on npm so you can use it in your own projects. Install it with `pnpm add -D eslint-plugin-nuxt-layers` and get immediate feedback in your editor.
&lt;p&gt;&lt;strong&gt;Package link&lt;/strong&gt;: &lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-nuxt-layers?activeTab=readme&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-nuxt-layers on npm&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Here’s how the validation logic works:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    A&lt;span&gt;[File being linted]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{Which layer?}&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|shared|&lt;/span&gt; C&lt;span&gt;[Can import: nothing]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|products|&lt;/span&gt; D&lt;span&gt;[Can import: shared only]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|cart|&lt;/span&gt; E&lt;span&gt;[Can import: shared only]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|project root|&lt;/span&gt; F&lt;span&gt;[Can import: all layers]&lt;/span&gt;

    D &lt;span&gt;-.-&amp;gt;&lt;/span&gt;&lt;span&gt;|❌|&lt;/span&gt; G&lt;span&gt;[products → cart]&lt;/span&gt;
    E &lt;span&gt;-.-&amp;gt;&lt;/span&gt;&lt;span&gt;|❌|&lt;/span&gt; H&lt;span&gt;[cart → products]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ESLint plugin gives you enforcement of your architecture. Your IDE will warn you immediately if you violate boundaries, and your CI/CD pipeline will fail if violations slip through.&lt;/p&gt;
&lt;h2&gt;Important Gotchas to Avoid&lt;/h2&gt;
&lt;p&gt;Working with Nuxt Layers comes with some quirks you should know about. I learned these the hard way, so let me save you the trouble:&lt;/p&gt;
&lt;aside&gt;
Layer order determines override priority: **earlier layers have higher priority and override later ones**. This matters when multiple layers define the same component, page, or config value.
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// shared overrides products, products overrides cart&lt;/span&gt;
&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./layers/shared&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./layers/products&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./layers/cart&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

&lt;span&gt;// cart overrides products, products overrides shared&lt;/span&gt;
&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;./layers/cart&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./layers/products&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;./layers/shared&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For dependency purposes, all extended layers are available to each other at runtime. However, for clean architecture, you should still organize by semantic importance—typically putting shared/base layers first, then feature layers. Use ESLint rules to prevent unwanted cross-layer imports regardless of order.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside&gt;
If multiple layers have a file at the same path (like `pages/index.vue`), only the first one wins. The later ones are silently ignored. This can cause confusing bugs where pages or components mysteriously disappear.
&lt;p&gt;I recommend using unique names or paths for pages in different layers to avoid this issue entirely.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside&gt;
  Changes to `nuxt.config.ts` files in layers don&apos;t always hot reload properly.
  When you modify layer configuration, restart your dev server. I learned this
  after spending 30 minutes debugging why my changes weren&apos;t applying!
&lt;/aside&gt;
&lt;p&gt;&lt;strong&gt;Route paths need full names&lt;/strong&gt;: Layer names don’t auto-prefix routes. If you have &lt;code&gt;layers/blog/pages/index.vue&lt;/code&gt;, it creates the &lt;code&gt;/&lt;/code&gt; route, not &lt;code&gt;/blog&lt;/code&gt;. You need &lt;code&gt;layers/blog/pages/blog/index.vue&lt;/code&gt; to get &lt;code&gt;/blog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Component auto-import prefixing&lt;/strong&gt;: By default, nested components get prefixed. A component at &lt;code&gt;components/form/Input.vue&lt;/code&gt; becomes &lt;code&gt;&amp;lt;FormInput&amp;gt;&lt;/code&gt;. You can disable this with &lt;code&gt;pathPrefix: false&lt;/code&gt; in the components config if you prefer explicit names.&lt;/p&gt;
&lt;h2&gt;When Should You Use This Pattern?&lt;/h2&gt;
&lt;p&gt;I want to be honest with you: Nuxt Layers add complexity. They’re powerful, but they’re not always the right choice. Here’s when I recommend using them:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your app has distinct features&lt;/strong&gt;: If you’re building an application with clear feature boundaries (products, cart, blog, admin panel), layers shine. Each feature gets its own layer with its own components, pages, and logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You have multiple developers&lt;/strong&gt;: Layers prevent teams from stepping on each other’s toes. The cart team works in their layer, the products team works in theirs. No more merge conflicts in a giant shared components folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You want to reuse code&lt;/strong&gt;: Building multiple apps that share functionality? Extract common features into layers and publish them as npm packages. Your marketing site and main app can share the same blog layer without code duplication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You’re thinking long-term&lt;/strong&gt;: A small project with 5 components doesn’t need layers. But a project that will grow to 50+ features over two years? Layers will save your sanity.&lt;/p&gt;
&lt;aside&gt;
  I don&apos;t recommend starting with layers on day one for small projects. Begin
  with a flat structure. When you notice features bleeding into each other and
  boundaries becoming unclear, that&apos;s the perfect time to refactor into layers.
  The patterns in this article will guide you through that migration.
&lt;/aside&gt;
&lt;h2&gt;The Benefits You’ll Get&lt;/h2&gt;
&lt;p&gt;After working with this pattern for several months, here are the concrete benefits I’ve experienced:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clear boundaries enforced by tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Import rules aren’t just documentation that developers ignore. Your build fails if someone violates the architecture. This is incredibly powerful for maintaining standards as your team grows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Independent development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Team members can work on different features without conflicts. The cart team never touches product code. Changes are isolated and safe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easy testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each layer has minimal dependencies. You can test features in complete isolation without complex mocking setups.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gradual extraction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If you need to extract a feature later (maybe to share across projects or even split into a micro frontend), you already have clean boundaries. You could publish a layer as its own npm package with minimal refactoring.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Better code review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When someone adds an import in a pull request, you immediately see if it crosses layer boundaries. Architecture violations become obvious during review.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scales with complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;As your app grows, you simply add new layers. Existing layers stay independent and unaffected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Better AI assistant context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You can add layer-specific documentation files (like &lt;code&gt;claude.md&lt;/code&gt;) to each layer with context tailored to that feature. When working with AI coding assistants like Claude or GitHub Copilot, changes to the cart layer will only pull in cart-specific context, making the AI’s suggestions more accurate and focused.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Targeted testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Running tests becomes more efficient. Instead of running your entire test suite, you can run only the tests related to the feature you’re working on.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Getting Started with Your Own Project&lt;/h2&gt;
&lt;p&gt;If you want to try this pattern, here’s how to get started:&lt;/p&gt;
&lt;h3&gt;1. Clone and Explore the Example&lt;/h3&gt;
&lt;p&gt;Start by exploring the complete example project:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# 📥 Clone the repository&lt;/span&gt;
&lt;span&gt;git&lt;/span&gt; clone https://github.com/alexanderop/nuxt-layer-example
&lt;span&gt;cd&lt;/span&gt; nuxt-layer-example

&lt;span&gt;# 📦 Install dependencies&lt;/span&gt;
&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt;

&lt;span&gt;# 🚀 Start development server&lt;/span&gt;
&lt;span&gt;pnpm&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browse through the layers to see how everything connects. Try making changes to understand how the boundaries work.&lt;/p&gt;
&lt;h3&gt;2. Create Your Own Layered Project&lt;/h3&gt;
&lt;p&gt;To start your own project from scratch:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# 📁 Create layer folders&lt;/span&gt;
&lt;span&gt;mkdir&lt;/span&gt; &lt;span&gt;-p&lt;/span&gt; layers/shared layers/products layers/cart

&lt;span&gt;# 🔧 Add a nuxt.config.ts to each layer&lt;/span&gt;
&lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;export default defineNuxtConfig({
  \&lt;span&gt;$meta&lt;/span&gt;: {
    name: &apos;shared&apos;,
    description: &apos;Shared UI and utilities&apos;
  }
})&quot;&lt;/span&gt; &lt;span&gt;&amp;gt;&lt;/span&gt; layers/shared/nuxt.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  Nuxt automatically discovers and extends layers in the `layers/` directory.
  You only need to explicitly configure `extends` in your `nuxt.config.ts` if
  you&apos;re using external layers (npm packages, git repositories) or if your
  layers are in a different location.
&lt;/aside&gt;
&lt;h3&gt;3. Add ESLint Enforcement&lt;/h3&gt;
&lt;p&gt;Install the ESLint plugin:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;# 📦 Install ESLint plugin&lt;/span&gt;
&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; &lt;span&gt;-D&lt;/span&gt; eslint-plugin-nuxt-layers
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  This ESLint plugin only works when auto-imports are disabled. You need to
  explicitly import from layers using `#layers/` aliases for the plugin to
  detect and validate cross-layer imports. If you rely on Nuxt&apos;s auto-import
  feature, the plugin won&apos;t be able to enforce boundaries.
&lt;/aside&gt;
&lt;p&gt;Configure it in your &lt;code&gt;eslint.config.mjs&lt;/code&gt; with the &lt;code&gt;layer-boundaries&lt;/code&gt; rule:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;plugins&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;nuxt-layers&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; nuxtLayers&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;rules&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;nuxt-layers/layer-boundaries&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;root&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;layers&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 📁 Your layers directory name&lt;/span&gt;
          &lt;span&gt;aliases&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;#layers&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;@layers&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 🔗 Path aliases that point to layers&lt;/span&gt;
          &lt;span&gt;layers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;shared&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 🏗️ Cannot import from any layer&lt;/span&gt;
            &lt;span&gt;products&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;shared&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 🛍️ Can only import from shared&lt;/span&gt;
            &lt;span&gt;cart&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;shared&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 🛒 Can only import from shared&lt;/span&gt;
            &lt;span&gt;// 🏠 Your project root files can import from all layers (use &apos;*&apos;)&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin will now enforce your architecture boundaries automatically. It detects violations in ES6 imports, dynamic imports, CommonJS requires, and export statements—giving you immediate feedback in your IDE and failing your CI/CD pipeline if boundaries are violated.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I’ve been working with modular monoliths for a while now, and I believe this pattern gives you the best of both worlds. You get the clear boundaries and independent development of micro frontends without the operational complexity of deployment, networking, and data consistency.&lt;/p&gt;
&lt;p&gt;Nuxt Layers makes this pattern accessible and practical. You get compile-time enforcement of boundaries through TypeScript. You get clear dependency graphs that are easy to visualize and understand. You get a structure that scales from small teams to large organizations without a rewrite.&lt;/p&gt;
&lt;p&gt;You can start with layers from day one, or you can refactor gradually as your application grows. Either way, your future self will thank you when your codebase is still maintainable after two years and 50+ features.&lt;/p&gt;
&lt;p&gt;I hope this blog post has been insightful and useful. The complete code is available for you to explore, learn from, and build upon. Clone it, break it, experiment with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full project repository&lt;/strong&gt;: &lt;a href=&quot;https://github.com/alexanderop/nuxt-layer-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/alexanderop/nuxt-layer-example&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have questions or want to share your own experiences with Nuxt Layers, I’d love to hear from you. This pattern has fundamentally changed how I approach application architecture, and I’m excited to see how you use it in your own projects.&lt;/p&gt;

          </content:encoded><category>nuxt</category><category>vue</category><category>architecture</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Handle API Calls in Pinia with The Elm Pattern</title><link>https://alexop.dev/posts/handling-api-calls-pinia-elm-pattern/</link><guid isPermaLink="true">https://alexop.dev/posts/handling-api-calls-pinia-elm-pattern/</guid><description>Learn how to handle API calls in Pinia using the Elm pattern for predictable, testable side effects. Includes complete examples with the Pokemon API.</description><pubDate>Fri, 17 Oct 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;aside&gt;
If your goal is to cache backend results or manage server state, Pinia is not the right tool.
Libraries such as [pinia-colada](https://pinia-colada.esm.dev/), [TanStack Vue Query](https://tanstack.com/query/vue), or [RStore](https://rstore.dev/) are designed for this purpose.
They provide built-in caching, background refetching, and synchronization features that make them a better fit for working with APIs.
&lt;p&gt;The approach described in this post is useful when you want to stay within Pinia but still keep your logic functional, predictable, and easy to test.&lt;br /&gt;
It is best for local logic, explicit message-driven updates, or cases where you need fine control over how side effects are triggered and handled.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside&gt;
  This post builds on the concepts introduced in{&quot; &quot;}
  
    How to Write Better Pinia Stores with the Elm Pattern
  
  . If you&apos;re new to The Elm Architecture or want to understand the full pattern
  for structuring Pinia stores, start there first. This post focuses
  specifically on handling side effects like API calls.
&lt;/aside&gt;
&lt;h2&gt;Understanding Pure Functions and Side Effects&lt;/h2&gt;
&lt;p&gt;Before diving into the pattern, it’s important to understand the foundational concepts of functional programming that make this approach powerful.&lt;/p&gt;
&lt;h3&gt;What Is a Pure Function?&lt;/h3&gt;
&lt;p&gt;A pure function is a function that satisfies two key properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt;: Given the same inputs, it always returns the same output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No side effects&lt;/strong&gt;: It does not interact with anything outside its scope.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s a simple example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Pure function - always predictable&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Always returns 5&lt;/span&gt;
&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Always returns 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is pure because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It only depends on its inputs (&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;It always produces the same result for the same inputs&lt;/li&gt;
&lt;li&gt;It doesn’t modify any external state&lt;/li&gt;
&lt;li&gt;It doesn’t perform any I/O operations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What Is a Side Effect?&lt;/h3&gt;
&lt;p&gt;A side effect is any operation that interacts with the outside world or modifies state beyond the function’s return value.&lt;/p&gt;
&lt;p&gt;Common side effects include:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Side effect: Network request&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;fetchUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Network I/O&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Side effect: Modifying external state&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  count&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Mutates external variable&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Side effect: Writing to storage&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;saveUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;:&lt;/span&gt; User&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// I/O operation&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Side effect: Logging&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;calculate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Calculating...&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// I/O operation&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; x &lt;span&gt;*&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of these are pure functions because they interact with something beyond their inputs and outputs.&lt;/p&gt;
&lt;h3&gt;Why Does This Matter?&lt;/h3&gt;
&lt;p&gt;Pure functions are easier to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt;: No need to mock APIs, databases, or global state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reason about&lt;/strong&gt;: The function’s behavior is completely determined by its inputs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debug&lt;/strong&gt;: No hidden dependencies or unexpected state changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reuse&lt;/strong&gt;: Work anywhere without environmental setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, real applications need side effects. You can’t build useful software without API calls, database writes, or user interactions.&lt;/p&gt;
&lt;p&gt;The key insight from functional programming is not to eliminate side effects, but to &lt;strong&gt;separate&lt;/strong&gt; them from your business logic.&lt;/p&gt;
&lt;h2&gt;Why Side Effects Are a Problem&lt;/h2&gt;
&lt;p&gt;A pure function only depends on its inputs and always returns the same output.&lt;br /&gt;
If you include an API call or any asynchronous operation inside it, the function becomes unpredictable and hard to test.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; msg&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;msg&lt;span&gt;.&lt;/span&gt;type &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;FETCH_POKEMON&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://pokeapi.co/api/v2/pokemon/pikachu&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This mixes logic with side effects.&lt;br /&gt;
The function now depends on the network and the API structure, making it complex to test and reason about.&lt;/p&gt;
&lt;h2&gt;The Solution: Separate Logic and Effects&lt;/h2&gt;
&lt;p&gt;The Elm Architecture provides a simple way to handle side effects correctly.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep the update function pure.&lt;/li&gt;
&lt;li&gt;Move side effects into separate functions that receive a dispatch function.&lt;/li&gt;
&lt;li&gt;Use the store as the bridge between both layers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This separation keeps your business logic independent of the framework and easier to verify.&lt;/p&gt;
&lt;h3&gt;File Organization&lt;/h3&gt;
&lt;p&gt;Before diving into the code, here’s how we organize the files for a Pinia store using the Elm pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
└── stores/
    └── pokemon/
        ├── pokemonModel.ts    # Types and initial state
        ├── pokemonUpdate.ts   # Pure update function
        ├── pokemonEffects.ts  # Side effects (API calls)
        └── pokemon.ts         # Pinia store (connects everything)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each file has a clear, single responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pokemonModel.ts&lt;/code&gt;&lt;/strong&gt;: Defines the state shape and message types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pokemonUpdate.ts&lt;/code&gt;&lt;/strong&gt;: Contains pure logic for state transitions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pokemonEffects.ts&lt;/code&gt;&lt;/strong&gt;: Handles side effects like API calls&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pokemon.ts&lt;/code&gt;&lt;/strong&gt;: The Pinia store that wires everything together&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This structure makes it easy to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find and modify specific logic&lt;/li&gt;
&lt;li&gt;Test each piece independently&lt;/li&gt;
&lt;li&gt;Reuse the update logic in different contexts&lt;/li&gt;
&lt;li&gt;Add new effects without touching business logic&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example: Fetching Data from the Pokémon API&lt;/h2&gt;
&lt;p&gt;This example demonstrates how to handle an API call using this pattern.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;pokemonModel.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The model defines the structure of the state and the possible messages that can change it.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PokemonModel&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  pokemon&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; initialModel&lt;span&gt;:&lt;/span&gt; PokemonModel &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  pokemon&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PokemonMsg&lt;/span&gt; &lt;span&gt;=&lt;/span&gt;
  &lt;span&gt;|&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_REQUEST&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;|&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_SUCCESS&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;|&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_FAILURE&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;pokemonUpdate.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The update function handles all state transitions in a pure way.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;:&lt;/span&gt; PokemonModel&lt;span&gt;,&lt;/span&gt; msg&lt;span&gt;:&lt;/span&gt; PokemonMsg&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; PokemonModel &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;switch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;msg&lt;span&gt;.&lt;/span&gt;type&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;FETCH_REQUEST&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;FETCH_SUCCESS&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;pokemon &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;FETCH_FAILURE&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; msg&lt;span&gt;.&lt;/span&gt;error &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; model&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function has no side effects.&lt;br /&gt;
It only describes how the state changes in response to a message.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;pokemonEffects.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This file performs the network request and communicates back through the dispatch function.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;fetchPokemon&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;m&lt;span&gt;:&lt;/span&gt; PokemonMsg&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_REQUEST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; name &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; res &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;https://pokeapi.co/api/v2/pokemon/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;name&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;res&lt;span&gt;.&lt;/span&gt;ok&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Not found&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; res&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_SUCCESS&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;name &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_FAILURE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; e&lt;span&gt;.&lt;/span&gt;message &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function does not depend on Pinia or Vue.&lt;br /&gt;
It simply performs the side effect and dispatches messages based on the result.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;pokemon.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The Pinia store connects the pure logic and the side effect layer.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  initialModel&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PokemonModel&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PokemonMsg&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;./pokemonModel&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; usePokemonStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;pokemon&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;PokemonModel&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialModel&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;msg&lt;span&gt;:&lt;/span&gt; PokemonMsg&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    model&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt; msg&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;load&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetchPokemon&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;name&lt;span&gt;,&lt;/span&gt; dispatch&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    state&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    load&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The store contains no direct logic for handling API responses.&lt;br /&gt;
It only coordinates updates and side effects.&lt;/p&gt;
&lt;h3&gt;Usage in a Component&lt;/h3&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const store = usePokemonStore();
const name = ref(&quot;pikachu&quot;);

function fetchIt() {
  store.load(name.value);
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    
    &amp;lt;button @click=&quot;fetchIt&quot;&amp;gt;Search&amp;lt;/button&amp;gt;

    &amp;lt;p v-if=&quot;store.state.isLoading&quot;&amp;gt;Loading...&amp;lt;/p&amp;gt;
    &amp;lt;p v-else-if=&quot;store.state.error&quot;&amp;gt;Error: {{ store.state.error }}&amp;lt;/p&amp;gt;
    &amp;lt;p v-else-if=&quot;store.state.pokemon&quot;&amp;gt;Found: {{ store.state.pokemon }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component only interacts with the public API of the store.&lt;br /&gt;
It does not mutate the state directly.&lt;/p&gt;
&lt;h2&gt;Why This Approach Works&lt;/h2&gt;
&lt;p&gt;Separating logic and effects provides several benefits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The update function is pure and easy to test.&lt;/li&gt;
&lt;li&gt;The side effect functions are independent and reusable.&lt;/li&gt;
&lt;li&gt;The store focuses only on coordination.&lt;/li&gt;
&lt;li&gt;The overall data flow remains predictable and maintainable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method is especially effective in projects where you want full control over how and when side effects are executed.&lt;/p&gt;
&lt;aside&gt;
Because the `update` function is pure and framework-agnostic, you can test it with simple assertions without any mocking:
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;pokemon update&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sets loading state on fetch request&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; state &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isLoading&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;state&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;FETCH_REQUEST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pikachu&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;isLoading&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Pinia setup, no component mounting, just pure function testing.&lt;/p&gt;
&lt;/aside&gt;
&lt;aside&gt;
The Elm pattern adds structure and discipline, but it comes with trade-offs:
&lt;p&gt;&lt;strong&gt;Not ideal for simple stores:&lt;/strong&gt;&lt;br /&gt;
If your store just fetches data and displays it with minimal logic, the traditional Pinia approach is perfectly fine. Creating four separate files for a simple CRUD operation adds unnecessary complexity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requires team buy-in:&lt;/strong&gt;&lt;br /&gt;
This pattern works best when your entire team embraces functional programming concepts. If your team isn’t comfortable with ideas like pure functions, immutability, and message-driven updates, this pattern will feel foreign and may be resisted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where it shines:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex business logic with multiple state transitions&lt;/li&gt;
&lt;li&gt;Stores that need rock-solid testing&lt;/li&gt;
&lt;li&gt;Applications with sophisticated side effect orchestration (retries, cancellation, queuing)&lt;/li&gt;
&lt;li&gt;Projects where state predictability is critical&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; Start simple. Adopt this pattern when complexity demands it and your team is ready for functional programming principles. Don’t use it just because it’s clever—use it when it solves real problems.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2&gt;Other Side Effects You Can Handle with This Pattern&lt;/h2&gt;
&lt;p&gt;This pattern is not limited to API requests.&lt;br /&gt;
You can manage any kind of asynchronous or external operation the same way.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing to or reading from &lt;code&gt;localStorage&lt;/code&gt; or &lt;code&gt;IndexedDB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sending analytics or telemetry events&lt;/li&gt;
&lt;li&gt;Performing authentication or token refresh logic&lt;/li&gt;
&lt;li&gt;Communicating with WebSockets or event streams&lt;/li&gt;
&lt;li&gt;Scheduling background tasks with &lt;code&gt;setTimeout&lt;/code&gt; or &lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Reading files or using browser APIs such as the Clipboard or File System&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By using the same structure, you can keep these effects organized and testable.&lt;br /&gt;
Each effect becomes an independent unit that transforms external data into messages for your update function.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;If you only need caching or background synchronization, use a specialized library such as &lt;a href=&quot;https://pinia-colada.esm.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pinia-colada&lt;/a&gt;, &lt;a href=&quot;https://tanstack.com/query/vue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TanStack Vue Query&lt;/a&gt;, or &lt;a href=&quot;https://rstore.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RStore&lt;/a&gt;.&lt;br /&gt;
If you need to stay within Pinia and still maintain a functional structure, this approach is effective.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define your model and messages.&lt;/li&gt;
&lt;li&gt;Keep the update function pure.&lt;/li&gt;
&lt;li&gt;Implement effects as separate functions that take a dispatch function.&lt;/li&gt;
&lt;li&gt;Connect them inside the store.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This structure keeps your Pinia stores predictable, testable, and easy to extend to any type of side effect.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Write Better Pinia Stores with the Elm Pattern</title><link>https://alexop.dev/posts/tea-architecture-pinia-private-store-pattern/</link><guid isPermaLink="true">https://alexop.dev/posts/tea-architecture-pinia-private-store-pattern/</guid><description>Learn how to combine The Elm Architecture (TEA) principles with Pinia&apos;s private store pattern for testable, framework-agnostic state management in Vue applications.</description><pubDate>Fri, 03 Oct 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;The Problem: Pinia Gives You Freedom, Not Rules&lt;/h2&gt;
&lt;p&gt;Pinia is a fantastic state management library for Vue, but it doesn’t enforce any architectural patterns. It gives you complete freedom to structure your stores however you want. This flexibility is powerful, but it comes with a hidden cost: without discipline, your stores can become unpredictable and hard to test.&lt;/p&gt;
&lt;p&gt;The core issue? Pinia stores are inherently mutable and framework-coupled. While this makes them convenient for rapid development, it creates three problems:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Traditional Pinia approach - tightly coupled to Vue&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; useTodosStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todos&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; todos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;addTodo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; id&lt;span&gt;:&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; text&lt;span&gt;,&lt;/span&gt; done&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; todos&lt;span&gt;,&lt;/span&gt; addTodo &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem? Components can bypass your API and directly manipulate state:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const store = useTodosStore();

// Intended way
store.addTodo(&quot;Learn Pinia&quot;);

// But this also works! Direct state manipulation
store.todos.push({ id: 999, text: &quot;Hack the state&quot;, done: false });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This leads to unpredictable state changes, makes testing difficult (requires mocking Pinia’s entire runtime), and couples your business logic tightly to Vue’s reactivity system.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TB
    C1&lt;span&gt;[Component A]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;store.addTodo() ✓&quot;|&lt;/span&gt; API&lt;span&gt;[Intended API]&lt;/span&gt;
    C2&lt;span&gt;[Component B]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;store.todos.push() ✗&quot;|&lt;/span&gt; State&lt;span&gt;[Direct State Access]&lt;/span&gt;
    C3&lt;span&gt;[Component C]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;store.todos[0].done = true ✗&quot;|&lt;/span&gt; State
    API &lt;span&gt;--&amp;gt;&lt;/span&gt; Store&lt;span&gt;[Store State]&lt;/span&gt;
    &lt;span&gt;State&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; Store
    Store &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|unpredictable changes|&lt;/span&gt; Debug&lt;span&gt;[Difficult to Debug]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Solution: TEA + Private Store Pattern&lt;/h2&gt;
&lt;p&gt;What if we could keep Pinia’s excellent developer experience while adding the predictability and testability of functional patterns? Enter The Elm Architecture (TEA) combined with the “private store” technique from &lt;a href=&quot;https://masteringpinia.com/blog/how-to-create-private-state-in-stores&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Mastering Pinia&lt;/a&gt; by Eduardo San Martin Morote (creator of Pinia).&lt;/p&gt;
&lt;p&gt;This hybrid approach gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure, testable business logic&lt;/strong&gt; that’s framework-agnostic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controlled state mutations&lt;/strong&gt; through a single dispatch function&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seamless Vue integration&lt;/strong&gt; with Pinia’s reactivity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full devtools support&lt;/strong&gt; for debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll use a private internal store for mutable state, expose only selectors and a dispatch function publicly, and keep your update logic pure and framework-agnostic.&lt;/p&gt;
&lt;aside&gt;
  This pattern shines when you have complex business logic, need framework
  portability, or want rock-solid testing. For simple CRUD operations with
  minimal logic, traditional Pinia stores are perfectly fine. Ask yourself:
  &quot;Would I benefit from testing this logic in complete isolation?&quot; If yes, this
  pattern is worth it.
&lt;/aside&gt;
&lt;aside&gt;
  The Elm Architecture emerged from the [Elm programming
  language](https://guide.elm-lang.org/architecture/), which pioneered a purely
  functional approach to building web applications. This pattern later inspired
  Redux&apos;s architecture in the JavaScript ecosystem, demonstrating the value of
  unidirectional data flow and immutable updates. While Elm enforces these
  patterns through its type system, we can achieve similar benefits in Vue with
  disciplined patterns.
&lt;/aside&gt;
&lt;h2&gt;Understanding The Elm Architecture&lt;/h2&gt;
&lt;p&gt;Before we dive into the implementation, let’s understand the core concepts of TEA:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;: The state of your application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update&lt;/strong&gt;: Pure functions that transform state based on messages/actions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View&lt;/strong&gt;: Rendering UI based on the current model&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    M&lt;span&gt;[Model&amp;lt;br/&amp;gt;Current State]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|renders|&lt;/span&gt; V&lt;span&gt;[View&amp;lt;br/&amp;gt;UI Display]&lt;/span&gt;
    V &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|user interaction&amp;lt;br/&amp;gt;produces|&lt;/span&gt; Msg&lt;span&gt;[Message/Action]&lt;/span&gt;
    Msg &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|dispatched to|&lt;/span&gt; U&lt;span&gt;[Update Function&amp;lt;br/&amp;gt;Pure Logic]&lt;/span&gt;
    U &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|returns new|&lt;/span&gt; M

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key insight is that update functions are pure—given the same state and action, they always return the same new state. This makes them trivial to test without any framework dependencies.&lt;/p&gt;
&lt;h2&gt;How It Works: Combining TEA with Private State&lt;/h2&gt;
&lt;p&gt;The pattern uses three key pieces: a private internal store for mutable state, pure update functions for business logic, and a public store that exposes only selectors and dispatch.&lt;/p&gt;
&lt;h3&gt;The Private Internal Store&lt;/h3&gt;
&lt;p&gt;First, create a private store that holds the mutable model. This stays in the same file as your public store but is not exported:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Inside stores/todos.ts - NOT exported!&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; useTodosPrivate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todos-private&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TodosModel&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; model &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key here: no &lt;code&gt;export&lt;/code&gt; keyword means components can’t access this directly.&lt;/p&gt;
&lt;h3&gt;Pure Update Function&lt;/h3&gt;
&lt;p&gt;Next, define your business logic as pure functions:&lt;/p&gt;
&lt;aside&gt;
  A pure function always returns the same output for the same inputs and has no
  side effects. No API calls, no mutations of external state, no `console.log`.
  Just input → transformation → output. This makes them trivially easy to test
  and reason about: `update(state, action)` always produces the same new state.
&lt;/aside&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// stores/todos-update.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;:&lt;/span&gt; TodosModel&lt;span&gt;,&lt;/span&gt; message&lt;span&gt;:&lt;/span&gt; TodosMessage&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; TodosModel &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;switch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;message&lt;span&gt;.&lt;/span&gt;type&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;ADD_TODO&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt;
        todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt; id&lt;span&gt;:&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; text&lt;span&gt;:&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;text&lt;span&gt;,&lt;/span&gt; done&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;TOGGLE_TODO&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;...&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt;
        todos&lt;span&gt;:&lt;/span&gt; model&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo &lt;span&gt;=&amp;gt;&lt;/span&gt;
          todo&lt;span&gt;.&lt;/span&gt;id &lt;span&gt;===&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;id &lt;span&gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;todo&lt;span&gt;,&lt;/span&gt; done&lt;span&gt;:&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;todo&lt;span&gt;.&lt;/span&gt;done &lt;span&gt;}&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; todo
        &lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; model&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This update function is completely framework-agnostic. You can test it with simple assertions:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;update&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;adds a todo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; initial &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initial&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;ADD_TODO&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Test&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveLength&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Test&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  If you&apos;ve used Redux, this pattern will feel familiar—the `update` function is
  like a reducer, and `TodosMessage` is like an action. The key difference?
  We&apos;re using Pinia&apos;s reactivity instead of Redux&apos;s subscription model, and
  we&apos;re keeping the private store pattern to prevent direct state access. This
  gives you Redux&apos;s testability with Pinia&apos;s developer experience.
&lt;/aside&gt;
&lt;h3&gt;Public Store with Selectors + Dispatch&lt;/h3&gt;
&lt;p&gt;Finally, combine everything in a single file. The private store is defined but not exported:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// stores/todos.ts (this is what components import)&lt;/span&gt;
&lt;span&gt;// Private store - not exported!&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; useTodosPrivate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todos-private&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TodosModel&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; model &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Public store - this is what gets exported&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; useTodosStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todos&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; privateStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useTodosPrivate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Selectors&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; todos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; privateStore&lt;span&gt;.&lt;/span&gt;model&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Dispatch&lt;/span&gt;
  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;message&lt;span&gt;:&lt;/span&gt; TodosMessage&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    privateStore&lt;span&gt;.&lt;/span&gt;model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;privateStore&lt;span&gt;.&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; todos&lt;span&gt;,&lt;/span&gt; dispatch &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    Component&lt;span&gt;[Component]&lt;/span&gt;
    Component &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|dispatch message|&lt;/span&gt; Public&lt;span&gt;[Public Store]&lt;/span&gt;
    Public &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|call|&lt;/span&gt; Update&lt;span&gt;[Update Function&amp;lt;br/&amp;gt;Pure Logic]&lt;/span&gt;
    Update &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|new state|&lt;/span&gt; Private&lt;span&gt;[Private Store]&lt;/span&gt;
    Private &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|selectors|&lt;/span&gt; Public
    Public &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|reactive data|&lt;/span&gt; Component
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
Reddit user [maertensen](https://www.reddit.com/user/maertensen/) helpfully pointed out that the private store pattern only prevents direct mutation of primitives, **not arrays and objects**. Components can still mutate through selectors:
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const publicTodosStore = useTodosStore();

function mutateTodosSelector() {
  // This still works and bypasses dispatch! ✗
  publicTodosStore.todos.push({
    id: Date.now(),
    text: &quot;Mutated by selector&quot;,
    done: false,
  });
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is why I recommend using &lt;strong&gt;&lt;code&gt;readonly&lt;/code&gt; only&lt;/strong&gt; (shown below) instead of the private store pattern.&lt;/p&gt;
&lt;/aside&gt;
&lt;h3&gt;Usage in Components&lt;/h3&gt;
&lt;p&gt;Components interact with the public store:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const store = useTodosStore();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;input
      @keyup.enter=&quot;
        store.dispatch({ type: &apos;ADD_TODO&apos;, text: $event.target.value })
      &quot;
    /&amp;gt;

    &amp;lt;div v-for=&quot;todo in store.todos&quot; :key=&quot;todo.id&quot;&amp;gt;
      &amp;lt;input
        type=&quot;checkbox&quot;
        :checked=&quot;todo.done&quot;
        @change=&quot;store.dispatch({ type: &apos;TOGGLE_TODO&apos;, id: todo.id })&quot;
      /&amp;gt;
      {{ todo.text }}
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Simpler Alternative: Using Vue’s readonly&lt;/h2&gt;
&lt;p&gt;If you want to prevent direct state mutations without creating a private store, Vue’s &lt;code&gt;readonly&lt;/code&gt; utility provides a simpler approach:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// stores/todos.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; useTodosStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;todos&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TodosModel&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Dispatch&lt;/span&gt;
  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;dispatch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;message&lt;span&gt;:&lt;/span&gt; TodosMessage&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    model&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt; message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Only expose readonly state&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;readonly&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;model&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    dispatch&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;readonly&lt;/code&gt;, any attempt to mutate the state from a component will fail:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const store = useTodosStore();

// ✓ Works - using dispatch
store.dispatch({ type: &quot;ADD_TODO&quot;, text: &quot;Learn Vue&quot; });

// ✓ Works - accessing readonly state
const todos = store.model.todos;

// ✗ TypeScript error - readonly prevents mutation
store.model.todos.push({ id: 1, text: &quot;Hack&quot;, done: false });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  **Prefer `readonly` over the private store pattern.** The private store
  pattern has a critical flaw: it doesn&apos;t prevent mutation of arrays and objects
  (see warning above). Using `readonly` is simpler, more effective, and truly
  prevents all direct state mutations.
&lt;/aside&gt;
&lt;h2&gt;Benefits of This Approach&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pure business logic&lt;/strong&gt;: The &lt;code&gt;update&lt;/code&gt; function has zero dependencies on Vue or Pinia&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy testing&lt;/strong&gt;: Test your update function with simple unit tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework flexibility&lt;/strong&gt;: Could swap Vue for React without changing update logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type safety&lt;/strong&gt;: TypeScript ensures message types are correct&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Devtools support&lt;/strong&gt;: Still works with Pinia devtools since we’re using real stores&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encapsulation&lt;/strong&gt;: Private store is an implementation detail&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TB
    &lt;span&gt;subgraph&lt;/span&gt; T&lt;span&gt;[&quot;Traditional Pinia&quot;]&lt;/span&gt;
        TC&lt;span&gt;[Component]&lt;/span&gt;
        TC &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|direct|&lt;/span&gt; TS&lt;span&gt;[State]&lt;/span&gt;
        TC &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|actions|&lt;/span&gt; TA&lt;span&gt;[Actions]&lt;/span&gt;
        TA &lt;span&gt;--&amp;gt;&lt;/span&gt; TS
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; P&lt;span&gt;[&quot;TEA + Private Store&quot;]&lt;/span&gt;
        PC&lt;span&gt;[Component]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|dispatch|&lt;/span&gt; PD&lt;span&gt;[Dispatch]&lt;/span&gt;
        PD &lt;span&gt;--&amp;gt;&lt;/span&gt; PU&lt;span&gt;[Update]&lt;/span&gt;
        PU &lt;span&gt;--&amp;gt;&lt;/span&gt; PM&lt;span&gt;[Model]&lt;/span&gt;
        PM &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|selectors|&lt;/span&gt; PC
    &lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By combining The Elm Architecture with Pinia’s private store pattern, we achieve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pure, testable business logic&lt;/li&gt;
&lt;li&gt;Clear separation of concerns&lt;/li&gt;
&lt;li&gt;Framework-agnostic state management&lt;/li&gt;
&lt;li&gt;Full Pinia devtools integration&lt;/li&gt;
&lt;li&gt;Type-safe message dispatching&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This pattern scales from simple forms to complex domain logic while keeping your code maintainable and your tests simple.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Credit: This post synthesizes ideas from &lt;a href=&quot;https://guide.elm-lang.org/architecture/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Elm Architecture&lt;/a&gt; and Eduardo San Martin Morote’s &lt;a href=&quot;https://masteringpinia.com/blog/how-to-create-private-state-in-stores&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;“private store” pattern&lt;/a&gt; from Mastering Pinia.&lt;/em&gt;&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to build Microfrontends with Module Federation and Vue</title><link>https://alexop.dev/posts/how-to-build-microfrontends-with-module-federation-and-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-build-microfrontends-with-module-federation-and-vue/</guid><description>Build a Vue 3 microfrontend app with Module Federation. Clear decisions, working code, and a small reference project.</description><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>
            
            
  Monorepo with `pnpm`. Vue 3 SPA. Client side composition with Module
  Federation. Host owns routing. Events for navigation. Cart sync through
  localStorage plus events. Shared UI library for consistency. Fallbacks for
  remote failures. Code:
  https://github.com/alexanderop/tractorStoreVueModuleFederation


  You know Vue and basic bundling. You want to split a SPA into independent
  parts without tight coupling. If you do not have multiple teams or deployment
  bottlenecks, you likely do not need microfrontends.

&lt;p&gt;I wanted to write about microfrontends three years ago. My first touchpoint was the book &lt;em&gt;Micro Frontends in Action&lt;/em&gt; by Michael Geers. It taught me a lot, and I was still confused. This post shows a practical setup that works today with Vue 3 and Module Federation.&lt;/p&gt;
&lt;h2&gt;Scope&lt;/h2&gt;
&lt;p&gt;We build microfrontends for a Vue 3 SPA with client side composition. Server and universal rendering exist, but they are out of scope here.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Microfrontends are the technical representation of a business subdomain. They allow independent implementations with minimal shared code and single team ownership.&lt;br /&gt;
(Luca Mezzalira)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Tractor Store in one minute&lt;/h2&gt;
&lt;p&gt;The Tractor Store is a reference shop that lets us compare microfrontend approaches on the same feature set (explore, decide, checkout). It is clear enough to show boundaries and realistic enough to surface routing, shared state, and styling issues.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;h2&gt;Architecture decisions&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repo layout&lt;/td&gt;
&lt;td&gt;Monorepo with &lt;code&gt;pnpm&lt;/code&gt; workspaces&lt;/td&gt;
&lt;td&gt;Shared configs, atomic refactors, simple local dev&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Client side with Module Federation&lt;/td&gt;
&lt;td&gt;Fast iteration, simple hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;Host owns routing&lt;/td&gt;
&lt;td&gt;One place for guards, links, and errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team boundaries&lt;/td&gt;
&lt;td&gt;Explore, Decide, Checkout, plus Host&lt;/td&gt;
&lt;td&gt;Map to clear user flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication&lt;/td&gt;
&lt;td&gt;Custom events for navigation. Cart via localStorage plus events&lt;/td&gt;
&lt;td&gt;Low coupling and no shared global store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI consistency&lt;/td&gt;
&lt;td&gt;Shared UI library in &lt;code&gt;packages/shared&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Buttons, inputs, tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure handling&lt;/td&gt;
&lt;td&gt;Loading and error fallbacks in host. Retry once&lt;/td&gt;
&lt;td&gt;Keep the shell usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styles&lt;/td&gt;
&lt;td&gt;Team prefixes or Vue scoped styles. Tokens in shared&lt;/td&gt;
&lt;td&gt;Prevent leakage and keep a common look&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Repository layout:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── apps/
│   ├── host/          (Shell application)
│   ├── explore/       (Product discovery)
│   ├── decide/        (Product detail)
│   └── checkout/      (Cart and checkout)
├── packages/
│   └── shared/        (UI, tokens, utils)
└── pnpm-workspace.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Workspace definition:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span&gt;packages&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&quot;apps/*&quot;&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&quot;packages/*&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;High level view:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Monorepo (@tractor)&quot;&lt;/span&gt;
        &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;apps/&quot;&lt;/span&gt;
            Host&lt;span&gt;(&quot;host&quot;)&lt;/span&gt;
            Explore&lt;span&gt;(&quot;explore&quot;)&lt;/span&gt;
            Decide&lt;span&gt;(&quot;decide&quot;)&lt;/span&gt;
            Checkout&lt;span&gt;(&quot;checkout&quot;)&lt;/span&gt;
        &lt;span&gt;end&lt;/span&gt;
        &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;packages/&quot;&lt;/span&gt;
            Shared&lt;span&gt;(&quot;shared&quot;)&lt;/span&gt;
        &lt;span&gt;end&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    Host &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Consumes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Explore
    Host &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Consumes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Decide
    Host &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Consumes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Checkout

    Explore &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Uses&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Shared
    Decide &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Uses&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Shared
    Checkout &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Uses&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Shared
    Host &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Uses&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Shared
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;We compose at the client in the host. The host loads remote components at runtime and routes between them.&lt;/p&gt;
&lt;h3&gt;Host router&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// apps/host/src/router.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  history&lt;span&gt;:&lt;/span&gt; &lt;span&gt;createWebHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  routes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;remote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;explore/HomePage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/products/:category?&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;remote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;explore/CategoryPage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/product/:id&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;remote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;decide/ProductPage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/checkout/cart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;remote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;checkout/CartPage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;remote()&lt;/code&gt; utility&lt;/h3&gt;

Vue `defineAsyncComponent` wraps any loader in a friendly component. It gives lazy loading, built in states, and retries. This is why it fits microfrontends.
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;const&lt;/span&gt; AsyncComp &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineAsyncComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; Promise&lt;span&gt;.&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* component */&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;delay&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;timeout&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;3000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;onError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;error&lt;span&gt;,&lt;/span&gt; retry&lt;span&gt;,&lt;/span&gt; fail&lt;span&gt;,&lt;/span&gt; attempts&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;attempts &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;retry&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;else&lt;/span&gt; &lt;span&gt;fail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// apps/host/src/utils/remote.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;remote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; delay &lt;span&gt;=&lt;/span&gt; &lt;span&gt;150&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;defineAsyncComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; loader &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;window &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;getComponent&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;loader&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Missing loader for &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;loader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    delay&lt;span&gt;,&lt;/span&gt;
    loadingComponent&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;render&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;h&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;mf-loading&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;Loading...&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    errorComponent&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;render&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;h&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;mf-error&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;Failed to load.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;onError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;error&lt;span&gt;,&lt;/span&gt; retry&lt;span&gt;,&lt;/span&gt; fail&lt;span&gt;,&lt;/span&gt; attempts&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;attempts &lt;span&gt;&amp;lt;=&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;retry&lt;span&gt;,&lt;/span&gt; &lt;span&gt;200&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;else&lt;/span&gt; &lt;span&gt;fail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  Module Federation enables dynamic loading of JavaScript modules from different
  applications at runtime. Think of it as splitting your app into independently
  deployable pieces that can share code and communicate. You can use **Webpack
  5**, **Rspack**, or **Vite** as bundlers. **Rspack offers the most
  comprehensive Module Federation support** with excellent performance, but for
  this solution I used **Rspack** (for the host) and **Vite** (for one remote)
  to showcase interoperability between different bundlers.

&lt;h3&gt;Module Federation runtime&lt;/h3&gt;
&lt;p&gt;The host bootstraps Module Federation and exposes a single loader.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// apps/host/src/mf.ts&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  createInstance&lt;span&gt;,&lt;/span&gt;
  loadRemote&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;@module-federation/enhanced/runtime&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;declare&lt;/span&gt; global &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Window&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;getComponent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;createInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;host&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  remotes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;decide&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      entry&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;http://localhost:5175/mf-manifest.json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      alias&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;decide&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;explore&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      entry&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;http://localhost:3004/mf-manifest.json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      alias&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;explore&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;checkout&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      entry&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;http://localhost:3003/mf-manifest.json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      alias&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;checkout&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;fallback-plugin&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;errorLoadRemote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;args&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Failed to load remote: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;args&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; args&lt;span&gt;.&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&amp;lt;div style=&quot;padding: 2rem; text-align: center; border: 1px solid #ddd; border-radius: 8px; background: #f9f9f9; margin: 1rem 0;&quot;&amp;gt;
              &amp;lt;h3 style=&quot;color: #c00; margin-bottom: 1rem;&quot;&amp;gt;Remote unavailable&amp;lt;/h3&amp;gt;
              &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Remote:&amp;lt;/strong&amp;gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;args&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/p&amp;gt;
              &amp;lt;p&amp;gt;Try again or check the service.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getComponent&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; mod &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;loadRemote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; mod&lt;span&gt;.&lt;/span&gt;default &lt;span&gt;||&lt;/span&gt; mod&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this setup&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The host is the single source of truth for remote URLs and versions.&lt;/li&gt;
&lt;li&gt;URLs can change without rebuilding remotes.&lt;/li&gt;
&lt;li&gt;The fallback plugin gives a consistent error experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Communication&lt;/h2&gt;
&lt;p&gt;We avoid a shared global store. We use two small patterns that keep coupling low.&lt;/p&gt;

A global store looks handy. In microfrontends it creates tight runtime coupling. That kills independent deploys.
&lt;p&gt;What goes wrong:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lockstep releases (one store change breaks other teams)&lt;/li&gt;
&lt;li&gt;Hidden contracts (store shape is an API that drifts)&lt;/li&gt;
&lt;li&gt;Boot order traps (who creates the store and plugins)&lt;/li&gt;
&lt;li&gt;Bigger blast radius (a store error can break the whole app)&lt;/li&gt;
&lt;li&gt;Harder tests (cross team mocks and brittle fixtures)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do this instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each microfrontend owns its state&lt;/li&gt;
&lt;li&gt;Communicate with explicit custom events&lt;/li&gt;
&lt;li&gt;Use URL and localStorage for simple shared reads&lt;/li&gt;
&lt;li&gt;Share code not state (tokens, UI, pure utils)&lt;/li&gt;
&lt;li&gt;If shared state grows, revisit boundaries rather than centralize it&lt;br /&gt;
&lt;/li&gt;&lt;/ul&gt;


  You could use VueUse&apos;s `useEventBus` for more Vue-like event communication
  instead of vanilla JavaScript events. It provides a cleaner API with
  TypeScript support and automatic cleanup in component lifecycle. However,
  adding VueUse means another dependency in your microfrontends. The tradeoff is
  developer experience vs. bundle size and dependency management: vanilla
  JavaScript events keep things lightweight and framework-agnostic.

&lt;h3&gt;Navigation through custom events&lt;/h3&gt;
&lt;p&gt;Remotes dispatch &lt;code&gt;mf:navigate&lt;/code&gt; with &lt;code&gt;{ to }&lt;/code&gt;. The host listens and calls &lt;code&gt;router.push&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// apps/host/src/main.ts&lt;/span&gt;
window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;mf:navigate&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;:&lt;/span&gt; Event&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; to &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e &lt;span&gt;as&lt;/span&gt; CustomEvent&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;detail&lt;span&gt;?.&lt;/span&gt;to&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;to&lt;span&gt;)&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;to&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cart sync through localStorage plus events&lt;/h3&gt;
&lt;p&gt;The checkout microfrontend owns cart logic. It listens for &lt;code&gt;add-to-cart&lt;/code&gt; and &lt;code&gt;remove-from-cart&lt;/code&gt;. After each change, it writes to localStorage and dispatches &lt;code&gt;updated-cart&lt;/code&gt;. Any component can listen and re read.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// apps/checkout/src/stores/cartStore.ts&lt;/span&gt;
window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;add-to-cart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;ev&lt;span&gt;:&lt;/span&gt; Event&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; sku &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;ev &lt;span&gt;as&lt;/span&gt; CustomEvent&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;detail&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// update cart array here&lt;/span&gt;
  localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;cart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;cart&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dispatchEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt; &lt;span&gt;CustomEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;updated-cart&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Styling&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Design tokens in &lt;code&gt;packages/shared&lt;/code&gt; (CSS variables).&lt;/li&gt;
&lt;li&gt;Shared UI in &lt;code&gt;packages/shared/ui&lt;/code&gt; (Button, Input, Card).&lt;/li&gt;
&lt;li&gt;Local isolation with Vue scoped styles or BEM with team prefixes (&lt;code&gt;e_&lt;/code&gt;, &lt;code&gt;d_&lt;/code&gt;, &lt;code&gt;c_&lt;/code&gt;, &lt;code&gt;h_&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Host provides &lt;code&gt;.mf-loading&lt;/code&gt; and &lt;code&gt;.mf-error&lt;/code&gt; classes for fallback UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example with BEM plus team prefix:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;/* decide */&lt;/span&gt;
&lt;span&gt;.d_ProductPage__title&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-primary&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.d_ProductPage__title--featured&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-accent&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or Vue scoped styles:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;product-information&quot;&amp;gt;
    &amp;lt;h1 class=&quot;title&quot;&amp;gt;{{ product.name }}&amp;lt;/h1&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.product-information {
  padding: 1rem;
}
.title {
  font-size: 2rem;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Operations&lt;/h2&gt;
&lt;p&gt;Plan for failure and keep the shell alive.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each remote shows a clear loading state and a clear error state.&lt;/li&gt;
&lt;li&gt;Navigation works even if a remote fails.&lt;/li&gt;
&lt;li&gt;Logs are visible in the host during development.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Global styles in the host help here:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;/* apps/host/src/styles.css */&lt;/span&gt;
&lt;span&gt;.mf-loading&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; flex&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;align-items&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; center&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;justify-content&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; center&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-text-muted&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;.mf-error&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-error-background&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;border&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1px solid &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-error-border&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;--color-error-text&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;border-radius&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 4px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More Module Federation features (short hints)&lt;/h2&gt;
&lt;p&gt;Our solution is simple on purpose. Module Federation can do more. Use these when your app needs them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prefetch&lt;/strong&gt; (requires registering lazyLoadComponentPlugin)&lt;br /&gt;
Pre load JS, CSS, and data for a remote before the user clicks (for example on hover). This cuts wait time.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// after registering lazyLoadComponentPlugin on the runtime instance&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; mf &lt;span&gt;=&lt;/span&gt; &lt;span&gt;getInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;onHover&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  mf&lt;span&gt;.&lt;/span&gt;&lt;span&gt;prefetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;shop/Button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;dataFetchParams&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;productId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;12345&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;preloadComponentResource&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// also fetch JS and CSS&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Component level data fetching&lt;/strong&gt;&lt;br /&gt;
Expose a &lt;code&gt;.data&lt;/code&gt; loader next to a component. The consumer can ask the runtime to fetch data before render (works in CSR and SSR). Use a &lt;code&gt;.data.client.ts&lt;/code&gt; file for client loaders when SSR falls back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caching for loaders&lt;/strong&gt;&lt;br /&gt;
Wrap expensive loaders in a cache helper (with maxAge, revalidate, tag, and custom keys). You get stale while revalidate behavior and tag based invalidation.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// inside your DataLoader file&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; fetchDashboard &lt;span&gt;=&lt;/span&gt; &lt;span&gt;cache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;getStats&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;maxAge&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;120000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;revalidate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;60000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Type hinting for remotes&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;@module-federation/enhanced&lt;/code&gt; can generate types for exposes. Add this to tsconfig.json so editors resolve remote types and hot reload them.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;compilerOptions&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;paths&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;./@mf-types/*&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;include&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;./@mf-types/*&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue bridge for app level modules&lt;/strong&gt;&lt;br /&gt;
Mount a whole remote Vue app into a route when you need an application level integration (not just a component).&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;const&lt;/span&gt; RemoteApp &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRemoteAppComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;loadRemote&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;remote1/export-app&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// router: { path: &apos;/remote1/:pathMatch(.*)*&apos;, component: RemoteApp }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can read more about these interesting techniques at &lt;a href=&quot;https://module-federation.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;module-federation.io&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;This was my first attempt to understand &lt;strong&gt;microfrontends&lt;/strong&gt; better while solving the &lt;em&gt;Tractor Store&lt;/em&gt; exercise.&lt;/p&gt;
&lt;p&gt;In the future, I may also try it with &lt;strong&gt;SSR&lt;/strong&gt; and &lt;strong&gt;universal rendering&lt;/strong&gt;, which I find interesting.&lt;/p&gt;
&lt;p&gt;Another option could be to use &lt;strong&gt;Nuxt Layers&lt;/strong&gt; and take a &lt;em&gt;“microfrontend-ish”&lt;/em&gt; approach at build time.&lt;/p&gt;

          &lt;/figure&gt;</content:encoded><category>vue</category><category>microfrontends</category><category>module-federation</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Why You Need Something Hard in Your Life</title><link>https://alexop.dev/posts/why-you-need-something-hard-in-your-life/</link><guid isPermaLink="true">https://alexop.dev/posts/why-you-need-something-hard-in-your-life/</guid><description>The biggest paradox in life: the hardest things are usually the ones that help you grow. Exploring why challenge and difficulty are essential for meaning and personal development.</description><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;The biggest paradox in life is simple: the hardest things are usually the ones that help you grow. They force you out of your comfort zone. They make you stronger.&lt;/p&gt;
&lt;p&gt;One reason so many people in my generation are depressed is that they do not have a hard thing that is worth aiming for. They live on autopilot. Work 9 to 5. Watch Netflix. Go to a party on the weekend. And then repeat.&lt;/p&gt;
&lt;p&gt;Sometimes life gives you that hard thing automatically. Having a kid, for example, is brutally hard but also meaningful. But what if you do not have that? What do you do now?&lt;/p&gt;
&lt;p&gt;That is why so many millennials run marathons. A marathon is something hard. It gives you structure, meaning, and a clear goal. It demands you change your habits. It tells you who you are when things get tough.&lt;/p&gt;
&lt;p&gt;For me, I am happiest when I have a goal in front of me. Something hard. Not impossible, but not easy either. If it is too easy, I get bored. If it is too hard, I give up. But the sweet spot, where I have to fight for it, that is where life feels good. I am reading Flow right now by the Hungarian psychologist Mihály Csíkszentmihályi, and he explains exactly this. Real happiness does not come from comfort. It comes from challenge. From stretching yourself just enough that you lose track of time and become fully absorbed in the thing you are doing. That is where meaning lives.&lt;/p&gt;
&lt;p&gt;One of my favorite sports anime, Blue Lock, explains the concept of flow perfectly in &lt;a href=&quot;https://www.youtube.com/watch?v=KTHqbv2M0aA&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this video&lt;/a&gt;. It shows how athletes enter a state where everything else disappears and they become completely absorbed in the challenge at hand. This is flow in action.&lt;/p&gt;
&lt;p&gt;The times when I was unhappy were always the times when I had no goal. I was just living. Eating badly. Drinking too much. Slowly sinking into a life I did not want.&lt;/p&gt;
&lt;p&gt;So here is my takeaway:&lt;/p&gt;
&lt;p&gt;Find something hard.&lt;/p&gt;
&lt;p&gt;Stick with it.&lt;/p&gt;
&lt;p&gt;Let it shape you.&lt;/p&gt;
&lt;p&gt;Because without it, life gets empty fast.&lt;/p&gt;

          </content:encoded><category>personal-development</category><category>productivity</category><category>motivation</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>What Is the Model Context Protocol (MCP)? How It Works</title><link>https://alexop.dev/posts/what-is-model-context-protocol-mcp/</link><guid isPermaLink="true">https://alexop.dev/posts/what-is-model-context-protocol-mcp/</guid><description>Learn how MCP (Model Context Protocol) standardizes AI tool integration, enabling LLMs to interact with external services, databases, and APIs through a universal protocol similar to USB-C for AI applications.</description><pubDate>Sun, 10 Aug 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I did not see how powerful MCP was until I used Claude Code with the Playwright MCP.&lt;br /&gt;
&lt;strong&gt;Playwright MCP lets an AI use a real browser.&lt;/strong&gt; It can open a page, click buttons, fill forms, and take screenshots.&lt;br /&gt;
I asked Claude to audit my site for SEO. It ran checks in a real browser, gave me the results, and sent screenshots. You can read more about how I use Claude Code for doing SEO audits.&lt;br /&gt;
&lt;strong&gt;That was when I saw it.&lt;/strong&gt; This was not just text prediction. This was an AI that can see and work with the web like a human tester.&lt;/p&gt;
&lt;h2&gt;What is MCP&lt;/h2&gt;
&lt;p&gt;MCP means Model Context Protocol.&lt;br /&gt;
Before we define it, let us see how we got here.&lt;/p&gt;
&lt;h2&gt;How it started&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    U&lt;span&gt;[User]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A&lt;span&gt;[LLM]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; U
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In 2022 ChatGPT made AI open to everyone.&lt;br /&gt;
You typed a question. It predicted the next tokens and sent back text.&lt;br /&gt;
You could ask for your favorite author or create code.&lt;/p&gt;
&lt;h2&gt;The problem with plain LLMs&lt;/h2&gt;
&lt;p&gt;A plain LLM is a text generator.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It has no live data&lt;/li&gt;
&lt;li&gt;It cannot read your files&lt;/li&gt;
&lt;li&gt;It struggles with math&lt;/li&gt;
&lt;li&gt;It cannot tell you who won yesterday in football&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can read more in my post about LLM limits.&lt;/p&gt;
&lt;h2&gt;The first fix: tools&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    U&lt;span&gt;[User]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A&lt;span&gt;[LLM]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;{Needs Python?}&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; P&lt;span&gt;[Run code in Python sandbox]&lt;/span&gt;
    P &lt;span&gt;--&amp;gt;&lt;/span&gt; O&lt;span&gt;[Execution result]&lt;/span&gt;
    O &lt;span&gt;--&amp;gt;&lt;/span&gt; A
    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; A
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; U
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When OpenAI added a Python sandbox, LLMs could run code and give exact results.&lt;/p&gt;
&lt;h2&gt;More tools mean more power&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    U&lt;span&gt;[User]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A&lt;span&gt;[LLM]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;{Needs external tool?}&lt;/span&gt;

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Python|&lt;/span&gt; P&lt;span&gt;[Run code in Python sandbox]&lt;/span&gt;
    P &lt;span&gt;--&amp;gt;&lt;/span&gt; O&lt;span&gt;[Execution result]&lt;/span&gt;
    O &lt;span&gt;--&amp;gt;&lt;/span&gt; A

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Web search|&lt;/span&gt; W&lt;span&gt;[Search the web for information]&lt;/span&gt;
    W &lt;span&gt;--&amp;gt;&lt;/span&gt; R&lt;span&gt;[Search result]&lt;/span&gt;
    R &lt;span&gt;--&amp;gt;&lt;/span&gt; A

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; A
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; U
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web search gave live knowledge. Now the model could answer fresh questions.&lt;/p&gt;
&lt;h2&gt;Even more tools&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    U&lt;span&gt;[User]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A&lt;span&gt;[LLM]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;{Needs external tool?}&lt;/span&gt;

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Python|&lt;/span&gt; P&lt;span&gt;[Run code in Python sandbox]&lt;/span&gt;
    P &lt;span&gt;--&amp;gt;&lt;/span&gt; O&lt;span&gt;[Execution result]&lt;/span&gt;
    O &lt;span&gt;--&amp;gt;&lt;/span&gt; A

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Web search|&lt;/span&gt; W&lt;span&gt;[Search the web for information]&lt;/span&gt;
    W &lt;span&gt;--&amp;gt;&lt;/span&gt; R&lt;span&gt;[Search result]&lt;/span&gt;
    R &lt;span&gt;--&amp;gt;&lt;/span&gt; A

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Google Calendar|&lt;/span&gt; G&lt;span&gt;[Check / update calendar events]&lt;/span&gt;
    G &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Calendar data]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; A

    D &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; A
    A &lt;span&gt;--&amp;gt;&lt;/span&gt; U
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anthropic added more tools to Claude like Google Calendar and email.&lt;br /&gt;
You can ask it what meetings you have next week and it tells you.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;The solution: a protocol&lt;/h2&gt;
&lt;p&gt;We need a standard.&lt;br /&gt;
One tool for Google Calendar that any agent can use.&lt;br /&gt;
In November the Model Context Protocol was released.&lt;/p&gt;
&lt;h2&gt;Definition&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; is an open protocol that lets apps give context to LLMs in a standard way.&lt;/p&gt;
&lt;p&gt;Think of &lt;strong&gt;USB-C&lt;/strong&gt;. You plug in power, a display, or storage and it just works.&lt;/p&gt;
&lt;p&gt;MCP does the same for AI with data sources and tools.&lt;/p&gt;
&lt;p&gt;With MCP you can build agents and workflows without custom glue code.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How MCP Works (mental model)&lt;/h2&gt;
&lt;p&gt;At its core, MCP has &lt;strong&gt;three roles&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Host&lt;/strong&gt; → LLM applications that initiate connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt; → Connectors within the host application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server&lt;/strong&gt; → Services that provide context and capabilities&lt;/li&gt;
&lt;/ul&gt;

  MCP takes some inspiration from the Language Server Protocol, which
  standardizes how to add support for programming languages across a whole
  ecosystem of development tools. In a similar way, MCP standardizes how to
  integrate additional context and tools into the ecosystem of AI applications.

&lt;p&gt;The host embeds clients, and those clients connect to one or more servers.&lt;br /&gt;
Your VS Code could have a Playwright MCP server for browser automation and another MCP server for your docs — all running at the same time.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
  U&lt;span&gt;((User))&lt;/span&gt;
  U &lt;span&gt;--&amp;gt;&lt;/span&gt; H&lt;span&gt;[Host UI&amp;lt;br&amp;gt;Claude Desktop, VS Code/Claude Code]&lt;/span&gt;
  H &lt;span&gt;--&amp;gt;&lt;/span&gt; C1&lt;span&gt;[MCP Client 1]&lt;/span&gt;
  H &lt;span&gt;--&amp;gt;&lt;/span&gt; C2&lt;span&gt;[MCP Client 2]&lt;/span&gt;
  H &lt;span&gt;--&amp;gt;&lt;/span&gt; C3&lt;span&gt;[MCP Client 3]&lt;/span&gt;
  C1 &lt;span&gt;--&amp;gt;&lt;/span&gt; S1&lt;span&gt;[MCP Server A]&lt;/span&gt;
  C2 &lt;span&gt;--&amp;gt;&lt;/span&gt; S2&lt;span&gt;[MCP Server B]&lt;/span&gt;
  C3 &lt;span&gt;--&amp;gt;&lt;/span&gt; S3&lt;span&gt;[MCP Server C]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;How MCP Connects: Transports&lt;/h2&gt;
&lt;p&gt;MCP uses &lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt; for all messages and supports two main transport mechanisms:&lt;/p&gt;
&lt;p&gt;&amp;lt;GridCards&lt;br /&gt;
cards={[&lt;br /&gt;
{&lt;br /&gt;
title: “stdio (local)”,&lt;br /&gt;
icon: “📍”,&lt;br /&gt;
items: [&lt;br /&gt;
“Server runs as subprocess of the client”,&lt;br /&gt;
“Messages flow through stdin/stdout pipes”,&lt;br /&gt;
“No network latency - instant communication”,&lt;br /&gt;
“Perfect for local tools and dev environments”,&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
title: “Streamable HTTP (remote)”,&lt;br /&gt;
icon: “🌐”,&lt;br /&gt;
items: [&lt;br /&gt;
“Single HTTP endpoint for all operations”,&lt;br /&gt;
“POST for sending messages, GET for listening”,&lt;br /&gt;
“Server-Sent Events (SSE) for streaming”,&lt;br /&gt;
“Ideal for cloud-hosted MCP servers”,&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Messages are UTF-8 encoded JSON-RPC&lt;/li&gt;
&lt;li&gt;stdio uses newline-delimited JSON (one message per line)&lt;/li&gt;
&lt;li&gt;HTTP supports session management via &lt;code&gt;Mcp-Session-Id&lt;/code&gt; headers&lt;/li&gt;
&lt;li&gt;Both transports handle requests, responses, and notifications equally well&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The transport choice depends on your use case: stdio for local tools with minimal latency, HTTP for remote services that multiple clients can connect to.&lt;/p&gt;
&lt;h2&gt;What servers can expose&lt;/h2&gt;
&lt;p&gt;An MCP server can offer any combination of three capabilities:&lt;/p&gt;
&lt;h3&gt;Tools: Functions the AI can call&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Give AI ability to execute actions (check weather, query databases, solve math)&lt;/li&gt;
&lt;li&gt;Each tool describes what it does and what info it needs&lt;/li&gt;
&lt;li&gt;AI sends parameters → server runs function → returns results&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Simple calculator tool example&lt;/span&gt;
server&lt;span&gt;.&lt;/span&gt;&lt;span&gt;registerTool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;calculate&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Calculator&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Perform mathematical calculations&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    inputSchema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      operation&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;enum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;add&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;subtract&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;multiply&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;divide&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      a&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      b&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; operation&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;,&lt;/span&gt; b &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; result&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;switch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;operation&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;add&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        result &lt;span&gt;=&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;subtract&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        result &lt;span&gt;=&lt;/span&gt; a &lt;span&gt;-&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;multiply&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        result &lt;span&gt;=&lt;/span&gt; a &lt;span&gt;*&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;case&lt;/span&gt; &lt;span&gt;&quot;divide&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        result &lt;span&gt;=&lt;/span&gt; b &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; a &lt;span&gt;/&lt;/span&gt; b &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Error: Division by zero&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt;
          type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;a&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;operation&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;b&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;result&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Resources: Context and data&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;AI can read files, docs, database schemas&lt;/li&gt;
&lt;li&gt;Provides context before answering questions or using tools&lt;/li&gt;
&lt;li&gt;Supports change notifications when files update&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;server&lt;span&gt;.&lt;/span&gt;&lt;span&gt;registerResource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;app-config&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;config://application&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Application Configuration&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Current app settings and environment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    mimeType&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; uri &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    contents&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        uri&lt;span&gt;:&lt;/span&gt; uri&lt;span&gt;.&lt;/span&gt;href&lt;span&gt;,&lt;/span&gt;
        text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            environment&lt;span&gt;:&lt;/span&gt; process&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NODE_ENV&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            version&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            features&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
              darkMode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
              analytics&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
              beta&lt;span&gt;:&lt;/span&gt; process&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;BETA&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;true&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;2&lt;/span&gt;
        &lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prompts: Templates for interaction&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pre-made templates for common tasks (code review, data analysis)&lt;/li&gt;
&lt;li&gt;Exposed as slash commands or UI elements&lt;/li&gt;
&lt;li&gt;Makes repetitive workflows quick and consistent&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;server&lt;span&gt;.&lt;/span&gt;&lt;span&gt;registerPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;code-review&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Code Review&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Review code for quality and best practices&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    argsSchema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      language&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;enum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;javascript&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;typescript&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;python&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;go&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      code&lt;span&gt;:&lt;/span&gt; z&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      focus&lt;span&gt;:&lt;/span&gt; z
        &lt;span&gt;.&lt;/span&gt;&lt;span&gt;enum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;security&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;performance&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;readability&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;all&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;.&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;all&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; language&lt;span&gt;,&lt;/span&gt; code&lt;span&gt;,&lt;/span&gt; focus &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    messages&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
            &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Please review this &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;language&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; code focusing on &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;focus&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;```&quot;&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; language&lt;span&gt;,&lt;/span&gt;
            code&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;```&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;&quot;Provide feedback on:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            focus &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;all&quot;&lt;/span&gt;
              &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&quot;- Security issues\n- Performance optimizations\n- Code readability\n- Best practices&quot;&lt;/span&gt;
              &lt;span&gt;:&lt;/span&gt; focus &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;security&quot;&lt;/span&gt;
                &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&quot;- Potential security vulnerabilities\n- Input validation\n- Authentication/authorization issues&quot;&lt;/span&gt;
                &lt;span&gt;:&lt;/span&gt; focus &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;performance&quot;&lt;/span&gt;
                  &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&quot;- Time complexity\n- Memory usage\n- Potential optimizations&quot;&lt;/span&gt;
                  &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;- Variable naming\n- Code structure\n- Comments and documentation&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;\n&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What a Client can expose&lt;/h2&gt;
&lt;p&gt;An MCP client can provide capabilities that let servers interact with the world beyond their sandbox:&lt;/p&gt;
&lt;h3&gt;Roots: Filesystem boundaries&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Client tells server which directories it can access&lt;/li&gt;
&lt;li&gt;Creates secure sandbox (e.g., only your project folder)&lt;/li&gt;
&lt;li&gt;Prevents access to system files or other projects&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sampling: Nested LLM calls&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Servers can request AI completions through the client&lt;/li&gt;
&lt;li&gt;No API keys needed on server side&lt;/li&gt;
&lt;li&gt;Enables autonomous, agentic behaviors&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Elicitation: Asking users for input&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Servers request missing info from users via client UI&lt;/li&gt;
&lt;li&gt;Client handles forms and validation&lt;/li&gt;
&lt;li&gt;Users can accept, decline, or cancel requests&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example: How we can use MCPS in Vscode&lt;/h2&gt;
&lt;p&gt;Your &lt;code&gt;mcp.json&lt;/code&gt; could look like this:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;servers&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;playwright&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;gallery&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;npx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;@playwright/mcp@latest&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;stdio&quot;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;deepwiki&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;http&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;https://mcp.deepwiki.com/sse&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;gallery&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;playwright&lt;/strong&gt; → Runs &lt;code&gt;npx @playwright/mcp@latest&lt;/code&gt; locally over stdio for low-latency browser automation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deepwiki&lt;/strong&gt; → Connects over HTTP/SSE to &lt;code&gt;https://mcp.deepwiki.com/sse&lt;/code&gt; for live docs and codebase search&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gallery: true&lt;/strong&gt; → Makes them visible in tool pickers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What MCP is not&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Not a hosted service&lt;/strong&gt; — It is a protocol&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not a replacement&lt;/strong&gt; for your app logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not a magic fix&lt;/strong&gt; for every hallucination — It gives access to real tools and data&lt;/li&gt;
&lt;li&gt;You still need good prompts and good UX&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Simple example of your first MCP Server&lt;/h2&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;#!/usr/bin/env node&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; server &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;McpServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;echo-onefile&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  version&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

server&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;echo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;Echo back the provided text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    text&lt;span&gt;:&lt;/span&gt; z
      &lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;Text cannot be empty&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;.&lt;/span&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Text to echo back&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; text &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; text &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; transport &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;StdioServerTransport&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

server
  &lt;span&gt;.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;transport&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;.&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Echo MCP server listening on stdio&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;.&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    process&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example uses the official &lt;a href=&quot;https://modelcontextprotocol.io/docs/sdk&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MCP SDK for TypeScript&lt;/a&gt;, which provides type-safe abstractions for building MCP servers.&lt;/p&gt;
&lt;p&gt;The server exposes a single tool called “echo” that takes text input and returns it back. We’re using &lt;a href=&quot;https://zod.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Zod&lt;/a&gt; for runtime schema validation, ensuring the input matches our expected structure with proper type safety and clear error messages.&lt;/p&gt;
&lt;h2&gt;Simple MCP Client Example&lt;/h2&gt;
&lt;p&gt;Here’s how to connect to an MCP server and use its capabilities:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Create a client that connects to your MCP server&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;connectToServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Create transport - this runs your server as a subprocess&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; transport &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;StdioClientTransport&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    command&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;node&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    args&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;./echo-server.js&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Create and connect the client&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; client &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Client&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;my-mcp-client&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    version&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;connect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;transport&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; client&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Use the server&apos;s capabilities&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; client &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;connectToServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// List available tools&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; tools &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;listTools&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Available tools:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; tools&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Call a tool&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;callTool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;echo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    arguments&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Hello from MCP client!&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Tool result:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// List and read resources&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; resources &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;listResources&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; resource &lt;span&gt;of&lt;/span&gt; resources&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; content &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readResource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      uri&lt;span&gt;:&lt;/span&gt; resource&lt;span&gt;.&lt;/span&gt;uri&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Resource &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;resource&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Get and execute a prompt&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; prompts &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;listPrompts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;prompts&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; prompt &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; prompts&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;,&lt;/span&gt;
      arguments&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        code&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;console.log(&apos;test&apos;)&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        language&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;javascript&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Prompt messages:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; prompt&lt;span&gt;.&lt;/span&gt;messages&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Clean up&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Run the client&lt;/span&gt;
&lt;span&gt;useServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This client example shows how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connect to an MCP server using stdio transport&lt;/li&gt;
&lt;li&gt;List and call tools with arguments&lt;/li&gt;
&lt;li&gt;Read resources from the server&lt;/li&gt;
&lt;li&gt;Get and use prompt templates&lt;/li&gt;
&lt;li&gt;Properly close the connection when done&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Use it with Vscode&lt;/h2&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;servers&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;echo&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;gallery&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;stdio&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;command&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;node&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;--import&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;tsx&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;/absolute/path/echo-server.ts&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This was just my starter post for MCP to give an overview. I will write more blog posts that will go in depth about the different topics.&lt;/p&gt;

  If you need a TypeScript starter template for your next MCP server, you can
  use my
  [mcp-server-starter-ts](https://github.com/alexanderop/mcp-server-starter-ts)
  repository to get up and running quickly.

          </content:encoded><category>mcp</category><category>typescript</category><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How VueUse Solves SSR Window Errors in Vue Applications</title><link>https://alexop.dev/posts/how-vueuse-solves-ssr-window-errors-vue-applications/</link><guid isPermaLink="true">https://alexop.dev/posts/how-vueuse-solves-ssr-window-errors-vue-applications/</guid><description>Discover how VueUse solves SSR issues with browser APIs and keeps your Vue composables safe from &apos;window is not defined&apos; errors.</description><pubDate>Mon, 14 Jul 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I am a big fan of &lt;a href=&quot;https://vueuse.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VueUse&lt;/a&gt;. Every time I browse the docs I discover a new utility that saves me hours of work. Yet VueUse does more than offer nice functions. It also keeps your code safe when you mix client-side JavaScript with server-side rendering (SSR). In this post I show the typical &lt;strong&gt;“&lt;code&gt;window&lt;/code&gt; is not defined”&lt;/strong&gt; problem, explain why it happens, and walk through the simple tricks VueUse uses to avoid it.&lt;/p&gt;
&lt;h2&gt;The Usual Pain: &lt;code&gt;window&lt;/code&gt; Fails on the Server&lt;/h2&gt;
&lt;p&gt;When you run a Vue app with SSR, Vue executes in &lt;strong&gt;two&lt;/strong&gt; places:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Server (Node.js)&lt;/strong&gt; – It renders HTML so the user sees a fast first screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser (JavaScript runtime in the user’s tab)&lt;/strong&gt; – It takes over and adds interactivity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The server uses &lt;strong&gt;Node.js&lt;/strong&gt;, which has &lt;strong&gt;no browser objects&lt;/strong&gt; like &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, or &lt;code&gt;navigator&lt;/code&gt;. The browser has them. If code that needs &lt;code&gt;window&lt;/code&gt; runs on the server, Node.js throws an error and the page render breaks.&lt;/p&gt;
&lt;h3&gt;Diagram: How SSR Works&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; B as Browser
    &lt;span&gt;participant&lt;/span&gt; S as Server &lt;span&gt;(Node.js)&lt;/span&gt;
    B&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; Request page
    S&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;S&lt;span&gt;:&lt;/span&gt; Run Vue code&amp;lt;br/&amp;gt;&lt;span&gt;(no window)&lt;/span&gt;
    S&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Send HTML
    B&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Hydrate app&amp;lt;br/&amp;gt;&lt;span&gt;(has window)&lt;/span&gt;
    &lt;span&gt;Note over&lt;/span&gt; S,B&lt;span&gt;:&lt;/span&gt; If Vue code touches &amp;lt;br/&amp;gt;&lt;span&gt;&quot;window&quot;&lt;/span&gt; on the server, SSR crashes
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Node.js vs Browser: Two Different Worlds&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Node.js on the server&lt;/th&gt;
&lt;th&gt;Browser in the tab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ not defined&lt;/td&gt;
&lt;td&gt;✅ defined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navigator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM access&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goal&lt;/td&gt;
&lt;td&gt;Render HTML fast&lt;/td&gt;
&lt;td&gt;Add interactivity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A Vue &lt;em&gt;composable&lt;/em&gt; that reads the mouse position or listens to scroll events needs those browser objects. It must &lt;strong&gt;not&lt;/strong&gt; run while the server renders.&lt;/p&gt;
&lt;h2&gt;How VueUse Solves the Problem&lt;/h2&gt;
&lt;p&gt;VueUse uses three small patterns: a &lt;strong&gt;client check&lt;/strong&gt;, &lt;strong&gt;safe defaults&lt;/strong&gt;, and an &lt;strong&gt;SSR guard&lt;/strong&gt; inside each composable.&lt;/p&gt;
&lt;h3&gt;1. One-Line Client Check&lt;/h3&gt;
&lt;p&gt;VueUse first asks, “Are we in the browser?” It does that in &lt;a href=&quot;https://github.com/vueuse/vueuse/blob/main/packages/shared/utils/is.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;is.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; isClient &lt;span&gt;=&lt;/span&gt;
  &lt;span&gt;typeof&lt;/span&gt; window &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; document &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&quot;undefined&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Diagram&lt;/h4&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
  Start &lt;span&gt;--&amp;gt;&lt;/span&gt; Test&lt;span&gt;{window exists?}&lt;/span&gt;
  Test &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;yes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Client&lt;span&gt;[isClient = true]&lt;/span&gt;
  Test &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;no&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Server&lt;span&gt;[isClient = false]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Safe Defaults for Browser Objects&lt;/h3&gt;
&lt;p&gt;Instead of making &lt;em&gt;you&lt;/em&gt; write &lt;code&gt;if (isClient)&lt;/code&gt; checks, VueUse exports harmless fallbacks from &lt;a href=&quot;https://github.com/vueuse/vueuse/blob/main/packages/core/_configurable.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;_configurable.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultWindow &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; window &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultDocument &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;document &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; defaultNavigator &lt;span&gt;=&lt;/span&gt; isClient &lt;span&gt;?&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;navigator &lt;span&gt;:&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the server these constants are &lt;code&gt;undefined&lt;/code&gt;. That value is safe to read, so nothing crashes.&lt;/p&gt;
&lt;h4&gt;Diagram&lt;/h4&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
  Check&lt;span&gt;[isClient?]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|true|&lt;/span&gt; Real&lt;span&gt;[Return real window]&lt;/span&gt;
  Check &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|false|&lt;/span&gt; Undef&lt;span&gt;[Return undefined]&lt;/span&gt;
  Real &lt;span&gt;--&amp;gt;&lt;/span&gt; Compose&lt;span&gt;[Composable receives safe value]&lt;/span&gt;
  Undef &lt;span&gt;--&amp;gt;&lt;/span&gt; Compose
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. The SSR Guard Inside Every Composable&lt;/h3&gt;
&lt;p&gt;Each composable that might touch the DOM adds a simple guard. Example: &lt;a href=&quot;https://github.com/vueuse/vueuse/blob/main/packages/core/onElementRemoval/index.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;onElementRemoval&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;onElementRemoval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;options&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; window &lt;span&gt;=&lt;/span&gt; defaultWindow &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;window&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;// server path&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// no-op&lt;/span&gt;

  &lt;span&gt;// browser logic goes here&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;window&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, the function returns a no-op and exits. The server render keeps going without errors.&lt;/p&gt;
&lt;h4&gt;Diagram&lt;/h4&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
  Run&lt;span&gt;[Composable starts]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; IsWin&lt;span&gt;{defaultWindow ?}&lt;/span&gt;
  IsWin &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;no&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Noop&lt;span&gt;[Return empty function]&lt;/span&gt;
  IsWin &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;yes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Logic&lt;span&gt;[Run browser code]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Extra Safety with &lt;code&gt;useSupported&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Sometimes you &lt;strong&gt;are&lt;/strong&gt; in the browser, but the user’s browser lacks a feature. VueUse offers &lt;a href=&quot;https://github.com/vueuse/vueuse/blob/main/packages/core/useSupported/index.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;useSupported&lt;/code&gt;&lt;/a&gt; to check that:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useSupported&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isMounted &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isMounted&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// make it reactive&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;Boolean&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Example: &lt;code&gt;useEyeDropper&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;useEyeDropper&lt;/code&gt; checks both SSR and feature support (see the full file &lt;a href=&quot;https://github.com/vueuse/vueuse/blob/main/packages/core/useEyeDropper/index.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useEyeDropper&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isSupported &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useSupported&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; window &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;&quot;EyeDropper&quot;&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; window
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;isSupported&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// safe exit&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; eyeDropper &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;window &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EyeDropper&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; eyeDropper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isSupported&lt;span&gt;,&lt;/span&gt; open &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrap-Up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt; renders HTML but lacks browser globals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VueUse&lt;/strong&gt; avoids crashes with three steps:
&lt;ol&gt;
&lt;li&gt;A single &lt;strong&gt;&lt;code&gt;isClient&lt;/code&gt;&lt;/strong&gt; flag tells where the code runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe defaults&lt;/strong&gt; turn &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, and &lt;code&gt;navigator&lt;/code&gt; into &lt;code&gt;undefined&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;Every composable adds a quick &lt;strong&gt;SSR guard&lt;/strong&gt; that eliminates environment concerns.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of this design you can import any VueUse composable, even ones that touch the DOM, and trust it to work in SSR without extra code.&lt;/p&gt;
&lt;h3&gt;Learn More&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VueUse guidelines that inspired these patterns: &lt;a href=&quot;https://vueuse.org/guidelines&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://vueuse.org/guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Full VueUse repository: &lt;a href=&quot;https://github.com/vueuse/vueuse&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/vueuse/vueuse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Mastering GraphQL Fragments in Vue 3: Component-Driven Data Fetching</title><link>https://alexop.dev/posts/mastering-graphql-fragments-vue3-component-driven-data-fetching/</link><guid isPermaLink="true">https://alexop.dev/posts/mastering-graphql-fragments-vue3-component-driven-data-fetching/</guid><description>Part 3 of the Vue 3 + GraphQL series: Learn how to use GraphQL fragments with fragment masking to create truly component-driven data fetching in Vue 3.</description><pubDate>Sun, 06 Jul 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[&quot;❌ Traditional Approach&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A1&lt;span&gt;[&quot;Monolithic Queries&quot;]&lt;/span&gt;
    A1 &lt;span&gt;--&amp;gt;&lt;/span&gt; A2&lt;span&gt;[&quot;Over-fetching&quot;]&lt;/span&gt;
    A1 &lt;span&gt;--&amp;gt;&lt;/span&gt; A3&lt;span&gt;[&quot;Tight Coupling&quot;]&lt;/span&gt;
    A1 &lt;span&gt;--&amp;gt;&lt;/span&gt; A4&lt;span&gt;[&quot;Implicit Dependencies&quot;]&lt;/span&gt;

    B&lt;span&gt;[&quot;✅ Fragment Masking&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B1&lt;span&gt;[&quot;Component-Owned Data&quot;]&lt;/span&gt;
    B1 &lt;span&gt;--&amp;gt;&lt;/span&gt; B2&lt;span&gt;[&quot;Type Safety&quot;]&lt;/span&gt;
    B1 &lt;span&gt;--&amp;gt;&lt;/span&gt; B3&lt;span&gt;[&quot;Data Encapsulation&quot;]&lt;/span&gt;
    B1 &lt;span&gt;--&amp;gt;&lt;/span&gt; B4&lt;span&gt;[&quot;Safe Refactoring&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why Fragments Are a Game-Changer&lt;/h2&gt;
&lt;p&gt;In Part 2, we achieved type safety with GraphQL Code Generator. But our queries are still monolithic—each component doesn’t declare its own data needs. This creates several problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Over-fetching&lt;/strong&gt;: Parent components request data their children might not need&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Under-fetching&lt;/strong&gt;: Adding a field means hunting down every query using that type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tight coupling&lt;/strong&gt;: Components depend on their parents to provide the right data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit dependencies&lt;/strong&gt;: Parent components can accidentally rely on data from child fragments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brittle refactoring&lt;/strong&gt;: Changing a component’s data needs can break unrelated components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Enter GraphQL fragments with &lt;strong&gt;fragment masking&lt;/strong&gt;—the pattern that Relay popularized and that Apollo Client 3.12 has made even more powerful. This transforms how we think about data fetching by providing &lt;strong&gt;true data encapsulation&lt;/strong&gt; at the component level.&lt;/p&gt;
&lt;h2&gt;What Are GraphQL Fragments?&lt;/h2&gt;
&lt;p&gt;GraphQL fragments are &lt;strong&gt;reusable units of fields&lt;/strong&gt; that components can declare for themselves. But they’re more than just field groupings—when combined with fragment masking, they provide &lt;strong&gt;data access control&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;fragment&lt;/span&gt; &lt;span&gt;CountryBasicInfo&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;Country&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;code&lt;/span&gt;
  &lt;span&gt;name&lt;/span&gt;
  &lt;span&gt;emoji&lt;/span&gt;
  &lt;span&gt;capital&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fragment masking&lt;/strong&gt; is the key innovation that makes fragments truly powerful. It ensures that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data is encapsulated&lt;/strong&gt;: Only the component that defines a fragment can access its fields&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependencies are explicit&lt;/strong&gt;: Components can’t accidentally rely on data from other fragments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactoring is safe&lt;/strong&gt;: Changing a fragment won’t break unrelated components&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type safety is enforced&lt;/strong&gt;: TypeScript prevents accessing fields you didn’t request&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Understanding Fragments Through the Spread Operator&lt;/h2&gt;
&lt;p&gt;If you’re familiar with JavaScript’s spread operator, fragments work exactly the same way:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;// JavaScript objects&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; basicInfo &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;code&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;US&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;United States&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; fullCountry &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;basicInfo&lt;span&gt;,&lt;/span&gt; &lt;span&gt;capital&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Washington D.C.&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;# GraphQL fragments&lt;/span&gt;
&lt;span&gt;fragment&lt;/span&gt; &lt;span&gt;CountryBasicInfo&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;Country&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;code&lt;/span&gt;
  &lt;span&gt;name&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;query&lt;/span&gt; &lt;span&gt;GetCountryDetails&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;country&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;US&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;&lt;span&gt;CountryBasicInfo&lt;/span&gt; &lt;span&gt;# Spread fragment fields&lt;/span&gt;
    &lt;span&gt;capital&lt;/span&gt; &lt;span&gt;# Add extra fields&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fragment masking&lt;/strong&gt; takes this further by ensuring components can only access the data they explicitly request—pioneered by &lt;strong&gt;Relay&lt;/strong&gt; and now enhanced in &lt;strong&gt;Apollo Client 3.12&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Step 1: Enable Fragment Masking&lt;/h2&gt;
&lt;p&gt;Ensure your &lt;code&gt;codegen.ts&lt;/code&gt; uses the client preset (from Part 2):&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; config&lt;span&gt;:&lt;/span&gt; CodegenConfig &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  overwrite&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;https://countries.trevorblades.com/graphql&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  documents&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;src/**/*.vue&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;src/**/*.graphql&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  generates&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;src/gql/&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      preset&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;client&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      config&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; useTypeImports&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FragmentType&amp;lt;T&amp;gt;&lt;/code&gt;: Masked fragment types for props&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useFragment()&lt;/code&gt;: Function to unmask fragment data&lt;/li&gt;
&lt;li&gt;Type safety to prevent accessing non-fragment fields&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 2: Your First Fragment with Masking&lt;/h2&gt;
&lt;p&gt;Let’s create a &lt;code&gt;CountryCard&lt;/code&gt; component that declares its own data requirements:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// Define what data this component needs
const CountryCard_CountryFragment = graphql(`
  fragment CountryCard_CountryFragment on Country {
    code
    name
    emoji
    capital
    phone
    currency
  }
`);

// Props accept a masked fragment type
interface Props {
  country: FragmentType&amp;lt;typeof CountryCard_CountryFragment&amp;gt;;
}

const props = defineProps&amp;lt;Props&amp;gt;();

// Unmask to access the actual data (reactive for Vue)
const country = computed(() =&amp;gt;
  useFragment(CountryCard_CountryFragment, props.country)
);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;country-card&quot;&amp;gt;
    &amp;lt;h3&amp;gt;{{ country.emoji }} {{ country.name }}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;Capital: {{ country.capital }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Phone: +{{ country.phone }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Currency: {{ country.currency }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Understanding Fragment Masking: The Key to Data Isolation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Fragment masking&lt;/strong&gt; is the core concept that makes this pattern so powerful. It’s not just about code organization—it’s about &lt;strong&gt;data access control and encapsulation&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;What Fragment Masking Actually Does&lt;/h3&gt;
&lt;p&gt;Think of fragment masking like &lt;strong&gt;access control in programming languages&lt;/strong&gt;. Just as a module can have private and public methods, fragment masking controls which components can access which pieces of data.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Without fragment masking (traditional approach)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;GET_COUNTRIES&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; countries &lt;span&gt;=&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;?.&lt;/span&gt;countries &lt;span&gt;||&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ❌ Parent can access ANY field from the query&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Works&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;capital&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Works&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;currency&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With fragment masking enabled:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// ✅ Parent component CANNOT access fragment fields&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; name &lt;span&gt;=&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;?.&lt;/span&gt;countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// TypeScript error!&lt;/span&gt;

&lt;span&gt;// ✅ Only CountryCard can access its fragment data&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; country &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFragment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;CountryCard_CountryFragment&lt;span&gt;,&lt;/span&gt; props&lt;span&gt;.&lt;/span&gt;country&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;country&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Works!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Power of Data Encapsulation&lt;/h3&gt;
&lt;p&gt;Fragment masking provides &lt;strong&gt;true data encapsulation&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prevents Implicit Dependencies&lt;/strong&gt;: Parent components can’t accidentally rely on data their children need&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catches Breaking Changes Early&lt;/strong&gt;: If a child component removes a field, the parent can’t access it anymore&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enforces Component Boundaries&lt;/strong&gt;: Each component owns its data requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enables Safe Refactoring&lt;/strong&gt;: Change a fragment without breaking unrelated components&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;Without fragment masking, parent components can accidentally depend on child fragment data. When the child removes a field, the parent breaks at runtime. With fragment masking, TypeScript catches this at compile time.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Parent can only access explicitly requested fields&lt;/span&gt;
countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// ✅ Works (parent requested this)&lt;/span&gt;
countries&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// ❌ TypeScript error (only in fragment)&lt;/span&gt;

&lt;span&gt;// Child components unmask their fragment data&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; country &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFragment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;CountryCard_CountryFragment&lt;span&gt;,&lt;/span&gt; props&lt;span&gt;.&lt;/span&gt;country&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
country&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// ✅ Works (component owns this fragment)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📝 Vue Reactivity Note&lt;/strong&gt;: Always wrap &lt;code&gt;useFragment&lt;/code&gt; in a &lt;code&gt;computed()&lt;/code&gt; for Vue reactivity. This ensures the component updates when fragment data changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 3: Parent Component Uses the Fragment&lt;/h2&gt;
&lt;p&gt;Now the parent component includes the child’s fragment in its query:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const COUNTRIES_WITH_DETAILS_QUERY = graphql(`
  query CountriesWithDetails {
    countries {
      code
      # Parent can add its own fields
      region
      # Child component&apos;s fragment
      ...CountryCard_CountryFragment
    }
  }
`);

const { result, loading, error } = useQuery(COUNTRIES_WITH_DETAILS_QUERY);

// Parent can access its own fields
const countriesByRegion = computed(() =&amp;gt; {
  if (!result.value?.countries) return {};

  return result.value.countries.reduce(
    (acc, country) =&amp;gt; {
      const region = country.region;
      if (!acc[region]) acc[region] = [];
      acc[region].push(country);
      return acc;
    },
    {} as Record&amp;lt;string, typeof result.value.countries&amp;gt;
  );
});

// But parent CANNOT access fragment fields:
// const name = result.value?.countries[0].name ❌ TypeScript error!
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;countries-container&quot;&amp;gt;
    &amp;lt;div v-if=&quot;loading&quot;&amp;gt;Loading countries...&amp;lt;/div&amp;gt;
    &amp;lt;div v-else-if=&quot;error&quot;&amp;gt;Error: {{ error.message }}&amp;lt;/div&amp;gt;
    &amp;lt;div v-else class=&quot;countries-by-region&quot;&amp;gt;
      &amp;lt;div
        v-for=&quot;(countries, region) in countriesByRegion&quot;
        :key=&quot;region&quot;
        class=&quot;region-section&quot;
      &amp;gt;
        &amp;lt;h2&amp;gt;{{ region }}&amp;lt;/h2&amp;gt;
        &amp;lt;div class=&quot;country-grid&quot;&amp;gt;
          &amp;lt;CountryCard
            v-for=&quot;country in countries&quot;
            :key=&quot;country.code&quot;
            :country=&quot;country&quot;
          /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Magic of Fragment Masking&lt;/h2&gt;
&lt;p&gt;Here’s what just happened:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TB
    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;GraphQL Query Result&quot;&lt;/span&gt;
        QR&lt;span&gt;[&quot;countries: Country[]&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Parent Component&quot;&lt;/span&gt;
        PC&lt;span&gt;[&quot;Parent can access:&amp;lt;br/&amp;gt;• countries[].code&amp;lt;br/&amp;gt;• countries[].region&amp;lt;br/&amp;gt;❌ countries[].name&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Child Component&quot;&lt;/span&gt;
        CC&lt;span&gt;[&quot;CountryCard receives:&amp;lt;br/&amp;gt;Masked Fragment Data&quot;]&lt;/span&gt;
        UF&lt;span&gt;[&quot;useFragment() unmasks:&amp;lt;br/&amp;gt;• code&amp;lt;br/&amp;gt;• name&amp;lt;br/&amp;gt;• emoji&amp;lt;br/&amp;gt;• capital&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    QR &lt;span&gt;--&amp;gt;&lt;/span&gt; PC
    QR &lt;span&gt;--&amp;gt;&lt;/span&gt; CC
    CC &lt;span&gt;--&amp;gt;&lt;/span&gt; UF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parent component &lt;strong&gt;cannot access&lt;/strong&gt; fields from &lt;code&gt;CountryCard_Fragment&lt;/code&gt;—they’re masked! Only &lt;code&gt;CountryCard&lt;/code&gt; can unmask and use that data.&lt;/p&gt;
&lt;h2&gt;Step 4: Nested Fragments&lt;/h2&gt;
&lt;p&gt;Fragments can include other fragments, creating a hierarchy:&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;# Basic fragment&lt;/span&gt;
&lt;span&gt;fragment&lt;/span&gt; &lt;span&gt;LanguageItem_LanguageFragment&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;Language&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;code&lt;/span&gt;
  &lt;span&gt;name&lt;/span&gt;
  &lt;span&gt;native&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;# Fragment that uses other fragments&lt;/span&gt;
&lt;span&gt;fragment&lt;/span&gt; &lt;span&gt;CountryWithLanguages_CountryFragment&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;Country&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;code&lt;/span&gt;
  &lt;span&gt;name&lt;/span&gt;
  &lt;span&gt;emoji&lt;/span&gt;
  &lt;span&gt;languages&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;&lt;span&gt;LanguageItem_LanguageFragment&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Child components use their own fragments:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- LanguageItem.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const LanguageItem_LanguageFragment = graphql(`
  fragment LanguageItem_LanguageFragment on Language {
    code
    name
    native
  }
`);

interface Props {
  language: FragmentType&amp;lt;typeof LanguageItem_LanguageFragment&amp;gt;;
}

const props = defineProps&amp;lt;Props&amp;gt;();
const language = computed(() =&amp;gt;
  useFragment(LanguageItem_LanguageFragment, props.language)
);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;language-item&quot;&amp;gt;
    &amp;lt;span&amp;gt;{{ language.name }} ({{ language.code }})&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fragment Dependency Management&lt;/h2&gt;
&lt;p&gt;Notice how the query automatically includes all nested fragments:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; LR
    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Components&quot;&lt;/span&gt;
        A&lt;span&gt;[CountryDetailPage]&lt;/span&gt;
        B&lt;span&gt;[CountryWithLanguages.vue]&lt;/span&gt;
        C&lt;span&gt;[LanguageItem.vue]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; &lt;span&gt;&quot;Their Fragments&quot;&lt;/span&gt;
        A1&lt;span&gt;[Page Fields:&amp;lt;br/&amp;gt;code]&lt;/span&gt;
        B1&lt;span&gt;[Country Fragment:&amp;lt;br/&amp;gt;code, name, emoji&amp;lt;br/&amp;gt;languages]&lt;/span&gt;
        C1&lt;span&gt;[Language Fragment:&amp;lt;br/&amp;gt;code, name, native]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    A &lt;span&gt;-.-&amp;gt;&lt;/span&gt; A1
    B &lt;span&gt;-.-&amp;gt;&lt;/span&gt; B1
    C &lt;span&gt;-.-&amp;gt;&lt;/span&gt; C1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 5: Conditional Fragments&lt;/h2&gt;
&lt;p&gt;Use GraphQL directives to conditionally include fragments:&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;query&lt;/span&gt; &lt;span&gt;CountriesConditional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;$includeLanguages&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Boolean&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;countries&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;code&lt;/span&gt;
    &lt;span&gt;name&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;&lt;span&gt;CountryDetails_CountryFragment&lt;/span&gt; &lt;span&gt;@include&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;$includeLanguages&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This enables dynamic data loading based on user interactions or application state.&lt;/p&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;Key Guidelines&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Naming&lt;/strong&gt;: Use &lt;code&gt;ComponentName_TypeNameFragment&lt;/code&gt; convention&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue Reactivity&lt;/strong&gt;: Always wrap &lt;code&gt;useFragment&lt;/code&gt; in &lt;code&gt;computed()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;: Use &lt;code&gt;FragmentType&amp;lt;typeof MyFragment&amp;gt;&lt;/code&gt; for props&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization&lt;/strong&gt;: Colocate fragments with components&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// ✅ Good naming and Vue reactivity&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; CountryCard_CountryFragment &lt;span&gt;=&lt;/span&gt; &lt;span&gt;graphql&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Props&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  country&lt;span&gt;:&lt;/span&gt; FragmentType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; CountryCard_CountryFragment&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; country &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
  &lt;span&gt;useFragment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;CountryCard_CountryFragment&lt;span&gt;,&lt;/span&gt; props&lt;span&gt;.&lt;/span&gt;country&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance Benefits&lt;/h2&gt;
&lt;p&gt;Fragments aren’t just about developer experience - they provide concrete performance and maintainability benefits:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[&quot;❌ Multiple Queries&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; A1&lt;span&gt;[&quot;3 Network Requests&quot;]&lt;/span&gt;
    A1 &lt;span&gt;--&amp;gt;&lt;/span&gt; A2&lt;span&gt;[&quot;Duplicate Data Fetching&quot;]&lt;/span&gt;
    A2 &lt;span&gt;--&amp;gt;&lt;/span&gt; A3&lt;span&gt;[&quot;Larger Bundle Size&quot;]&lt;/span&gt;

    B&lt;span&gt;[&quot;✅ Fragment Composition&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B1&lt;span&gt;[&quot;Single Network Request&quot;]&lt;/span&gt;
    B1 &lt;span&gt;--&amp;gt;&lt;/span&gt; B2&lt;span&gt;[&quot;Optimized Payload&quot;]&lt;/span&gt;
    B2 &lt;span&gt;--&amp;gt;&lt;/span&gt; B3&lt;span&gt;[&quot;Better Performance&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;GraphQL fragments with fragment masking enable &lt;strong&gt;component-driven data fetching&lt;/strong&gt; in Vue 3:&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Type Safety&lt;/strong&gt;: Components can only access their declared fields&lt;br /&gt;
✅ &lt;strong&gt;True Modularity&lt;/strong&gt;: Each component declares its exact data needs&lt;br /&gt;
✅ &lt;strong&gt;Better Performance&lt;/strong&gt;: Load only the data you need&lt;br /&gt;
✅ &lt;strong&gt;Maintainable Code&lt;/strong&gt;: Changes to fragments don’t break unrelated components&lt;/p&gt;
&lt;h2&gt;Migration Checklist&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Start with leaf components (no children)&lt;/li&gt;
&lt;li&gt;Always use &lt;code&gt;computed()&lt;/code&gt; with &lt;code&gt;useFragment&lt;/code&gt; for Vue reactivity&lt;/li&gt;
&lt;li&gt;Update TypeScript interfaces to use &lt;code&gt;FragmentType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm run codegen&lt;/code&gt; after fragment changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What’s Next?&lt;/h2&gt;
&lt;p&gt;This is Part 3 of our Vue 3 + GraphQL series:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Part 1&lt;/strong&gt;: Setting up Apollo Client with Vue 3&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2&lt;/strong&gt;: Type-safe queries with GraphQL Code Generator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3&lt;/strong&gt;: Advanced fragments and component-driven data fetching (current)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4&lt;/strong&gt;: GraphQL Caching Strategies in Vue 3 (coming next!)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Other Fragment Use Cases&lt;/h2&gt;
&lt;p&gt;Beyond component-driven data fetching, fragments offer additional powerful patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fragments on Unions and Interfaces&lt;/strong&gt;: Handle polymorphic types with inline fragments (&lt;code&gt;... on Type&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch Operations&lt;/strong&gt;: Share field selections between queries, mutations, and subscriptions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema Documentation&lt;/strong&gt;: Use fragments as living documentation of data shapes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Create fragment mocks for isolated component testing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fragment Composition&lt;/strong&gt;: Build complex queries from simple, reusable pieces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more advanced fragment patterns, see the &lt;a href=&quot;https://apollo.vuejs.org/guide-composable/fragments&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Apollo Fragments documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Source Code&lt;/h2&gt;
&lt;p&gt;Find the full demo for this series here: &lt;a href=&quot;https://github.com/alexanderop/vue-graphql-simple-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;example&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The code for this tutorial is on the &lt;code&gt;part3&lt;/code&gt; branch.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;git&lt;/span&gt; clone https://github.com/alexanderop/vue-graphql-simple-example.git
&lt;span&gt;cd&lt;/span&gt; vue-graphql-simple-example
&lt;span&gt;git&lt;/span&gt; checkout part3
&lt;/code&gt;&lt;/pre&gt;

          </content:encoded><category>graphql</category><category>vue</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Use Claude Code for Doing SEO Audits</title><link>https://alexop.dev/posts/how-i-use-claude-code-for-doing-seo-audits/</link><guid isPermaLink="true">https://alexop.dev/posts/how-i-use-claude-code-for-doing-seo-audits/</guid><description>Learn how to leverage Claude Code with Puppeteer MCP to perform comprehensive SEO audits in minutes, complete with automated analysis and actionable reports.</description><pubDate>Thu, 26 Jun 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I’m building a Nuxt blog starter called &lt;a href=&quot;https://github.com/alexanderop/NuxtPapier&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;NuxtPapier&lt;/a&gt;. Like any developer who wants their project to show up in search results, I needed to make sure it works well with search engines. Manual SEO audits take too much time and I often miss things, so I used Claude Code with Puppeteer MCP to do this automatically.&lt;/p&gt;
&lt;h2&gt;What is Claude Code?&lt;/h2&gt;
&lt;p&gt;Claude Code is Anthropic’s official command-line tool that brings AI help right into your coding workflow. Think of it like having a skilled developer sitting next to you, ready to help with any coding task. What makes it really powerful for SEO audits is that it can use MCP (Model Context Protocol) tools. For a deeper look at what Claude Code can do, see my [[understanding-claude-code-full-stack|overview of the full Claude Code feature stack]].&lt;/p&gt;
&lt;h2&gt;Enter Puppeteer MCP&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is MCP?&lt;/strong&gt;&lt;br /&gt;
According to &lt;a href=&quot;https://www.anthropic.com/news/model-context-protocol&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Anthropic’s official documentation&lt;/a&gt;, the Model Context Protocol (MCP) is “an open standard that enables developers to build secure, two-way connections between their data sources and AI-powered tools.” It provides a universal interface for connecting AI systems with external tools, systems, and data sources, replacing fragmented integrations with a single protocol.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MCP lets Claude Code connect with outside tools and services. I was using the &lt;a href=&quot;https://github.com/merajmehrabi/puppeteer-mcp-server&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Puppeteer MCP server&lt;/a&gt; which gives Claude Code the power to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Control a real web browser with code&lt;/li&gt;
&lt;li&gt;Go to any website&lt;/li&gt;
&lt;li&gt;Take screenshots&lt;/li&gt;
&lt;li&gt;Pull data from web pages&lt;/li&gt;
&lt;li&gt;Check page structure and speed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means Claude Code can visit your website, look at it the same way search engines do, and give you detailed insights.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Claude Code SEO Audit Workflow
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;graph&lt;/span&gt; LR
    A&lt;span&gt;[Claude Code CLI]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Puppeteer MCP]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Real Browser Instance]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Your Website]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Extract Data]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[SEO Analysis]&lt;/span&gt;
    F &lt;span&gt;--&amp;gt;&lt;/span&gt; G&lt;span&gt;[Comprehensive Report]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;My SEO Audit Prompt&lt;/h2&gt;
&lt;p&gt;Here’s the exact prompt I used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You are a SEO expert your name is John Wick. I gave you so much money now you have to do a SEO audit for this blog. When you are done write down everything into a own markdown file so I can solve the problems later. Use Puppeteer MCP, the server is already running.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple and direct. It uses Claude Code’s ability to act like an expert while using the Puppeteer tools.&lt;/p&gt;
&lt;p&gt;Claude Code quickly responded with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I’ll do a complete SEO audit for your blog, Senpai Alex. Let me start by making a plan and then checking your site using Puppeteer.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The Automated Process&lt;/h2&gt;
&lt;p&gt;One of the coolest things is how Claude Code organizes itself by making todos. It quickly made its own plan:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code’s Self-Generated Todo List:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Navigate to the homepage and take screenshots&lt;/li&gt;
&lt;li&gt;[ ] Analyze meta tags and SEO elements&lt;/li&gt;
&lt;li&gt;[ ] Check blog post pages for optimization&lt;/li&gt;
&lt;li&gt;[ ] Evaluate page structure and heading hierarchy&lt;/li&gt;
&lt;li&gt;[ ] Analyze site performance and loading times&lt;/li&gt;
&lt;li&gt;[ ] Check mobile responsiveness&lt;/li&gt;
&lt;li&gt;[ ] Review robots.txt and sitemap availability&lt;/li&gt;
&lt;li&gt;[ ] Document all findings in a markdown report&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What amazed me was how Claude Code worked through each todo step by step, using Puppeteer to get real data from my site - checking meta tags, measuring how fast pages load, checking structured data, and even taking mobile screenshots. It checked off each item as it finished them.&lt;/p&gt;
&lt;p&gt;While working, Claude Code told me what it found:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I found a big SEO problem - the robots meta tag says ‘noindex, nofollow’ which stops search engines from finding your site. This is a major issue.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Automated SEO Analysis Process
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; CC as Claude Code
    &lt;span&gt;participant&lt;/span&gt; P as Puppeteer
    &lt;span&gt;participant&lt;/span&gt; B as Browser
    &lt;span&gt;participant&lt;/span&gt; W as Website

    CC&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;CC&lt;span&gt;:&lt;/span&gt; Generate Todo List
    CC&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;P&lt;span&gt;:&lt;/span&gt; Initialize browser session
    P&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Launch browser

    &lt;span&gt;loop&lt;/span&gt; For each SEO check
        CC&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;P&lt;span&gt;:&lt;/span&gt; Navigate to page
        P&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Go to URL
        B&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;W&lt;span&gt;:&lt;/span&gt; Load page
        W&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;B&lt;span&gt;:&lt;/span&gt; Return content
        B&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;P&lt;span&gt;:&lt;/span&gt; Extract data
        P&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;CC&lt;span&gt;:&lt;/span&gt; Return analysis data
        CC&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;CC&lt;span&gt;:&lt;/span&gt; ✓ Mark todo complete
    &lt;span&gt;end&lt;/span&gt;

    CC&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;CC&lt;span&gt;:&lt;/span&gt; Generate final report
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Report&lt;/h2&gt;
&lt;p&gt;Claude Code created a detailed SEO audit report that I could actually use. It included:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Critical Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My site had &lt;code&gt;noindex, nofollow&lt;/code&gt; meta tags (development setting blocking all search engines!)&lt;/li&gt;
&lt;li&gt;robots.txt was blocking all crawlers&lt;/li&gt;
&lt;li&gt;Duplicate H1 tags on blog posts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Positive Findings:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proper structured data implementation&lt;/li&gt;
&lt;li&gt;Good mobile responsiveness&lt;/li&gt;
&lt;li&gt;Fast page load times (200ms)&lt;/li&gt;
&lt;li&gt;Clean URL structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What to Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick fixes with code examples&lt;/li&gt;
&lt;li&gt;Short-term improvements ranked by importance&lt;/li&gt;
&lt;li&gt;Long-term improvement plans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The report even told me exactly which files to change and what code to fix.&lt;/p&gt;
&lt;p&gt;Claude Code wrapped up with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“The audit found big blocking problems - search engines can’t see your site because of noindex, nofollow meta tags and development robots.txt settings. But the good news is your blog has great technical basics with proper structured data, mobile-friendly design, and fast loading.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; SEO Audit Report Structure
&lt;span&gt;---&lt;/span&gt;
mindmap
  root&lt;span&gt;((SEO Audit Report))&lt;/span&gt;
    Critical Issues
      noindex/nofollow tags
      Blocked robots.txt
      Duplicate H1 tags
    Positive Findings
      Structured data ✓
      Mobile responsive ✓
      Fast load times ✓
      Clean URLs ✓
    Recommendations
      Immediate fixes
        Code examples
        File locations
      Short-term improvements
        Priority ranking
      Long-term strategies
        Enhancement roadmap
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Workflow Works So Well&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt;: Claude Code finished in minutes what would take me hours to do by hand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complete&lt;/strong&gt;: Claude Code checked things I might have missed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Useful Results&lt;/strong&gt;: Not just problems, but solutions with code examples&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: Everything saved in a markdown file I can use later&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;To copy this workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Claude Code: &lt;code&gt;npm install -g @anthropic/claude-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set up the &lt;a href=&quot;https://github.com/merajmehrabi/puppeteer-mcp-server&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Puppeteer MCP server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Start your development server&lt;/li&gt;
&lt;li&gt;Give Claude Code a clear prompt about what you want checked&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Claude Code’s smarts plus Puppeteer’s browser control makes a powerful SEO audit tool that any developer can use. No more guessing about SEO problems - just run the audit and get a professional report in minutes. You can further automate workflows like this using [[claude-code-customization-guide-claudemd-skills-subagents|&lt;a href=&quot;http://CLAUDE.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CLAUDE.md&lt;/a&gt;, skills, and subagents]].&lt;/p&gt;
&lt;p&gt;Try it on your own projects and see what SEO problems you might be missing!&lt;/p&gt;

          </content:encoded><category>claude-code</category><category>ai</category><category>seo</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Age of the Generalist</title><link>https://alexop.dev/posts/the-age-of-the-generalist/</link><guid isPermaLink="true">https://alexop.dev/posts/the-age-of-the-generalist/</guid><description>How AI is transforming software development and why high-agency generalists will thrive in this new era of technology.</description><pubDate>Sun, 15 Jun 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;AI this, AI that. Like many of you, I’m constantly switching between &lt;em&gt;“Wow, you can do that with AI?”&lt;/em&gt; and &lt;em&gt;“Ugh, not AI again.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But here we are. AI is changing how we work and how we live.&lt;br /&gt;
I’m a millennial. The last major disruption technology brought to my life was social media (and then the iPhone).&lt;/p&gt;
&lt;p&gt;Now there is a new wave coming. It will slowly change everything.&lt;/p&gt;
&lt;p&gt;And yes, it is AI.&lt;/p&gt;
&lt;p&gt;How does AI change the work of software developers?&lt;/p&gt;
&lt;p&gt;Short answer: we will all become more like generalists.&lt;/p&gt;
&lt;p&gt;This shift is already happening. Most teams no longer separate frontend and backend. They hire full-stack developers. This helps teams move faster. Traditional separation creates communication overhead and slows execution.&lt;/p&gt;
&lt;p&gt;So we are already becoming generalists.&lt;/p&gt;
&lt;p&gt;If you join a startup as a dev, you will not find neat job titles. There is no budget for that. You wear whatever hat the day requires.&lt;/p&gt;
&lt;p&gt;Now add AI to the mix.&lt;/p&gt;
&lt;p&gt;Tasks that took hours , writing code, setting up tooling , can now be done in minutes. You can delegate to AI, generate boilerplate, spin up components, scaffold tests. You can work in parallel. You will probably spend more time reading code than writing it.&lt;/p&gt;
&lt;p&gt;But AI has no understanding of architecture. It does not know what good design looks like. It cannot distinguish between cohesion and coupling. It does not know when to break something into modules or when to leave it flat. It has no initiative. It only works when a human prompts it.&lt;/p&gt;
&lt;p&gt;That is why &lt;strong&gt;high agency&lt;/strong&gt; is more important than ever.&lt;/p&gt;
&lt;p&gt;AI does not replace builders. It replaces waiters.&lt;br /&gt;
If you wait to be told what to do, you will fall behind.&lt;br /&gt;
If you take action, ask questions, and push things forward, you will stay ahead.&lt;/p&gt;
&lt;p&gt;High agency means seeing a mess and deciding what to clean up. It means figuring out what matters without someone else making the roadmap.&lt;br /&gt;
AI can give you answers, but it will never tell you what is worth building.&lt;/p&gt;
&lt;p&gt;So what should developers focus on?&lt;/p&gt;
&lt;p&gt;Become a generalist with high agency.&lt;/p&gt;
&lt;p&gt;Think of Leonardo da Vinci. He painted &lt;em&gt;The Last Supper&lt;/em&gt; and &lt;em&gt;Mona Lisa&lt;/em&gt;. He dissected human bodies and sketched the nervous system. He designed flying machines. He wrote about optics, engineering, and warfare.&lt;br /&gt;
He did not pick a lane. He learned widely and built from what he learned.&lt;/p&gt;
&lt;p&gt;That mindset , curious, self-directed, and hands-on , is what will matter most in the age of AI.&lt;/p&gt;

          </content:encoded><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Use LLMs</title><link>https://alexop.dev/posts/how-i-use-llms/</link><guid isPermaLink="true">https://alexop.dev/posts/how-i-use-llms/</guid><description>Learn how I use LLMs to improve my productivity and efficiency.</description><pubDate>Sun, 25 May 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Motivated by the awesome YouTube video from Andrew Karpathy &lt;a href=&quot;https://www.youtube.com/watch?v=EWvNQjAaOHw&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How I use LLMs&lt;/a&gt;, I decided to give two talks on how I use LLMs, both at my company and at the TypeScript meetup in Munich.&lt;/p&gt;
&lt;p&gt;This blog post is the written version of those talks.&lt;/p&gt;
&lt;p&gt;Keep in mind that while some things might change, especially regarding the models I currently use, I hope these tips will remain helpful for a long time.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;As a junior developer, you might think your job is all about coding. However, as you gain experience, you realize that’s not entirely true. We developers spend a significant amount of time learning new things or explaining concepts to others. That’s why, when it comes to using LLMs, we shouldn’t focus solely on code generation.&lt;/p&gt;
&lt;p&gt;We should also consider how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Research faster&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document better&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn more effectively&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of my tips won’t be about how to use Cursor AI or Copilot better. I think that would be worth its own blog post or a short video.&lt;/p&gt;
&lt;h2&gt;Which model should I choose&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;It’s annoying that we even have to think about which model to use for which task. I would guess that in the future (Cursor AI is already doing this), there will be a model as a kind of router in the middle that understands which prompt relates to which model.&lt;/p&gt;
&lt;p&gt;But for now, this isn’t the case, so here’s my guideline. In the picture, you see that I came up with four categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Everyday tasks&lt;/strong&gt; (like fixing spelling, writing something better)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick Refactoring&lt;/strong&gt; (like adding console logs to debug something, small refactorings)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical Tasks&lt;/strong&gt; (like doing research)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex Tasks&lt;/strong&gt; (tasks that definitely need long reasoning and thinking)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s important for me, since I don’t have an unlimited amount of o3, for example, to try to use o4-mini-high if I think I don’t need long reasoning for something.&lt;/p&gt;
&lt;p&gt;As I said, these models will change daily, but I think the categories will remain.&lt;/p&gt;
&lt;p&gt;So most of the time, I ask myself if I need a model that requires reasoning or not.&lt;/p&gt;
&lt;h2&gt;o3 is a mini agent&lt;/h2&gt;
&lt;p&gt;What’s also clear is that new models like o3 are mini agents. This means they’re not only predicting the next token but also have tools. With these tools, they can gain better context or perform operations with Python.&lt;/p&gt;
&lt;p&gt;This is why Simon Willison’s blog post explains how he used o3 to guess his location. As his title says: Watching o3 guess a photo’s location is surreal, dystopian, and wildly entertaining, but it also shows how powerful this can be. Read his blog post &lt;a href=&quot;https://simonwillison.net/2025/Apr/26/o3-photo-locations/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also wrote a blog post once where I gave o3 a hard chess puzzle to solve. Feel free to read it &lt;a href=&quot;https://alexop.dev//../how-03-model-tries-chess-puzzle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Some tips on how to get more out of Copilot and co&lt;/h2&gt;
&lt;p&gt;My first tip is to index your codebase, either with a local index or remote. With this, Cursor or Copilot can perform better searches.&lt;/p&gt;
&lt;p&gt;It all falls back to automatic retrieval. Keep in mind that an LLM doesn’t know where your files are located. So it always has to search against your codebase.&lt;/p&gt;
&lt;p&gt;One technique besides keyword search that can help is dense vector or embedding search. You can read the docs on how to implement that.&lt;/p&gt;
&lt;p&gt;Another tip: when you have a project that’s indexed, you can use Copilot’s ask mode and use @workspace. Now you can ask business questions or even solve simple tickets in one shot (if there are well-written tickets). For more information on how to index your repositories for Copilot Chat, refer to the &lt;a href=&quot;https://docs.github.com/en/copilot/using-github-copilot/copilot-chat/indexing-repositories-for-copilot-chat&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub Copilot documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My last tip, where I use Gemini 2.0 Flash or GPT-4.1, is to do little refactorings or code changes quickly. I quickly mark the related lines and then use a prompt to make the changes.&lt;/p&gt;
&lt;h2&gt;How can we improve the output of an LLM&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;In the book &lt;a href=&quot;https://www.oreilly.com/library/view/ai-engineering/9781098166298/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;“AI Engineering”&lt;/a&gt; by Chip Huyen, she explains that there are three main ways to improve the output of an LLM:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;With Prompts&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per RAG&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With fine-tuning&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Of course, all three ways will increase in effort and maybe ROI, but it’s clear that better prompts are always the first step to improving the output of an LLM.&lt;/p&gt;
&lt;h2&gt;The almighty System Prompt&lt;/h2&gt;
&lt;p&gt;The idea of a System Prompt is simple but genius. We change the default behavior of an LLM and customize it to our needs.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;In the picture, you see an example of a system prompt that I use to write blog posts.&lt;/p&gt;
&lt;p&gt;In the picture, you see an example of a system prompt that can be used to write Jira tickets. At work, I have something like that and use it together with Copilot. My goal is to quickly write what needs to be done, and the LLM handles the rest. It also asks questions when something is not clear.&lt;/p&gt;
&lt;p&gt;You can use that for many problems, and also keep in mind that every LLM provider, like OpenAI or Claude, has their own system prompt. One use case, for example, is to explain which tools an LLM has available, etc. At &lt;a href=&quot;https://github.com/jujumilk3/leaked-system-prompts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub&lt;/a&gt;, you can read some of the leaked system prompts.&lt;/p&gt;
&lt;p&gt;This is why this is a good structure to think about when you write system prompts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Role Definition&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step-by-Step Instructions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output Format&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Cases&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style Guidelines&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you tell the LLM which role it has, it will already use words and tokens that are useful for this role in its next prediction.&lt;/p&gt;
&lt;p&gt;Clear steps can help for a more complex workflow so the LLM knows when it’s done, etc.&lt;/p&gt;
&lt;p&gt;For something like a Jira ticket, we should also add a concrete output format with an example.&lt;/p&gt;
&lt;p&gt;In my experience, edge cases are something that you will add over time. We need to play with the LLM and see what vibe we get from it.&lt;/p&gt;
&lt;p&gt;Style guidelines are useful. For example, I love easy words and active voice.&lt;/p&gt;
&lt;p&gt;You can also ask the LLM how a system prompt should look for the problem you want to solve and use that as your version 1. This approach can provide a solid starting point for further refinement.&lt;/p&gt;
&lt;h2&gt;Googling is dead&lt;/h2&gt;
&lt;p&gt;Don’t get me wrong, I think Google is winning the AI arms race. As noted in &lt;a href=&quot;https://www.thealgorithmicbridge.com/p/google-is-winning-on-every-ai-front&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Algorithmic Bridge&lt;/a&gt;, Google is excelling on every AI front. But the classical googling, where we typed a query and the first five results had an ad and it was hard to find an organic result, is over.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;Most of the time, I use a reasoning model with a web search tool. This helps me as a starter to find related blog posts, etc., for my problem. I only use Google when I know the site I want to reach or I know which blog post I want to read.&lt;/p&gt;
&lt;h2&gt;Get all tokens out of a repo&lt;/h2&gt;
&lt;p&gt;If you change GitHub to Uithub for any repo, you will get all text in a way that you can just copy-paste it into a model with a high context, like Google Gemini.&lt;/p&gt;
&lt;p&gt;This can be useful to either ask questions against the codebase or to learn how it works or to rebuild something similar without needing to increase the depth of your node modules.&lt;/p&gt;
&lt;figure&gt;
&lt;h2&gt;Generate a Wiki out of any repo&lt;/h2&gt;
&lt;p&gt;When you go to &lt;a href=&quot;https://deepwiki.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://deepwiki.org/&lt;/a&gt;, you can generate a wiki out of any repo. Useful for understanding other repos or even for your own little side projects. What I like is that the LLMs generate mermaid diagrams, and sometimes they are really useful.&lt;/p&gt;
&lt;h2&gt;Generate diagrams&lt;/h2&gt;
&lt;p&gt;I think there are now three ways to generate good diagrams with an LLM:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;As SVG&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;As Mermaid&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Or as a picture with the new model&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I already wrote about how to use ChatGPT to generate mermaid diagrams. Read it &lt;a href=&quot;https://alexop.dev//../how-to-use-ai-for-effective-diagram-creation-a-guide-to-chatgpt-and-mermaid&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Rules Rules Rules&lt;/h2&gt;
&lt;p&gt;We human developers need rules, and the same is true for LLMs to write better code. This is why both Copilot and Cursor have their own rule system. For detailed information on how to set up and use rules in Cursor, check out the &lt;a href=&quot;https://docs.cursor.com/context/rules&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;official Cursor documentation on rules&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One idea when you have a monorepo could be something like this:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;my-app/
├── .cursor/
│   └── rules/
│       └── project-guidelines.mdc       # General code style, naming, formatting
├── frontend/
│   ├── .cursor/
│   │   └── rules/
│   │       ├── vue-components.mdc       # Naming + structure for components
│   │       └── tailwind-usage.mdc       # Utility-first CSS rules
│   └── src/
│       └── ...
├── backend/
│   ├── .cursor/
│   │   └── rules/
│   │       ├── api-structure.mdc        # REST/GraphQL structure conventions
│   │       └── service-patterns.mdc     # How to organize business logic
│   └── src/
│       └── ...
├── shared/
│   ├── .cursor/
│   │   └── rules/
│   │       └── shared-types.mdc         # How to define + use shared TypeScript types
│   └── src/
│       └── ...
├── README.md
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One rule could then look like this:&lt;/p&gt;
&lt;pre class=&quot;language-mdc&quot;&gt;&lt;code class=&quot;language-mdc&quot;&gt;---
description: Base project guidelines and conventions
globs:
  - &quot;**/*.ts&quot;
  - &quot;**/*.vue&quot;
alwaysApply: false
---

- **Use `PascalCase` for component names.**
- **Use `camelCase` for variables, functions, and file names (except components).**
- **Prefer composition API (`setup()`) over options API.**
- **Type everything. Avoid `any` unless absolutely necessary.**
- **Keep files under 150 LOC. Split logic into composables or utilities.**
- **Use absolute imports from `@/` instead of relative paths.**
- **Every module must have tests that reflect the feature&apos;s acceptance criteria.**
- **Commit messages must follow Conventional Commits format.**
- **Use TODO: and FIXME: comments with your initials (e.g., `// TODO: refactor`).**
- **Format code with Prettier. Lint with ESLint before committing.**

Referenced files:
@.eslintrc.js
@.prettierrc
@tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an example for Cursor. The idea is to give a more fine-grained context. In our example, maybe it would even be better to only have a .vue and separate .ts rule.&lt;/p&gt;
&lt;p&gt;In Agent mode, Cursor will then automatically apply this rule as context.&lt;/p&gt;
&lt;h2&gt;Write better image prompts&lt;/h2&gt;
&lt;p&gt;One technique that I think can be useful is to describe which image you want and then say, “give me that back as a Midjourney prompt.” This has the advantage that the description of the image is nicely formatted.&lt;/p&gt;
&lt;h2&gt;When should you use an LLM directly&lt;/h2&gt;
&lt;p&gt;An interesting question that I got from the TypeScript meetup was when I would directly vibe code and just tell Cursor to implement feature X and when not.&lt;/p&gt;
&lt;p&gt;In my experience, it all depends on the topic and how much training data is available for that.&lt;/p&gt;
&lt;p&gt;For example, last week I was using Nuxt together with NuxtUI, a good UI library for Nuxt, but the problem was that the LLM doesn’t understand how the components are structured, etc. So in that case, it would be better if I were the main driver and not the LLM.&lt;/p&gt;
&lt;p&gt;So always ask yourself if there is enough training data out there for your problem. Was it already solved in the past?&lt;/p&gt;
&lt;p&gt;Sometimes you will waste time by just blindly doing vibe coding.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;There are many ways we developers can use LLMs to be more productive and also have more fun. I believe most of us don’t want to spend too much time writing tickets. This is where LLMs can help us. I believe it’s important to be open and try out these tools.&lt;/p&gt;
&lt;p&gt;If you want to get better with these tools, also try to understand the fundamentals. I wrote a blog post explaining &lt;a href=&quot;https://alexop.dev//../how-chatgpt-works-for-dummies&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;how ChatGPT works&lt;/a&gt; that might help you understand what’s happening under the hood.&lt;/p&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>ai</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>No Server, No Database: Smarter Related Posts in Astro with `transformers.js`</title><link>https://alexop.dev/posts/semantic-related-posts-astro-transformersjs/</link><guid isPermaLink="true">https://alexop.dev/posts/semantic-related-posts-astro-transformersjs/</guid><description>How I used Hugging Face embeddings to create smart “Related Posts” for my Astro blog—no backend, no database, just TypeScript.</description><pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;I recently read a interesting blog post about Embeddings at &lt;a href=&quot;https://technicalwriting.dev/ml/embeddings/overview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Embeddings in Technical Writing&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I could tell you exactly how to advance technical writing with embeddings, but where’s the fun in that?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Challenge accepted!&lt;br /&gt;
In this post, I show how I used &lt;strong&gt;Hugging Face’s &lt;code&gt;transformers.js&lt;/code&gt;&lt;/strong&gt; to create smarter related-post suggestions for my Astro blog, without servers or databases.&lt;/p&gt;
&lt;h2&gt;Why Embeddings Are Better Than Tags&lt;/h2&gt;
&lt;p&gt;Tags group posts by labels, but not by meaning. Posts about Vue 3 and deep reactivity concepts get mixed up together.&lt;br /&gt;
Embeddings capture the meaning of text using numeric vectors. Two posts become related when their content is similar, not just when tags match.&lt;/p&gt;
&lt;h3&gt;Vectors and Cosine Similarity&lt;/h3&gt;
&lt;p&gt;Words like “cat” and “kitty” are close in meaning, while “dog” is slightly different:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;word&lt;/th&gt;
&lt;th&gt;vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[0, 1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kitty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[0, 0.9]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1, -1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Cosine similarity measures how similar these vectors are.&lt;br /&gt;
For a deeper dive into TypeScript and vectors, check out my post on &lt;a href=&quot;https://alexop.dev//../how-to-implement-a-cosine-similarity-function-in-typescript-for-vector-comparison/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Transformers.js in Action&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;transformers.js&lt;/code&gt; lets you run Hugging Face models directly in JavaScript:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;sentence-transformers/all-MiniLM-L6-v2&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; extractor &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;pipeline&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;feature-extraction&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; model&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; embedding &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;extractor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Hello, world!&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  pooling&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;mean&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  normalize&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;embedding&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Float32Array with 384 dimensions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You don’t need Python or a server. Everything runs in your browser or Node.js.&lt;/p&gt;
&lt;h2&gt;My Simple Workflow&lt;/h2&gt;
&lt;p&gt;Here’s how my workflow works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load markdown files (&lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.mdx&lt;/code&gt;) from my blog.&lt;/li&gt;
&lt;li&gt;Remove markdown formatting to get plain text.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;transformers.js&lt;/code&gt; to create embeddings.&lt;/li&gt;
&lt;li&gt;Calculate cosine similarity between all posts.&lt;/li&gt;
&lt;li&gt;Find the top 5 most related posts for each post.&lt;/li&gt;
&lt;li&gt;Save the results in a JSON file (&lt;code&gt;similarities.json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Display these related posts with Astro.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Main Script (TypeScript)&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// --------- Configurations ---------&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;GLOB&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;src/content/**/*.{md,mdx}&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Where to find Markdown content&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;OUT&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;src/assets/similarities.json&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Output file for results&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;TOP_N&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Number of similar docs to keep&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;MODEL&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Snowflake/snowflake-arctic-embed-m-v2.0&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Embedding model&lt;/span&gt;

&lt;span&gt;// --------- Type Definitions ---------&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Frontmatter&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  slug&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;k&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Document&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  frontmatter&lt;span&gt;:&lt;/span&gt; Frontmatter&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;SimilarityResult&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;Frontmatter&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  similarity&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// --------- Utils ---------&lt;/span&gt;

&lt;span&gt;/**
 * Normalizes a vector to unit length (L2 norm == 1)
 * This makes cosine similarity a simple dot product!
 */&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;normalize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;:&lt;/span&gt; Float32Array&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Float32Array &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; len &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vec&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// L2 norm&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;len&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; vec&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Float32Array&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;x &lt;span&gt;=&amp;gt;&lt;/span&gt; x &lt;span&gt;/&lt;/span&gt; len&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Computes dot product of two same-length vectors.
 * Vectors MUST be normalized before using this for cosine similarity!
 */&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;dot&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; Float32Array&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; Float32Array&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
  a&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sum&lt;span&gt;,&lt;/span&gt; ai&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; sum &lt;span&gt;+&lt;/span&gt; ai &lt;span&gt;*&lt;/span&gt; b&lt;span&gt;[&lt;/span&gt;i&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;/**
 * Strips markdown formatting, import/export lines, headings, tables, etc.
 * Returns plain text for semantic analysis.
 */&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;getPlainText&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;md&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; txt &lt;span&gt;=&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;remark&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;strip&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;md&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^import .*?$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gm&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^export .*?$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gm&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^\s*(TLDR|Introduction|Conclusion|Summary|Quick Setup Guide|Rules?)\s*$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gim&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;&quot;&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^[A-Z\s]{4,}$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gm&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^\|.*\|$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gm&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;(Rule\s\d+:.*)(?=\s*Rule\s\d+:)&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;$1\n&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\n{3,}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;\n\n&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\n{2}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;\n\n&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot; &quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\s{2,}&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot; &quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; txt&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;/**
 * Parses and validates a single Markdown file.
 * - Extracts frontmatter (slug, etc.)
 * - Converts content to plain text
 * - Skips drafts or files with no slug
 */&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;processFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Document &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; content&lt;span&gt;,&lt;/span&gt; data &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;matter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;slug &lt;span&gt;||&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;draft&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; plain &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;getPlainText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;,&lt;/span&gt; content&lt;span&gt;:&lt;/span&gt; plain&lt;span&gt;,&lt;/span&gt; frontmatter&lt;span&gt;:&lt;/span&gt; data &lt;span&gt;as&lt;/span&gt; Frontmatter &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Processes an array of Markdown file paths into Documents
 */&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;loadDocs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;paths&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; docs&lt;span&gt;:&lt;/span&gt; Document&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; p &lt;span&gt;of&lt;/span&gt; paths&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; d &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;processFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;p&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;d&lt;span&gt;)&lt;/span&gt; docs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;d&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; docs&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Generates vector embeddings for each document&apos;s plain text.
 * - Uses HuggingFace model
 * - Normalizes each vector for fast cosine similarity search
 */&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;embedDocs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  docs&lt;span&gt;:&lt;/span&gt; Document&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  extractor&lt;span&gt;:&lt;/span&gt; FeatureExtractionPipeline
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;docs&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// Don&apos;t let the model normalize, we do it manually for safety&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; res &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;extractor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    docs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;d &lt;span&gt;=&amp;gt;&lt;/span&gt; d&lt;span&gt;.&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt; pooling&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;mean&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; normalize&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;n&lt;span&gt;,&lt;/span&gt; dim&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; res&lt;span&gt;.&lt;/span&gt;dims&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// Each embedding vector is normalized for performance&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; length&lt;span&gt;:&lt;/span&gt; n &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;_&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
    &lt;span&gt;normalize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;res&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;i &lt;span&gt;*&lt;/span&gt; dim&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;i &lt;span&gt;+&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; dim&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Computes the top-N most similar documents for the given document index.
 * - Uses dot product of normalized vectors for cosine similarity
 * - Returns only the top-N
 */&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;topSimilar&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  idx&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  docs&lt;span&gt;:&lt;/span&gt; Document&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  embs&lt;span&gt;:&lt;/span&gt; Float32Array&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  n&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; SimilarityResult&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; docs
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;d&lt;span&gt;,&lt;/span&gt; j&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
      j &lt;span&gt;===&lt;/span&gt; idx
        &lt;span&gt;?&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;
        &lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;...&lt;/span&gt;d&lt;span&gt;.&lt;/span&gt;frontmatter&lt;span&gt;,&lt;/span&gt;
            path&lt;span&gt;:&lt;/span&gt; d&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;,&lt;/span&gt;
            similarity&lt;span&gt;:&lt;/span&gt; &lt;span&gt;+&lt;/span&gt;&lt;span&gt;dot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;embs&lt;span&gt;[&lt;/span&gt;idx&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; embs&lt;span&gt;[&lt;/span&gt;j&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// higher = more similar&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Boolean&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;b &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;similarity &lt;span&gt;-&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;similarity&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; n&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; SimilarityResult&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Computes all similarities for every document, returns as {slug: SimilarityResult[]} map.
 */&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;allSimilarities&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;docs&lt;span&gt;:&lt;/span&gt; Document&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; embs&lt;span&gt;:&lt;/span&gt; Float32Array&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; n&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; Object&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fromEntries&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    docs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;d&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;d&lt;span&gt;.&lt;/span&gt;frontmatter&lt;span&gt;.&lt;/span&gt;slug&lt;span&gt;,&lt;/span&gt; &lt;span&gt;topSimilar&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;i&lt;span&gt;,&lt;/span&gt; docs&lt;span&gt;,&lt;/span&gt; embs&lt;span&gt;,&lt;/span&gt; n&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * Saves result object as JSON file.
 * - Ensures output directory exists.
 */&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;saveJson&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;obj&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; out&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mkdirSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dirname&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;out&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; recursive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;writeFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;out&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;obj&lt;span&gt;,&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// --------- Main Execution Flow ---------&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// 1. Load transformer model for embeddings&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; extractor &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;pipeline&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;feature-extraction&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;MODEL&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// 2. Find all Markdown files&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; files &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;glob&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;GLOB&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;files&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;chalk&lt;span&gt;.&lt;/span&gt;&lt;span&gt;yellow&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;No content files found.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// 3. Parse and process all files&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; docs &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;loadDocs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;files&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;docs&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;chalk&lt;span&gt;.&lt;/span&gt;&lt;span&gt;red&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;No documents loaded.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// 4. Generate &amp;amp; normalize embeddings&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; embs &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;embedDocs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;docs&lt;span&gt;,&lt;/span&gt; extractor&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;embs&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;chalk&lt;span&gt;.&lt;/span&gt;&lt;span&gt;red&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;No embeddings.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// 5. Calculate similarities for each doc&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; results &lt;span&gt;=&lt;/span&gt; &lt;span&gt;allSimilarities&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;docs&lt;span&gt;,&lt;/span&gt; embs&lt;span&gt;,&lt;/span&gt; &lt;span&gt;TOP_N&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// 6. Save results to disk&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;saveJson&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;results&lt;span&gt;,&lt;/span&gt; &lt;span&gt;OUT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;chalk&lt;span&gt;.&lt;/span&gt;&lt;span&gt;green&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Similarity results saved to &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;OUT&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;chalk&lt;span&gt;.&lt;/span&gt;&lt;span&gt;red&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Error:&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; e&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    process&lt;span&gt;.&lt;/span&gt;exitCode &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;This Will Produce a JSON file with the following structure:&lt;/h2&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;vue-introduction&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;slug&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;typescript-advanced-types&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Advanced Types in TypeScript&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;date&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;2024-06-03T00:00:00.000Z&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;path&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;src/content/typescript-advanced-types.md&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;similarity&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0.35&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;// Additional similar documents...&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;// Additional document entries...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Astro Component&lt;/h3&gt;
&lt;pre class=&quot;language-astro&quot;&gt;&lt;code class=&quot;language-astro&quot;&gt;---
if (similarities[post.slug]) {
  mostRelatedPosts = similarities[post.slug]
    .filter((p: RelatedPost) =&amp;gt; !p.draft)
    .sort(
      (a: RelatedPost, b: RelatedPost) =&amp;gt;
        (b.similarity ?? 0) - (a.similarity ?? 0)
    )
    .slice(0, 3);
}
---

{
  mostRelatedPosts.length &amp;gt; 0 &amp;amp;&amp;amp; (
    &amp;lt;div data-pagefind-ignore class=&quot;mb-8 mt-16&quot;&amp;gt;
      &amp;lt;h2 class=&quot;mb-6 text-3xl font-bold text-skin-accent&quot;&amp;gt;
        Most Related Posts
      &amp;lt;/h2&amp;gt;
      &amp;lt;div class=&quot;md:grid-cols-3 grid grid-cols-1 gap-6&quot;&amp;gt;
        {mostRelatedPosts.map((relatedPost: RelatedPost) =&amp;gt; (
          &amp;lt;a
            href={`/posts/${relatedPost.slug}/`}
            class=&quot;related-post-card group&quot;
          &amp;gt;
            &amp;lt;div class=&quot;p-5&quot;&amp;gt;
              &amp;lt;h3 class=&quot;related-post-title&quot;&amp;gt;{relatedPost.title}&amp;lt;/h3&amp;gt;
              &amp;lt;p class=&quot;related-post-description&quot;&amp;gt;{relatedPost.description}&amp;lt;/p&amp;gt;
              &amp;lt;div class=&quot;flex items-center justify-between text-xs text-skin-base text-opacity-60&quot;&amp;gt;
                &amp;lt;Datetime
                  pubDatetime={relatedPost.pubDatetime}
                  modDatetime={relatedPost.modDatetime}
                  size=&quot;sm&quot;
                /&amp;gt;
                &amp;lt;span class=&quot;related-post-tag&quot;&amp;gt;{relatedPost.tags?.[0]}&amp;lt;/span&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/a&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Does It Work?&lt;/h2&gt;
&lt;p&gt;Yes! Now, my blog suggests truly related content, not random posts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What I Learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No extra servers or databases&lt;/strong&gt;: Everything runs during build time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to use&lt;/strong&gt;: Works in both browsers and Node.js.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;/strong&gt;: Quickly change the model or method.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have a static blog and want better recommendations, give embeddings and Astro a try. Let me know how it goes!&lt;/p&gt;
&lt;p&gt;Of course, this is far from perfect. I also don’t know which model would be ideal, but at the moment I’m getting much better related posts than before, so I’m happy with the results.&lt;br /&gt;
If you want to play with the script yourself check out &lt;a href=&quot;https://github.com/alexanderop/post-matcher-ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;post-matcher-ai&lt;/a&gt;&lt;/p&gt;

          </content:encoded><category>ai</category><category>astro</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Type-Safe GraphQL Queries in Vue 3 with GraphQL Code Generator</title><link>https://alexop.dev/posts/type-safe-graphql-queries-vue3-codegen/</link><guid isPermaLink="true">https://alexop.dev/posts/type-safe-graphql-queries-vue3-codegen/</guid><description>Part 2 of the Vue 3 + GraphQL series: generate fully-typed `useQuery` composables in Vue 3 with GraphQL Code Generator</description><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;figure&gt;
&lt;h2&gt;Why plain TypeScript isn’t enough&lt;/h2&gt;
&lt;p&gt;If you hover over the &lt;code&gt;result&lt;/code&gt; from &lt;code&gt;useQuery&lt;/code&gt; in last week’s code, you’ll still see &lt;code&gt;Ref&amp;lt;any&amp;gt;&lt;/code&gt;. That means:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;li v-for=&quot;c in result?.countries2&quot; :key=&quot;c.code&quot;&amp;gt;&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…slips right past TypeScript.&lt;/p&gt;
&lt;figure&gt;
&lt;p&gt;It’s time to bring in &lt;strong&gt;GraphQL Code Generator&lt;/strong&gt; which gives us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100% typed operations, variables, and results&lt;/li&gt;
&lt;li&gt;Build-time schema validation (&lt;em&gt;fail fast, ship safe&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 1: Install the right packages&lt;/h2&gt;
&lt;p&gt;Let’s start by installing the necessary dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; i graphql
&lt;span&gt;npm&lt;/span&gt; i &lt;span&gt;-D&lt;/span&gt; typescript @graphql-codegen/cli
&lt;span&gt;npm&lt;/span&gt; i &lt;span&gt;-D&lt;/span&gt; @parcel/watcher
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;code&gt;@parcel/watcher&lt;/code&gt; is a dev dependency.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Step 2: Create a clean &lt;code&gt;codegen.ts&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Next, use the CLI to generate your config file:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx graphql-code-generator init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When prompted, answer as follows:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;? What &lt;span&gt;type&lt;/span&gt; of application are you building? Application built with Vue
? Where is your schema?: &lt;span&gt;(&lt;/span&gt;path or url&lt;span&gt;)&lt;/span&gt; https://countries.trevorblades.com/graphql
? Where are your operations and fragments?: src/**/*.vue
? Where to &lt;span&gt;write&lt;/span&gt; the output: src/gql/
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script &lt;span&gt;in&lt;/span&gt; package.json should run the codegen? codegen
Fetching latest versions of selected plugins&lt;span&gt;..&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your generated &lt;code&gt;codegen.ts&lt;/code&gt; should look like this:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; config&lt;span&gt;:&lt;/span&gt; CodegenConfig &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  overwrite&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;https://countries.trevorblades.com/graphql&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  documents&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;src/**/*.vue&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  generates&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;src/gql/&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      preset&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;client&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; config&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Add dev scripts and watch mode&lt;/h2&gt;
&lt;p&gt;Update your &lt;code&gt;package.json&lt;/code&gt; scripts to streamline development:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;codegen&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;graphql-codegen --config codegen.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;codegen:watch&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;graphql-codegen --watch --config codegen.ts&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 4: Write your first typed query&lt;/h2&gt;
&lt;p&gt;Create a new file at &lt;code&gt;src/queries/countries.graphql&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;query&lt;/span&gt; &lt;span&gt;AllCountries&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;countries&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;code&lt;/span&gt;
    &lt;span&gt;name&lt;/span&gt;
    &lt;span&gt;emoji&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, generate your types:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; run codegen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command writes all generated types to &lt;code&gt;src/gql/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Update your &lt;code&gt;CountryList.vue&lt;/code&gt; component to use the generated types&lt;/h3&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { result, loading, error } = useQuery(AllCountriesDocument);
const countries = computed(() =&amp;gt; result.value?.countries);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;
&lt;h3&gt;Inline queries with the generated &lt;code&gt;graphql&lt;/code&gt; tag&lt;/h3&gt;
&lt;p&gt;Alternatively, define the query directly in your component using the generated &lt;code&gt;graphql&lt;/code&gt; tag:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const COUNTRIES_QUERY = graphql(`
  query AllCountries {
    countries {
      code
      name
      emoji
    }
  }
`);

const { result, loading, error } = useQuery(COUNTRIES_QUERY);
const countries = computed(() =&amp;gt; result.value?.countries);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Watch mode&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;@parcel/watcher&lt;/code&gt; installed, you can enable watch mode for a smoother development experience. If you frequently change your GraphQL schema while developing, simply run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; run codegen:watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GraphQL Code Generator immediately throws an error when your local operations drift from the live schema.&lt;br /&gt;
Remember, your GraphQL server needs to be running for this to work.&lt;/p&gt;
&lt;h2&gt;Bonus: Proper validation out of the box&lt;/h2&gt;
&lt;p&gt;A powerful benefit of this setup is &lt;strong&gt;automatic validation&lt;/strong&gt;. If the Countries GraphQL API ever changes—say, it renames &lt;code&gt;code&lt;/code&gt; to &lt;code&gt;code2&lt;/code&gt;—you’ll get an error when generating types.&lt;/p&gt;
&lt;p&gt;For example, if you query for &lt;code&gt;code2&lt;/code&gt;, you’ll see:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;⚠ Generate outputs
  ❯ Generate to src/gql/
    ✔ Load GraphQL schemas
    ✔ Load GraphQL documents
    ✖ GraphQL Document Validation failed with &lt;span&gt;1&lt;/span&gt; errors&lt;span&gt;;&lt;/span&gt;
      Error &lt;span&gt;0&lt;/span&gt;: Cannot query field &lt;span&gt;&quot;code2&quot;&lt;/span&gt; on &lt;span&gt;type&lt;/span&gt; &lt;span&gt;&quot;Country&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt; Did you mean &lt;span&gt;&quot;code&quot;&lt;/span&gt;?
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Should you commit generated files?&lt;/h2&gt;
&lt;p&gt;A common question: should you commit the generated types to your repository?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commit them&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast onboarding · Diff visibility&lt;/td&gt;
&lt;td&gt;Noisy PRs · Merge conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ignore them&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean history · Zero conflicts&lt;/td&gt;
&lt;td&gt;Extra &lt;code&gt;npm run generate&lt;/code&gt; in CI/local&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Many teams choose to commit generated files, &lt;strong&gt;but&lt;/strong&gt; enforce &lt;code&gt;npm run generate -- --check&lt;/code&gt; in CI to guard against stale artifacts.&lt;/p&gt;
&lt;h2&gt;Up next (Part 3)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fragments without repetition&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary &amp;amp; Key Takeaways&lt;/h2&gt;
&lt;p&gt;In this part of the Vue 3 + GraphQL series, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up GraphQL Code Generator v5 to create fully-typed queries and composables for Vue 3&lt;/li&gt;
&lt;li&gt;Learned how to configure &lt;code&gt;codegen.ts&lt;/code&gt; for a remote schema and local &lt;code&gt;.vue&lt;/code&gt; operations&lt;/li&gt;
&lt;li&gt;Automated type generation with dev scripts and watch mode for a smooth DX&lt;/li&gt;
&lt;li&gt;Used generated types and the &lt;code&gt;graphql&lt;/code&gt; tag to eliminate &lt;code&gt;any&lt;/code&gt; and catch schema errors at build time&lt;/li&gt;
&lt;li&gt;Discussed whether to commit generated files and best practices for CI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What you learned&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;How to make your GraphQL queries type-safe and schema-validated in Vue 3&lt;/li&gt;
&lt;li&gt;How to avoid runtime errors and catch breaking API changes early&lt;/li&gt;
&lt;li&gt;How to streamline your workflow with codegen scripts and watch mode&lt;/li&gt;
&lt;li&gt;The tradeoffs of committing vs. ignoring generated files in your repo&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Actionable reminders&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Always run &lt;code&gt;npm run generate&lt;/code&gt; after changing queries or schema&lt;/li&gt;
&lt;li&gt;Use the generated types in your components for full type safety&lt;/li&gt;
&lt;li&gt;Consider enforcing type checks in CI to prevent stale artifacts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stay tuned for Part 3, where we’ll cover fragments and avoid repetition in your queries!&lt;/p&gt;
&lt;h2&gt;Source Code&lt;/h2&gt;
&lt;p&gt;Find the full demo for this series here: &lt;a href=&quot;https://github.com/alexanderop/vue-graphql-simple-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;example&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br /&gt;
The code for this tutorial is on the &lt;code&gt;part-two&lt;/code&gt; branch.&lt;br /&gt;
After cloning the repository, make sure to check out the correct branch:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;git&lt;/span&gt; clone https://github.com/alexanderop/vue-graphql-simple-example.git
&lt;span&gt;cd&lt;/span&gt; vue-graphql-simple-example
&lt;span&gt;git&lt;/span&gt; checkout part-two
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/vue-graphql-simple-example/tree/part-two&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;View the branch directly on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>graphql</category><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>LLM-Powered Search: o4-mini-high vs o3 vs Deep Research</title><link>https://alexop.dev/posts/llm-powered-search-comparison-o4-mini-high-o3-deep-research/</link><guid isPermaLink="true">https://alexop.dev/posts/llm-powered-search-comparison-o4-mini-high-o3-deep-research/</guid><description>A practical benchmark of three OpenAI models—o4-mini-high, o3, and Deep Research—for LLM-powered search. Compare their speed, depth, accuracy, citations, and cost when tackling real research questions like &apos;How does Vercel use Speakeasy for API testing?Ideal for developers exploring AI-assisted technical research</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;tldr:&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; “How does Vercel use Speakeasy for API testing?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature / Model&lt;/th&gt;
&lt;th&gt;o-4-mini-high&lt;/th&gt;
&lt;th&gt;o3&lt;/th&gt;
&lt;th&gt;Deep Research&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Instant&lt;/td&gt;
&lt;td&gt;🕒 Conversational&lt;/td&gt;
&lt;td&gt;🐢 Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Depth of Response&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢 Basic facts&lt;/td&gt;
&lt;td&gt;🟡 Balanced depth&lt;/td&gt;
&lt;td&gt;🔵 Comprehensive analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Citation Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inline links only&lt;/td&gt;
&lt;td&gt;Inline links&lt;/td&gt;
&lt;td&gt;30+ footnotes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency Friction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High (3-min delay)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;💸 Lowest&lt;/td&gt;
&lt;td&gt;💸 Moderate&lt;/td&gt;
&lt;td&gt;💸💸 Highest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sanity-checks, quick verification&lt;/td&gt;
&lt;td&gt;Background research, architectural docs&lt;/td&gt;
&lt;td&gt;Formal research, literature-style analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output Length&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium (~4.8k characters)&lt;/td&gt;
&lt;td&gt;Longer (~7.5k characters)&lt;/td&gt;
&lt;td&gt;Very Long (~13.9k characters)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sources Used&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ideal Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slack pings, fact-checks&lt;/td&gt;
&lt;td&gt;Blog prep, decision-making&lt;/td&gt;
&lt;td&gt;Deep dive reports, whitepapers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;While reading about the “Docs as Tests” approach to API documentation, I found something interesting about Vercel using Speakeasy for their API testing. This caught my attention because I wanted to learn more about how they put this into practice.&lt;/p&gt;
&lt;p&gt;Last week, Simon Willison had published a compelling argument that modern LLMs have essentially “solved” web search for everyday research tasks (with the caveat that you should still verify any potential inaccuracies). (&lt;a href=&quot;https://simonwillison.net/2025/Apr/21/ai-assisted-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AI assisted search-based research actually works now&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;This gave me an idea: instead of using Google like most people would, I could test different LLM models against each other. I decided to ask three different OpenAI models the same research question:&lt;/p&gt;
&lt;h2&gt;Why is this now working?&lt;/h2&gt;
&lt;p&gt;The key difference with models like o3 is that they’re no longer just language models that predict the next token—they’re becoming mini agents. This means they can also predict when to call a function or use a tool, like running a Python script or performing a web search.&lt;/p&gt;
&lt;p&gt;For example, o3 is often powered by tools like Bing for real-time web access or a Python interpreter for calculations. That’s why it can generate tables, analyze data, and reason with up-to-date information.&lt;/p&gt;
&lt;p&gt;Traditional “vanilla” models (that only predict the next token without tools) are limited—they rely solely on their internal knowledge, which can be outdated and prone to hallucinations. But once you give a model access to tools, it can fetch accurate, real-world context on demand, making its next-token prediction much smarter.&lt;/p&gt;
&lt;h2&gt;Model Response Analysis&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt:&lt;/strong&gt; “How does Vercel use Speakeasy for API testing?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Raw Response Samples&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;(First words of each, to give you a feel for the tone)&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;🟢 o-4-mini-high&lt;/h3&gt;
&lt;p&gt;Vercel leverages Speakeasy’s OpenAPI-driven toolchain to generate both their official TypeScript SDK and a comprehensive suite of contract tests based on their public spec at &lt;code&gt;https://openapi.vercel.sh&lt;/code&gt; (&lt;a href=&quot;https://github.com/vercel/sdk/blob/main/tests-overlay.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;sdk/tests-overlay.yaml at main · vercel/sdk · GitHub&lt;/a&gt;, &lt;a href=&quot;https://www.speakeasy.com/product/api-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing | Speakeasy&lt;/a&gt;). Using the Speakeasy CLI’s &lt;code&gt;speakeasy test&lt;/code&gt; command, they spin up a mock server and validate every endpoint against its schema automatically (&lt;a href=&quot;https://github.com/speakeasy-api/speakeasy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub - speakeasy-api/speakeasy&lt;/a&gt;). To handle endpoints that are still evolving, Vercel maintains a &lt;code&gt;tests-overlay.yaml&lt;/code&gt; overlay file in their SDK repository, which disables specific tests by annotating operations with &lt;code&gt;x-speakeasy-test: false&lt;/code&gt; (&lt;a href=&quot;https://github.com/vercel/sdk/blob/main/tests-overlay.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;sdk/tests-overlay.yaml at main · vercel/sdk · GitHub&lt;/a&gt;). This entire process is woven into their CI/CD via GitHub Actions: running &lt;code&gt;speakeasy configure tests&lt;/code&gt; scaffolds a “Test SDKs” workflow that executes these tests on every pull request (&lt;a href=&quot;https://www.speakeasy.com/docs/customize-testing/github-actions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing in GitHub Actions&lt;/a&gt;). As a showcase customer, Vercel is explicitly listed on Speakeasy’s product page among world-class API teams using automated API testing in their pipelines (&lt;a href=&quot;https://www.speakeasy.com/product/api-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing | Speakeasy&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thought for 41 seconds · 10 sources&lt;/em&gt;&lt;br /&gt;
&lt;em&gt;Characters: 4,811&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chatgpt.com/share/6813248e-cd28-8004-8cb4-ab662014271b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Full conversation →&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;🟡 o3 Result&lt;/h3&gt;
&lt;p&gt;Speakeasy sits in Vercel’s CI/CD pipeline as an &lt;strong&gt;OpenAPI-driven “safety-net”&lt;/strong&gt;: every time the Vercel REST-API spec changes, Speakeasy (1) regenerates an SDK, (2) boots a ready-made Bun test-suite that hits live endpoints, and (3) compares the new spec against the last published contract in its registry. The workflow automatically runs in GitHub Actions; if an endpoint breaks schema, status-codes, or core behaviour, the job fails and engineers are alerted before the change ships. Below is a deep-dive into each moving part and how they fit together.&lt;/p&gt;
&lt;h4&gt;Why Vercel Automates API Tests&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Vercel’s public API is used by thousands of deploy hooks, CLIs, and partners; even a minor breaking change can halt customer CI pipelines. Speakeasy markets its testing product to “world-class API companies” and lists Vercel as a reference logo, highlighting reliability as a selling point. (&lt;a href=&quot;https://www.speakeasy.com/product/api-testing?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing | Speakeasy&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;In Speakeasy’s Series A announcement the company calls out Vercel as one of the “fast-growing innovators” that relies on automated contract testing to “make sure no unintentional breaking changes get shipped.” (&lt;a href=&quot;https://www.speakeasy.com/post/fundraising-series-a?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Speakeasy’s $15M Series A&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Thought for 1m 25s · 15 Sources&lt;/em&gt;&lt;br /&gt;
&lt;em&gt;Characters: 7,483&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chatgpt.com/share/68132509-ea28-8004-8cf0-beeb88c77577&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Full conversation →&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;🔵 Deep Research&lt;/h3&gt;
&lt;h4&gt;Vercel’s Speakeasy-Powered API Toolchain&lt;/h4&gt;
&lt;p&gt;Vercel uses Speakeasy to automate its REST API tooling – generating SDKs, docs, mocks and tests from an OpenAPI spec. In late 2024 Vercel announced a new &lt;strong&gt;TypeScript SDK&lt;/strong&gt; (&lt;code&gt;@vercel/sdk&lt;/code&gt;) built with Speakeasy (&lt;a href=&quot;https://vercel.com/changelog/introducing-the-vercel-typescript-sdk#:~:text=This%20SDK%20includes%3A&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Introducing the Vercel TypeScript SDK - Vercel&lt;/a&gt;). The SDK is &lt;strong&gt;fully type-safe&lt;/strong&gt; (using Zod schemas) and provides complete coverage of the Vercel REST API (&lt;a href=&quot;https://vercel.com/changelog/introducing-the-vercel-typescript-sdk#:~:text=This%20SDK%20includes%3A&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Introducing the Vercel TypeScript SDK - Vercel&lt;/a&gt;) (&lt;a href=&quot;https://vercel.com/docs/rest-api/sdk#:~:text=The%20%60%40vercel%2Fsdk%60%20is%20a%20type,of%20the%20Vercel%20REST%20API&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Using the Vercel SDK - Vercel API Docs&lt;/a&gt;). In fact, Vercel’s SDK repo explicitly notes “SDK Created by Speakeasy” (&lt;a href=&quot;https://github.com/vercel/sdk?trk=public_post_comment-text#:~:text=SDK%20Created%20by%20Speakeasy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub - vercel/sdk&lt;/a&gt;). Under the hood, Vercel maintains an OpenAPI spec (&lt;code&gt;vercel-spec.json&lt;/code&gt;) and a Speakeasy config (&lt;code&gt;.speakeasy/gen.yaml&lt;/code&gt;) that drives code generation. A GitHub Actions workflow invokes the Speakeasy CLI on each update, regenerating the SDK (and publishing to npm) whenever the API spec changes. This ensures the SDK and docs stay in sync with the API. As a result, every Vercel endpoint is exposed via the SDK with up-to-date &lt;strong&gt;documentation and code examples&lt;/strong&gt;, and the generated code is optimized (e.g. “standalone functions” for tree-shaking) (&lt;a href=&quot;https://vercel.com/changelog/introducing-the-vercel-typescript-sdk#:~:text=,REST%20API%20with%20Zod%20schemas&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Introducing the Vercel TypeScript SDK - Vercel&lt;/a&gt;) (&lt;a href=&quot;https://vercel.com/docs/rest-api/sdk#:~:text=The%20%60%40vercel%2Fsdk%60%20is%20a%20type,of%20the%20Vercel%20REST%20API&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Using the Vercel SDK - Vercel API Docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Research completed in 3m · 31 sources&lt;/em&gt;&lt;br /&gt;
&lt;em&gt;Characters: 13,942&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chatgpt.com/share/6813258e-4a70-8004-a4ec-a229ac12ac6c&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Full conversation →&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Key Finding&lt;/h3&gt;
&lt;p&gt;For &lt;em&gt;search-focused&lt;/em&gt; tasks, &lt;strong&gt;o3 beat Deep Research&lt;/strong&gt;. I got almost the same depth but twice as fast and for a fraction of the cost.&lt;/p&gt;
&lt;h2&gt;Choosing the Right Model&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[What do you need?]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{Quick answer?}&lt;/span&gt;
    B &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Yes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; C&lt;span&gt;[o-4-mini-high]&lt;/span&gt;
    B &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;No&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; D&lt;span&gt;{Need balanced speed &amp;amp; depth?}&lt;/span&gt;
    D &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;Yes&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; E&lt;span&gt;[o3]&lt;/span&gt;
    D &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;No&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; F&lt;span&gt;[Deep Research]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices for LLM Research&lt;/h2&gt;
&lt;p&gt;My testing matches what Simon Willison recently said about using AI to search for information. He made a strong point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I still don’t fully trust these tools not to make mistakes. But for small, low-risk tasks, I might skip double-checking.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;LLMs are great for quick, helpful answers, but you still need to check their work if it really matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My simple rule:&lt;/strong&gt;&lt;br /&gt;
If the answer is more important than a tweet, double-check it. Look for two good sources or ask a second AI.&lt;br /&gt;
You’ll catch most errors in under a minute.&lt;br /&gt;
Also its always worth to check the original sources.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;LLM search helps you &lt;em&gt;start&lt;/em&gt; a research rabbit-hole in seconds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;o3&lt;/strong&gt; for deeper answers that balance depth and speed&lt;/li&gt;
&lt;li&gt;Switch to &lt;strong&gt;o-4-mini-high&lt;/strong&gt; when time is of the essence&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Deep Research&lt;/strong&gt; only when you need a comprehensive report with extensive citations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, cost considerations play a significant role in model selection. With a $20 monthly subscription, my usage of Deep Research and o3 needs to be strategic. The key is matching the model to both your needs and context: When I’m on my smartphone and need quick answers, o4-mini-high is my go-to choice for its balance of speed and simplicity.&lt;/p&gt;
&lt;p&gt;A more practical use case is finding the right doctor for a specific problem. Instead of dealing with Google’s clutter (like ads, SEO traps, and scattered reviews), I can just ask a reasoning model to do the heavy lifting. It can quickly suggest the top three doctors who best match my situation. Then I can check their websites myself to get a feel for them. This way, I do not just save time; I also make more informed decisions.&lt;/p&gt;

          </content:encoded><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Watching OpenAI&apos;s o3 Model Sweat Over a Paul Morphy Mate-in-2</title><link>https://alexop.dev/posts/how-03-model-tries-chess-puzzle/</link><guid isPermaLink="true">https://alexop.dev/posts/how-03-model-tries-chess-puzzle/</guid><description>A breakdown of how an AI model attempts to solve a complex chess puzzle, showcasing its human-like reasoning, problem-solving attempts, and eventual reliance on external information.</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;When I gave OpenAI’s o3 model a tough chess puzzle, it behaved almost like a human: thinking, doubting, retrying, and finally googling the answer. 🤣&lt;/p&gt;
&lt;p&gt;Before I break it down step-by-step, here’s the funniest part: it spent 8 minutes calculating and pixel-measuring squares… and then cheated by using Bing.&lt;/p&gt;
&lt;figure&gt;
&lt;h2&gt;First the Problem if you want you can try to solve it yourself&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;This is a famous chess puzzle from Paul Morphy check out &lt;a href=&quot;https://www.chess.com/blog/ThePawnSlayer/checkmate-in-two-puzzles-test-very-hard&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this link&lt;/a&gt; for more.&lt;/p&gt;
&lt;p&gt;See the full conversation &lt;a href=&quot;https://chatgpt.com/share/680e5290-99d8-8004-9455-4fd779a9bfda&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;1. &lt;strong&gt;Careful Board Analysis&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;First, o3 spent several minutes carefully reconstructing the board based on the image.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“The board has White’s King on c8, White’s Rook on a1, and a Pawn on b6. Black’s King on a8, Bishop on b8, and Pawns on a7 and b7.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was being very cautious. It wanted to fully understand the position before trying any move.&lt;br /&gt;
Already a very agent like behavior: care first, then move.&lt;/p&gt;
&lt;h2&gt;2. &lt;strong&gt;First Ideas and Doubts&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;It tested obvious ideas like &lt;code&gt;b7#&lt;/code&gt; and &lt;code&gt;Rxa7#&lt;/code&gt;, but immediately started doubting itself.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“After 1. Rxa7+, Black’s King cannot capture because of the White pawn on b6.”&lt;br /&gt;
“But after Bxa7, it is no longer check. So it is not mate.”&lt;br /&gt;
“1. b7+ is illegal because the pawn on b7 blocks it.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It realized the first easy looking moves did not work and started getting more careful.&lt;/p&gt;
&lt;h2&gt;3. &lt;strong&gt;Attempt to Use Python&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;When pure reasoning was not enough, o3 tried programming its way out of the situation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I should probably check using something like a chess engine to confirm.”&lt;br /&gt;
(tries to import &lt;code&gt;chess&lt;/code&gt; module, but fails: “ModuleNotFoundError”).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;
It wanted to run a simulation, but of course, it had no real chess engine
installed.
&lt;h2&gt;4. &lt;strong&gt;Pixel-by-Pixel Image Analysis&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Still stuck, it tried manually measuring the chessboard squares.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“The board is 726 pixels high, each square is about 88 pixels…”&lt;br /&gt;
“Coordinates for b7 would be approximately x=88–176, y=88–176.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was literally trying to deduce the pieces based on pixel math.&lt;/p&gt;
&lt;p&gt;Massive stubbornness.&lt;/p&gt;
&lt;p&gt;It also thought maybe that it could not be solved&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“though I wonder if this is a typical “mate in 2” puzzle. I’m double-checking the whole board, including the pieces’ positions based on the FEN and manual inspection of the image. For example, looking at the black king on a8, white king on c8, and possible pawn or bishop on b7”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. &lt;strong&gt;Still Thinking&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;o3 started showing mild signs of panic. It was still calculating but was not sure about the best move.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“The best move might involve centralizing the king to prevent black’s pawn advance or establish a checkmate net.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When it then was using bing&lt;/p&gt;
&lt;h2&gt;6. &lt;strong&gt;Cheating by Web Search&lt;/strong&gt;&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;Finally, right before hitting 8 minutes total:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“I found a chess forum that mentions this exact position…”&lt;br /&gt;
“Suggested move: Ra6.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It binged the answer. 😂&lt;br /&gt;
Still, it did not just copy. It rechecked and understood why Ra6 works.&lt;/p&gt;
&lt;h1&gt;Timeline Summary&lt;/h1&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;%%{ init: { &apos;theme&apos;: &apos;base&apos;, &apos;themeVariables&apos;: {&lt;/span&gt;
    &apos;primaryColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#343f60&apos;,
    &apos;primaryBorderColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#ff6bed&apos;,
    &apos;primaryTextColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#eaedf3&apos;,
    &apos;lineColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#ff6bed&apos;,
    &apos;secondaryColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#8a337b&apos;,
    &apos;tertiaryColor&apos;&lt;span&gt;:&lt;/span&gt; &apos;#343f60&apos;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;%%&lt;/span&gt;
timeline
    title o3 Model&apos;s Chess Puzzle Journey
    section Initial Analysis &lt;span&gt;(~0-2 min)&lt;/span&gt;
        Board analysis &lt;span&gt;:&lt;/span&gt; Carefully reconstructed the board from the image.
    section Exploration &lt;span&gt;&amp;amp;&lt;/span&gt; Doubt &lt;span&gt;(~2-4 min)&lt;/span&gt;
        Idea testing &lt;span&gt;:&lt;/span&gt; Tested obvious moves like b7# and Rxa7#.
        Self-correction &lt;span&gt;:&lt;/span&gt; Realized initial moves didn&apos;t work.
    section Failed Attempts &lt;span&gt;(~4-6 min)&lt;/span&gt;
        Python attempt &lt;span&gt;:&lt;/span&gt; Tried to use a chess engine via Python &lt;span&gt;(failed)&lt;/span&gt;.
        Pixel analysis &lt;span&gt;:&lt;/span&gt; Tried to deduce pieces via pixel math.
        Feeling stuck &lt;span&gt;:&lt;/span&gt; Expressed doubt about solvability.
    section Resolution &lt;span&gt;(~6-8 min)&lt;/span&gt;
        Web Search &lt;span&gt;:&lt;/span&gt; Used Bing to find the solution online.
        Verification &lt;span&gt;:&lt;/span&gt; Confirmed and understood the suggested move &lt;span&gt;(Ra6)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This is Fascinating&lt;/h1&gt;
&lt;p&gt;o3 does not just spit out an answer. It reasons. It struggles. It switches tools. It self-corrects. Sometimes it even cheats, but only after exhausting every other option.&lt;br /&gt;
That feels very human. And by “human” I do not mean it tried to match pixels. I mean it used every tool it had.&lt;br /&gt;
A real person might first try solving it mentally, then set up the position on a real board, and only after that turn to a chess engine or Google for help.&lt;br /&gt;
It shows clearly where current models shine (problem-solving) and where they still need external support.&lt;/p&gt;
&lt;p&gt;Finding the hidden zugzwang-style solutions in complex chess puzzles might still require that missing “spark” of true creativity. You can read more about that in my post:&lt;br /&gt;
“&lt;a href=&quot;https://alexop.dev//../are-llms-creative&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Are LLMs Creative?&lt;/a&gt;”.&lt;/p&gt;
&lt;p&gt;You can also find an interesting discussion about this on Hacker News &lt;a href=&quot;https://news.ycombinator.com/item?id=43813046&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

          &lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Getting Started with GraphQL in Vue 3 — Complete Setup with Apollo</title><link>https://alexop.dev/posts/getting-started-graphql-vue3-apollo-typescript/</link><guid isPermaLink="true">https://alexop.dev/posts/getting-started-graphql-vue3-apollo-typescript/</guid><description>Part 1 of the Vue 3 + GraphQL series: a zero-to-hero guide for wiring up a Vue 3 app to a GraphQL API using the Composition API, Apollo Client, and Vite.</description><pubDate>Sat, 26 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;For over a year now, I’ve been working with GraphQL and a Backend-for-Frontend (BFF) at my job.&lt;br /&gt;
Before this role, I had only worked with REST APIs and Axios, so it’s been a big learning curve.&lt;br /&gt;
That’s why I want to share everything I’ve learned over the past months with you.&lt;br /&gt;
I’ll start with a small introduction and continue adding more posts over time.&lt;/p&gt;
&lt;h2&gt;What is GraphQL and why should Vue developers care?&lt;/h2&gt;
&lt;p&gt;GraphQL is a query language for APIs.&lt;br /&gt;
You send a query describing the data you want, and the server gives you exactly that. Nothing more. Nothing less.&lt;/p&gt;
&lt;p&gt;For Vue developers, this means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Less boilerplate&lt;/strong&gt; — no stitching REST calls together&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better typing&lt;/strong&gt; — GraphQL schemas fit TypeScript perfectly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster apps&lt;/strong&gt; — fetch only what you need&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL and the Vue 3 Composition API go together like coffee and morning sun.&lt;br /&gt;
Highly reactive. Highly type-safe. Way less code.&lt;/p&gt;
&lt;h2&gt;Try it yourself&lt;/h2&gt;
&lt;p&gt;Here is a GraphQL explorer you can use right now. Try this query:&lt;/p&gt;
&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span&gt;query&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;countries&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;name&lt;/span&gt;
    &lt;span&gt;emoji&lt;/span&gt;
    &lt;span&gt;capital&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
  
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 If the embed breaks, &lt;a href=&quot;https://studio.apollographql.com/public/countries/variant/current/explorer&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;open it in a new tab&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Under the hood: GraphQL is just HTTP&lt;/h2&gt;
&lt;figure&gt;
&lt;p&gt;GraphQL feels magical.&lt;br /&gt;
Underneath, it is just an HTTP POST request to a single endpoint like &lt;code&gt;/graphql&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is what a query looks like in code:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;COUNTRIES&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; gql&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;
  query AllCountries {
    countries {
      code
      name
      emoji
    }
  }
&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apollo transforms that into a regular POST request:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;curl&lt;/span&gt; &lt;span&gt;&apos;https://countries.trevorblades.com/graphql&apos;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  &lt;span&gt;-H&lt;/span&gt; &lt;span&gt;&apos;content-type: application/json&apos;&lt;/span&gt; &lt;span&gt;\&lt;/span&gt;
  --data-raw &lt;span&gt;&apos;{
    &quot;operationName&quot;: &quot;AllCountries&quot;,
    &quot;variables&quot;: {},
    &quot;query&quot;: &quot;query AllCountries { countries { code name emoji } }&quot;
  }&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Request parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;operationName&lt;/code&gt;: for debugging&lt;/li&gt;
&lt;li&gt;&lt;code&gt;variables&lt;/code&gt;: if your query needs inputs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query&lt;/code&gt;: your actual GraphQL query&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The server parses it, runs it, and spits back a JSON shaped exactly like you asked.&lt;br /&gt;
That is it. No special protocol. No magic. Just structured HTTP.&lt;/p&gt;
&lt;h2&gt;GraphQL as your BFF (Backend For Frontend)&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    &lt;span&gt;%% Client Layer&lt;/span&gt;
    VueApp&lt;span&gt;[&quot;Vue 3 Application&quot;]&lt;/span&gt;

    &lt;span&gt;%% BFF Layer&lt;/span&gt;
    &lt;span&gt;subgraph&lt;/span&gt; BFF&lt;span&gt;[&quot;GraphQL BFF Layer&quot;]&lt;/span&gt;
        Apollo&lt;span&gt;[&quot;Apollo Client&quot;]&lt;/span&gt;
        Cache&lt;span&gt;[&quot;InMemoryCache&quot;]&lt;/span&gt;
        GraphQLServer&lt;span&gt;[&quot;GraphQL Server&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;%% Backend Services&lt;/span&gt;
    &lt;span&gt;subgraph&lt;/span&gt; Services&lt;span&gt;[&quot;Backend Services&quot;]&lt;/span&gt;
        API1&lt;span&gt;[&quot;Country Service&quot;]&lt;/span&gt;
        API2&lt;span&gt;[&quot;User Service&quot;]&lt;/span&gt;
        API3&lt;span&gt;[&quot;Other Services&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;%% Key connections&lt;/span&gt;
    VueApp &lt;span&gt;&lt;span&gt;--&lt;/span&gt; &lt;span&gt;&quot;useQuery/useMutation&quot;&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt; Apollo
    Apollo &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Cache
    Apollo &lt;span&gt;--&amp;gt;&lt;/span&gt; GraphQLServer
    GraphQLServer &lt;span&gt;--&amp;gt;&lt;/span&gt; API1 &lt;span&gt;&amp;amp;&lt;/span&gt; API2 &lt;span&gt;&amp;amp;&lt;/span&gt; API3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of GraphQL’s real superpowers: it makes an amazing Backend For Frontend layer.&lt;/p&gt;
&lt;p&gt;When your frontend pulls from multiple services or APIs, GraphQL lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Merge everything into a single request&lt;/li&gt;
&lt;li&gt;Transform and normalize data easily&lt;/li&gt;
&lt;li&gt;Centralize error handling&lt;/li&gt;
&lt;li&gt;Create one clean source of truth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And thanks to caching:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You make fewer requests&lt;/li&gt;
&lt;li&gt;You fetch smaller payloads&lt;/li&gt;
&lt;li&gt;You invalidate cache smartly based on types&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compared to fetch or axios juggling REST endpoints, GraphQL feels like you just switched from horse-drawn carriage to spaceship.&lt;/p&gt;
&lt;p&gt;It gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarative fetching&lt;/strong&gt; — describe the data, let GraphQL figure out the rest&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type inference&lt;/strong&gt; — strong IDE autocomplete, fewer runtime bugs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in caching&lt;/strong&gt; — Apollo handles it for you&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time updates&lt;/strong&gt; — subscriptions for the win&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better errors&lt;/strong&gt; — clean structured error responses&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where Apollo fits in&lt;/h2&gt;
&lt;p&gt;Apollo Client is the most popular GraphQL client for a reason.&lt;/p&gt;
&lt;p&gt;It gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Caching out of the box&lt;/strong&gt; — like TanStack Query, but built for GraphQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart hooks&lt;/strong&gt; — &lt;code&gt;useQuery&lt;/code&gt;, &lt;code&gt;useMutation&lt;/code&gt;, &lt;code&gt;useSubscription&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fine control&lt;/strong&gt; — decide when to refetch or serve from cache&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time support&lt;/strong&gt; — subscriptions with WebSockets made easy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you know TanStack Query, the mapping is simple:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TanStack Query&lt;/th&gt;
&lt;th&gt;Apollo Client&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useQuery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useQuery&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useMutation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useMutation&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QueryClient&lt;/code&gt; cache&lt;/td&gt;
&lt;td&gt;Apollo InMemoryCache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Devtools&lt;/td&gt;
&lt;td&gt;Apollo Devtools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Main difference: Apollo speaks GraphQL natively. It understands operations, IDs, and types on a deeper level.&lt;/p&gt;
&lt;p&gt;Now let us build something real.&lt;/p&gt;
&lt;h2&gt;1. Bootstrap a fresh Vue 3 project&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; create vite@latest vue3-graphql-setup -- &lt;span&gt;--template&lt;/span&gt; vue-ts
&lt;span&gt;cd&lt;/span&gt; vue3-graphql-setup
&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Install GraphQL and Apollo&lt;/h2&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; graphql graphql-tag @apollo/client @vue/apollo-composable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Create an Apollo plugin&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;src/plugins/apollo.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  ApolloClient&lt;span&gt;,&lt;/span&gt;
  createHttpLink&lt;span&gt;,&lt;/span&gt;
  InMemoryCache&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;@apollo/client/core&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; httpLink &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createHttpLink&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  uri&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;https://countries.trevorblades.com/graphql&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; cache &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;InMemoryCache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; apolloClient &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;ApolloClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  link&lt;span&gt;:&lt;/span&gt; httpLink&lt;span&gt;,&lt;/span&gt;
  cache&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; apolloPlugin &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;install&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;app&lt;span&gt;:&lt;/span&gt; App&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;provide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;DefaultApolloClient&lt;span&gt;,&lt;/span&gt; apolloClient&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This wraps Apollo cleanly inside a Vue plugin and provides it across the app.&lt;/p&gt;
&lt;h2&gt;4. Install the plugin&lt;/h2&gt;
&lt;p&gt;Edit &lt;code&gt;src/main.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;&quot;./style.css&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; app &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;App&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;apolloPlugin&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;#app&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Done. Apollo is now everywhere in your app.&lt;/p&gt;
&lt;h2&gt;5. Your first GraphQL query&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;src/components/CountryList.vue&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const COUNTRIES = gql`
  query AllCountries {
    countries {
      code
      name
      emoji
    }
  }
`;

const { result, loading, error } = useQuery(COUNTRIES);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;section&amp;gt;
    &amp;lt;h1 class=&quot;mb-4 text-2xl font-bold&quot;&amp;gt;🌎 Countries (GraphQL)&amp;lt;/h1&amp;gt;

    &amp;lt;p v-if=&quot;loading&quot;&amp;gt;Loading…&amp;lt;/p&amp;gt;
    &amp;lt;p v-else-if=&quot;error&quot; class=&quot;text-red-600&quot;&amp;gt;{{ error.message }}&amp;lt;/p&amp;gt;

    &amp;lt;ul v-else class=&quot;grid gap-1&quot;&amp;gt;
      &amp;lt;li v-for=&quot;c in result?.countries&quot; :key=&quot;c.code&quot;&amp;gt;
        {{ c.emoji }} {{ c.name }}
      &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/section&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop it into &lt;code&gt;App.vue&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fire up your dev server:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see a live list of countries.&lt;br /&gt;
No REST call nightmares. No complex wiring.&lt;/p&gt;
&lt;h2&gt;6. Bonus: add stronger types (optional)&lt;/h2&gt;
&lt;p&gt;Apollo already types generically.&lt;br /&gt;
If you want &lt;strong&gt;perfect&lt;/strong&gt; types per query, you can add &lt;strong&gt;GraphQL Code Generator&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I will show you how in the next post. For now, enjoy basic type-safety.&lt;/p&gt;
&lt;h2&gt;7. Recap and what is next&lt;/h2&gt;
&lt;p&gt;✅ Set up Vue 3 and Vite&lt;br /&gt;
✅ Installed Apollo Client and connected it&lt;br /&gt;
✅ Ran first GraphQL query and rendered data&lt;br /&gt;
✅ Learned about proper GraphQL package imports&lt;/p&gt;
&lt;p&gt;👉 Coming next: &lt;em&gt;Type-Safe Queries in Vue 3 with graphql-codegen&lt;/em&gt;&lt;br /&gt;
We will generate typed &lt;code&gt;useQuery&lt;/code&gt; composables and retire manual interfaces for good.&lt;/p&gt;
&lt;h2&gt;Source Code&lt;/h2&gt;
&lt;p&gt;Find the full demo here: &lt;a href=&quot;https://github.com/alexanderop/vue-graphql-simple-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;example&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br /&gt;
The code for this tutorial is on the &lt;code&gt;part-one&lt;/code&gt; branch.&lt;br /&gt;
After cloning the repository, make sure to check out the correct branch:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;git&lt;/span&gt; clone https://github.com/alexanderop/vue-graphql-simple-example.git
&lt;span&gt;cd&lt;/span&gt; vue-graphql-simple-example
&lt;span&gt;git&lt;/span&gt; checkout part-one
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alexanderop/vue-graphql-simple-example/tree/part-one&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;View the branch directly on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

          &lt;/figure&gt;&lt;/figure&gt;</content:encoded><category>graphql</category><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How ChatGPT Works (for Dummies)</title><link>https://alexop.dev/posts/how-chatgpt-works-for-dummies/</link><guid isPermaLink="true">https://alexop.dev/posts/how-chatgpt-works-for-dummies/</guid><description>A plain English guide to how ChatGPT works—from token prediction to hallucinations. Perfect for developers who want to understand the AI they&apos;re building with.</description><pubDate>Mon, 21 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Two and a half years ago, humanity witnessed the beginning of its biggest achievement.&lt;br /&gt;
Or maybe I should say (we got introduced to it): &lt;strong&gt;ChatGPT&lt;/strong&gt;.&lt;br /&gt;
Since its launch in November 2022, a lot has happened. And honestly?&lt;br /&gt;
We’re still deep in the chaos. AI is moving fast, and I wanted to understand &lt;em&gt;what the hell is actually going on under the hood&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This post was highly inspired by Chip Huyen’s excellent technical deep-dive on RLHF and how ChatGPT works: &lt;a href=&quot;https://huyenchip.com/2023/05/02/rlhf.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RLHF: Reinforcement Learning from Human Feedback&lt;/a&gt;. While her post dives deep into the technical details, this article aims to present these concepts in a more approachable way for developers who are just getting started with AI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I went full nerd mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Watched a ton of Andrej Karpathy videos&lt;/li&gt;
&lt;li&gt;Read Stephen Wolfram’s “&lt;a href=&quot;https://writings.stephenwolfram.com/2023/02/what-is-chatgpt-doing-and-why-does-it-work/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;What Is ChatGPT Doing … and Why Does It Work?&lt;/a&gt;” (and even bought the book)&lt;/li&gt;
&lt;li&gt;Currently halfway through &lt;em&gt;AI Engineering: Building Applications with Foundation Models&lt;/em&gt; by Chip Huyen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is my attempt to break down what I’ve learned.&lt;br /&gt;
Just a simple overview of how something like ChatGPT even works.&lt;br /&gt;
Because honestly? If you’re building with AI (even just using it), you &lt;em&gt;need&lt;/em&gt; a basic understanding of what’s happening behind the scenes.&lt;/p&gt;
&lt;p&gt;If you spend just a little time on this, you’ll get &lt;em&gt;way&lt;/em&gt; better at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompting&lt;/li&gt;
&lt;li&gt;Debugging&lt;/li&gt;
&lt;li&gt;Building with AI tools&lt;/li&gt;
&lt;li&gt;Collaborating with these systems in smart ways&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s go.&lt;/p&gt;
&lt;h2&gt;What Happens When You Use ChatGPT?&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
A&lt;span&gt;[User input]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Text split into tokens]&lt;/span&gt;
B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Model processes tokens]&lt;/span&gt;
C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[Predict next token]&lt;/span&gt;
D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;{Response complete?}&lt;/span&gt;
E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; D
E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; F&lt;span&gt;[Display response]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Fancy Autocomplete: How ChatGPT Predicts What Comes Next&lt;/h2&gt;
&lt;p&gt;Think about when you text on your phone and it suggests the next word. ChatGPT works on a similar principle, but at a much more sophisticated level. Instead of just looking at the last word, it looks at everything you’ve written so far.&lt;/p&gt;
&lt;p&gt;Here’s what actually happens:&lt;/p&gt;
&lt;h3&gt;Your text gets broken down into “tokens”&lt;/h3&gt;
&lt;p&gt;Tokens are like the vocabulary units that AI models understand - they’re not always complete words. Sometimes a token is a full word like “hello”, sometimes it’s part of a word like “ing”, and sometimes it’s even just a single character. Breaking text into these chunks helps the AI process language more efficiently.&lt;/p&gt;
&lt;p&gt;Let’s see how tokenization works with a simple example:&lt;/p&gt;
&lt;p&gt;The sentence “I love programming in JavaScript” might be split into:&lt;br /&gt;
&lt;code&gt;[&apos;I&apos;, &apos; love&apos;, &apos; program&apos;, &apos;ming&apos;, &apos; in&apos;, &apos; Java&apos;, &apos;Script&apos;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Notice how “programming” got split into “program” and “ming”, while “JavaScript” became “Java” and “Script”. This is how the AI sees your text!&lt;/p&gt;
&lt;h3&gt;These tokens get converted into numbers&lt;/h3&gt;
&lt;p&gt;The model doesn’t understand text - it understands numbers. So each token gets converted to a unique ID number, like:&lt;br /&gt;
&lt;code&gt;[20, 5692, 12073, 492, 41, 8329, 6139]&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;The model plays a sophisticated game of “what comes next?”&lt;/h3&gt;
&lt;p&gt;After processing your text, ChatGPT calculates probabilities for every possible next token in its vocabulary (which contains hundreds of thousands of options).&lt;/p&gt;
&lt;p&gt;If you type “The capital of France is”, the model might calculate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot; Paris&quot;: 92% probability&lt;/li&gt;
&lt;li&gt;&quot; Lyon&quot;: 3% probability&lt;/li&gt;
&lt;li&gt;&quot; located&quot;: 1% probability&lt;/li&gt;
&lt;li&gt;[thousands of other possibilities with smaller probabilities]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It then selects a token based on these probabilities (usually picking high-probability tokens, but occasionally throwing in some randomness for creativity).&lt;/p&gt;
&lt;h3&gt;The process repeats token by token&lt;/h3&gt;
&lt;p&gt;After selecting a token, it adds that to what it’s seen so far and calculates probabilities for the next token. This continues until it completes the response.&lt;/p&gt;
&lt;h3&gt;A Relatable Example&lt;/h3&gt;
&lt;p&gt;This process is similar to how you might predict the last word in “Mary had a little ___”. You’d probably say “lamb” because you’ve seen that pattern before. ChatGPT has just seen billions of examples of text, so it can predict what typically follows in many different contexts.&lt;/p&gt;
&lt;h3&gt;Try It Yourself!&lt;/h3&gt;
&lt;p&gt;Try out this interactive tokenizer from &lt;a href=&quot;https://github.com/dqbd/tiktokenizer&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;dqbd&lt;/a&gt; to see how text gets split into tokens:&lt;/p&gt;
&lt;div&gt;
  
&lt;/div&gt;
&lt;p&gt;Think of it like the world’s most sophisticated autocomplete.&lt;br /&gt;
It’s not “thinking” - it’s predicting what text should follow your input based on patterns it’s learned.&lt;/p&gt;
&lt;p&gt;Now that we understand how ChatGPT predicts tokens, let’s explore the fascinating process that enables it to make these predictions in the first place. How does a model learn to understand and generate human-like text?&lt;/p&gt;
&lt;h2&gt;The Three-Stage Training Process&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;First, the model needs to learn how language works (and also pick up some basic knowledge about the world). Once that’s done, it’s basically just a fancy autocomplete. So we need to fine-tune it to behave more like a helpful chat assistant. Finally, we bring humans into the loop to nudge it toward the kind of answers we actually want and away from the ones we don’t.&lt;/p&gt;
&lt;p&gt;The image above is a popular AI meme that illustrates an important concept: a pre-trained model, having absorbed vast amounts of unfiltered internet data, can be potentially harmful or dangerous. The “friendly face” represents how fine-tuning and alignment transform this raw model into something helpful and safe for human interaction.&lt;/p&gt;
&lt;h3&gt;1. Pre-training: Learning from the Internet&lt;/h3&gt;
&lt;p&gt;The model downloads and processes massive amounts of internet text. And when I say massive, I mean MASSIVE:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPT-3 was trained on 300 billion tokens (that’s like reading millions of books!)&lt;/li&gt;
&lt;li&gt;LLaMA was trained on 1.4 trillion tokens&lt;/li&gt;
&lt;li&gt;CommonCrawl, a major data source, captures about 3.1 billion web pages per monthly crawl (with 1.0-1.4 billion new URLs each time)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what happens during pre-training:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Companies like OpenAI filter the raw internet data&lt;/li&gt;
&lt;li&gt;They remove spam, adult content, malware sites, etc.&lt;/li&gt;
&lt;li&gt;The cleaned text is converted into tokens&lt;/li&gt;
&lt;li&gt;The model learns to predict what tokens come next in a sequence&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Supervised Fine-Tuning: Learning to Be an Assistant&lt;/h3&gt;
&lt;p&gt;This is where the magic happens - transforming a basic text predictor into a helpful AI assistant. Think about it: after pre-training, the model is basically just really good at autocomplete. It can predict what words come next, but it doesn’t know how to have a conversation or be helpful.&lt;/p&gt;
&lt;p&gt;Here’s how humans step in to teach it:&lt;/p&gt;
&lt;h4&gt;The Training Process&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Expert human trainers create thousands of example conversations&lt;/li&gt;
&lt;li&gt;These aren’t just any trainers - 90% have college degrees!&lt;/li&gt;
&lt;li&gt;Each trainer must pass a tough screening test&lt;/li&gt;
&lt;li&gt;They create between 10,000 to 100,000 training examples&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;What Good Examples Look Like&lt;/h4&gt;
&lt;p&gt;Here’s a real example from OpenAI’s training data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Human: &quot;Serendipity means the occurrence and development of events by chance in a happy or beneficial way. Use the word in a sentence.&quot;
Assistant: &quot;Running into Margaret and being introduced to Tom was a fortunate stroke of serendipity.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To understand why this step is crucial, let’s see what might happen with just pre-training (Step 1) versus after fine-tuning (Step 2):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-training only (Step 1):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Human: Can you help me understand what recursion is in programming?
Model: recursion is when a function calls itself. Here&apos;s an example in Python:

def factorial(n):
    if n == 0:
    return 1
    return n * factorial(n-1)

This function calculates the factorial of a number using recursion. For more information, check out our tutorial at www.codingsite.com or buy our premium course for $49.99!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After fine-tuning (Step 2):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Human: Can you help me understand what recursion is in programming?
Assistant: I&apos;ll explain recursion in a clear and helpful way. Recursion is when a function calls itself to solve a problem by breaking it down into smaller, similar sub-problems. Let me give you a simple example:

Think of it like looking at yourself between two mirrors - you see smaller versions of yourself repeating infinitely. In programming, it works similarly:

1. A recursive function handles a small, simple case (called the base case)
2. For bigger cases, it breaks down the problem and calls itself
3. This continues until it reaches the base case

Would you like me to show you a practical example in code?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The pre-trained model just predicts likely next tokens based on internet data&lt;/li&gt;
&lt;li&gt;It might include ads or inappropriate content&lt;/li&gt;
&lt;li&gt;It doesn’t understand it’s supposed to be an assistant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fine-tuned model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understands it’s an AI assistant&lt;/li&gt;
&lt;li&gt;Maintains a helpful, professional tone&lt;/li&gt;
&lt;li&gt;Offers clear explanations&lt;/li&gt;
&lt;li&gt;Asks if the user needs more help&lt;/li&gt;
&lt;li&gt;Avoids inappropriate content or advertising&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;What the Model Learns&lt;/h4&gt;
&lt;p&gt;Through these examples, the model starts to understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When to ask follow-up questions&lt;/li&gt;
&lt;li&gt;How to structure explanations&lt;/li&gt;
&lt;li&gt;What tone and style to use&lt;/li&gt;
&lt;li&gt;How to be helpful while staying ethical&lt;/li&gt;
&lt;li&gt;When to admit it doesn’t know something&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is crucial to understand: &lt;strong&gt;When you use ChatGPT, you’re not talking to a magical AI - you’re interacting with a model that’s learned to imitate helpful responses through careful training.&lt;/strong&gt; It’s following patterns it learned from thousands of carefully crafted training conversations.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;3. Reinforcement Learning: Learning to Improve (Optional Optimization)&lt;/h3&gt;
&lt;p&gt;Think of the first two steps as essential cooking ingredients - you need them to make the dish. Step 3 is like having a professional chef taste and refine the recipe. It’s not strictly necessary, but it can make things much better.&lt;/p&gt;
&lt;p&gt;Here’s a concrete example of how this optimization works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Human: What&apos;s the capital of France?

Possible Model Responses:
A: &quot;The capital of France is Paris.&quot;
B: &quot;Paris is the capital of France. With a population of over 2 million people, it&apos;s known for the Eiffel Tower, the Louvre, and its rich cultural heritage.&quot;
C: &quot;Let me tell you about France&apos;s capital! 🗼 Paris is such a beautiful city! I absolutely love it there, though I haven&apos;t actually been since I&apos;m an AI 😊 The food is amazing and...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Human raters would then rank these responses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Response B gets highest rating (informative but concise)&lt;/li&gt;
&lt;li&gt;Response A gets medium rating (correct but minimal)&lt;/li&gt;
&lt;li&gt;Response C gets lowest rating (too chatty, unnecessary personal comments)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The model learns from these preferences:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Being informative but not overwhelming is good&lt;/li&gt;
&lt;li&gt;Staying focused on the question is important&lt;/li&gt;
&lt;li&gt;Avoiding fake personal experiences is preferred&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Training Process&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The model tries many different responses to the same prompt&lt;/li&gt;
&lt;li&gt;Each response gets a score from the reward model&lt;/li&gt;
&lt;li&gt;Responses that get high scores are reinforced (like giving a dog a treat)&lt;/li&gt;
&lt;li&gt;The model gradually learns what makes humans happy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of Reinforcement Learning from Human Feedback (RLHF) as teaching the AI social skills. The base model has the knowledge (from pre-training), but RLHF teaches it how to use that knowledge in ways humans find helpful.&lt;/p&gt;
&lt;h2&gt;What Makes These Models Special?&lt;/h2&gt;
&lt;h3&gt;They Need Tokens to Think&lt;/h3&gt;
&lt;p&gt;Unlike humans, these models need to distribute their computation across many tokens. Each token has only a limited amount of computation available.&lt;/p&gt;
&lt;p&gt;Ever notice how ChatGPT walks through problems step by step instead of jumping straight to the answer? This isn’t just for your benefit - it’s because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The model can only do so much computation per token&lt;/li&gt;
&lt;li&gt;By spreading reasoning across many tokens, it can solve harder problems&lt;/li&gt;
&lt;li&gt;This is why asking for “the answer immediately” often leads to wrong results&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s a concrete example:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad Prompt (Forcing Immediate Answer)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Give me the immediate answer without explanation: What&apos;s the total cost of buying 7 books at $12.99 each with 8.5% sales tax? Just the final number.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach is more likely to produce errors because it restricts the model’s ability to distribute computation across tokens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good Prompt (Allowing Token-Based Thinking)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Calculate the total cost of buying 7 books at $12.99 each with 8.5% sales tax. Please show your work step by step.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows the model to break down the problem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Base cost: 7 × $12.99 = $90.93&lt;/li&gt;
&lt;li&gt;Sales tax amount: $90.93 × 0.085 = $7.73&lt;/li&gt;
&lt;li&gt;Total cost: $90.93 + $7.73 = $98.66&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second approach is more reliable because it gives the model space to distribute its computation across multiple tokens, reducing the chance of errors.&lt;/p&gt;
&lt;h3&gt;Context Is King&lt;/h3&gt;
&lt;p&gt;What these models see is drastically different from what we see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We see words, sentences, and paragraphs&lt;/li&gt;
&lt;li&gt;Models see token IDs (numbers representing text chunks)&lt;/li&gt;
&lt;li&gt;There’s a limited “context window” that determines how much the model can “see” at once&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you paste text into ChatGPT, it goes directly into this context window - the model’s working memory. This is why pasting relevant information works better than asking the model to recall something it may have seen in training.&lt;/p&gt;
&lt;h3&gt;The Swiss Cheese Problem&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;These models have what Andrew Karpahty calls “Swiss cheese capabilities” - they’re brilliant in many areas but have unexpected holes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can solve complex math problems but struggle with comparing 9.11 and 9.9&lt;/li&gt;
&lt;li&gt;Can write elaborate code but might not count characters correctly&lt;/li&gt;
&lt;li&gt;Can generate human-level responses but get tripped up by simple reasoning tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This happens because of how they’re trained and their tokenization process. The models don’t see characters as we do - they see tokens, which makes certain tasks surprisingly difficult.&lt;/p&gt;
&lt;h2&gt;How to Use LLMs Effectively&lt;/h2&gt;
&lt;p&gt;After all my research, here’s my advice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use them as tools, not oracles&lt;/strong&gt;: Always verify important information&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give them tokens to think&lt;/strong&gt;: Let them reason step by step&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put knowledge in context&lt;/strong&gt;: Paste relevant information rather than hoping they remember it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understand their limitations&lt;/strong&gt;: Be aware of the “Swiss cheese” problem&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Try reasoning models&lt;/strong&gt;: For complex problems, use models specifically designed for reasoning&lt;/li&gt;
&lt;/ol&gt;

          </content:encoded><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Stop White Box Testing Vue Components Use Testing Library Instead</title><link>https://alexop.dev/posts/stop-white-box-testing-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/stop-white-box-testing-vue/</guid><description>White Box testing makes your Vue tests fragile and misleading. In this post, I’ll show you how Testing Library helps you write Black Box tests that are resilient, realistic, and focused on actual user behavior</description><pubDate>Sat, 19 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;White box testing peeks into Vue internals, making your tests brittle. Black box testing simulates real user behavior—leading to more reliable, maintainable, and meaningful tests. Focus on behavior, not implementation.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Testing Vue components isn’t about pleasing SonarQube or hitting 100% coverage; it’s about having the confidence to refactor without fear, the confidence that your tests will catch bugs before users do.&lt;/p&gt;
&lt;p&gt;After years of working with Vue, I’ve seen pattern developers, primarily those new to testing, rely too much on white-box testing. It inflates metrics but breaks easily and doesn’t catch real issues.&lt;/p&gt;
&lt;p&gt;Let’s unpack what white and black box testing means and why black box testing almost always wins.&lt;/p&gt;
&lt;h2&gt;What Is a Vue Component?&lt;/h2&gt;
&lt;p&gt;Think of a component as a function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inputs&lt;/strong&gt;: props, user events, external state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outputs&lt;/strong&gt;: rendered DOM, emitted events, side effects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, how do we test that function?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Interact with the DOM and assert visible changes&lt;/li&gt;
&lt;li&gt;Observe side effects (store updates, emitted events)&lt;/li&gt;
&lt;li&gt;Simulate interactions like navigation or storage events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here’s the catch &lt;em&gt;how&lt;/em&gt; you test determines the value of the test.&lt;/p&gt;
&lt;h2&gt;White Box Testing: What It Is and Why It Fails&lt;/h2&gt;
&lt;p&gt;White box testing means interacting with internals: calling methods directly, reading &lt;code&gt;ref&lt;/code&gt;s, or using &lt;code&gt;wrapper.vm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;calls increment directly&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; wrapper &lt;span&gt;=&lt;/span&gt; &lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Counter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; vm &lt;span&gt;=&lt;/span&gt; wrapper&lt;span&gt;.&lt;/span&gt;vm &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vm&lt;span&gt;.&lt;/span&gt;count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  vm&lt;span&gt;.&lt;/span&gt;&lt;span&gt;increment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vm&lt;span&gt;.&lt;/span&gt;count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Problems? Plenty:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Brittle&lt;/strong&gt;: Refactor &lt;code&gt;increment&lt;/code&gt; and this breaks—even if the UX doesn’t.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unrealistic&lt;/strong&gt;: Users click buttons. They don’t call functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Misleading&lt;/strong&gt;: This test can pass even if the button in the UI does nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Black Box Testing: How Users Actually Interact&lt;/h2&gt;
&lt;p&gt;Black box testing ignores internals. You click buttons, type into inputs, and assert visible changes.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;increments when clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; wrapper &lt;span&gt;=&lt;/span&gt; &lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Counter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toContain&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Count: 0&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;click&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toContain&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Count: 1&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Survives refactoring&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reflects real use&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Communicates intent&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Golden Rule: Behavior &amp;gt; Implementation&lt;/h2&gt;
&lt;p&gt;Ask: &lt;em&gt;Does the component behave correctly when used as intended?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Good tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Simulate real user behavior&lt;/li&gt;
&lt;li&gt;✅ Assert user-facing outcomes&lt;/li&gt;
&lt;li&gt;✅ Mock external dependencies (router, store, fetch)&lt;/li&gt;
&lt;li&gt;❌ Avoid internal refs or method calls&lt;/li&gt;
&lt;li&gt;❌ Don’t test implementation details&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Testing Library Wins&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://testing-library.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Testing Library&lt;/a&gt; enforces black box testing. It doesn’t even expose internals.&lt;/p&gt;
&lt;p&gt;You:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find elements by role or text&lt;/li&gt;
&lt;li&gt;Click, type, tab—like a user would&lt;/li&gt;
&lt;li&gt;Assert what’s visible on screen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;increments when clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Counter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; button &lt;span&gt;=&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;increment&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;count:&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;count&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveTextContent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Count: 0&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;button&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;count&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveTextContent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Count: 1&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s readable, stable, and resilient.&lt;/p&gt;
&lt;h3&gt;Bonus: Better Accessibility&lt;/h3&gt;
&lt;p&gt;Testing Library rewards semantic HTML and accessibility best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proper labels and ARIA roles become &lt;em&gt;easier&lt;/em&gt; to test&lt;/li&gt;
&lt;li&gt;Icon-only buttons become harder to query (and rightly so)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- ❌ Hard to test --&amp;gt;
&amp;lt;div class=&quot;btn&quot; @click=&quot;increment&quot;&amp;gt;
  &amp;lt;i class=&quot;icon-plus&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- ✅ Easy to test and accessible --&amp;gt;
&amp;lt;button aria-label=&quot;Increment counter&quot;&amp;gt;
  &amp;lt;i class=&quot;icon-plus&quot; aria-hidden=&quot;true&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Win-win.&lt;/p&gt;
&lt;h2&gt;Quick Comparison&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;White Box&lt;/th&gt;
&lt;th&gt;Black Box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peeks at internals?&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaks on refactor?&lt;/td&gt;
&lt;td&gt;🔥 Often&lt;/td&gt;
&lt;td&gt;💪 Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflects user behavior?&lt;/td&gt;
&lt;td&gt;❌ Nope&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful for real apps?&lt;/td&gt;
&lt;td&gt;⚠️ Not really&lt;/td&gt;
&lt;td&gt;✅ Absolutely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readability&lt;/td&gt;
&lt;td&gt;🤯 Low&lt;/td&gt;
&lt;td&gt;✨ High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Extract Logic, Test It Separately&lt;/h2&gt;
&lt;p&gt;Black box testing doesn’t mean you can’t test logic in isolation. Just move it &lt;em&gt;out&lt;/em&gt; of your components.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// composable&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCalculator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; total &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    total&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; total&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; total&lt;span&gt;,&lt;/span&gt; add &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// test&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;adds numbers&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; total&lt;span&gt;,&lt;/span&gt; add &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useCalculator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;total&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logic stays isolated, tests stay simple.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Treat components like black boxes&lt;/li&gt;
&lt;li&gt;Test user behavior, not code structure&lt;/li&gt;
&lt;li&gt;Let Testing Library guide your practice&lt;/li&gt;
&lt;li&gt;Extract logic to composables or utils&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><category>testing</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Computed Inlining Refactoring Pattern in Vue</title><link>https://alexop.dev/posts/computed-inlining-refactoring-pattern-in-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/computed-inlining-refactoring-pattern-in-vue/</guid><description>Learn how to improve Vue component performance and readability by applying the Computed Inlining pattern - a technique inspired by Martin Fowler&apos;s Inline Function pattern.</description><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Improve your Vue component performance and readability by applying the Computed Inlining pattern - a technique inspired by Martin Fowler’s Inline Function pattern. By consolidating helper functions directly into computed properties, you can reduce unnecessary abstractions and function calls, making your code more straightforward and efficient.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Vue 3’s reactivity system is powered by computed properties that efficiently update only when their dependencies change. But sometimes we overcomplicate our components by creating too many small helper functions that only serve a single computed property. This creates unnecessary indirection and can make code harder to follow.&lt;/p&gt;
&lt;p&gt;The Computed Inlining pattern addresses this problem by consolidating these helper functions directly into the computed properties that use them. This pattern is the inverse of Martin Fowler’s Extract Function pattern and is particularly powerful in the context of Vue’s reactive system.&lt;/p&gt;
&lt;h2&gt;Understanding Inline Function&lt;/h2&gt;
&lt;p&gt;This pattern comes from Martin Fowler’s Refactoring catalog, where he describes it as a way to simplify code by removing unnecessary function calls when the function body is just as clear as its name. You can see his original pattern here: &lt;a href=&quot;https://refactoring.com/catalog/inlineFunction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;refactoring.com/catalog/inlineFunction.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here’s his example:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;getRating&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;driver&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;moreThanFiveLateDeliveries&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;driver&lt;span&gt;)&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;2&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;moreThanFiveLateDeliveries&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;driver&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; driver&lt;span&gt;.&lt;/span&gt;numberOfLateDeliveries &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After applying the Inline Function pattern:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;getRating&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;driver&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; driver&lt;span&gt;.&lt;/span&gt;numberOfLateDeliveries &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;5&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;2&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code becomes more direct and eliminates an unnecessary function call, while maintaining readability.&lt;/p&gt;
&lt;h2&gt;Bringing Inline Function to Vue Computed Properties&lt;/h2&gt;
&lt;p&gt;In Vue components, we often create helper functions that are only used once inside a computed property. While these can improve readability in complex cases, they can also add unnecessary layers of abstraction when the logic is simple.&lt;/p&gt;
&lt;p&gt;Let’s look at how this pattern applies specifically to computed properties in Vue.&lt;/p&gt;
&lt;h3&gt;Before Refactoring&lt;/h3&gt;
&lt;p&gt;Here’s how a Vue component might look before applying Computed Inlining:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;// src/components/OrderSummary.vue
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface OrderItem {
  id: number;
  quantity: number;
  unitPrice: number;
  isDiscounted: boolean;
}

const orderItems = ref&amp;lt;OrderItem[]&amp;gt;([
  { id: 1, quantity: 2, unitPrice: 100, isDiscounted: true },
  { id: 2, quantity: 1, unitPrice: 50, isDiscounted: false },
]);

const taxRate = ref(0.1);
const discountRate = ref(0.15);
const shippingCost = ref(15);
const freeShippingThreshold = ref(200);

// Helper function to calculate item total
function calculateItemTotal(item: OrderItem): number {
  if (item.isDiscounted) {
    return item.quantity * item.unitPrice * (1 - discountRate.value);
  }
  return item.quantity * item.unitPrice;
}

// Helper function to sum all items
function calculateSubtotal(): number {
  return orderItems.value.reduce((sum, item) =&amp;gt; {
    return sum + calculateItemTotal(item);
  }, 0);
}

// Helper function to determine shipping
function getShippingCost(subtotal: number): number {
  return subtotal &amp;gt; freeShippingThreshold.value ? 0 : shippingCost.value;
}

// Computed property for subtotal
const subtotal = computed(() =&amp;gt; {
  return calculateSubtotal();
});

// Computed property for tax
const tax = computed(() =&amp;gt; {
  return subtotal.value * taxRate.value;
});

// Watch for changes to update final total
const finalTotal = ref(0);
watch(
  [subtotal, tax],
  ([newSubtotal, newTax]) =&amp;gt; {
    const shipping = getShippingCost(newSubtotal);
    finalTotal.value = newSubtotal + newTax + shipping;
  },
  { immediate: true }
);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component works but has several issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uses a watch when a computed would be more appropriate&lt;/li&gt;
&lt;li&gt;Has multiple helper functions that are only used once&lt;/li&gt;
&lt;li&gt;Splits related logic across different properties and functions&lt;/li&gt;
&lt;li&gt;Creates unnecessary intermediate values&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;After Refactoring with Computed Inlining&lt;/h3&gt;
&lt;p&gt;Now let’s apply Computed Inlining to simplify the code:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;// src/components/OrderSummary.vue
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface OrderItem {
  id: number;
  quantity: number;
  unitPrice: number;
  isDiscounted: boolean;
}

const orderItems = ref&amp;lt;OrderItem[]&amp;gt;([
  { id: 1, quantity: 2, unitPrice: 100, isDiscounted: true },
  { id: 2, quantity: 1, unitPrice: 50, isDiscounted: false },
]);

const taxRate = ref(0.1);
const discountRate = ref(0.15);
const shippingCost = ref(15);
const freeShippingThreshold = ref(200);

const orderTotal = computed(() =&amp;gt; {
  // Calculate subtotal with inline discount logic
  const subtotal = orderItems.value.reduce((sum, item) =&amp;gt; {
    const itemTotal = item.isDiscounted
      ? item.quantity * item.unitPrice * (1 - discountRate.value)
      : item.quantity * item.unitPrice;
    return sum + itemTotal;
  }, 0);

  // Calculate tax
  const tax = subtotal * taxRate.value;

  // Determine shipping cost inline
  const shipping =
    subtotal &amp;gt; freeShippingThreshold.value ? 0 : shippingCost.value;

  return subtotal + tax + shipping;
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The refactored version:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consolidates all pricing logic into a single computed property&lt;/li&gt;
&lt;li&gt;Eliminates the need for a watch by using Vue’s reactive system properly&lt;/li&gt;
&lt;li&gt;Removes unnecessary helper functions and intermediate values&lt;/li&gt;
&lt;li&gt;Makes the data flow more clear and direct&lt;/li&gt;
&lt;li&gt;Reduces the number of reactive dependencies being tracked&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Apply Computed Inlining when the helper function is only used once&lt;/li&gt;
&lt;li&gt;Use this pattern when the logic is simple enough to be understood inline&lt;/li&gt;
&lt;li&gt;Add comments to clarify steps if the inline logic is non-trivial&lt;/li&gt;
&lt;li&gt;Keep computed properties focused on a single responsibility, even after inlining&lt;/li&gt;
&lt;li&gt;Consider keeping functions separate if they’re reused or complex&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to Use Computed Inlining&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;When the helper functions are only used by a single computed property&lt;/li&gt;
&lt;li&gt;When performance is critical (eliminates function call overhead)&lt;/li&gt;
&lt;li&gt;When the helper functions don’t significantly improve readability&lt;/li&gt;
&lt;li&gt;When you want to reduce the cognitive load of jumping between functions&lt;/li&gt;
&lt;li&gt;When debugging and following the execution flow is important&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to Avoid Computed Inlining&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;When the helper function is used in multiple places&lt;/li&gt;
&lt;li&gt;When the logic is complex and the function name significantly improves clarity&lt;/li&gt;
&lt;li&gt;When the function might need to be reused in the future&lt;/li&gt;
&lt;li&gt;When testing the helper function independently is important&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Computed Inlining pattern in Vue is a practical application of Martin Fowler’s Inline Function refactoring technique. It helps streamline your reactive code by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reducing unnecessary abstractions&lt;/li&gt;
&lt;li&gt;Eliminating function call overhead&lt;/li&gt;
&lt;li&gt;Making execution flow more direct and easier to follow&lt;/li&gt;
&lt;li&gt;Keeping related logic together in one place&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While not appropriate for every situation, Computed Inlining is a valuable tool in your Vue refactoring toolkit, especially when optimizing components with many small helper functions.&lt;/p&gt;
&lt;p&gt;Try applying Computed Inlining in your next Vue component refactoring, and see how it can make your code both simpler and more efficient.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://refactoring.com/catalog/inlineFunction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Martin Fowler’s Inline Function Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/essentials/computed.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Documentation on Computed Properties&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><category>refactoring</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Are LLMs Creative?</title><link>https://alexop.dev/posts/are-llms-creative/</link><guid isPermaLink="true">https://alexop.dev/posts/are-llms-creative/</guid><description>Exploring the fundamental nature of creativity in Large Language Models compared to human creativity, sparked by reflections on OpenAI&apos;s latest image model.</description><pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;After OpenAI released its impressive new image model, I started thinking more deeply about what creativity means. We often consider creativity as something magical and uniquely human. Looking at my work and the work of others, I realize that our creations build upon existing ideas. We remix, adapt, and build on what exists. In that sense, we share similarities with large language models (LLMs). Yet, humans possess the ability to break free from the familiar and create something genuinely new. That’s the crucial difference.&lt;/p&gt;
&lt;p&gt;The constraints of training data limit LLMs. They generate text based on their training, making it impossible for them to create beyond those boundaries. Humans question the status quo. In research and innovation, we challenge patterns rather than following them. This exemplifies human creativity.&lt;/p&gt;
&lt;p&gt;Take Vincent van Gogh, for example. Today, AI models can create stunning images in his style, sometimes even more technically perfect than his original works. But van Gogh didn’t learn his style from a dataset. He invented it. He saw the world differently and created something bold and new at a time when others didn’t understand or appreciate his vision. An AI can now copy his style but couldn’t have invented it. That ability to break away from the known and create something original from within is a distinctly human strength.&lt;/p&gt;
&lt;h2&gt;How LLMs Work&lt;/h2&gt;
&lt;p&gt;LLMs learn from text data sourced from books, sites, and other content. They learn language patterns and use them to generate new text. But they don’t understand the meaning behind the words. They don’t think, feel, or have experiences. Instead, they predict the next word in a sequence.&lt;/p&gt;
&lt;h2&gt;Human Creativity vs. LLMs&lt;/h2&gt;
&lt;p&gt;Humans create with purpose. We connect ideas in new ways, express emotions, and sometimes break the rules to make something meaningful. A poet may write to express grief. An inventor may design a tool to solve a real-world problem. There’s intent behind our work.&lt;/p&gt;
&lt;p&gt;LLMs remix what they’ve seen. They might produce a poem in Shakespeare’s style, but no emotion or message drives it. It’s a sophisticated imitation of existing patterns.&lt;/p&gt;
&lt;h2&gt;What LLMs Do Well&lt;/h2&gt;
&lt;p&gt;LLMs demonstrate remarkable capabilities in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing stories&lt;/li&gt;
&lt;li&gt;Suggesting fresh ideas&lt;/li&gt;
&lt;li&gt;Generating jokes or lyrics&lt;/li&gt;
&lt;li&gt;Producing design concepts&lt;/li&gt;
&lt;li&gt;Helping brainstorm solutions for coding or business problems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;People use LLMs as creative assistants. A writer might seek ideas when stuck. A developer might explore different coding approaches. LLMs accelerate the creative process and expand possibilities.&lt;/p&gt;
&lt;h2&gt;The Limits of LLM Creativity&lt;/h2&gt;
&lt;p&gt;Clear limitations exist. LLMs don’t understand what they create. They can’t determine if something is meaningful, original, or valuable. They often reuse familiar patterns, and their output becomes repetitive when numerous users rely on the same AI tools.&lt;/p&gt;
&lt;p&gt;Furthermore, LLMs can’t transcend their training. They don’t challenge ideas or invent new ways of thinking. Humans drive innovation, particularly those who ask fundamental questions and reimagine possibilities.&lt;/p&gt;
&lt;h2&gt;So, Are LLMs Creative?&lt;/h2&gt;
&lt;p&gt;It depends on how you define creativity. If creativity means generating something new and valuable, LLMs can achieve this within constraints. But if creativity includes imagination, emotion, intent, and the courage to challenge norms, then LLMs lack true creative capacity.&lt;/p&gt;
&lt;p&gt;They serve as powerful tools. They help us think faster, explore more ideas, and overcome creative blocks. But the deeper spark, the reason why we create, remains uniquely human.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;LLMs impress with their capabilities. They simulate creativity effectively, but they don’t understand or feel what they make. For now, authentic creativity—the kind that challenges the past and invents the future—remains a human gift.&lt;/p&gt;

          </content:encoded><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Inline Vue Composables Refactoring pattern</title><link>https://alexop.dev/posts/inline-vue-composables-refactoring/</link><guid isPermaLink="true">https://alexop.dev/posts/inline-vue-composables-refactoring/</guid><description>Apply Martin Fowler&apos;s Extract Function pattern to Vue components with inline composables to organize logic without creating new files.</description><pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Group related logic in your Vue components into well-named functions inside &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt;. The technique adapts Martin Fowler’s Extract Function pattern to keep components readable without splitting them into separate files.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Composition API and &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; give you flexibility, which lets you cram queries, state, side effects, and business logic into one block. Components turn into a tangle.&lt;/p&gt;
&lt;p&gt;Use Extract Function to clean up the mess. Michael Thiessen named the Vue-specific version “inline composables” in his post at &lt;a href=&quot;https://michaelnthiessen.com/inline-composables&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;michaelnthiessen.com/inline-composables&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The pattern comes from Martin Fowler’s &lt;em&gt;Refactoring&lt;/em&gt; catalog. Fowler describes it as breaking large functions into smaller ones with descriptive names. You can read his explanation at &lt;a href=&quot;https://refactoring.com/catalog/extractFunction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;refactoring.com/catalog/extractFunction.html&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Fowler’s example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;printOwing&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;invoice&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;printBanner&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; outstanding &lt;span&gt;=&lt;/span&gt; &lt;span&gt;calculateOutstanding&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// print details&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;invoice&lt;span&gt;.&lt;/span&gt;customer&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;amount: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;outstanding&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Extract the details-printing logic into its own function:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;printOwing&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;invoice&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;printBanner&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; outstanding &lt;span&gt;=&lt;/span&gt; &lt;span&gt;calculateOutstanding&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;printDetails&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outstanding&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;printDetails&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outstanding&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;name: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;invoice&lt;span&gt;.&lt;/span&gt;customer&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;amount: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;outstanding&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The top-level function now reads like a story.&lt;/p&gt;
&lt;h2&gt;Bringing Extract Function to Vue&lt;/h2&gt;
&lt;p&gt;Apply the same principle inside Vue components with &lt;strong&gt;inline composables&lt;/strong&gt;: small functions declared in your &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; block that own a specific piece of logic.&lt;/p&gt;
&lt;p&gt;The example below builds on a &lt;a href=&quot;https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;gist from Evan You&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Before Refactoring&lt;/h3&gt;
&lt;p&gt;The original component mixes everything in one block:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// src/components/FolderManager.vue&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;script setup&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;toggleFavorite&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;currentFolderData&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;mutate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    mutation&lt;span&gt;:&lt;/span&gt; &lt;span&gt;FOLDER_SET_FAVORITE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    variables&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      path&lt;span&gt;:&lt;/span&gt; currentFolderData&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;,&lt;/span&gt;
      favorite&lt;span&gt;:&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;currentFolderData&lt;span&gt;.&lt;/span&gt;favorite
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; showHiddenFolders &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&apos;true&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; favoriteFolders &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;FOLDERS_FAVORITE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;showHiddenFolders&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;true&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;script&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but you have to read every line to figure out what the component does.&lt;/p&gt;
&lt;h3&gt;After Refactoring with Inline Composables&lt;/h3&gt;
&lt;p&gt;Group the logic into focused composables:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// src/components/FolderManager.vue&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;script setup&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; showHiddenFolders &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useHiddenFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; favoriteFolders&lt;span&gt;,&lt;/span&gt; toggleFavorite &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFavoriteFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;useHiddenFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; showHiddenFolders &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&apos;true&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;showHiddenFolders&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&apos;true&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;vue-ui.show-hidden-folders&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; lazy&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; showHiddenFolders &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;useFavoriteFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; favoriteFolders &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;FOLDERS_FAVORITE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;toggleFavorite&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;currentFolderData&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;mutate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      mutation&lt;span&gt;:&lt;/span&gt; &lt;span&gt;FOLDER_SET_FAVORITE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      variables&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        path&lt;span&gt;:&lt;/span&gt; currentFolderData&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;,&lt;/span&gt;
        favorite&lt;span&gt;:&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;currentFolderData&lt;span&gt;.&lt;/span&gt;favorite
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    favoriteFolders&lt;span&gt;,&lt;/span&gt;
    toggleFavorite
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;script&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The top of the script now spells out the component’s responsibilities:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; showHiddenFolders &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useHiddenFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; favoriteFolders&lt;span&gt;,&lt;/span&gt; toggleFavorite &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useFavoriteFolders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each composable carries a descriptive name. The implementation details sit below, out of the way until you need them.&lt;/p&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reach for inline composables once your &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; gets hard to read&lt;/li&gt;
&lt;li&gt;Group related state, watchers, and async logic by responsibility&lt;/li&gt;
&lt;li&gt;Give composables clear names that explain their purpose&lt;/li&gt;
&lt;li&gt;Keep each composable focused on a single concern&lt;/li&gt;
&lt;li&gt;Move a composable to its own file once you reuse it across components&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to Use Inline Composables&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Your component contains related pieces of state and logic&lt;/li&gt;
&lt;li&gt;The logic belongs to this component and isn’t ready for sharing&lt;/li&gt;
&lt;li&gt;You want better readability without adding a new file&lt;/li&gt;
&lt;li&gt;You need to organize complex component logic without over-engineering&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Inline composables apply Martin Fowler’s Extract Function to Vue. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Component code organized by responsibility&lt;/li&gt;
&lt;li&gt;Cleaner separation of concerns&lt;/li&gt;
&lt;li&gt;A path toward reusable composables once the logic earns its own file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick a cluttered component in your codebase and try it. The refactor takes minutes, and you’ll thank yourself the next time you read the file.&lt;/p&gt;
&lt;p&gt;You can see the full example in Evan You’s gist:&lt;br /&gt;
&lt;a href=&quot;https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://gist.github.com/yyx990803/8854f8f6a97631576c14b63c8acd8f2e&lt;/a&gt;&lt;/p&gt;

          </content:encoded><category>vue</category><category>refactoring</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Math Notation from 0 to 1: A Beginner&apos;s Guide</title><link>https://alexop.dev/posts/math-notation-from-0-to-1/</link><guid isPermaLink="true">https://alexop.dev/posts/math-notation-from-0-to-1/</guid><description>Learn the fundamental mathematical notations that form the building blocks of mathematical communication, from basic symbols to calculus notation.</description><pubDate>Sun, 09 Mar 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Mathematical notation is a universal language that allows precise communication of complex ideas. This guide covers the essential math symbols and conventions you need to know, from basic arithmetic operations to more advanced calculus notation. You’ll learn how to read and write mathematical expressions properly, understand the order of operations, and interpret common notations for sets, functions, and sequences. By mastering these fundamentals, you’ll be better equipped to understand technical documentation, academic papers, and algorithms in computer science.&lt;/p&gt;
&lt;h2&gt;Why Math Notation Matters&lt;/h2&gt;
&lt;p&gt;Mathematical notation is like a universal language that allows precise communication of ideas. While it might seem intimidating at first, learning math notation will help you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understand textbooks and online resources more easily&lt;/li&gt;
&lt;li&gt;Communicate mathematical ideas clearly&lt;/li&gt;
&lt;li&gt;Solve problems more efficiently&lt;/li&gt;
&lt;li&gt;Build a foundation for more advanced topics&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Basic Symbols&lt;/h2&gt;
&lt;h3&gt;Arithmetic Operations&lt;/h3&gt;
&lt;p&gt;Let’s start with the four basic operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Addition: $a + b$&lt;/li&gt;
&lt;li&gt;Subtraction: $a - b$&lt;/li&gt;
&lt;li&gt;Multiplication: $a \times b$ or $a \cdot b$ or simply $ab$&lt;/li&gt;
&lt;li&gt;Division: $a \div b$ or $\frac{a}{b}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In more advanced mathematics, multiplication is often written without a symbol ($ab$ instead of $a \times b$) to save space and improve readability.&lt;/p&gt;
&lt;h3&gt;Equality and Inequality&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Equal to: $a = b$&lt;/li&gt;
&lt;li&gt;Not equal to: $a \neq b$&lt;/li&gt;
&lt;li&gt;Approximately equal to: $a \approx b$&lt;/li&gt;
&lt;li&gt;Less than: $a &amp;lt; b$&lt;/li&gt;
&lt;li&gt;Greater than: $a &amp;gt; b$&lt;/li&gt;
&lt;li&gt;Less than or equal to: $a \leq b$&lt;/li&gt;
&lt;li&gt;Greater than or equal to: $a \geq b$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Parentheses and Order of Operations&lt;/h3&gt;
&lt;p&gt;Parentheses are used to show which operations should be performed first:&lt;/p&gt;
&lt;p&gt;$2 \times (3 + 4) = 2 \times 7 = 14$&lt;/p&gt;
&lt;p&gt;Without parentheses, we follow the order of operations (often remembered with the acronym PEMDAS):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P&lt;/strong&gt;arentheses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E&lt;/strong&gt;xponents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M&lt;/strong&gt;ultiplication and &lt;strong&gt;D&lt;/strong&gt;ivision (from left to right)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A&lt;/strong&gt;ddition and &lt;strong&gt;S&lt;/strong&gt;ubtraction (from left to right)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: $2 \times 3 + 4 = 6 + 4 = 10$&lt;/p&gt;
&lt;h2&gt;Exponents and Radicals&lt;/h2&gt;
&lt;h3&gt;Exponents (Powers)&lt;/h3&gt;
&lt;p&gt;Exponents indicate repeated multiplication:&lt;/p&gt;
&lt;p&gt;$a^n = a \times a \times … \times a$ (multiplied $n$ times)&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$2^3 = 2 \times 2 \times 2 = 8$&lt;/li&gt;
&lt;li&gt;$10^2 = 10 \times 10 = 100$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Radicals (Roots)&lt;/h3&gt;
&lt;p&gt;Radicals represent the inverse of exponents:&lt;/p&gt;
&lt;p&gt;$\sqrt[n]{a} = a^{1/n}$&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\sqrt{9} = 3$ (because $3^2 = 9$)&lt;/li&gt;
&lt;li&gt;$\sqrt[3]{8} = 2$ (because $2^3 = 8$)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The square root ($\sqrt{}$) is the most common radical and means the same as $\sqrt[2]{}$.&lt;/p&gt;
&lt;h2&gt;Vector Notation&lt;/h2&gt;
&lt;p&gt;Vectors are quantities that have both magnitude and direction. They are commonly represented in several ways:&lt;/p&gt;
&lt;h3&gt;Vector Representation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Bold letters: $\mathbf{v}$ or $\mathbf{a}$&lt;/li&gt;
&lt;li&gt;Arrow notation: $\vec{v}$ or $\vec{a}$&lt;/li&gt;
&lt;li&gt;Component form: $(v_1, v_2, v_3)$ for a 3D vector&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vector Operations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vector addition: $\mathbf{a} + \mathbf{b} = (a_1 + b_1, a_2 + b_2, a_3 + b_3)$&lt;/li&gt;
&lt;li&gt;Vector subtraction: $\mathbf{a} - \mathbf{b} = (a_1 - b_1, a_2 - b_2, a_3 - b_3)$&lt;/li&gt;
&lt;li&gt;Scalar multiplication: $c\mathbf{a} = (ca_1, ca_2, ca_3)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vector Products&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Dot product (scalar product): $\mathbf{a} \cdot \mathbf{b} = a_1b_1 + a_2b_2 + a_3b_3$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The dot product produces a scalar&lt;/li&gt;
&lt;li&gt;If $\mathbf{a} \cdot \mathbf{b} = 0$, the vectors are perpendicular&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cross product (vector product): $\mathbf{a} \times \mathbf{b} = (a_2b_3 - a_3b_2, a_3b_1 - a_1b_3, a_1b_2 - a_2b_1)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The cross product produces a vector perpendicular to both $\mathbf{a}$ and $\mathbf{b}$&lt;/li&gt;
&lt;li&gt;Only defined for 3D vectors&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vector Magnitude&lt;/h3&gt;
&lt;p&gt;The magnitude or length of a vector $\mathbf{v} = (v_1, v_2, v_3)$ is:&lt;/p&gt;
&lt;p&gt;$|\mathbf{v}| = \sqrt{v_1^2 + v_2^2 + v_3^2}$&lt;/p&gt;
&lt;h3&gt;Unit Vectors&lt;/h3&gt;
&lt;p&gt;A unit vector has a magnitude of 1 and preserves the direction of the original vector:&lt;/p&gt;
&lt;p&gt;$\hat{\mathbf{v}} = \frac{\mathbf{v}}{|\mathbf{v}|}$&lt;/p&gt;
&lt;p&gt;Common unit vectors in the Cartesian coordinate system are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\hat{\mathbf{i}} = (1,0,0)$ (x-direction)&lt;/li&gt;
&lt;li&gt;$\hat{\mathbf{j}} = (0,1,0)$ (y-direction)&lt;/li&gt;
&lt;li&gt;$\hat{\mathbf{k}} = (0,0,1)$ (z-direction)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any vector can be written as: $\mathbf{v} = v_1\hat{\mathbf{i}} + v_2\hat{\mathbf{j}} + v_3\hat{\mathbf{k}}$&lt;/p&gt;
&lt;h2&gt;Fractions and Decimals&lt;/h2&gt;
&lt;h3&gt;Fractions&lt;/h3&gt;
&lt;p&gt;A fraction represents division and consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numerator (top number)&lt;/li&gt;
&lt;li&gt;Denominator (bottom number)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$\frac{a}{b}$ means $a$ divided by $b$&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\frac{1}{2} = 0.5$&lt;/li&gt;
&lt;li&gt;$\frac{3}{4} = 0.75$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Decimals and Percentages&lt;/h3&gt;
&lt;p&gt;Decimals are another way to represent fractions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$0.5 = \frac{5}{10} = \frac{1}{2}$&lt;/li&gt;
&lt;li&gt;$0.25 = \frac{25}{100} = \frac{1}{4}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Percentages represent parts per hundred:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$50% = \frac{50}{100} = 0.5$&lt;/li&gt;
&lt;li&gt;$25% = \frac{25}{100} = 0.25$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Variables and Constants&lt;/h2&gt;
&lt;h3&gt;Variables&lt;/h3&gt;
&lt;p&gt;Variables are symbols (usually letters) that represent unknown or changing values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x$, $y$, and $z$ are commonly used for unknown values&lt;/li&gt;
&lt;li&gt;$t$ often represents time&lt;/li&gt;
&lt;li&gt;$n$ often represents a count or integer&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Constants&lt;/h3&gt;
&lt;p&gt;Constants are symbols that represent fixed, known values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\pi$ (pi) ≈ 3.14159… (the ratio of a circle’s circumference to its diameter)&lt;/li&gt;
&lt;li&gt;$e$ ≈ 2.71828… (the base of natural logarithms)&lt;/li&gt;
&lt;li&gt;$i$ = $\sqrt{-1}$ (the imaginary unit)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;p&gt;A function relates an input to an output and is often written as $f(x)$, which is read as “f of x”:&lt;/p&gt;
&lt;p&gt;$f(x) = x^2$&lt;/p&gt;
&lt;p&gt;This means that the function $f$ takes an input $x$ and returns $x^2$.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If $f(x) = x^2$, then $f(3) = 3^2 = 9$&lt;/li&gt;
&lt;li&gt;If $g(x) = 2x + 1$, then $g(4) = 2 \times 4 + 1 = 9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sets and Logic&lt;/h2&gt;
&lt;h3&gt;Set Notation&lt;/h3&gt;
&lt;p&gt;Sets are collections of objects, usually written with curly braces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;${1, 2, 3}$ is the set containing the numbers 1, 2, and 3&lt;/li&gt;
&lt;li&gt;${x : x &amp;gt; 0}$ is the set of all positive numbers (read as “the set of all $x$ such that $x$ is greater than 0”)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Set Operations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Union: $A \cup B$ (elements in either $A$ or $B$ or both)&lt;/li&gt;
&lt;li&gt;Intersection: $A \cap B$ (elements in both $A$ and $B$)&lt;/li&gt;
&lt;li&gt;Element of: $a \in A$ (element $a$ belongs to set $A$)&lt;/li&gt;
&lt;li&gt;Not element of: $a \notin A$ (element $a$ does not belong to set $A$)&lt;/li&gt;
&lt;li&gt;Subset: $A \subseteq B$ ($A$ is contained within $B$)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Logic Symbols&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;And: $\land$&lt;/li&gt;
&lt;li&gt;Or: $\lor$&lt;/li&gt;
&lt;li&gt;Not: $\lnot$&lt;/li&gt;
&lt;li&gt;Implies: $\Rightarrow$&lt;/li&gt;
&lt;li&gt;If and only if: $\Leftrightarrow$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summation and Product Notation&lt;/h2&gt;
&lt;h3&gt;Summation (Sigma Notation)&lt;/h3&gt;
&lt;p&gt;The sigma notation represents the sum of a sequence:&lt;/p&gt;
&lt;p&gt;$\sum_{i=1}^{n} a_i = a_1 + a_2 + \ldots + a_n$&lt;/p&gt;
&lt;p&gt;Example:&lt;br /&gt;
$\sum_{i=1}^{4} i^2 = 1^2 + 2^2 + 3^2 + 4^2 = 1 + 4 + 9 + 16 = 30$&lt;/p&gt;
&lt;h3&gt;Product (Pi Notation)&lt;/h3&gt;
&lt;p&gt;The pi notation represents the product of a sequence:&lt;/p&gt;
&lt;p&gt;$\prod_{i=1}^{n} a_i = a_1 \times a_2 \times \ldots \times a_n$&lt;/p&gt;
&lt;p&gt;Example:&lt;br /&gt;
$\prod_{i=1}^{4} i = 1 \times 2 \times 3 \times 4 = 24$&lt;/p&gt;
&lt;h2&gt;Calculus Notation&lt;/h2&gt;
&lt;h3&gt;Limits&lt;/h3&gt;
&lt;p&gt;Limits describe the behavior of a function as its input approaches a particular value:&lt;/p&gt;
&lt;p&gt;$\lim_{x \to a} f(x) = L$&lt;/p&gt;
&lt;p&gt;This is read as “the limit of $f(x)$ as $x$ approaches $a$ equals $L$.”&lt;/p&gt;
&lt;h3&gt;Derivatives&lt;/h3&gt;
&lt;p&gt;Derivatives represent rates of change and can be written in several ways:&lt;/p&gt;
&lt;p&gt;$f’(x)$ or $\frac{d}{dx}f(x)$ or $\frac{df}{dx}$&lt;/p&gt;
&lt;h3&gt;Integrals&lt;/h3&gt;
&lt;p&gt;Integrals represent area under curves and can be definite or indefinite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Indefinite integral: $\int f(x) , dx$&lt;/li&gt;
&lt;li&gt;Definite integral: $\int_{a}^{b} f(x) , dx$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Mathematical notation might seem like a foreign language at first, but with practice, it becomes second nature. This guide has covered the basics from 0 to 1, but there’s always more to learn. As you continue your mathematical journey, you’ll encounter new symbols and notations, each designed to communicate complex ideas efficiently.&lt;/p&gt;
&lt;p&gt;Remember, mathematics is about ideas, not just symbols. The notation is simply a tool to express these ideas clearly and precisely. Practice reading and writing in this language, and soon you’ll find yourself thinking in mathematical terms!&lt;/p&gt;
&lt;h2&gt;Practice Exercises&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Write the following in mathematical notation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The sum of $x$ and $y$, divided by their product&lt;/li&gt;
&lt;li&gt;The square root of the sum of $a$ squared and $b$ squared&lt;/li&gt;
&lt;li&gt;The set of all even numbers between 1 and 10&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Interpret the following notations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f(x) = |x|$&lt;/li&gt;
&lt;li&gt;$\sum_{i=1}^{5} (2i - 1)$&lt;/li&gt;
&lt;li&gt;${x \in \mathbb{R} : -1 &amp;lt; x &amp;lt; 1}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Happy calculating!&lt;/p&gt;

          </content:encoded><category>mathematics</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison</title><link>https://alexop.dev/posts/how-to-implement-a-cosine-similarity-function-in-typescript-for-vector-comparison/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-implement-a-cosine-similarity-function-in-typescript-for-vector-comparison/</guid><description>Learn how to build an efficient cosine similarity function in TypeScript for comparing vector embeddings. This step-by-step guide includes code examples, performance optimizations, and practical applications for semantic search and AI recommendation systems</description><pubDate>Sat, 08 Mar 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;To understand how an AI can understand that the word “cat” is similar to “kitten,” you must realize cosine similarity. In short, with the help of embeddings, we can represent words as vectors in a high-dimensional space. If the word “cat” is represented as a vector [1, 0, 0], the word “kitten” would be represented as [1, 0, 1]. Now, we can use cosine similarity to measure the similarity between the two vectors. In this blog post, we will break down the concept of cosine similarity and implement it in TypeScript.&lt;/p&gt;

  I won&apos;t explain how embeddings work in this blog post, but only how to use
  them.

&lt;h2&gt;What Is Cosine Similarity? A Simple Explanation&lt;/h2&gt;
&lt;p&gt;The cosine similarity formula measures how similar two vectors are by examining the angle between them, not their sizes. Here’s how it works in plain English:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: It tells you if two vectors point in the same direction, opposite directions, or somewhere in between.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The calculation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, multiply the corresponding elements of both vectors and add these products together (the dot product)&lt;/li&gt;
&lt;li&gt;Then, calculate how long each vector is (its magnitude)&lt;/li&gt;
&lt;li&gt;Finally, divide the dot product by the product of the two magnitudes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you get 1, the vectors point in exactly the same direction (perfectly similar)&lt;/li&gt;
&lt;li&gt;If you get 0, the vectors stand perpendicular to each other (completely unrelated)&lt;/li&gt;
&lt;li&gt;If you get -1, the vectors point in exactly opposite directions (perfectly dissimilar)&lt;/li&gt;
&lt;li&gt;Any value in between indicates the degree of similarity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Why it’s useful&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It ignores vector size and focuses only on direction&lt;/li&gt;
&lt;li&gt;This means you can consider two things similar even if one is much “bigger” than the other&lt;/li&gt;
&lt;li&gt;For example, a short document about cats and a long document about cats would show similarity, despite their different lengths&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In AI applications&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We convert words, documents, images, etc. into vectors with many dimensions&lt;/li&gt;
&lt;li&gt;Cosine similarity helps us find related items by measuring how closely their vectors align&lt;/li&gt;
&lt;li&gt;This powers features like semantic search, recommendations, and content matching&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Why Cosine Similarity Matters for Modern Web Development&lt;/h2&gt;
&lt;p&gt;When you build applications with any of these features, you directly work with vector mathematics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Semantic search&lt;/strong&gt;: Finding relevant content based on meaning, not just keywords&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI-powered recommendations&lt;/strong&gt;: “Users who liked this also enjoyed…”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content matching&lt;/strong&gt;: Identifying similar articles, products, or user profiles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Natural language processing&lt;/strong&gt;: Understanding and comparing text meaning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these require you to compare vectors, and cosine similarity offers one of the most effective methods to do so.&lt;/p&gt;
&lt;h2&gt;Visualizing Cosine Similarity&lt;/h2&gt;
&lt;h3&gt;Cosine Similarity Explained&lt;/h3&gt;
&lt;p&gt;Cosine similarity measures the cosine of the angle between two vectors, showing how similar they are regardless of their magnitude. The value ranges from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;+1&lt;/strong&gt;: When vectors point in the same direction (perfectly similar)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0&lt;/strong&gt;: When vectors stand perpendicular (no similarity)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-1&lt;/strong&gt;: When vectors point in opposite directions (completely dissimilar)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the interactive visualization above, you can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Move both vectors by dragging the colored circles at their endpoints&lt;/li&gt;
&lt;li&gt;Observe how the angle between them changes&lt;/li&gt;
&lt;li&gt;See how cosine similarity relates to this angle&lt;/li&gt;
&lt;li&gt;Note that cosine similarity depends only on the angle, not the vectors’ lengths&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step-by-Step Example Calculation&lt;/h2&gt;
&lt;p&gt;Let me walk you through a manual calculation of cosine similarity between two simple vectors. This helps build intuition before we implement it in code.&lt;/p&gt;
&lt;p&gt;Given two vectors: $\vec{v_1} = [3, 4]$ and $\vec{v_2} = [5, 2]$&lt;/p&gt;
&lt;p&gt;I’ll calculate their cosine similarity step by step:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Calculate the dot product.&lt;/p&gt;
&lt;p&gt;$$&lt;br /&gt;
\vec{v_1} \cdot \vec{v_2} = 3 \times 5 + 4 \times 2 = 15 + 8 = 23&lt;br /&gt;
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Calculate the magnitude of each vector.&lt;/p&gt;
&lt;p&gt;$$&lt;br /&gt;
||\vec{v_1}|| = \sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5&lt;br /&gt;
$$&lt;/p&gt;
&lt;p&gt;$$&lt;br /&gt;
||\vec{v_2}|| = \sqrt{5^2 + 2^2} = \sqrt{25 + 4} = \sqrt{29} \approx 5.385&lt;br /&gt;
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Calculate the cosine similarity by dividing the dot product by the product of magnitudes.&lt;/p&gt;
&lt;p&gt;$$&lt;br /&gt;
\cos(\theta) = \frac{\vec{v_1} \cdot \vec{v_2}}{||\vec{v_1}|| \cdot ||\vec{v_2}||}&lt;br /&gt;
$$&lt;/p&gt;
&lt;p&gt;$$&lt;br /&gt;
= \frac{23}{5 \times 5.385} = \frac{23}{26.925} \approx 0.854&lt;br /&gt;
$$&lt;/p&gt;
&lt;p&gt;Therefore, the cosine similarity between vectors $\vec{v_1}$ and $\vec{v_2}$ is approximately 0.854, which shows that these vectors point in roughly the same direction.&lt;/p&gt;
&lt;h2&gt;Building a Cosine Similarity Function in TypeScript&lt;/h2&gt;
&lt;p&gt;Let’s implement an optimized cosine similarity function in TypeScript that combines the functional approach with the more efficient &lt;code&gt;Math.hypot()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;/**
 * Calculates the cosine similarity between two vectors
 * @param vecA First vector
 * @param vecB Second vector
 * @returns A value between -1 and 1, where 1 means identical
 */&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; vecB&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;!==&lt;/span&gt; vecB&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Vectors must have the same dimensions&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Calculate dot product: A·B = Σ(A[i] * B[i])&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; dotProduct &lt;span&gt;=&lt;/span&gt; vecA&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sum&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; sum &lt;span&gt;+&lt;/span&gt; a &lt;span&gt;*&lt;/span&gt; vecB&lt;span&gt;[&lt;/span&gt;i&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Calculate magnitudes using Math.hypot()&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; magnitudeA &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vecA&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; magnitudeB &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vecB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Check for zero magnitude&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;magnitudeA &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; magnitudeB &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// Calculate cosine similarity: (A·B) / (|A|*|B|)&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; dotProduct &lt;span&gt;/&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;magnitudeA &lt;span&gt;*&lt;/span&gt; magnitudeB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing Our Implementation&lt;/h2&gt;
&lt;p&gt;Let’s see how our function works with some example vectors:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Example 1: Similar vectors pointing in roughly the same direction&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecA &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;4&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecB &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Similarity: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;,&lt;/span&gt; vecB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Output: Similarity: 0.857&lt;/span&gt;

&lt;span&gt;// Example 2: Perpendicular vectors&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecC &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecD &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Similarity: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecC&lt;span&gt;,&lt;/span&gt; vecD&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Output: Similarity: 0.000&lt;/span&gt;

&lt;span&gt;// Example 3: Opposite vectors&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecE &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; vecF &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Similarity: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecE&lt;span&gt;,&lt;/span&gt; vecF&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Output: Similarity: -1.000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mathematically, we can verify these results:&lt;/p&gt;
&lt;p&gt;For Example 1:&lt;br /&gt;
$$\text{cosine similarity} = \frac{3 \times 5 + 4 \times 2}{\sqrt{3^2 + 4^2} \times \sqrt{5^2 + 2^2}} = \frac{15 + 8}{\sqrt{25} \times \sqrt{29}} = \frac{23}{5 \times \sqrt{29}} \approx 0.857$$&lt;/p&gt;
&lt;p&gt;For Example 2:&lt;br /&gt;
$$\text{cosine similarity} = \frac{1 \times 0 + 0 \times 1}{\sqrt{1^2 + 0^2} \times \sqrt{0^2 + 1^2}} = \frac{0}{1 \times 1} = 0$$&lt;/p&gt;
&lt;p&gt;For Example 3:&lt;br /&gt;
$$\text{cosine similarity} = \frac{2 \times (-2) + 3 \times (-3)}{\sqrt{2^2 + 3^2} \times \sqrt{(-2)^2 + (-3)^2}} = \frac{-4 - 9}{\sqrt{13} \times \sqrt{13}} = \frac{-13}{13} = -1$$&lt;/p&gt;
&lt;h2&gt;Complete TypeScript Solution&lt;/h2&gt;
&lt;p&gt;Here’s a complete TypeScript solution that includes our cosine similarity function along with some utility methods:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;VectorUtils&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;/**
   * Calculates the cosine similarity between two vectors
   */&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; vecB&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;!==&lt;/span&gt; vecB&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Vector dimensions don&apos;t match: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;vecA&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; vs &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;vecB&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; dotProduct &lt;span&gt;=&lt;/span&gt; vecA&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sum&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; sum &lt;span&gt;+&lt;/span&gt; a &lt;span&gt;*&lt;/span&gt; vecB&lt;span&gt;[&lt;/span&gt;i&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; magnitudeA &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vecA&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; magnitudeB &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vecB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;magnitudeA &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; magnitudeB &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; dotProduct &lt;span&gt;/&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;magnitudeA &lt;span&gt;*&lt;/span&gt; magnitudeB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;/**
   * Calculates the dot product of two vectors
   */&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;dotProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; vecB&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;vecA&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;!==&lt;/span&gt; vecB&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Vector dimensions don&apos;t match: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;vecA&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; vs &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;vecB&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; vecA&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sum&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;,&lt;/span&gt; i&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; sum &lt;span&gt;+&lt;/span&gt; a &lt;span&gt;*&lt;/span&gt; vecB&lt;span&gt;[&lt;/span&gt;i&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;/**
   * Calculates the magnitude (length) of a vector
   */&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;magnitude&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hypot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;vec&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;/**
   * Normalizes a vector (converts to unit vector)
   */&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;normalize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; mag &lt;span&gt;=&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;magnitude&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;mag &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;vec&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; vec&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;v &lt;span&gt;=&amp;gt;&lt;/span&gt; v &lt;span&gt;/&lt;/span&gt; mag&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;/**
   * Converts cosine similarity to angular distance in degrees
   */&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;similarityToDegrees&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;similarity&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Clamp similarity to [-1, 1] to handle floating point errors&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; clampedSimilarity &lt;span&gt;=&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; similarity&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;acos&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;clampedSimilarity&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;180&lt;/span&gt; &lt;span&gt;/&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PI&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using Cosine Similarity in Real Web Applications&lt;/h2&gt;
&lt;p&gt;When you work with AI in web applications, you’ll often need to calculate similarity between vectors. Here’s a practical example:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Example: Semantic search implementation&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;semanticSearch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  queryEmbedding&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  documentEmbeddings&lt;span&gt;:&lt;/span&gt; DocumentWithEmbedding&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; SearchResult&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; documentEmbeddings
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;doc &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      document&lt;span&gt;:&lt;/span&gt; doc&lt;span&gt;,&lt;/span&gt;
      relevance&lt;span&gt;:&lt;/span&gt; VectorUtils&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;queryEmbedding&lt;span&gt;,&lt;/span&gt; doc&lt;span&gt;.&lt;/span&gt;embedding&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result &lt;span&gt;=&amp;gt;&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;relevance &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0.7&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// Only consider relevant results&lt;/span&gt;
    &lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; b&lt;span&gt;.&lt;/span&gt;relevance &lt;span&gt;-&lt;/span&gt; a&lt;span&gt;.&lt;/span&gt;relevance&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using OpenAI Embedding Models with Cosine Similarity&lt;/h2&gt;
&lt;p&gt;While the examples above used simple vectors for clarity, real-world AI applications typically use embedding models that transform text and other data into high-dimensional vector spaces.&lt;/p&gt;
&lt;p&gt;OpenAI provides powerful embedding models that you can easily incorporate into your applications. These models transform text into vectors with hundreds or thousands of dimensions that capture semantic meaning:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Example of using OpenAI embeddings with our cosine similarity function&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;compareTextSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  textA&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  textB&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Get embeddings from OpenAI API&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; responseA &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://api.openai.com/v1/embeddings&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    method&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    headers&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      Authorization&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Bearer &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;process&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    body&lt;span&gt;:&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      model&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text-embedding-3-large&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      input&lt;span&gt;:&lt;/span&gt; textA&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; responseB &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;https://api.openai.com/v1/embeddings&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    method&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    headers&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      Authorization&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Bearer &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;process&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    body&lt;span&gt;:&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      model&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text-embedding-3-large&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      input&lt;span&gt;:&lt;/span&gt; textB&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; embeddingA &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; responseA&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;embedding&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; embeddingB &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; responseB&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;embedding&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Calculate similarity using our function&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; VectorUtils&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cosineSimilarity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;embeddingA&lt;span&gt;,&lt;/span&gt; embeddingB&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  In a production environment, you should pre-compute embeddings for your
  content (like blog posts, products, or documents) and store them in a vector
  database (like Pinecone, Qdrant, or Milvus). Re-computing embeddings for every
  user request as shown in this example wastes resources and slows performance.
  A better approach: embed your content once during indexing, store the vectors,
  and only embed the user&apos;s query when performing a search.

&lt;p&gt;OpenAI’s latest embedding models like &lt;code&gt;text-embedding-3-large&lt;/code&gt; have up to 3,072 dimensions, capturing extremely nuanced semantic relationships between words and concepts. These high-dimensional embeddings enable much more accurate similarity measurements than simpler vector representations.&lt;/p&gt;
&lt;p&gt;For more information on OpenAI’s embedding models, including best practices and implementation details, check out their documentation at &lt;a href=&quot;https://platform.openai.com/docs/guides/embeddings&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://platform.openai.com/docs/guides/embeddings&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Understanding vectors and cosine similarity provides practical tools that empower you to work effectively with modern AI features. By implementing these concepts in TypeScript, you gain a deeper understanding and precise control over calculating similarity in your applications.&lt;br /&gt;
The interactive visualizations we’ve explored help you build intuition about these mathematical concepts, while the TypeScript implementation gives you the tools to apply them in real-world scenarios.&lt;br /&gt;
Whether you build recommendation systems, semantic search, or content-matching features, the foundation you’ve gained here will help you implement more intelligent, accurate, and effective AI-powered features in your web applications.&lt;/p&gt;
&lt;h2&gt;Join the Discussion&lt;/h2&gt;
&lt;p&gt;This article has sparked interesting discussions across different platforms. Join the conversation to share your thoughts, ask questions, or learn from others’ perspectives about implementing cosine similarity in AI applications.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=43307541&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Join the discussion on Hacker News →&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/typescript/comments/1j73whg/how_to_implement_a_cosine_similarity_function_in/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Discuss on Reddit r/typescript →&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;

          </content:encoded><category>typescript</category><category>ai</category><category>mathematics</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How I Added llms.txt to My Astro Blog</title><link>https://alexop.dev/posts/how-i-added-llms-txt-to-my-astro-blog/</link><guid isPermaLink="true">https://alexop.dev/posts/how-i-added-llms-txt-to-my-astro-blog/</guid><description>I built a simple way to load my blog content into any LLM with one click. This post shows how you can do it too.</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;I created an endpoint in my Astro blog that outputs all posts in plain text format. This lets me copy my entire blog with one click and paste it into any LLM with adequate context window. The setup uses TypeScript and Astro’s API routes, making it work with any Astro content collection.&lt;/p&gt;
&lt;h2&gt;Why I Built This&lt;/h2&gt;
&lt;p&gt;I wanted a quick way to ask AI models questions about my own blog content. Copying posts one by one is slow. With this solution, I can give any LLM all my blog posts at once.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The solution creates a special endpoint that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Gets all blog posts&lt;/li&gt;
&lt;li&gt;Converts them to plain text&lt;/li&gt;
&lt;li&gt;Formats them with basic metadata&lt;/li&gt;
&lt;li&gt;Outputs everything as one big text file&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Setting Up the File&lt;/h2&gt;
&lt;p&gt;First, I created a new TypeScript file in my Astro pages directory:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// src/pages/llms.txt.ts&lt;/span&gt;
&lt;span&gt;// Function to extract the frontmatter as text&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; extractFrontmatter &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; frontmatterMatch &lt;span&gt;=&lt;/span&gt; content&lt;span&gt;.&lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^---\n([\s\S]*?)\n---&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; frontmatterMatch &lt;span&gt;?&lt;/span&gt; frontmatterMatch&lt;span&gt;[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Function to clean content while keeping frontmatter&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; cleanContent &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;content&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Extract the frontmatter as text&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; frontmatterText &lt;span&gt;=&lt;/span&gt; &lt;span&gt;extractFrontmatter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Remove the frontmatter delimiters&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; cleanedContent &lt;span&gt;=&lt;/span&gt; content&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^---\n[\s\S]*?\n---&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Clean up MDX-specific imports&lt;/span&gt;
  cleanedContent &lt;span&gt;=&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;import\s+.*\s+from\s+[&apos;&quot;].*[&apos;&quot;];?\s*&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;&quot;&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Remove MDX component declarations&lt;/span&gt;
  cleanedContent &lt;span&gt;=&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&amp;lt;\w+\s+.*?\/&amp;gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Remove Shiki Twoslash syntax like&lt;/span&gt;
  cleanedContent &lt;span&gt;=&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\/\/\s*@noErrors&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  cleanedContent &lt;span&gt;=&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\/\/\s*@(.*?)$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gm&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Remove other Shiki Twoslash directives&lt;/span&gt;

  &lt;span&gt;// Clean up multiple newlines&lt;/span&gt;
  cleanedContent &lt;span&gt;=&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\n\s*\n\s*\n&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;\n\n&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Return the frontmatter as text, followed by the cleaned content&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; frontmatterText &lt;span&gt;+&lt;/span&gt; &lt;span&gt;&quot;\n\n&quot;&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; cleanedContent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;GET&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;APIRoute&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Get all blog posts sorted by date (newest first)&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; posts &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; data &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;draft&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; sortedPosts &lt;span&gt;=&lt;/span&gt; posts&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
        &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;b&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;pubDatetime&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;valueOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;
        &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;pubDatetime&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;valueOf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Generate the content&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; llmsContent &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; post &lt;span&gt;of&lt;/span&gt; sortedPosts&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;// Add post metadata in the format similar to the example&lt;/span&gt;
      llmsContent &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;--- title: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;title&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; description: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;description&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; tags: [&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;tags&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;tag &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;tag&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;, &quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;] ---\n\n&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Add the post title as a heading&lt;/span&gt;
      llmsContent &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;# &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;title&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;\n\n&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Process the content, keeping frontmatter as text&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; processedContent &lt;span&gt;=&lt;/span&gt; &lt;span&gt;cleanContent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      llmsContent &lt;span&gt;+=&lt;/span&gt; processedContent &lt;span&gt;+&lt;/span&gt; &lt;span&gt;&quot;\n\n&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Add separator between posts&lt;/span&gt;
      llmsContent &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;&quot;---\n\n&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;// Return the response as plain text&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;llmsContent&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      headers&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;text/plain; charset=utf-8&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Failed to generate llms.txt:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Error generating llms.txt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; status&lt;span&gt;:&lt;/span&gt; &lt;span&gt;500&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code accomplishes four key functions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It uses Astro’s &lt;code&gt;getCollection&lt;/code&gt; function to grab all published blog posts&lt;/li&gt;
&lt;li&gt;It sorts them by date with newest first&lt;/li&gt;
&lt;li&gt;It cleans up each post’s content with helper functions&lt;/li&gt;
&lt;li&gt;It formats each post with its metadata and content&lt;/li&gt;
&lt;li&gt;It returns everything as plain text&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to Use It&lt;/h2&gt;
&lt;p&gt;Using this is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Visit &lt;code&gt;alexop.dev/llms.txt&lt;/code&gt; in your browser&lt;/li&gt;
&lt;li&gt;Press Ctrl+A (or Cmd+A on Mac) to select all the text&lt;/li&gt;
&lt;li&gt;Copy it (Ctrl+C or Cmd+C)&lt;/li&gt;
&lt;li&gt;Paste it into any LLM with adequate context window (like ChatGPT, Claude, Llama, etc.)&lt;/li&gt;
&lt;li&gt;Ask questions about your blog content&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The LLM now has all your blog posts in its context window. You can ask questions such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What topics have I written about?”&lt;/li&gt;
&lt;li&gt;“Summarize my post about [topic]”&lt;/li&gt;
&lt;li&gt;“Find code examples in my posts that use [technology]”&lt;/li&gt;
&lt;li&gt;“What have I written about [specific topic]?”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Benefits of This Approach&lt;/h2&gt;
&lt;p&gt;This approach offers distinct advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Works with any Astro blog&lt;/li&gt;
&lt;li&gt;Requires a single file to set up&lt;/li&gt;
&lt;li&gt;Makes your content easy to query with any LLM&lt;/li&gt;
&lt;li&gt;Keeps useful metadata with each post&lt;/li&gt;
&lt;li&gt;Formats content in a way LLMs understand well&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By adding one straightforward TypeScript file to your Astro blog, you can create a fast way to chat with your own content using any LLM with adequate context window. This makes it easy to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find information in your old posts&lt;/li&gt;
&lt;li&gt;Get summaries of your content&lt;/li&gt;
&lt;li&gt;Find patterns across your writing&lt;/li&gt;
&lt;li&gt;Generate new ideas based on your past content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Give it a try! The setup takes minutes, and it makes interacting with your blog content much faster.&lt;/p&gt;

          </content:encoded><category>astro</category><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Do Visual Regression Testing in Vue with Vitest?</title><link>https://alexop.dev/posts/visual-regression-testing-with-vue-and-vitest-browser/</link><guid isPermaLink="true">https://alexop.dev/posts/visual-regression-testing-with-vue-and-vitest-browser/</guid><description>Learn how to implement visual regression testing in Vue.js using Vitest&apos;s browser mode. This comprehensive guide covers setting up screenshot-based testing, creating component stories, and integrating with CI/CD pipelines for automated visual testing.</description><pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;TL;DR:&lt;br /&gt;
Visual regression testing detects unintended UI changes by comparing screenshots. With Vitest’s experimental browser mode and Playwright, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run tests in a real browser environment&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Define component stories for different states&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capture screenshots and compare them with baseline images using snapshot testing&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this guide, you’ll learn how to set up visual regression testing for Vue components using Vitest.&lt;/p&gt;
&lt;p&gt;Our test will generate this screenshot:&lt;/p&gt;
&lt;p&gt;&amp;lt;CaptionedImage&lt;br /&gt;
src={import(“…/…/assets/images/all-button-variants.png”)}&lt;br /&gt;
alt=“Screenshot showing all button variants rendered side by side”&lt;br /&gt;
caption=“Generated screenshot of all button variants in different states and styles”&lt;br /&gt;
/&amp;gt;&lt;/p&gt;

  Visual regression testing captures screenshots of UI components and compares
  them against baseline images to flag visual discrepancies. This ensures
  consistent styling and layout across your design system.

&lt;h2&gt;Vitest Configuration&lt;/h2&gt;
&lt;p&gt;Start by configuring Vitest with the Vue plugin:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;vue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting Up Browser Testing&lt;/h2&gt;
&lt;p&gt;Visual regression tests need a real browser environment. Install these dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; &lt;span&gt;-D&lt;/span&gt; vitest @vitest/browser playwright
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use the following command to initialize the browser mode:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx vitest init browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, configure Vitest to support both unit and browser tests using a workspace file, &lt;code&gt;vitest.workspace.ts&lt;/code&gt;. For more details on workspace configuration, see the &lt;a href=&quot;https://vitest.dev/guide/workspace.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vitest Workspace Documentation&lt;/a&gt;.&lt;/p&gt;

  Using a workspace configuration allows you to maintain separate settings for
  unit and browser tests while sharing common configuration. This makes it
  easier to manage different testing environments in your project.

&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineWorkspace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;extends&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;./vitest.config.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    test&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;unit&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      include&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;**/*.spec.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;**/*.spec.tsx&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      exclude&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;**/*.browser.spec.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;**/*.browser.spec.tsx&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      environment&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;jsdom&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;extends&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;./vitest.config.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    test&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;browser&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      include&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;**/*.browser.spec.ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;**/*.browser.spec.tsx&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      browser&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        enabled&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        provider&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;playwright&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        headless&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        instances&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; browser&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;chromium&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add scripts in your &lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;test&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;vitest&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;test:unit&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;vitest --project unit&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;&quot;test:browser&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;vitest --project browser&quot;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can run tests in separate environments like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; run test:unit
&lt;span&gt;npm&lt;/span&gt; run test:browser
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The BaseButton Component&lt;/h2&gt;
&lt;p&gt;Consider the &lt;code&gt;BaseButton.vue&lt;/code&gt; component a reusable button with customizable size, variant, and disabled state:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;button
    :class=&quot;[
      &apos;button&apos;,
      `button--${size}`,
      `button--${variant}`,
      { &apos;button--disabled&apos;: disabled },
    ]&quot;
    :disabled=&quot;disabled&quot;
    @click=&quot;$emit(&apos;click&apos;, $event)&quot;
  &amp;gt;
    &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Props {
  size?: &quot;small&quot; | &quot;medium&quot; | &quot;large&quot;;
  variant?: &quot;primary&quot; | &quot;secondary&quot; | &quot;outline&quot;;
  disabled?: boolean;
}

defineProps&amp;lt;Props&amp;gt;();
defineEmits&amp;lt;{
  (e: &quot;click&quot;, event: MouseEvent): void;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Additional styling available in the GitHub repository */
}

/* Size, variant, and state modifiers available in the GitHub repository */
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Defining Stories for Testing&lt;/h2&gt;
&lt;p&gt;Create “stories” to showcase different button configurations:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; buttonStories &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Primary Medium&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;primary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; size&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;medium&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    slots&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Primary Button&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Secondary Medium&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; size&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;medium&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    slots&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Secondary Button&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;// and much more ...&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each story defines a name, props, and slot content.&lt;/p&gt;
&lt;h2&gt;Rendering Stories for Screenshots&lt;/h2&gt;
&lt;p&gt;Render all stories in one container to capture a comprehensive screenshot:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Story&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  props&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  slots&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;renderStories&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  component&lt;span&gt;:&lt;/span&gt; Component&lt;span&gt;,&lt;/span&gt;
  stories&lt;span&gt;:&lt;/span&gt; Story&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; HTMLElement &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; container &lt;span&gt;=&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  container&lt;span&gt;.&lt;/span&gt;style&lt;span&gt;.&lt;/span&gt;display &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;flex&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  container&lt;span&gt;.&lt;/span&gt;style&lt;span&gt;.&lt;/span&gt;flexDirection &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;column&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  container&lt;span&gt;.&lt;/span&gt;style&lt;span&gt;.&lt;/span&gt;gap &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;16px&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  container&lt;span&gt;.&lt;/span&gt;style&lt;span&gt;.&lt;/span&gt;padding &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;20px&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  container&lt;span&gt;.&lt;/span&gt;style&lt;span&gt;.&lt;/span&gt;backgroundColor &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;#ffffff&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  stories&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;story &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; storyWrapper &lt;span&gt;=&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; label &lt;span&gt;=&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;h3&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    label&lt;span&gt;.&lt;/span&gt;textContent &lt;span&gt;=&lt;/span&gt; story&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;;&lt;/span&gt;
    storyWrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;label&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; container&lt;span&gt;:&lt;/span&gt; storyContainer &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;component&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      props&lt;span&gt;:&lt;/span&gt; story&lt;span&gt;.&lt;/span&gt;props&lt;span&gt;,&lt;/span&gt;
      slots&lt;span&gt;:&lt;/span&gt; story&lt;span&gt;.&lt;/span&gt;slots&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    storyWrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;storyContainer&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    container&lt;span&gt;.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;storyWrapper&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; container&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Writing the Visual Regression Test&lt;/h2&gt;
&lt;p&gt;Write a test that renders the stories and captures a screenshot:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// [buttonStories and renderStories defined above]&lt;/span&gt;

&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;BaseButton&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;visual regression&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should match all button variants snapshot&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; container &lt;span&gt;=&lt;/span&gt; &lt;span&gt;renderStories&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;BaseButton&lt;span&gt;,&lt;/span&gt; buttonStories&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      document&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;container&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;const&lt;/span&gt; screenshot &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;screenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;all-button-variants.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// this assertion is acutaly not doing anything&lt;/span&gt;
      &lt;span&gt;// but otherwise you would get a warning about the screenshot not being taken&lt;/span&gt;
      &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screenshot&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeTruthy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      document&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;container&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;render&lt;/code&gt; from &lt;code&gt;vitest-browser-vue&lt;/code&gt; to capture components as they appear in a real browser.&lt;/p&gt;

  Save this file with a `.browser.spec.ts` extension (e.g.,
  `BaseButton.browser.spec.ts`) to match your browser test configuration.

&lt;h2&gt;Beyond Screenshots: Automated Comparison&lt;/h2&gt;
&lt;p&gt;Automate image comparison by encoding screenshots in base64 and comparing them against baseline snapshots:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Helper function to take and compare screenshots&lt;/span&gt;
&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;takeAndCompareScreenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; element&lt;span&gt;:&lt;/span&gt; HTMLElement&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; screenshotDir &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;./__screenshots__&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; snapshotDir &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;./__snapshots__&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; screenshotPath &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;screenshotDir&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;name&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.png&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Append element to body&lt;/span&gt;
  document&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;element&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Take screenshot&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; screenshot &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; page&lt;span&gt;.&lt;/span&gt;&lt;span&gt;screenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    path&lt;span&gt;:&lt;/span&gt; screenshotPath&lt;span&gt;,&lt;/span&gt;
    base64&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Compare base64 snapshot&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screenshot&lt;span&gt;.&lt;/span&gt;base64&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMatchFileSnapshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;snapshotDir&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;name&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.snap&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Save PNG for reference&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screenshot&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeTruthy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Cleanup&lt;/span&gt;
  document&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeChild&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;element&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then update the test:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;BaseButton&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;visual regression&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should match all button variants snapshot&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; container &lt;span&gt;=&lt;/span&gt; &lt;span&gt;renderStories&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;BaseButton&lt;span&gt;,&lt;/span&gt; buttonStories&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;takeAndCompareScreenshot&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;all-button-variants&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; container&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;resolves&lt;span&gt;.&lt;/span&gt;not&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toThrow&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

  Vitest is discussing native screenshot comparisons in browser mode. Follow and
  contribute at
  [github.com/vitest-dev/vitest/discussions/690](https://github.com/vitest-dev/vitest/discussions/690).

&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[Render Component]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Capture Screenshot]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;{Compare with Baseline}&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Match|&lt;/span&gt; D&lt;span&gt;[Test Passes]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Difference|&lt;/span&gt; E&lt;span&gt;[Review Changes]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Accept|&lt;/span&gt; F&lt;span&gt;[Update Baseline]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Reject|&lt;/span&gt; G&lt;span&gt;[Fix Component]&lt;/span&gt;
    G &lt;span&gt;--&amp;gt;&lt;/span&gt; A
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Vitest’s experimental browser mode empowers developers to perform accurate visual regression testing of Vue components in real browser environments.&lt;br /&gt;
While the current workflow requires manual review of screenshot comparisons, it establishes a foundation for more automated visual testing in the future.&lt;br /&gt;
This approach also strengthens collaboration between developers and UI designers.&lt;br /&gt;
Designers can review visual changes to components before production deployment by accessing the generated screenshots in the component library.&lt;br /&gt;
For advanced visual testing capabilities, teams should explore dedicated tools like Playwright or Cypress that offer more features and maturity.&lt;br /&gt;
Keep in mind to perform visual regression tests against your Base components.&lt;/p&gt;

          </content:encoded><category>vue</category><category>testing</category><category>vitest</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Test Vue Router Components with Testing Library and Vitest</title><link>https://alexop.dev/posts/how-to-test-vue-router-components-with-testing-library-and-vitest/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-test-vue-router-components-with-testing-library-and-vitest/</guid><description>Learn how to test Vue Router components using Testing Library and Vitest. This guide covers real router integration, mocked router setups, and best practices for testing navigation, route guards, and dynamic components in Vue applications.</description><pubDate>Sun, 16 Feb 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;This guide shows you how to test Vue Router components using real router integration and isolated component testing with mocks. You’ll learn to verify router-link interactions, programmatic navigation, and navigation guard handling.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Modern Vue applications need thorough testing to ensure reliable navigation and component performance. We’ll cover testing strategies using Testing Library and Vitest to simulate real-world scenarios through router integration and component isolation.&lt;/p&gt;
&lt;h2&gt;Vue Router Testing Techniques with Testing Library and Vitest&lt;/h2&gt;
&lt;p&gt;Let’s explore how to write effective tests for Vue Router components using both real router instances and mocks.&lt;/p&gt;
&lt;h2&gt;Testing Vue Router Navigation Components&lt;/h2&gt;
&lt;h3&gt;Navigation Component Example&lt;/h3&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- NavigationMenu.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const router = useRouter();
const goToProfile = () =&amp;gt; {
  router.push(&quot;/profile&quot;);
};
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;nav&amp;gt;
    &amp;lt;router-link to=&quot;/dashboard&quot; class=&quot;nav-link&quot;&amp;gt;Dashboard&amp;lt;/router-link&amp;gt;
    &amp;lt;router-link to=&quot;/settings&quot; class=&quot;nav-link&quot;&amp;gt;Settings&amp;lt;/router-link&amp;gt;
    &amp;lt;button @click=&quot;goToProfile&quot;&amp;gt;Profile&amp;lt;/button&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Real Router Integration Testing&lt;/h3&gt;
&lt;p&gt;Test complete routing behavior with a real router instance:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;NavigationMenu&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should navigate using router links&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      history&lt;span&gt;:&lt;/span&gt; &lt;span&gt;createWebHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      routes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Dashboard&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/settings&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Settings&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/profile&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Profile&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Home&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;NavigationMenu&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      global&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;router&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isReady&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Profile&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/profile&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mocked Router Testing&lt;/h3&gt;
&lt;p&gt;Test components in isolation with router mocks:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; mockPush &lt;span&gt;=&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;vue-router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  useRouter&lt;span&gt;:&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;NavigationMenu with mocked router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle navigation with mocked router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; mockRouter &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      push&lt;span&gt;:&lt;/span&gt; mockPush&lt;span&gt;,&lt;/span&gt;
      currentRoute&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; Router&lt;span&gt;;&lt;/span&gt;

    vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mocked&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;useRouter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mockImplementation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; mockRouter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;NavigationMenu&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Profile&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;mockPush&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/profile&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RouterLink Stub for Isolated Testing&lt;/h3&gt;
&lt;p&gt;Create a RouterLink stub to test navigation without router-link behavior:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// test-utils.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; RouterLinkStub&lt;span&gt;:&lt;/span&gt; Component &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;RouterLinkStub&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  props&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    to&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;String&lt;span&gt;,&lt;/span&gt; Object&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      required&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    tag&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      type&lt;span&gt;:&lt;/span&gt; String&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;a&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    exact&lt;span&gt;:&lt;/span&gt; Boolean&lt;span&gt;,&lt;/span&gt;
    exactPath&lt;span&gt;:&lt;/span&gt; Boolean&lt;span&gt;,&lt;/span&gt;
    append&lt;span&gt;:&lt;/span&gt; Boolean&lt;span&gt;,&lt;/span&gt;
    replace&lt;span&gt;:&lt;/span&gt; Boolean&lt;span&gt;,&lt;/span&gt;
    activeClass&lt;span&gt;:&lt;/span&gt; String&lt;span&gt;,&lt;/span&gt;
    exactActiveClass&lt;span&gt;:&lt;/span&gt; String&lt;span&gt;,&lt;/span&gt;
    exactPathActiveClass&lt;span&gt;:&lt;/span&gt; String&lt;span&gt;,&lt;/span&gt;
    event&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;String&lt;span&gt;,&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;click&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;props&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;navigate&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;props&lt;span&gt;.&lt;/span&gt;to&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; navigate &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;h&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;tag&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;onClick&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;navigate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;$slots&lt;span&gt;.&lt;/span&gt;default&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the RouterLinkStub in tests:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; mockPush &lt;span&gt;=&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;vue-router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  useRouter&lt;span&gt;:&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;NavigationMenu with mocked router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle navigation with mocked router&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; mockRouter &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      push&lt;span&gt;:&lt;/span&gt; mockPush&lt;span&gt;,&lt;/span&gt;
      currentRoute&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; Router&lt;span&gt;;&lt;/span&gt;

    vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mocked&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;useRouter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mockImplementation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; mockRouter&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;NavigationMenu&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      global&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        stubs&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          RouterLink&lt;span&gt;:&lt;/span&gt; RouterLinkStub&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;mockPush&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Testing Navigation Guards&lt;/h3&gt;
&lt;p&gt;Test navigation guards by rendering the component within a route context:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
onBeforeRouteLeave(() =&amp;gt; {
  return window.confirm(&quot;Do you really want to leave this page?&quot;);
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Route Leave Guard Demo&amp;lt;/h1&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;nav&amp;gt;
        &amp;lt;router-link to=&quot;/&quot;&amp;gt;Home&amp;lt;/router-link&amp;gt; |
        &amp;lt;router-link to=&quot;/about&quot;&amp;gt;About&amp;lt;/router-link&amp;gt; |
        &amp;lt;router-link to=&quot;/guard-demo&quot;&amp;gt;Guard Demo&amp;lt;/router-link&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test the navigation guard:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; routes &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; RouteLeaveGuardDemo &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt; path&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/about&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;&amp;lt;div&amp;gt;About&amp;lt;/div&amp;gt;&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  history&lt;span&gt;:&lt;/span&gt; &lt;span&gt;createWebHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  routes&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; App &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; template&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;&amp;lt;router-view /&amp;gt;&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;RouteLeaveGuardDemo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;beforeEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clearAllMocks&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    window&lt;span&gt;.&lt;/span&gt;confirm &lt;span&gt;=&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isReady&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should prompt when guard is triggered and user confirms&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Set window.confirm to simulate a user confirming the prompt&lt;/span&gt;
    window&lt;span&gt;.&lt;/span&gt;confirm &lt;span&gt;=&lt;/span&gt; vi&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Render the component within a router context&lt;/span&gt;
    &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;App&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      global&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;router&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Find the &apos;About&apos; link and simulate a user click&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; aboutLink &lt;span&gt;=&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;link&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;About&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;aboutLink&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Assert that the confirm dialog was shown with the correct message&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;window&lt;span&gt;.&lt;/span&gt;confirm&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;&quot;Do you really want to leave this page?&quot;&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Verify that the navigation was allowed and the route changed to &apos;/about&apos;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/about&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reusable Router Test Helper&lt;/h3&gt;
&lt;p&gt;Create a helper function to simplify router setup:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// test-utils.ts&lt;/span&gt;
&lt;span&gt;// path of the definition of your routes&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;RenderWithRouterOptions&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;Omit&lt;span&gt;&amp;lt;&lt;/span&gt;RenderOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;global&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  initialRoute&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  routerOptions&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    routes&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; routes&lt;span&gt;;&lt;/span&gt;
    history&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; createWebHistory&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;renderWithRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  Component&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  options&lt;span&gt;:&lt;/span&gt; RenderWithRouterOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; initialRoute &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; routerOptions &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;renderOptions &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; options&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; router &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    history&lt;span&gt;:&lt;/span&gt; &lt;span&gt;createWebHistory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;// Use provided routes or import from your router file&lt;/span&gt;
    routes&lt;span&gt;:&lt;/span&gt; routerOptions&lt;span&gt;.&lt;/span&gt;routes &lt;span&gt;||&lt;/span&gt; routes&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialRoute&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Return everything from regular render, plus the router instance&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Component&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      global&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;router&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;...&lt;/span&gt;renderOptions&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    router&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the helper in tests:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;NavigationMenu&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should navigate using router links&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; router &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;renderWithRouter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;NavigationMenu&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      initialRoute&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; router&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isReady&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; userEvent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;await&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;&lt;span&gt;click&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;router&lt;span&gt;.&lt;/span&gt;currentRoute&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;path&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/dashboard&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion: Best Practices for Vue Router Component Testing&lt;/h3&gt;
&lt;p&gt;When we test components that rely on the router, we need to consider whether we want to test the functionality in the most realistic use case or in isolation. In my humble opinion, the more you mock a test, the worse it will get. My personal advice would be to aim to use the real router instead of mocking it. Sometimes, there are exceptions, so keep that in mind.&lt;/p&gt;
&lt;p&gt;Also, you can help yourself by focusing on components that don’t rely on router functionality. Reserve router logic for view/page components. While keeping our components simple, we will never have the problem of mocking the router in the first place.&lt;/p&gt;

          </content:encoded><category>vue</category><category>testing</category><category>vue-router</category><category>vitest</category><category>testing-library</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building a Pinia Plugin for Cross-Tab State Syncing</title><link>https://alexop.dev/posts/building-pinia-plugin-cross-tab-sync/</link><guid isPermaLink="true">https://alexop.dev/posts/building-pinia-plugin-cross-tab-sync/</guid><description>Learn how to create a Pinia plugin that synchronizes state across browser tabs using the BroadcastChannel API and Vue 3&apos;s Script Setup syntax.</description><pubDate>Sun, 09 Feb 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Create a Pinia plugin that enables state synchronization across browser tabs using the BroadcastChannel API. The plugin allows you to mark specific stores for cross-tab syncing and handles state updates automatically with timestamp-based conflict resolution.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In modern web applications, users often work with multiple browser tabs open. When using Pinia for state management, we sometimes need to ensure that state changes in one tab are reflected across all open instances of our application. This post will guide you through creating a plugin that adds cross-tab state synchronization to your Pinia stores.&lt;/p&gt;
&lt;h2&gt;Understanding Pinia Plugins&lt;/h2&gt;
&lt;p&gt;A Pinia plugin is a function that extends the functionality of Pinia stores. Plugins are powerful tools that help:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce code duplication&lt;/li&gt;
&lt;li&gt;Add reusable functionality across stores&lt;/li&gt;
&lt;li&gt;Keep store definitions clean and focused&lt;/li&gt;
&lt;li&gt;Implement cross-cutting concerns&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cross-Tab Communication with BroadcastChannel&lt;/h2&gt;
&lt;p&gt;The BroadcastChannel API provides a simple way to send messages between different browser contexts (tabs, windows, or iframes) of the same origin. It’s perfect for our use case of synchronizing state across tabs.&lt;/p&gt;
&lt;p&gt;Key features of BroadcastChannel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built-in browser API&lt;/li&gt;
&lt;li&gt;Same-origin security model&lt;/li&gt;
&lt;li&gt;Simple pub/sub messaging pattern&lt;/li&gt;
&lt;li&gt;No need for external dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How BroadcastChannel Works&lt;/h3&gt;
&lt;p&gt;The BroadcastChannel API operates on a simple principle: any browsing context (window, tab, iframe, or worker) can join a channel by creating a &lt;code&gt;BroadcastChannel&lt;/code&gt; object with the same channel name. Once joined:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Messages are sent using the &lt;code&gt;postMessage()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;Messages are received through the &lt;code&gt;onmessage&lt;/code&gt; event handler&lt;/li&gt;
&lt;li&gt;Contexts can leave the channel using the &lt;code&gt;close()&lt;/code&gt; method&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Implementing the Plugin&lt;/h2&gt;
&lt;h3&gt;Store Configuration&lt;/h3&gt;
&lt;p&gt;To use our plugin, stores need to opt-in to state sharing through configuration:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; useCounterStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;counter&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; doubleCount &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;*&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;function&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; doubleCount&lt;span&gt;,&lt;/span&gt; increment &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;{&lt;/span&gt;
    share&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      enable&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      initialize&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;share&lt;/code&gt; option enables cross-tab synchronization and controls whether the store should initialize its state from other tabs.&lt;/p&gt;
&lt;h3&gt;Plugin Registration &lt;code&gt;main.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Register the plugin when creating your Pinia instance:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; pinia &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createPinia&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
pinia&lt;span&gt;.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;PiniaSharedState&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plugin Implementation &lt;code&gt;plugin/plugin.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Here’s our complete plugin implementation with TypeScript support:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Serializer&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; StateTree&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;serialize&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;deserialize&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;BroadcastMessage&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;STATE_UPDATE&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;SYNC_REQUEST&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  timestamp&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  state&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;PluginOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; StateTree&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  enable&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  initialize&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  serializer&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Serializer&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;StoreOptions&lt;span&gt;&amp;lt;&lt;/span&gt;
  &lt;span&gt;S&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; StateTree &lt;span&gt;=&lt;/span&gt; StateTree&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;G&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; object&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;A&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; object&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;DefineStoreOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;S&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;G&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;A&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  share&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; PluginOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;S&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Add type extension for Pinia&lt;/span&gt;
&lt;span&gt;declare&lt;/span&gt; &lt;span&gt;module&lt;/span&gt; &lt;span&gt;&quot;pinia&quot;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// eslint-disable-next-line @typescript-eslint/no-unused-vars&lt;/span&gt;
  &lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;DefineStoreOptionsBase&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;S&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Store&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    share&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; PluginOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;S&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;PiniaSharedState&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; StateTree&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  enable &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  initialize &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  serializer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    serialize&lt;span&gt;:&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;stringify&lt;span&gt;,&lt;/span&gt;
    deserialize&lt;span&gt;:&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;parse&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; PluginOptions&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; store&lt;span&gt;,&lt;/span&gt; options &lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; PiniaPluginContext&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;options&lt;span&gt;.&lt;/span&gt;share&lt;span&gt;?.&lt;/span&gt;enable &lt;span&gt;??&lt;/span&gt; enable&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; channel &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;BroadcastChannel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;store&lt;span&gt;.&lt;/span&gt;$id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; timestamp &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;let&lt;/span&gt; externalUpdate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Initial state sync&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;options&lt;span&gt;.&lt;/span&gt;share&lt;span&gt;?.&lt;/span&gt;initialize &lt;span&gt;??&lt;/span&gt; initialize&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      channel&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;SYNC_REQUEST&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    &lt;span&gt;// State change listener&lt;/span&gt;
    store&lt;span&gt;.&lt;/span&gt;&lt;span&gt;$subscribe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;_mutation&lt;span&gt;,&lt;/span&gt; state&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;externalUpdate&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      timestamp &lt;span&gt;=&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      channel&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;STATE_UPDATE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        timestamp&lt;span&gt;,&lt;/span&gt;
        state&lt;span&gt;:&lt;/span&gt; serializer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;serialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;state &lt;span&gt;as&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;// Message handler&lt;/span&gt;
    channel&lt;span&gt;.&lt;/span&gt;&lt;span&gt;onmessage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;event&lt;span&gt;:&lt;/span&gt; MessageEvent&lt;span&gt;&amp;lt;&lt;/span&gt;BroadcastMessage&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; event&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
        data&lt;span&gt;.&lt;/span&gt;type &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;STATE_UPDATE&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        data&lt;span&gt;.&lt;/span&gt;timestamp &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        data&lt;span&gt;.&lt;/span&gt;timestamp &lt;span&gt;&amp;gt;&lt;/span&gt; timestamp &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        data&lt;span&gt;.&lt;/span&gt;state
      &lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        externalUpdate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        timestamp &lt;span&gt;=&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;timestamp&lt;span&gt;;&lt;/span&gt;
        store&lt;span&gt;.&lt;/span&gt;&lt;span&gt;$patch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;serializer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deserialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;state&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        externalUpdate &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;

      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;type &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;SYNC_REQUEST&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        channel&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
          type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;STATE_UPDATE&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          timestamp&lt;span&gt;,&lt;/span&gt;
          state&lt;span&gt;:&lt;/span&gt; serializer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;serialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;store&lt;span&gt;.&lt;/span&gt;$state &lt;span&gt;as&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin works by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating a BroadcastChannel for each store&lt;/li&gt;
&lt;li&gt;Subscribing to store changes and broadcasting updates&lt;/li&gt;
&lt;li&gt;Handling incoming messages from other tabs&lt;/li&gt;
&lt;li&gt;Using timestamps to prevent update cycles&lt;/li&gt;
&lt;li&gt;Supporting custom serialization for complex state&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Communication Flow Diagram&lt;/h3&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[User interacts with store in Tab 1]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Store state changes]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;[Plugin detects change]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt; D&lt;span&gt;[BroadcastChannel posts STATE_UPDATE]&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Other tabs receive STATE_UPDATE]&lt;/span&gt;
    E &lt;span&gt;--&amp;gt;&lt;/span&gt; F&lt;span&gt;[Plugin patches store state in Tab 2]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using the Synchronized Store&lt;/h2&gt;
&lt;p&gt;Components can use the synchronized store just like any other Pinia store:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; counterStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useCounterStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// State changes will automatically sync across tabs&lt;/span&gt;
counterStore&lt;span&gt;.&lt;/span&gt;&lt;span&gt;increment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With this Pinia plugin, we’ve added cross-tab state synchronization with minimal configuration. The solution is lightweight, type-safe, and leverages the built-in BroadcastChannel API. This pattern is particularly useful for applications where users frequently work across multiple tabs and need a consistent state experience.&lt;/p&gt;
&lt;p&gt;Remember to consider the following when using this plugin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only enable sharing for stores that truly need it&lt;/li&gt;
&lt;li&gt;Be mindful of performance with large state objects&lt;/li&gt;
&lt;li&gt;Consider custom serialization for complex data structures&lt;/li&gt;
&lt;li&gt;Test thoroughly across different browser scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Future Optimization: Web Workers&lt;/h2&gt;
&lt;p&gt;For applications with heavy cross-tab communication or complex state transformations, consider offloading the BroadcastChannel handling to a Web Worker. This approach can improve performance by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Moving message processing off the main thread&lt;/li&gt;
&lt;li&gt;Handling complex state transformations without blocking UI&lt;/li&gt;
&lt;li&gt;Reducing main thread load when syncing large state objects&lt;/li&gt;
&lt;li&gt;Buffering and batching state updates for better performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is particularly beneficial when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your application has many tabs open simultaneously&lt;/li&gt;
&lt;li&gt;State updates are frequent or computationally intensive&lt;/li&gt;
&lt;li&gt;You need to perform validation or transformation on synced data&lt;/li&gt;
&lt;li&gt;The application handles large datasets that need to be synced&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find the complete code for this plugin in the &lt;a href=&quot;https://github.com/alexanderop/pluginPiniaTabs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub repository&lt;/a&gt;.&lt;br /&gt;
It also has examples of how to use it with Web Workers.&lt;/p&gt;

          </content:encoded><category>vue</category><category>pinia</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Use AI for Effective Diagram Creation: A Guide to ChatGPT and Mermaid</title><link>https://alexop.dev/posts/how-to-use-ai-for-effective-diagram-creation-a-guide-to-chatgpt-and-mermaid/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-use-ai-for-effective-diagram-creation-a-guide-to-chatgpt-and-mermaid/</guid><description>Learn how to leverage ChatGPT and Mermaid to create effective diagrams for technical documentation and communication.</description><pubDate>Sun, 09 Feb 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Learn how to combine ChatGPT and Mermaid to quickly create professional diagrams for technical documentation. This approach eliminates the complexity of traditional diagramming tools while maintaining high-quality output.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Mermaid is a markdown-like script language that generates diagrams from text descriptions. When combined with ChatGPT, it becomes a powerful tool for creating technical diagrams quickly and efficiently.&lt;/p&gt;
&lt;h2&gt;Key Diagram Types&lt;/h2&gt;
&lt;h3&gt;Flowcharts&lt;/h3&gt;
&lt;p&gt;Perfect for visualizing processes:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;flowchart LR
  A[Customer selects products] --&amp;gt; B[Customer reviews order]
  B --&amp;gt; C{Payment Successful?}
  C --&amp;gt;|Yes| D[Generate Invoice]
  D --&amp;gt; E[Dispatch goods]
  C --&amp;gt;|No| F[Redirect to Payment]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; LR
  A&lt;span&gt;[Customer selects products]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;[Customer reviews order]&lt;/span&gt;
  B &lt;span&gt;--&amp;gt;&lt;/span&gt; C&lt;span&gt;{Payment Successful?}&lt;/span&gt;
  C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; D&lt;span&gt;[Generate Invoice]&lt;/span&gt;
  D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Dispatch goods]&lt;/span&gt;
  C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; F&lt;span&gt;[Redirect to Payment]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sequence Diagrams&lt;/h3&gt;
&lt;p&gt;Ideal for system interactions:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;sequenceDiagram
    participant Client
    participant Server
    Client-&amp;gt;&amp;gt;Server: Request (GET /resource)
    Server--&amp;gt;&amp;gt;Client: Response (200 OK)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
    &lt;span&gt;participant&lt;/span&gt; Client
    &lt;span&gt;participant&lt;/span&gt; Server
    Client&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Server&lt;span&gt;:&lt;/span&gt; Request &lt;span&gt;(GET /resource)&lt;/span&gt;
    Server&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Client&lt;span&gt;:&lt;/span&gt; Response &lt;span&gt;(200 OK)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using ChatGPT with Mermaid&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Ask ChatGPT to explain your concept&lt;/li&gt;
&lt;li&gt;Request a Mermaid diagram representation&lt;/li&gt;
&lt;li&gt;Iterate on the diagram with follow-up questions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example prompt: “Create a Mermaid sequence diagram showing how Nuxt.js performs server-side rendering”&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;sequenceDiagram
  participant Client as Client Browser
  participant Nuxt as Nuxt.js Server
  participant Vue as Vue.js Application
  participant API as Backend API

  Client-&amp;gt;&amp;gt;Nuxt: Initial Request
  Nuxt-&amp;gt;&amp;gt;Vue: SSR Starts
  Vue-&amp;gt;&amp;gt;API: API Calls (if any)
  API--&amp;gt;&amp;gt;Vue: API Responses
  Vue-&amp;gt;&amp;gt;Nuxt: Rendered HTML
  Nuxt--&amp;gt;&amp;gt;Client: HTML Content
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;
  &lt;span&gt;participant&lt;/span&gt; Client as Client Browser
  &lt;span&gt;participant&lt;/span&gt; Nuxt as Nuxt.js Server
  &lt;span&gt;participant&lt;/span&gt; Vue as Vue.js Application
  &lt;span&gt;participant&lt;/span&gt; API as Backend API

  Client&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Nuxt&lt;span&gt;:&lt;/span&gt; Initial Request
  Nuxt&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Vue&lt;span&gt;:&lt;/span&gt; SSR Starts
  Vue&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;API&lt;span&gt;:&lt;/span&gt; API Calls &lt;span&gt;(if any)&lt;/span&gt;
  API&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Vue&lt;span&gt;:&lt;/span&gt; API Responses
  Vue&lt;span&gt;-&amp;gt;&amp;gt;&lt;/span&gt;Nuxt&lt;span&gt;:&lt;/span&gt; Rendered HTML
  Nuxt&lt;span&gt;--&amp;gt;&amp;gt;&lt;/span&gt;Client&lt;span&gt;:&lt;/span&gt; HTML Content
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick Setup Guide&lt;/h2&gt;
&lt;h3&gt;Online Editor&lt;/h3&gt;
&lt;p&gt;Use &lt;a href=&quot;https://mermaid.live/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Mermaid Live Editor&lt;/a&gt; for quick prototyping.&lt;/p&gt;
&lt;h3&gt;VS Code Integration&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Install “Markdown Preview Mermaid Support” extension&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;.md&lt;/code&gt; file with Mermaid code blocks&lt;/li&gt;
&lt;li&gt;Preview with built-in markdown viewer&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Web Integration&lt;/h3&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span&gt;src&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;https://unpkg.com/mermaid/dist/mermaid.min.js&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
  mermaid&lt;span&gt;.&lt;/span&gt;&lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; &lt;span&gt;startOnLoad&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;mermaid&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;graph TD A--&amp;gt;B&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The combination of ChatGPT and Mermaid streamlines technical diagramming, making it accessible and efficient. Try it in your next documentation project to save time while creating professional diagrams.&lt;/p&gt;

          </content:encoded><category>ai</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Browser That Speaks 200 Languages: Building an AI Translator Without APIs</title><link>https://alexop.dev/posts/building-client-side-ai-translator-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/building-client-side-ai-translator-vue/</guid><description>Learn how to build a browser-based translator that works offline and handles 200 languages using Vue and Transformers.js</description><pubDate>Sun, 02 Feb 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Most AI translation tools rely on external APIs.&lt;br /&gt;
This means sending data to servers and paying for each request. But what if you could run translations directly in your browser? This guide shows you how to build a free, offline translator that handles 200 languages using Vue and Transformers.js.&lt;/p&gt;
&lt;h2&gt;The Tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vue 3 for the interface&lt;/li&gt;
&lt;li&gt;Transformers.js to run AI models locally&lt;/li&gt;
&lt;li&gt;Web Workers to handle heavy processing&lt;/li&gt;
&lt;li&gt;NLLB-200, Meta’s translation model&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Architecture Overview
&lt;span&gt;---&lt;/span&gt;

&lt;span&gt;graph&lt;/span&gt; LR
    Frontend&lt;span&gt;[Vue Frontend]&lt;/span&gt;
    Worker&lt;span&gt;[Web Worker]&lt;/span&gt;
    TJS&lt;span&gt;[Transformers.js]&lt;/span&gt;
    Model&lt;span&gt;[NLLB-200 Model]&lt;/span&gt;

    Frontend &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Text&quot;|&lt;/span&gt; Worker
    Worker &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Initialize&quot;|&lt;/span&gt; TJS
    TJS &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Load&quot;|&lt;/span&gt; Model
    Model &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Results&quot;|&lt;/span&gt; TJS
    TJS &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Stream&quot;|&lt;/span&gt; Worker
    Worker &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|&quot;Translation&quot;|&lt;/span&gt; Frontend

    &lt;span&gt;classDef&lt;/span&gt; default &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#344060&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#AB4B99&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#EAEDF3&lt;/span&gt;
    &lt;span&gt;classDef&lt;/span&gt; accent &lt;span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#8A337B&lt;span&gt;,&lt;/span&gt;&lt;span&gt;stroke&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#AB4B99&lt;span&gt;,&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;#EAEDF3&lt;/span&gt;

    &lt;span&gt;class&lt;/span&gt; TJS,Model accent
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building the Translator&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/vue-ai-translate.png&quot; alt=&quot;AI Translator&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. Set Up Your Project&lt;/h3&gt;
&lt;p&gt;Create a new Vue project with TypeScript:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; create vite@latest vue-translator -- &lt;span&gt;--template&lt;/span&gt; vue-ts
&lt;span&gt;cd&lt;/span&gt; vue-translator
&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt;
&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; @huggingface/transformers
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Create the Translation Worker&lt;/h3&gt;
&lt;p&gt;The translation happens in a background process. Create &lt;code&gt;src/worker/translation.worker.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  pipeline&lt;span&gt;,&lt;/span&gt;
  TextStreamer&lt;span&gt;,&lt;/span&gt;
  TranslationPipeline&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;&quot;@huggingface/transformers&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// Singleton pattern for the translation pipeline&lt;/span&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyTranslationPipeline&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; task&lt;span&gt;:&lt;/span&gt; PipelineType &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;translation&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// We use the distilled model for faster loading and inference&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; model &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Xenova/nllb-200-distilled-600M&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; instance&lt;span&gt;:&lt;/span&gt; TranslationPipeline &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;getInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;progress_callback&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ProgressCallback&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;instance&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;instance &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;pipeline&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;task&lt;span&gt;,&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;model&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        progress_callback&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; TranslationPipeline&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;instance&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Type definitions for worker messages&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;TranslationRequest&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  src_lang&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  tgt_lang&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Worker message handler&lt;/span&gt;
self&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;event&lt;span&gt;:&lt;/span&gt; MessageEvent&lt;span&gt;&amp;lt;&lt;/span&gt;TranslationRequest&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;// Initialize the translation pipeline with progress tracking&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; translator &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; MyTranslationPipeline&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInstance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;x &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        self&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;x&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Configure streaming for real-time translation updates&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; streamer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;TextStreamer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;translator&lt;span&gt;.&lt;/span&gt;tokenizer&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        skip_prompt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        skip_special_tokens&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;callback_function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;text&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          self&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
            status&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;update&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            output&lt;span&gt;:&lt;/span&gt; text&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Perform the translation&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; output &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;translator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;event&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;text&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;// @ts-ignore - Type definitions are incomplete&lt;/span&gt;
        tgt_lang&lt;span&gt;:&lt;/span&gt; event&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;tgt_lang&lt;span&gt;,&lt;/span&gt;
        src_lang&lt;span&gt;:&lt;/span&gt; event&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;src_lang&lt;span&gt;,&lt;/span&gt;
        streamer&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Send the final result&lt;/span&gt;
      self&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        status&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;complete&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        output&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      self&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        status&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        error&lt;span&gt;:&lt;/span&gt;
          error &lt;span&gt;instanceof&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;message &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;An unknown error occurred&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Build the Interface&lt;/h3&gt;
&lt;p&gt;Create a clean interface with two main components:&lt;/p&gt;
&lt;h4&gt;Language Selector (&lt;code&gt;src/components/LanguageSelector.vue&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// Language codes follow the ISO 639-3 standard with script codes
const LANGUAGES: Record&amp;lt;string, string&amp;gt; = {
  English: &quot;eng_Latn&quot;,
  French: &quot;fra_Latn&quot;,
  Spanish: &quot;spa_Latn&quot;,
  German: &quot;deu_Latn&quot;,
  Chinese: &quot;zho_Hans&quot;,
  Japanese: &quot;jpn_Jpan&quot;,
  // Add more languages as needed
};
// Strong typing for component props
interface Props {
  type: string;
  modelValue: string;
}

defineProps&amp;lt;Props&amp;gt;();
const emit = defineEmits&amp;lt;{
  (e: &quot;update:modelValue&quot;, value: string): void;
}&amp;gt;();

const onChange = (event: Event) =&amp;gt; {
  const target = event.target as HTMLSelectElement;
  emit(&quot;update:modelValue&quot;, target.value);
};
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;language-selector&quot;&amp;gt;
    &amp;lt;label&amp;gt;{{ type }}: &amp;lt;/label&amp;gt;
    &amp;lt;select :value=&quot;modelValue&quot; @change=&quot;onChange&quot;&amp;gt;
      &amp;lt;option
        v-for=&quot;[key, value] in Object.entries(LANGUAGES)&quot;
        :key=&quot;key&quot;
        :value=&quot;value&quot;
      &amp;gt;
        {{ key }}
      &amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.language-selector {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

select {
  padding: 0.5rem;
  border-radius: 4px;
  border: 1px solid rgb(var(--color-border));
  background-color: rgb(var(--color-card));
  color: rgb(var(--color-text-base));
  min-width: 200px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Progress Bar (&lt;code&gt;src/components/ProgressBar.vue&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps&amp;lt;{
  text: string;
  percentage: number;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;progress-container&quot;&amp;gt;
    &amp;lt;div class=&quot;progress-bar&quot; :style=&quot;{ width: `${percentage}%` }&quot;&amp;gt;
      {{ text }} ({{ percentage.toFixed(2) }}%)
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.progress-container {
  width: 100%;
  height: 20px;
  background-color: rgb(var(--color-card));
  border-radius: 10px;
  margin: 10px 0;
  overflow: hidden;
  border: 1px solid rgb(var(--color-border));
}

.progress-bar {
  height: 100%;
  background-color: rgb(var(--color-accent));
  transition: width 0.3s ease;
  display: flex;
  align-items: center;
  padding: 0 10px;
  color: rgb(var(--color-text-base));
  font-size: 0.9rem;
  white-space: nowrap;
}

.progress-bar:hover {
  background-color: rgb(var(--color-card-muted));
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Put It All Together&lt;/h3&gt;
&lt;p&gt;In your main app file:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface ProgressItem {
  file: string;
  progress: number;
}

// State
const worker = ref&amp;lt;Worker | null&amp;gt;(null);
const ready = ref&amp;lt;boolean | null&amp;gt;(null);
const disabled = ref(false);
const progressItems = ref&amp;lt;Map&amp;lt;string, ProgressItem&amp;gt;&amp;gt;(new Map());

const input = ref(&quot;I love walking my dog.&quot;);
const sourceLanguage = ref(&quot;eng_Latn&quot;);
const targetLanguage = ref(&quot;fra_Latn&quot;);
const output = ref(&quot;&quot;);

// Computed property for progress items array
const progressItemsArray = computed(() =&amp;gt; {
  return Array.from(progressItems.value.values());
});

// Watch progress items
watch(
  progressItemsArray,
  newItems =&amp;gt; {
    console.log(&quot;Progress items updated:&quot;, newItems);
  },
  { deep: true }
);

// Translation handler
const translate = () =&amp;gt; {
  if (!worker.value) return;

  disabled.value = true;
  output.value = &quot;&quot;;

  worker.value.postMessage({
    text: input.value,
    src_lang: sourceLanguage.value,
    tgt_lang: targetLanguage.value,
  });
};

// Worker message handler
const onMessageReceived = (e: MessageEvent) =&amp;gt; {
  switch (e.data.status) {
    case &quot;initiate&quot;:
      ready.value = false;
      progressItems.value.set(e.data.file, {
        file: e.data.file,
        progress: 0,
      });
      progressItems.value = new Map(progressItems.value);
      break;

    case &quot;progress&quot;:
      if (progressItems.value.has(e.data.file)) {
        progressItems.value.set(e.data.file, {
          file: e.data.file,
          progress: e.data.progress,
        });
        progressItems.value = new Map(progressItems.value);
      }
      break;

    case &quot;done&quot;:
      progressItems.value.delete(e.data.file);
      progressItems.value = new Map(progressItems.value);
      break;

    case &quot;ready&quot;:
      ready.value = true;
      break;

    case &quot;update&quot;:
      output.value += e.data.output;
      break;

    case &quot;complete&quot;:
      disabled.value = false;
      break;

    case &quot;error&quot;:
      console.error(&quot;Translation error:&quot;, e.data.error);
      disabled.value = false;
      break;
  }
};

// Lifecycle hooks
onMounted(() =&amp;gt; {
  worker.value = new Worker(
    new URL(&quot;./workers/translation.worker.ts&quot;, import.meta.url),
    { type: &quot;module&quot; }
  );
  worker.value.addEventListener(&quot;message&quot;, onMessageReceived);
});

onUnmounted(() =&amp;gt; {
  worker.value?.removeEventListener(&quot;message&quot;, onMessageReceived);
  worker.value?.terminate();
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;app&quot;&amp;gt;
    &amp;lt;h1&amp;gt;Transformers.js&amp;lt;/h1&amp;gt;
    &amp;lt;h2&amp;gt;ML-powered multilingual translation in Vue!&amp;lt;/h2&amp;gt;

    &amp;lt;div class=&quot;container&quot;&amp;gt;
      &amp;lt;div class=&quot;language-container&quot;&amp;gt;

      &amp;lt;/div&amp;gt;

      &amp;lt;div class=&quot;textbox-container&quot;&amp;gt;
        &amp;lt;textarea
          v-model=&quot;input&quot;
          rows=&quot;3&quot;
          placeholder=&quot;Enter text to translate...&quot;
        /&amp;gt;
        &amp;lt;textarea
          v-model=&quot;output&quot;
          rows=&quot;3&quot;
          readonly
          placeholder=&quot;Translation will appear here...&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;button :disabled=&quot;disabled || ready === false&quot; @click=&quot;translate&quot;&amp;gt;
      {{ ready === false ? &quot;Loading...&quot; : &quot;Translate&quot; }}
    &amp;lt;/button&amp;gt;

    &amp;lt;div class=&quot;progress-bars-container&quot;&amp;gt;
      &amp;lt;label v-if=&quot;ready === false&quot;&amp;gt; Loading models... (only run once) &amp;lt;/label&amp;gt;
      &amp;lt;div v-for=&quot;item in progressItemsArray&quot; :key=&quot;item.file&quot;&amp;gt;
        
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.container {
  margin: 2rem 0;
}

.language-container {
  display: flex;
  justify-content: center;
  gap: 2rem;
  margin-bottom: 1rem;
}

.textbox-container {
  display: flex;
  gap: 1rem;
}

textarea {
  flex: 1;
  padding: 0.5rem;
  border-radius: 4px;
  border: 1px solid rgb(var(--color-border));
  background-color: rgb(var(--color-card));
  color: rgb(var(--color-text-base));
  font-size: 1rem;
  min-height: 100px;
  resize: vertical;
}

button {
  padding: 0.5rem 2rem;
  font-size: 1.1rem;
  cursor: pointer;
  background-color: rgb(var(--color-accent));
  color: rgb(var(--color-text-base));
  border: none;
  border-radius: 4px;
  transition: background-color 0.3s;
}

button:hover:not(:disabled) {
  background-color: rgb(var(--color-card-muted));
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.progress-bars-container {
  margin-top: 2rem;
}

h1 {
  color: rgb(var(--color-text-base));
  margin-bottom: 0.5rem;
}

h2 {
  color: rgb(var(--color-card-muted));
  font-size: 1.2rem;
  font-weight: normal;
  margin-top: 0;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 5: Optimizing the Build&lt;/h2&gt;
&lt;p&gt;Configure Vite to handle our Web Workers and TypeScript efficiently:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;vue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  worker&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    format&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;es&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// Use ES modules format for workers&lt;/span&gt;
    plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// No additional plugins needed for workers&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  optimizeDeps&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    exclude&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;@huggingface/transformers&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// Prevent Vite from trying to bundle Transformers.js&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;You type text and select languages&lt;/li&gt;
&lt;li&gt;The text goes to a Web Worker&lt;/li&gt;
&lt;li&gt;Transformers.js loads the AI model (once)&lt;/li&gt;
&lt;li&gt;The model translates your text&lt;/li&gt;
&lt;li&gt;You see the translation appear in real time&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The translator works offline after the first run. No data leaves your browser. No API keys needed.&lt;/p&gt;
&lt;h2&gt;Try It Yourself&lt;/h2&gt;
&lt;p&gt;Want to explore the code further? Check out the complete source code on &lt;a href=&quot;https://github.com/alexanderop/vue-ai-translate-poc&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Want to learn more? Explore these resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/docs/transformers.js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Transformers.js docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/facebook/nllb-200-distilled-600M&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;NLLB-200 model details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Web Workers guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>vue</category><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Solving Prop Drilling in Vue: Modern State Management Strategies</title><link>https://alexop.dev/posts/solving-prop-drilling-in-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/solving-prop-drilling-in-vue/</guid><description>Eliminate prop drilling in Vue apps using Composition API, Provide/Inject, and Pinia. Learn when to use each approach with practical examples.</description><pubDate>Sat, 25 Jan 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TL;DR: Prop Drilling Solutions at a Glance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global state&lt;/strong&gt;: Pinia (Vue’s official state management)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusable logic&lt;/strong&gt;: Composables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component subtree sharing&lt;/strong&gt;: Provide/Inject&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid&lt;/strong&gt;: Event buses for state management&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Click the toggle button to see interactive diagram animations that demonstrate each concept.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;The Hidden Cost of Prop Drilling: A Real-World Scenario&lt;/h2&gt;
&lt;p&gt;Imagine building a Vue dashboard where the user’s name needs to be displayed in seven nested components. Every intermediate component becomes a middleman for data it doesn’t need. Imagine changing the prop name from &lt;code&gt;userName&lt;/code&gt; to &lt;code&gt;displayName&lt;/code&gt;. You’d have to update six components to pass along something they don’t use!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is prop drilling&lt;/strong&gt; – and it creates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🚨 &lt;strong&gt;Brittle code&lt;/strong&gt; that breaks during refactors&lt;/li&gt;
&lt;li&gt;🕵️ &lt;strong&gt;Debugging nightmares&lt;/strong&gt; from unclear data flow&lt;/li&gt;
&lt;li&gt;🐌 &lt;strong&gt;Performance issues&lt;/strong&gt; from unnecessary re-renders&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solution 1: Pinia for Global State Management&lt;/h2&gt;
&lt;h3&gt;When to Use: App-wide state (user data, auth state, cart items)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;// stores/user.js&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; useUserStore &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineStore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; username &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;username&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;&apos;Guest&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; isLoggedIn &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; username&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;&apos;Guest&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;newUsername&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    username&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; newUsername&lt;span&gt;;&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;username&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; newUsername&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    username&lt;span&gt;,&lt;/span&gt;
    isLoggedIn&lt;span&gt;,&lt;/span&gt;
    setUsername
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Component Usage&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- DeeplyNestedComponent.vue --&amp;gt;
&amp;lt;script setup&amp;gt;
const user = useUserStore();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;user-info&quot;&amp;gt;
    Welcome, {{ user.username }}!
    &amp;lt;button v-if=&quot;!user.isLoggedIn&quot; @click=&quot;user.setUsername(&apos;John&apos;)&quot;&amp;gt;
      Log In
    &amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Centralized state with DevTools support&lt;/li&gt;
&lt;li&gt;TypeScript-friendly&lt;/li&gt;
&lt;li&gt;Built-in SSR support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overkill for small component trees&lt;/li&gt;
&lt;li&gt;Requires understanding of Flux architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solution 2: Composables for Reusable Logic&lt;/h2&gt;
&lt;h3&gt;When to Use: Shared component logic (user preferences, form state)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Implementation with TypeScript&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// composables/useUser.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; username &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;&quot;Guest&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;setUsername&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;newUsername&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    username&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; newUsername&lt;span&gt;;&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; newUsername&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    username&lt;span&gt;,&lt;/span&gt;
    setUsername&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Component Usage&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- UserProfile.vue --&amp;gt;
&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { username, setUsername } = useUser();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;user-profile&quot;&amp;gt;
    &amp;lt;h2&amp;gt;Welcome, {{ username }}!&amp;lt;/h2&amp;gt;
    &amp;lt;button @click=&quot;setUsername(&apos;John&apos;)&quot;&amp;gt;Update Username&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero-dependency solution&lt;/li&gt;
&lt;li&gt;Perfect for logic reuse across components&lt;/li&gt;
&lt;li&gt;Full TypeScript support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shared state requires singleton pattern&lt;/li&gt;
&lt;li&gt;No built-in DevTools integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSR Memory Leaks&lt;/strong&gt;: State declared outside component scope persists between requests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not SSR-Safe&lt;/strong&gt;: Using this pattern in SSR can lead to state pollution across requests&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution 3: Provide/Inject for Component Tree Scoping&lt;/h2&gt;
&lt;h3&gt;When to Use: Library components or feature-specific user data&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Type-Safe Implementation&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;
&lt;span&gt;// utilities/user.ts&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;UserContext&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  username&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;updateUsername&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; UserKey &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Symbol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; InjectionKey&lt;span&gt;&amp;lt;&lt;/span&gt;UserContext&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ParentComponent.vue&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;script setup lang&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;ts&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; username &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Guest&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;updateUsername&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  username&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; name&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;provide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;UserKey&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; username&lt;span&gt;,&lt;/span&gt; updateUsername &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;script&lt;span&gt;&amp;gt;&lt;/span&gt;

&lt;span&gt;// DeepChildComponent.vue&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;script setup lang&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;ts&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; username&lt;span&gt;,&lt;/span&gt; updateUsername &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;inject&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;UserKey&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  username&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Guest&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;updateUsername&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;No user provider!&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;script&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explicit component relationships&lt;/li&gt;
&lt;li&gt;Perfect for component libraries&lt;/li&gt;
&lt;li&gt;Type-safe with TypeScript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can create implicit dependencies&lt;/li&gt;
&lt;li&gt;Debugging requires tracing providers&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Event Buses Fail for State Management&lt;/h2&gt;
&lt;p&gt;Event buses create more problems than they solve for state management:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spaghetti Data Flow&lt;/strong&gt;&lt;br /&gt;
Components become invisibly coupled through arbitrary events. When &lt;code&gt;ComponentA&lt;/code&gt; emits &lt;code&gt;update-theme&lt;/code&gt;, who’s listening? Why? DevTools can’t help you track the chaos.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;State Inconsistencies&lt;/strong&gt;&lt;br /&gt;
Multiple components listening to the same event often maintain duplicate state:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;// Two components, two sources of truth&lt;/span&gt;
eventBus&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;login&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;isLoggedIn &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
eventBus&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;login&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;userStatus &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;active&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory Leaks&lt;/strong&gt;&lt;br /&gt;
Forgotten event listeners in unmounted components keep reacting to events, causing bugs and performance issues.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Where Event Buses Actually Work&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Global notifications (toasts, alerts)&lt;/li&gt;
&lt;li&gt;✅ Analytics tracking&lt;/li&gt;
&lt;li&gt;✅ Decoupled plugin events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Instead of Event Buses&lt;/strong&gt;: Use Pinia for state, composables for logic, and provide/inject for component trees.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Decision Guide: Choosing Your Weapon&quot;&lt;/span&gt;
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;graph&lt;/span&gt; TD
    A&lt;span&gt;[Need Shared State?]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; B&lt;span&gt;[Props/Events]&lt;/span&gt;
    A &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; C&lt;span&gt;{Scope?}&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|App-wide|&lt;/span&gt; D&lt;span&gt;[Pinia]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Component Tree|&lt;/span&gt; E&lt;span&gt;[Provide/Inject]&lt;/span&gt;
    C &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Reusable Logic|&lt;/span&gt; F&lt;span&gt;[Composables]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pro Tips for State Management Success&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start Simple&lt;/strong&gt;: Begin with props, graduate to composables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type Everything&lt;/strong&gt;: Use TypeScript for stores/injections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name Wisely&lt;/strong&gt;: Prefix stores (&lt;code&gt;useUserStore&lt;/code&gt;) and injection keys (&lt;code&gt;UserKey&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor Performance&lt;/strong&gt;: Use Vue DevTools to track reactivity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test State&lt;/strong&gt;: Write unit tests for Pinia stores/composables&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By mastering these patterns, you’ll write Vue apps that scale gracefully while keeping component relationships clear and maintainable.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Building Local-First Apps with Vue and Dexie.js</title><link>https://alexop.dev/posts/building-local-first-apps-vue-dexie/</link><guid isPermaLink="true">https://alexop.dev/posts/building-local-first-apps-vue-dexie/</guid><description>Learn how to create offline-capable, local-first applications using Vue 3 and Dexie.js. Discover patterns for data persistence, synchronization, and optimal user experience.</description><pubDate>Sat, 18 Jan 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Ever been frustrated when your web app stops working because the internet connection dropped? That’s where local-first applications come in! In this guide, we’ll explore how to build robust, offline-capable apps using Vue 3 and Dexie.js. If you’re new to local-first development, check out my &lt;a href=&quot;https://alexop.dev/posts/what-is-local-first-web-development/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;comprehensive introduction to local-first web development&lt;/a&gt; first.&lt;/p&gt;
&lt;h2&gt;What Makes an App “Local-First”?&lt;/h2&gt;
&lt;p&gt;Martin Kleppmann defines local-first software as systems where “the availability of another computer should never prevent you from working.” Think Notion’s desktop app or Figma’s offline mode - they store data locally first and seamlessly sync when online.&lt;/p&gt;
&lt;p&gt;Three key principles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Works without internet connection&lt;/li&gt;
&lt;li&gt;Users stay productive when servers are down&lt;/li&gt;
&lt;li&gt;Data syncs smoothly when connectivity returns&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Architecture Behind Local-First Apps&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Local-First Architecture with Central Server
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;flowchart&lt;/span&gt; LR
    &lt;span&gt;subgraph&lt;/span&gt; Client1&lt;span&gt;[&quot;Client Device&quot;]&lt;/span&gt;
        UI1&lt;span&gt;[&quot;UI&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; DB1&lt;span&gt;[&quot;Local Data&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Client2&lt;span&gt;[&quot;Client Device&quot;]&lt;/span&gt;
        UI2&lt;span&gt;[&quot;UI&quot;]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; DB2&lt;span&gt;[&quot;Local Data&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Server&lt;span&gt;[&quot;Central Server&quot;]&lt;/span&gt;
        SDB&lt;span&gt;[&quot;Server Data&quot;]&lt;/span&gt;
        Sync&lt;span&gt;[&quot;Sync Service&quot;]&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    DB1 &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Sync
    DB2 &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Sync
    Sync &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; SDB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How much data to store locally (full vs. partial dataset)&lt;/li&gt;
&lt;li&gt;How to handle multi-user conflict resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Enter Dexie.js: Your Local-First Swiss Army Knife&lt;/h2&gt;
&lt;p&gt;Dexie.js provides a robust offline-first architecture where database operations run against local IndexedDB first, ensuring responsiveness without internet connection.&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;---&lt;/span&gt;
title&lt;span&gt;:&lt;/span&gt; Dexie.js Local-First Implementation
&lt;span&gt;---&lt;/span&gt;
&lt;span&gt;flowchart&lt;/span&gt; LR
    &lt;span&gt;subgraph&lt;/span&gt; Client&lt;span&gt;[&quot;Client&quot;]&lt;/span&gt;
        App&lt;span&gt;[&quot;Application&quot;]&lt;/span&gt;
        Dexie&lt;span&gt;[&quot;Dexie.js&quot;]&lt;/span&gt;
        IDB&lt;span&gt;[&quot;IndexedDB&quot;]&lt;/span&gt;

        App &lt;span&gt;--&amp;gt;&lt;/span&gt; Dexie
        Dexie &lt;span&gt;--&amp;gt;&lt;/span&gt; IDB

        &lt;span&gt;subgraph&lt;/span&gt; DexieSync&lt;span&gt;[&quot;Dexie Sync&quot;]&lt;/span&gt;
            Rev&lt;span&gt;[&quot;Revision Tracking&quot;]&lt;/span&gt;
            Queue&lt;span&gt;[&quot;Sync Queue&quot;]&lt;/span&gt;
            Rev &lt;span&gt;--&amp;gt;&lt;/span&gt; Queue
        &lt;span&gt;end&lt;/span&gt;
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Cloud&lt;span&gt;[&quot;Dexie Cloud&quot;]&lt;/span&gt;
        Auth&lt;span&gt;[&quot;Auth Service&quot;]&lt;/span&gt;
        Store&lt;span&gt;[&quot;Data Store&quot;]&lt;/span&gt;
        Repl&lt;span&gt;[&quot;Replication Log&quot;]&lt;/span&gt;

        Auth &lt;span&gt;--&amp;gt;&lt;/span&gt; Store
        Store &lt;span&gt;--&amp;gt;&lt;/span&gt; Repl
    &lt;span&gt;end&lt;/span&gt;

    Dexie &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Rev
    Queue &lt;span&gt;&amp;lt;--&amp;gt;&lt;/span&gt; Auth
    IDB &lt;span&gt;-.-&amp;gt;&lt;/span&gt; Queue
    Queue &lt;span&gt;-.-&amp;gt;&lt;/span&gt; Store
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sync Strategies&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WebSocket Sync&lt;/strong&gt;: Real-time updates for collaborative apps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP Long-Polling&lt;/strong&gt;: Default sync mechanism, firewall-friendly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Worker Sync&lt;/strong&gt;: Optional background syncing when configured&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Setting Up Dexie Cloud&lt;/h2&gt;
&lt;p&gt;To enable multi-device synchronization and real-time collaboration, we’ll use Dexie Cloud. Here’s how to set it up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a Dexie Cloud Account&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Visit &lt;a href=&quot;https://dexie.org/cloud/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://dexie.org/cloud/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sign up for a free developer account&lt;/li&gt;
&lt;li&gt;Create a new database from the dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Required Packages&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; dexie-cloud-addon
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure Environment Variables&lt;/strong&gt;:&lt;br /&gt;
Create a &lt;code&gt;.env&lt;/code&gt; file in your project root:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;VITE_DEXIE_CLOUD_URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;https://db.dexie.cloud/db/&lt;span&gt;&amp;lt;&lt;/span&gt;your-db-id&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;&amp;lt;your-db-id&amp;gt;&lt;/code&gt; with the database ID from your Dexie Cloud dashboard.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable Authentication&lt;/strong&gt;:&lt;br /&gt;
Dexie Cloud provides built-in authentication. You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use email/password authentication&lt;/li&gt;
&lt;li&gt;Integrate with OAuth providers&lt;/li&gt;
&lt;li&gt;Create custom authentication flows&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The free tier includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Up to 50MB of data per database&lt;/li&gt;
&lt;li&gt;Up to 1,000 sync operations per day&lt;/li&gt;
&lt;li&gt;Basic authentication and access control&lt;/li&gt;
&lt;li&gt;Real-time sync between devices&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Building a Todo App&lt;/h2&gt;
&lt;p&gt;Let’s implement a practical example with a todo app:&lt;/p&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;flowchart&lt;/span&gt; TD
    &lt;span&gt;subgraph&lt;/span&gt; VueApp&lt;span&gt;[&quot;Vue Application&quot;]&lt;/span&gt;
        App&lt;span&gt;[&quot;App.vue&quot;]&lt;/span&gt;
        TodoList&lt;span&gt;[&quot;TodoList.vue&amp;lt;br&amp;gt;Component&quot;]&lt;/span&gt;
        UseTodo&lt;span&gt;[&quot;useTodo.ts&amp;lt;br&amp;gt;Composable&quot;]&lt;/span&gt;
        Database&lt;span&gt;[&quot;database.ts&amp;lt;br&amp;gt;Dexie Configuration&quot;]&lt;/span&gt;

        App &lt;span&gt;--&amp;gt;&lt;/span&gt; TodoList
        TodoList &lt;span&gt;--&amp;gt;&lt;/span&gt; UseTodo
        UseTodo &lt;span&gt;--&amp;gt;&lt;/span&gt; Database
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; DexieLayer&lt;span&gt;[&quot;Dexie.js Layer&quot;]&lt;/span&gt;
        IndexedDB&lt;span&gt;[&quot;IndexedDB&quot;]&lt;/span&gt;
        SyncEngine&lt;span&gt;[&quot;Dexie Sync Engine&quot;]&lt;/span&gt;

        Database &lt;span&gt;--&amp;gt;&lt;/span&gt; IndexedDB
        Database &lt;span&gt;--&amp;gt;&lt;/span&gt; SyncEngine
    &lt;span&gt;end&lt;/span&gt;

    &lt;span&gt;subgraph&lt;/span&gt; Backend&lt;span&gt;[&quot;Backend Services&quot;]&lt;/span&gt;
        Server&lt;span&gt;[&quot;Server&quot;]&lt;/span&gt;
        ServerDB&lt;span&gt;[&quot;Server Database&quot;]&lt;/span&gt;

        SyncEngine &lt;span&gt;&amp;lt;-.-&amp;gt;&lt;/span&gt; Server
        Server &lt;span&gt;&amp;lt;-.-&amp;gt;&lt;/span&gt; ServerDB
    &lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting Up the Database&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Todo&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  completed&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  createdAt&lt;span&gt;:&lt;/span&gt; Date&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;TodoDB&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;Dexie&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  todos&lt;span&gt;!&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Table&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;super&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;TodoDB&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; addons&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;dexieCloud&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stores&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      todos&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;@id, title, completed, createdAt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;configureSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;databaseUrl&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;cloud&lt;span&gt;.&lt;/span&gt;&lt;span&gt;configure&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      databaseUrl&lt;span&gt;,&lt;/span&gt;
      requireAuth&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      tryUseServiceWorker&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; db &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;TodoDB&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;meta&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;VITE_DEXIE_CLOUD_URL&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;VITE_DEXIE_CLOUD_URL environment variable is not defined&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

db&lt;span&gt;.&lt;/span&gt;&lt;span&gt;configureSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;meta&lt;span&gt;.&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;VITE_DEXIE_CLOUD_URL&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; currentUser &lt;span&gt;=&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;cloud&lt;span&gt;.&lt;/span&gt;currentUser&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;login&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;cloud&lt;span&gt;.&lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;logout&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;cloud&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating the Todo Composable&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTodos&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; newTodoTitle &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; todos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useObservable&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;liveQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;createdAt&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toArray&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; completedTodos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo &lt;span&gt;=&amp;gt;&lt;/span&gt; todo&lt;span&gt;.&lt;/span&gt;completed&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; pendingTodos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; todos&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;todo&lt;span&gt;.&lt;/span&gt;completed&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;addTodo&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;newTodoTitle&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;await&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        title&lt;span&gt;:&lt;/span&gt; newTodoTitle&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt;
        completed&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        createdAt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      newTodoTitle&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failed to add todo&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;toggleTodo&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;:&lt;/span&gt; Todo&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;.&lt;/span&gt;id&lt;span&gt;!&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        completed&lt;span&gt;:&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;todo&lt;span&gt;.&lt;/span&gt;completed&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failed to toggle todo&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;deleteTodo&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failed to delete todo&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;,&lt;/span&gt;
    newTodoTitle&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;,&lt;/span&gt;
    completedTodos&lt;span&gt;,&lt;/span&gt;
    pendingTodos&lt;span&gt;,&lt;/span&gt;
    addTodo&lt;span&gt;,&lt;/span&gt;
    toggleTodo&lt;span&gt;,&lt;/span&gt;
    deleteTodo&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Authentication Guard Component&lt;/h2&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from &quot;@/components/ui/card&quot;;
const user = useObservable(currentUser);
const isAuthenticated = computed(() =&amp;gt; !!user.value);
const isLoading = ref(false);

async function handleLogin() {
  isLoading.value = true;
  try {
    await login();
  } finally {
    isLoading.value = false;
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div
    v-if=&quot;!isAuthenticated&quot;
    class=&quot;bg-background flex min-h-screen flex-col items-center justify-center p-4&quot;
  &amp;gt;
    &amp;lt;Card class=&quot;w-full max-w-md&quot;&amp;gt;
      &amp;lt;!-- Login form content --&amp;gt;
    &amp;lt;/Card&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;template v-else&amp;gt;
    &amp;lt;div class=&quot;bg-card sticky top-0 z-20 border-b&quot;&amp;gt;
      &amp;lt;!-- User info and logout button --&amp;gt;
    &amp;lt;/div&amp;gt;
    
  &amp;lt;/template&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Better Architecture: Repository Pattern&lt;/h2&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;interface&lt;/span&gt; &lt;span&gt;TodoRepository&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;getAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;:&lt;/span&gt; Omit&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; todo&lt;span&gt;:&lt;/span&gt; Partial&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;observe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Observable&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;class&lt;/span&gt; &lt;span&gt;DexieTodoRepository&lt;/span&gt; &lt;span&gt;implements&lt;/span&gt; &lt;span&gt;TodoRepository&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;private&lt;/span&gt; db&lt;span&gt;:&lt;/span&gt; TodoDB&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;getAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toArray&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;observe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;liveQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;createdAt&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toArray&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;:&lt;/span&gt; Omit&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;todo&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; todo&lt;span&gt;:&lt;/span&gt; Partial&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;,&lt;/span&gt; todo&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;db&lt;span&gt;.&lt;/span&gt;todos&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;id&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useTodos&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;repository&lt;span&gt;:&lt;/span&gt; TodoRepository&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; newTodoTitle &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; todos &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;useObservable&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Todo&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;repository&lt;span&gt;.&lt;/span&gt;&lt;span&gt;observe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;addTodo&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;newTodoTitle&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; repository&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        title&lt;span&gt;:&lt;/span&gt; newTodoTitle&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt;
        completed&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        createdAt&lt;span&gt;:&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      newTodoTitle&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failed to add todo&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    todos&lt;span&gt;,&lt;/span&gt;
    newTodoTitle&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;,&lt;/span&gt;
    addTodo&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;// ... other methods&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Understanding the IndexedDB Structure&lt;/h2&gt;
&lt;p&gt;When you inspect your application in the browser’s DevTools under the “Application” tab &amp;gt; “IndexedDB”, you’ll see a database named “TodoDB-zy02f1…” with several object stores:&lt;/p&gt;
&lt;h3&gt;Internal Dexie Stores (Prefixed with $)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: These stores are only created when using Dexie Cloud for sync functionality.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$baseRevs&lt;/strong&gt;: Keeps track of base revisions for synchronization&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$jobs&lt;/strong&gt;: Manages background synchronization tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$logins&lt;/strong&gt;: Stores authentication data including your last login timestamp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$members_mutations&lt;/strong&gt;: Tracks changes to member data for sync&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$realms_mutations&lt;/strong&gt;: Tracks changes to realm/workspace data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$roles_mutations&lt;/strong&gt;: Tracks changes to role assignments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$syncState&lt;/strong&gt;: Maintains the current synchronization state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$todos_mutations&lt;/strong&gt;: Records all changes made to todos for sync and conflict resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Application Data Stores&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;members&lt;/strong&gt;: Contains user membership data with compound indexes:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[userId+realmId]&lt;/code&gt;: For quick user-realm lookups&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[email+realmId]&lt;/code&gt;: For email-based queries&lt;/li&gt;
&lt;li&gt;&lt;code&gt;realmId&lt;/code&gt;: For realm-specific queries&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;realms&lt;/strong&gt;: Stores available workspaces&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;roles&lt;/strong&gt;: Manages user role assignments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;todos&lt;/strong&gt;: Your actual todo items containing:
&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Completed status&lt;/li&gt;
&lt;li&gt;Creation timestamp&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s how a todo item actually looks in IndexedDB:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;tds0PI7ogcJqpZ1JCly0qyAheHmcom&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;test&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;completed&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;createdAt&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Tue Jan 21 2025 08:40:59 GMT+0100 (Central Europe)&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;owner&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;opalic.alexander@gmail.com&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&quot;realmId&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;opalic.alexander@gmail.com&quot;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each todo gets a unique &lt;code&gt;id&lt;/code&gt; generated by Dexie, and when using Dexie Cloud, additional fields like &lt;code&gt;owner&lt;/code&gt; and &lt;code&gt;realmId&lt;/code&gt; are automatically added for multi-user support.&lt;/p&gt;
&lt;p&gt;Each store in IndexedDB acts like a table in a traditional database, but is optimized for client-side storage and offline operations. The &lt;code&gt;$&lt;/code&gt;-prefixed stores are managed automatically by Dexie.js to handle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Offline Persistence&lt;/strong&gt;: Your todos are stored locally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-User Support&lt;/strong&gt;: User data in &lt;code&gt;members&lt;/code&gt; and &lt;code&gt;roles&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync Management&lt;/strong&gt;: All &lt;code&gt;*_mutations&lt;/code&gt; stores track changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Login state in &lt;code&gt;$logins&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Understanding Dexie’s Merge Conflict Resolution&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;&lt;span&gt;%%{init: {&apos;theme&apos;: &apos;base&apos;, &apos;themeVariables&apos;: { &apos;primaryColor&apos;: &apos;#344360&apos;, &apos;primaryBorderColor&apos;: &apos;#ab4b99&apos;, &apos;primaryTextColor&apos;: &apos;#eaedf3&apos;, &apos;lineColor&apos;: &apos;#ab4b99&apos;, &apos;textColor&apos;: &apos;#eaedf3&apos; }}}%%&lt;/span&gt;
&lt;span&gt;flowchart&lt;/span&gt; LR
    A&lt;span&gt;[Detect Change Conflict]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; B&lt;span&gt;{Different Fields?}&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|Yes|&lt;/span&gt; C&lt;span&gt;[Auto-Merge Changes]&lt;/span&gt;
    B &lt;span&gt;--&amp;gt;&lt;/span&gt;&lt;span&gt;|No|&lt;/span&gt; D&lt;span&gt;{Same Field Conflict}&lt;/span&gt;
    D &lt;span&gt;--&amp;gt;&lt;/span&gt; E&lt;span&gt;[Apply Server Version&amp;lt;br&amp;gt;Last-Write-Wins]&lt;/span&gt;
    F&lt;span&gt;[Delete Operation]&lt;/span&gt; &lt;span&gt;--&amp;gt;&lt;/span&gt; G&lt;span&gt;[Always Takes Priority&amp;lt;br&amp;gt;Over Updates]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dexie’s conflict resolution system is sophisticated and field-aware, meaning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Changes to different fields of the same record can be merged automatically&lt;/li&gt;
&lt;li&gt;Conflicts in the same field use last-write-wins with server priority&lt;/li&gt;
&lt;li&gt;Deletions always take precedence over updates to prevent “zombie” records&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach ensures smooth collaboration while maintaining data consistency across devices and users.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This guide demonstrated building local-first applications with Dexie.js and Vue. For simpler applications like todo lists or note-taking apps, Dexie.js provides an excellent balance of features and simplicity. For more complex needs similar to Linear, consider building a custom sync engine.&lt;/p&gt;
&lt;p&gt;Find the complete example code on &lt;a href=&quot;https://github.com/alexanderop/vue-dexie&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

          </content:encoded><category>vue</category><category>dexie</category><category>indexeddb</category><category>local-first</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Adding Obsidian-Style Wiki Links to My Astro Blog</title><link>https://alexop.dev/posts/adding-obsidian-wiki-links-to-astro-blog/</link><guid isPermaLink="true">https://alexop.dev/posts/adding-obsidian-wiki-links-to-astro-blog/</guid><description>I added [[wiki link]] syntax to my Astro blog with hover preview cards. Here&apos;s how it works and how you can build it too.</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;I built a remark plugin that transforms &lt;code&gt;[[slug]]&lt;/code&gt; syntax into internal links with hover preview cards. It supports multiple content collections, custom display text, and shows broken link warnings at build time.&lt;/p&gt;
&lt;h2&gt;Live Examples&lt;/h2&gt;
&lt;p&gt;Hover over these links to see the preview cards in action:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blog post:&lt;/strong&gt; [[are-llms-creative]]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blog post with alias:&lt;/strong&gt; [[are-llms-creative|my thoughts on LLM creativity]]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; [[til:dynamic-pinia-stores]]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt; [[notes:software-testing-with-generative-ai|Testing with AI]]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broken link:&lt;/strong&gt; [[this-post-does-not-exist]]&lt;/p&gt;
&lt;p&gt;All of these are written as simple &lt;code&gt;[[slug]]&lt;/code&gt; syntax in the markdown source.&lt;/p&gt;
&lt;h2&gt;Why I Built This&lt;/h2&gt;
&lt;p&gt;I use Obsidian for note-taking and love the &lt;code&gt;[[wiki link]]&lt;/code&gt; syntax. It’s fast to type and creates connections between notes naturally. I wanted the same experience when writing blog posts.&lt;/p&gt;
&lt;p&gt;Before this, I had an &lt;code&gt;InternalLink&lt;/code&gt; component that required MDX imports:&lt;/p&gt;
&lt;pre class=&quot;language-mdx&quot;&gt;&lt;code class=&quot;language-mdx&quot;&gt;Check out &amp;lt;InternalLink slug=&quot;some-post&quot;&amp;gt;this post&amp;lt;/InternalLink&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Too verbose. I wanted to just type &lt;code&gt;[[some-post]]&lt;/code&gt; and have it work.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The solution uses a custom remark plugin that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Finds &lt;code&gt;[[...]]&lt;/code&gt; patterns in markdown text&lt;/li&gt;
&lt;li&gt;Looks up the post metadata from the file system&lt;/li&gt;
&lt;li&gt;Generates the full preview card HTML at build time&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Supported Syntax&lt;/h3&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;[[slug]]                           # Links to blog post
[[slug|custom text]]               # With display text
[[til:slug]]                       # Links to TIL collection
[[notes:slug|my notes]]            # Other collections with alias
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Preview Card&lt;/h3&gt;
&lt;p&gt;Hover over any wiki link to see a preview card with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Post title&lt;/li&gt;
&lt;li&gt;Description (3 lines max)&lt;/li&gt;
&lt;li&gt;Tags (first 3)&lt;/li&gt;
&lt;li&gt;Publication date&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The card uses fixed positioning to escape overflow containers and flips below the link when too close to the viewport top.&lt;/p&gt;
&lt;h2&gt;Building the Remark Plugin&lt;/h2&gt;
&lt;p&gt;The plugin runs during markdown processing. It reads all content collection files at initialization and caches the metadata for fast lookups.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// src/lib/remarkWikiLinks.ts&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;WIKI_LINK_REGEX&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;remarkWikiLinks&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Load all posts at plugin init&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; cache &lt;span&gt;=&lt;/span&gt; &lt;span&gt;loadAllPosts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;tree&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;visit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;tree&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;text&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;node&lt;span&gt;,&lt;/span&gt; index&lt;span&gt;,&lt;/span&gt; parent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; matches &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;node&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matchAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;WIKI_LINK_REGEX&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;matches&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Replace matches with HTML nodes containing preview cards&lt;/span&gt;
      &lt;span&gt;// ...&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key insight: remark plugins can output raw HTML nodes. The plugin generates the complete preview card markup, so no separate rehype processing is needed.&lt;/p&gt;
&lt;h3&gt;Parsing the Syntax&lt;/h3&gt;
&lt;p&gt;The regex captures two groups:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The target (either &lt;code&gt;slug&lt;/code&gt; or &lt;code&gt;collection:slug&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The optional alias after the pipe&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;parseWikiLink&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; alias&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; collection &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; slug &lt;span&gt;=&lt;/span&gt; target&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;target&lt;span&gt;.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;col&lt;span&gt;,&lt;/span&gt; sl&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; target&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;til&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;notes&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;prompts&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;col&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      collection &lt;span&gt;=&lt;/span&gt; col&lt;span&gt;;&lt;/span&gt;
      slug &lt;span&gt;=&lt;/span&gt; sl&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; collection&lt;span&gt;,&lt;/span&gt; slug&lt;span&gt;,&lt;/span&gt; alias &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Loading Post Metadata&lt;/h3&gt;
&lt;p&gt;The plugin reads frontmatter directly from content files using &lt;code&gt;gray-matter&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;loadCollectionPosts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;collection&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; posts &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; dir &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;src/content/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;collection&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; file &lt;span&gt;of&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readdirSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dir&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; recursive&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.md&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.mdx&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;continue&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; content &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;dir&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;file&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; data &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;matter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;content&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;draft&lt;span&gt;)&lt;/span&gt; &lt;span&gt;continue&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; slug &lt;span&gt;=&lt;/span&gt; file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\.(md|mdx)$&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    posts&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;slug&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      title&lt;span&gt;:&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;title&lt;span&gt;,&lt;/span&gt;
      description&lt;span&gt;:&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;description&lt;span&gt;,&lt;/span&gt;
      tags&lt;span&gt;:&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;tags&lt;span&gt;,&lt;/span&gt;
      pubDatetime&lt;span&gt;:&lt;/span&gt; data&lt;span&gt;.&lt;/span&gt;pubDatetime&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; posts&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generating Preview Card HTML&lt;/h3&gt;
&lt;p&gt;The plugin outputs the same HTML structure as my existing &lt;code&gt;InternalLink&lt;/code&gt; component:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;createPreviewCardHtml&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;post&lt;span&gt;,&lt;/span&gt; href&lt;span&gt;,&lt;/span&gt; displayText&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;
    &amp;lt;span class=&quot;internal-link-wrapper&quot;&amp;gt;
      &amp;lt;a href=&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;href&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot; class=&quot;internal-link&quot;&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;displayText&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/a&amp;gt;
      &amp;lt;span class=&quot;preview-card&quot; role=&quot;tooltip&quot;&amp;gt;
        &amp;lt;span class=&quot;preview-content&quot;&amp;gt;
          &amp;lt;span class=&quot;preview-title&quot;&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;title&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/span&amp;gt;
          &amp;lt;span class=&quot;preview-description&quot;&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;post&lt;span&gt;.&lt;/span&gt;description&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/span&amp;gt;
          &amp;lt;!-- tags and date --&amp;gt;
        &amp;lt;/span&amp;gt;
      &amp;lt;/span&amp;gt;
    &amp;lt;/span&amp;gt;
  &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Broken Link Detection&lt;/h2&gt;
&lt;p&gt;When a wiki link references a non-existent post, the plugin:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Logs a warning during build: &lt;code&gt;[wiki-links] Post not found: blog:missing-slug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Renders the link with error styling (red wavy underline)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;postData&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;[wiki-links] Post not found: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;collection&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;slug&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&amp;lt;span class=&quot;wiki-link-broken&quot; title=&quot;Post not found: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;slug&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;displayText&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This catches typos and stale references before they hit production.&lt;/p&gt;
&lt;h2&gt;Adding the Plugin to Astro&lt;/h2&gt;
&lt;p&gt;Register the plugin in &lt;code&gt;astro.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  markdown&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    remarkPlugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      remarkWikiLinks&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;// other plugins...&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin runs first so wiki links are processed before other transformations.&lt;/p&gt;
&lt;h2&gt;The CSS&lt;/h2&gt;
&lt;p&gt;The styles match my existing &lt;code&gt;InternalLink&lt;/code&gt; component:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;.internal-link-wrapper&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; relative&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; inline-block&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;.internal-link&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;@apply&lt;/span&gt; text-skin-accent underline decoration-dashed&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;.preview-card&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; absolute&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;100% + 8px&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;visibility&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; hidden&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;transition&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; opacity 0.2s&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;.wiki-link-broken&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;@apply&lt;/span&gt; text-red-400 underline decoration-wavy&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Hover Script&lt;/h2&gt;
&lt;p&gt;A small inline script handles the preview card positioning:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;astro:page-load&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;querySelectorAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.internal-link-wrapper&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;mouseenter&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; card &lt;span&gt;=&lt;/span&gt; wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;querySelector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.preview-card&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;// Calculate position, flip if needed, show card&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script runs on &lt;code&gt;astro:page-load&lt;/code&gt; to work with Astro’s view transitions.&lt;/p&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;Now I can write posts with natural wiki link syntax:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;I wrote about [[are-llms-creative|LLM creativity]] last month.
See also my [[til:dynamic-pinia-stores|TIL on Pinia stores]].
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The links render with hover previews, and broken references get caught at build time. Much better than importing components everywhere.&lt;/p&gt;
&lt;h2&gt;What’s Next&lt;/h2&gt;
&lt;p&gt;A few improvements I’m considering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fuzzy matching for slug typos&lt;/li&gt;
&lt;li&gt;Backlinks section showing which posts link to the current one&lt;/li&gt;
&lt;li&gt;Support for heading anchors: &lt;code&gt;[[post#section]]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

          </content:encoded><category>astro</category><category>markdown</category><category>obsidian</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Unlocking Reading Insights: A Guide to Data Analysis with Claude and Readwise</title><link>https://alexop.dev/posts/supercharging-reading-comprehension-claude-readwise/</link><guid isPermaLink="true">https://alexop.dev/posts/supercharging-reading-comprehension-claude-readwise/</guid><description>Discover how to transform your reading data into actionable insights by combining Readwise exports with Claude AI&apos;s powerful analysis capabilities</description><pubDate>Sun, 05 Jan 2025 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Recently, I’ve been exploring &lt;a href=&quot;http://Claude.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude.ai&lt;/a&gt;’s new CSV analysis feature, which allows you to upload spreadsheet data for automated analysis and visualization. In this blog post, I’ll demonstrate how to leverage &lt;a href=&quot;http://Claude.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude.ai&lt;/a&gt;’s capabilities using Readwise data as an example. We’ll explore how crafting better prompts can help you extract more meaningful insights from your data. Additionally, we’ll peek under the hood to understand the technical aspects of how Claude processes and analyzes this information.&lt;/p&gt;
&lt;p&gt;Readwise is a powerful application that syncs and organizes highlights from your Kindle and other reading platforms. While this tutorial uses Readwise data as an example, the techniques demonstrated here can be applied to analyze any CSV dataset with Claude.&lt;/p&gt;
&lt;h2&gt;The Process: From Highlights to Insights&lt;/h2&gt;
&lt;h3&gt;1. Export and Initial Setup&lt;/h3&gt;
&lt;p&gt;First things first: export your Readwise highlights as CSV.&lt;br /&gt;
Just login into your Readwise account and go to -&amp;gt; &lt;a href=&quot;https://readwise.io/export&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://readwise.io/export&lt;/a&gt;&lt;br /&gt;
Scroll down to the bottom and click on “Export to CSV”&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/readwise_claude_csv/readwise_export_csv.png&quot; alt=&quot;Readwise Export CSV&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. Upload the CSV into Claude&lt;/h3&gt;
&lt;p&gt;Drop that CSV into Claude’s interface. Yes, it’s that simple. No need for complex APIs or coding knowledge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The CSV file must fit within Claude’s conversation context window. For very large export files, you may need to split them into smaller chunks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. Use Prompts to analyze the data&lt;/h3&gt;
&lt;h4&gt;a) First Approach&lt;/h4&gt;
&lt;p&gt;First we will use a generic Prompt to see what would happen if we don’t even know what to analyze for:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Please Claude, analyze this data for me.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Claude analyzed my Readwise data and provided a high-level overview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collection stats: 1,322 highlights across 131 books by 126 authors from 2018-2024&lt;/li&gt;
&lt;li&gt;Most highlighted books focused on writing and note-taking, with “How to Take Smart Notes” leading at 102 highlights&lt;/li&gt;
&lt;li&gt;Tag analysis showed “discard” as most common (177), followed by color tags and topical tags like “mental” and “tech”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude also offered to dive deeper into highlight lengths, reading patterns over time, tag relationships, and data visualization.&lt;br /&gt;
Even with this basic prompt, Claude provides valuable insights and analysis. The initial overview can spark ideas for deeper investigation and more targeted analysis. However, we can craft more specific prompts to extract even more meaningful insights from our data.&lt;/p&gt;
&lt;h3&gt;4. Visualization and Analysis&lt;/h3&gt;
&lt;p&gt;While our last Prompt did give use some insights, it was not very useful for me.&lt;br /&gt;
Also I am a visual person, so I want to see some visualizations.&lt;/p&gt;
&lt;p&gt;This is why I created this Prompt to get better Visualization I also added the Colors&lt;br /&gt;
from this blog since I love them.&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Create a responsive data visualization dashboard for my Readwise highlights using React and Recharts.

Theme Colors (Dark Mode):
- Background: rgb(33, 39, 55)
- Text: rgb(234, 237, 243)
- Accent: rgb(255, 107, 237)
- Card Background: rgb(52, 63, 96)
- Muted Elements: rgb(138, 51, 123)
- Borders: rgb(171, 75, 153)

Color Application:
- Use background color for main dashboard
- Apply text color for all typography
- Use accent color for interactive elements and highlights
- Apply card background for visualization containers
- Use muted colors for secondary information
- Implement borders for section separation

Input Data Structure:
- CSV format with columns:
  - Highlight text
  - Book Title
  - Book Author
  - Color
  - Tags
  - Location
  - Highlighted Date

Required Visualizations:
1. Reading Analytics:
   - Average reading time per book (calculated from highlight timestamps)
   - Reading patterns by time of day (heatmap using card background and accent colors)
   - Heat map showing active reading days
     - Base: rgb(52, 63, 96)
     - Intensity levels: rgb(138, 51, 123) → rgb(255, 107, 237)

2. Content Analysis:
   - Vertical bar chart: Top 10 most highlighted books
   - Bars: gradient from rgb(138, 51, 123) to rgb(255, 107, 237)
   - Labels: rgb(234, 237, 243)
   - Grid lines: rgba(171, 75, 153, 0.2)

3. Timeline View:
   - Monthly highlighting activity
   - Line color: rgb(255, 107, 237)
   - Area fill: rgba(255, 107, 237, 0.1)
   - Grid: rgba(171, 75, 153, 0.15)

4. Knowledge Map:
   - Interactive mind map using force-directed graph
   - Node colors: rgb(52, 63, 96)
   - Node borders: rgb(171, 75, 153)
   - Connections: rgba(255, 107, 237, 0.6)
   - Hover state: rgb(255, 107, 237)

5. Summary Statistics Card:
   - Background: rgb(52, 63, 96)
   - Border: rgb(171, 75, 153)
   - Headings: rgb(234, 237, 243)
   - Values: rgb(255, 107, 237)

Design Requirements:
- Typography:
  - Primary font: Light text on dark background
  - Base text: rgb(234, 237, 243)
  - Minimum 16px for body text
  - Headings: rgb(255, 107, 237)

- Card Design:
  - Background: rgb(52, 63, 96)
  - Border: 1px solid rgb(171, 75, 153)
  - Border radius: 8px
  - Box shadow: 0 4px 6px rgba(0, 0, 0, 0.1)

- Interaction States:
  - Hover: Accent color rgb(255, 107, 237)
  - Active: rgb(138, 51, 123)
  - Focus: 2px solid rgb(255, 107, 237)

- Responsive Design:
  - Desktop: Grid layout with 2-3 columns
  - Tablet: 2 columns
  - Mobile: Single column, stacked
  - Gap: 1.5rem
  - Padding: 2rem

Accessibility:
- Ensure contrast ratio ≥ 4.5:1 with text color
- Use rgba(234, 237, 243, 0.7) for secondary text
- Provide focus indicators using accent color
- Include aria-labels for interactive elements
- Support keyboard navigation

Performance:
- Implement CSS variables for theme colors
- Use CSS transitions for hover states
- Optimize SVG rendering for mind map
- Implement virtualization for large datasets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The interactive dashboard generated by Claude demonstrates the powerful synergy between generative AI and data analysis.&lt;br /&gt;
By combining Claude’s natural language processing capabilities with programmatic visualization, we can transform raw reading data into actionable insights. This approach allows us to extract meaningful patterns and trends that would be difficult to identify through manual analysis alone.&lt;/p&gt;
&lt;p&gt;Now I want to give you some tips on how to get the best out of claude.&lt;/p&gt;
&lt;h2&gt;Writing Effective Analysis Prompts&lt;/h2&gt;
&lt;p&gt;Here are key principles for crafting prompts that generate meaningful insights:&lt;/p&gt;
&lt;h3&gt;1. Start with Clear Objectives&lt;/h3&gt;
&lt;p&gt;Instead of vague requests, specify what you want to learn:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Analyze my reading data to identify:
1. Time-of-day reading patterns
2. Most engaged topics
3. Knowledge connection opportunities
4. Potential learning gaps
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Use Role-Based Prompting&lt;/h3&gt;
&lt;p&gt;Give Claude a specific expert perspective:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Act as a learning science researcher analyzing my reading patterns.
Focus on:
- Comprehension patterns
- Knowledge retention indicators
- Learning efficiency metrics
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Request Specific Visualizations&lt;/h3&gt;
&lt;p&gt;Be explicit about the visual insights you need:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Create visualizations showing:
1. Daily reading heatmap
2. Topic relationship network
3. Highlight frequency trends
Use theme-consistent colors for clarity
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bonus: Behind the Scenes - How the Analysis Tool Works&lt;/h2&gt;
&lt;p&gt;For those curious about the technical implementation, let’s peek under the hood at how Claude uses the analysis tool to process your Readwise data:&lt;/p&gt;
&lt;h3&gt;The JavaScript Runtime Environment&lt;/h3&gt;
&lt;p&gt;When you upload your Readwise CSV, Claude has access to a JavaScript runtime environment similar to a browser’s console. This environment comes pre-loaded with several powerful libraries:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;// Available libraries&lt;/span&gt;
&lt;span&gt;// For CSV processing&lt;/span&gt;
&lt;span&gt;// For data manipulation&lt;/span&gt;
&lt;span&gt;// For UI components&lt;/span&gt;
&lt;span&gt;// For visualizations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Data Processing Pipeline&lt;/h3&gt;
&lt;p&gt;The analysis happens in two main stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Initial Data Processing:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;analyzeReadingData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Read the CSV file&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; fileContent &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;readwisedata.csv&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;encoding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Parse CSV using Papaparse&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; parsedData &lt;span&gt;=&lt;/span&gt; Papa&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fileContent&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;header&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;skipEmptyLines&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;dynamicTyping&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Analyze time patterns&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; timeAnalysis &lt;span&gt;=&lt;/span&gt; parsedData&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;row&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; date &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;row&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Highlighted at&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;hour&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getHours&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; row&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Book Title&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;tags&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; row&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Tags&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Group and count data using lodash&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; hourlyDistribution &lt;span&gt;=&lt;/span&gt; _&lt;span&gt;.&lt;/span&gt;&lt;span&gt;countBy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timeAnalysis&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;hour&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Reading time distribution:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; hourlyDistribution&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visualization Component:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;ReadingPatterns&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;timeData&lt;span&gt;,&lt;/span&gt; setTimeData&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;topBooks&lt;span&gt;,&lt;/span&gt; setTopBooks&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;useEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;analyzeData&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;readwisedata.csv&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;encoding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Process time data for visualization&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; timeAnalysis &lt;span&gt;=&lt;/span&gt; parsedData&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;acc&lt;span&gt;,&lt;/span&gt; row&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;const&lt;/span&gt; hour &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;row&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;Highlighted at&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getHours&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        acc&lt;span&gt;[&lt;/span&gt;hour&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;acc&lt;span&gt;[&lt;/span&gt;hour&lt;span&gt;]&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; acc&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Format data for charts&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; timeDataForChart &lt;span&gt;=&lt;/span&gt; Object&lt;span&gt;.&lt;/span&gt;&lt;span&gt;entries&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timeAnalysis&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;hour&lt;span&gt;,&lt;/span&gt; count&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;hour&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;hour&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:00&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          count&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;setTimeData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timeDataForChart&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;analyzeData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;&amp;lt;&lt;/span&gt;div className&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;w-full space-y-8 p-4&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
      &lt;span&gt;&amp;lt;&lt;/span&gt;ResponsiveContainer width&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;100%&quot;&lt;/span&gt; height&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;100%&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
        &lt;span&gt;&amp;lt;&lt;/span&gt;BarChart data&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;timeData&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;

        &lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;BarChart&lt;span&gt;&amp;gt;&lt;/span&gt;
      &lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;ResponsiveContainer&lt;span&gt;&amp;gt;&lt;/span&gt;
    &lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;div&lt;span&gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Technical Features&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Asynchronous File Handling&lt;/strong&gt;: The &lt;code&gt;window.fs.readFile&lt;/code&gt; API provides async file access, similar to Node.js’s fs/promises.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Processing Libraries&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Papaparse handles CSV parsing with options for headers and type conversion&lt;/li&gt;
&lt;li&gt;Lodash provides efficient data manipulation functions&lt;/li&gt;
&lt;li&gt;React and Recharts enable interactive visualizations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;React Integration&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Components use hooks for state management&lt;/li&gt;
&lt;li&gt;Tailwind classes for styling&lt;/li&gt;
&lt;li&gt;Responsive container adapts to screen size&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: The code includes proper error boundaries and async/await patterns to handle potential issues gracefully.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This technical implementation allows Claude to process your reading data efficiently while providing interactive visualizations that help you understand your reading patterns better.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope this blog post demonstrates how AI can accelerate data analysis workflows. What previously required significant time and technical expertise can now be accomplished in minutes. This democratization of data analysis empowers people without coding backgrounds to gain valuable insights from their own data.&lt;/p&gt;

          </content:encoded><category>ai</category><category>productivity</category><category>reading</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The What Why and How of Goal Settings</title><link>https://alexop.dev/posts/the-what-why-and-how-of-goal-settings/</link><guid isPermaLink="true">https://alexop.dev/posts/the-what-why-and-how-of-goal-settings/</guid><description>A deep dive into the philosophy of goal-setting and personal development, exploring the balance between happiness and meaning while providing practical steps for achieving your goals in 2025.</description><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;There is beauty in having goals and in aiming to achieve them. This idea is perfectly captured by Jim Rohn’s quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Become a millionaire not for the million dollars, but for what it will make of you to achieve it.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This wisdom suggests that humans need goals to reach them and grow and improve through the journey. Yet, this perspective isn’t without its critics. Take, for instance, this provocative quote from Fight Club:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“SELF-IMPROVEMENT IS MASTURBATION, NOW SELF-DESTRUCTION…” - TYLER DURDEN&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This counter-view raises an interesting point: focusing too much on self-improvement can become narcissistic and isolating. Rather than connecting with others or making real change, someone might become trapped in an endless cycle of self-focus, similar to the character’s own psychological struggles.&lt;/p&gt;
&lt;p&gt;Despite these conflicting viewpoints, I find the pursuit of self-improvement invigorating, probably because I grew up watching anime. I have always loved the classic story arc, in which the hero faces a devastating loss, then trains and comes back stronger than before. This narrative speaks to something fundamental about human potential and resilience.&lt;/p&gt;
&lt;p&gt;But let’s dig deeper into the practical side of goal-setting. If you align more with Jim Rohn’s philosophy of continuous improvement, you might wonder how to reach your goals. However, I’ve found that what’s harder than the “how” is actually the “what” and “why.” Why do you even want to reach goals? This question becomes especially relevant in our modern Western society, where many people seem settled for working their 9-5, doing the bare minimum, then watching Netflix. Maybe they have a girlfriend or boyfriend, and their only adventure is visiting other countries. Or they just enjoy living in the moment. Or they have a kid, and that child becomes the whole meaning of life.&lt;/p&gt;
&lt;p&gt;These are all valid ways to live, but they raise an interesting question about happiness versus meaning. This reminds me of a profound conversation from the series “Heroes”:&lt;/p&gt;
&lt;p&gt;Mr. Linderman: “You see, I think there comes a time when a man has to ask himself whether he wants a life of happiness or a life of meaning.”&lt;/p&gt;
&lt;p&gt;Nathan Petrelli: “I’d like to think I have both.”&lt;/p&gt;
&lt;p&gt;Mr. Linderman: “Can’t be done. Two very different paths. I mean, to be truly happy, a man must live absolutely in the present. And with no thought of what’s gone before, and no thought of what lies ahead. But, a life of meaning… A man is condemned to wallow in the past and obsess about the future. And my guess is that you’ve done quite a bit of obsessing about yours these last few days.”&lt;/p&gt;
&lt;p&gt;This dialogue highlights a fundamental dilemma in goal-setting. If your sole aim is happiness, perhaps the wisest path would be to retreat to Tibet and meditate all day, truly living in the now. But for many of us, pursuing meaning through goals provides its own form of fulfillment.&lt;/p&gt;
&lt;p&gt;Before setting any goals, you need to honestly assess what you want. Sometimes, your goal is maintaining what you already have - a good job, house, spouse, and kids. However, this brings up another trap I’ve encountered personally. I used to think that once I had everything I wanted, I could stop trying, assuming things would stay the same. This is often a fundamental mistake. Even maintaining the status quo requires continuous work and attention.&lt;/p&gt;
&lt;p&gt;Once you understand your “why,” you can formulate specific goals. You need to develop a clear vision of how you want your life to look in the coming years. Let’s use weight loss as an example since it’s familiar and easily quantifiable.&lt;/p&gt;
&lt;p&gt;Consider this vision: “I want to be healthy and look good by the end of the year. I want to be more self-confident.”&lt;/p&gt;
&lt;p&gt;Now, let’s examine how not to structure your goal. Many people simply say, “My goal is to lose weight.” With such a vague objective, you might join the gym in January and countless others. Still, when life throws curveballs your way - illness, work stress, or missed training sessions - your commitment quickly fades because there’s no clear target to maintain your focus.&lt;/p&gt;
&lt;p&gt;A better approach would be setting a specific goal like “I want to weigh x KG by y date.” This brings clarity and measurability to your objective. However, even this improved goal isn’t enough on its own. You must build a system - an environment that naturally nudges you toward your goals. As James Clear, author of Atomic Habits, brilliantly puts it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“You do not rise to the level of your goals. You fall to the level of your systems.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This insight from one of the most influential books on habit formation reminds us that motivation alone is unreliable. Instead, you need to create sustainable habits that align with your goals. For a weight loss goal of 10kg by May, these habits might include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;weighing yourself daily&lt;/li&gt;
&lt;li&gt;tracking calories&lt;/li&gt;
&lt;li&gt;walking 10k steps&lt;/li&gt;
&lt;li&gt;going to the gym 3 times per week&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another powerful insight from James Clear concerns the language we use with ourselves. For instance, if you’re trying to quit smoking and someone offers you a cigarette, don’t say you’re trying to stop or that you’re an ex-smoker. Instead, firmly state, “I don’t smoke,” from day one. This simple shift in language helps reprogram your identity - you’re not just trying to become a non-smoker, you already are one. Fake it till you make it.&lt;/p&gt;
&lt;p&gt;While habit-tracking apps can be helpful tools when starting out, remember to be gentle with yourself. If you miss a day, don’t let it unravel your entire journey. This leads to the most important advice: don’t do it alone. Despite what some YouTube gurus might suggest about “monk mode” and isolation, finding a community of like-minded individuals can be crucial for success. Share your journey, find accountability partners, and don’t hesitate to work out with others.&lt;/p&gt;
&lt;p&gt;To summarize the path to reaching your goals:&lt;/p&gt;
&lt;h2&gt;Why&lt;/h2&gt;
&lt;p&gt;Be honest with yourself. Think about your life. Are you happy with it? What kind of meaning do you want to create?&lt;/p&gt;
&lt;h2&gt;What&lt;/h2&gt;
&lt;p&gt;If you’re content with your life, what aspects need maintenance? If not, what specific changes would create the life you envision? Think carefully about which goals would elevate your life’s quality and meaning.&lt;/p&gt;
&lt;h2&gt;How&lt;/h2&gt;
&lt;p&gt;Once you’ve identified a meaningful goal that resonates deeply with your values, the implementation becomes clearer:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write down the goal in specific, measurable terms&lt;/li&gt;
&lt;li&gt;Set a realistic timeline for accomplishment&lt;/li&gt;
&lt;li&gt;Study and adopt the habits of those who’ve already achieved similar goals&lt;/li&gt;
&lt;li&gt;Track your progress consistently&lt;/li&gt;
&lt;li&gt;Build a supportive community of like-minded people&lt;/li&gt;
&lt;li&gt;Distance yourself from influences that don’t align with your new direction (you know who they are)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Remember, the journey toward your goals is as important as reaching them. Through this process, you’ll discover not just what you can achieve but who you can become.&lt;/p&gt;

          </content:encoded><category>personal-development</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>XML-Style Tagged Prompts: A Framework for Reliable AI Responses</title><link>https://alexop.dev/posts/xml-tagged-prompts-framework-reliable-ai-responses/</link><guid isPermaLink="true">https://alexop.dev/posts/xml-tagged-prompts-framework-reliable-ai-responses/</guid><description>Learn how top AI engineers use XML-style prompts to consistently get structured, accurate responses from ChatGPT, Claude, and other LLMs. Step-by-step guide with real examples</description><pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Why Traditional AI Communication Falls Short&lt;/h2&gt;
&lt;p&gt;Getting consistent, well-structured responses can be challenging when working with LLMs. Traditional prompting often leads to unpredictable results, making relying on AI assistance for complex tasks difficult.&lt;br /&gt;
While exploring prompt engineering techniques this year, I discovered an interesting approach: Tagged Prompts. This method has improved my interactions with AI, delivering more thoughtful responses.&lt;/p&gt;
&lt;h2&gt;What Are Tagged Prompts? A Simple Guide&lt;/h2&gt;
&lt;p&gt;Tagged prompts borrow from XML’s structured approach to data organization. By wrapping different parts of our prompts in descriptive tags, we create a clear framework for AI responses. Think of it as creating a mental model that guides the AI’s thinking process.&lt;/p&gt;
&lt;h3&gt;The XML Connection&lt;/h3&gt;
&lt;p&gt;To understand tagged prompts, let’s first look at XML’s simple yet powerful structure:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;book&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;The Great Gatsby&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;author&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;F. Scott Fitzgerald&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;author&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;book&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This familiar structure provides the foundation for our prompt engineering approach.&lt;/p&gt;
&lt;h2&gt;How Tagged Prompts Work: XML-Style Structure&lt;/h2&gt;
&lt;p&gt;Let’s compare how an AI responds to the same question with and without tagged prompts:&lt;/p&gt;
&lt;h3&gt;Standard Response&lt;/h3&gt;
&lt;p&gt;&amp;lt;ChatUI&lt;br /&gt;
question=“I want to learn software engineering. What should I do?”&lt;br /&gt;
answer={`I’ll help you create a structured path to learn software engineering. Here’s a comprehensive approach:&lt;/p&gt;
&lt;p&gt;First, start with programming fundamentals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn a beginner-friendly programming language like Python or JavaScript&lt;/li&gt;
&lt;li&gt;Master basic concepts like variables, data types, control flow, functions, and object-oriented programming&lt;/li&gt;
&lt;li&gt;Practice with small projects to reinforce your learning&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you have the basics, focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version Control&lt;/strong&gt;: Learn Git and GitHub to manage code and collaborate with others&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Structures &amp;amp; Algorithms&lt;/strong&gt;: Understanding these is crucial for writing efficient code and passing technical interviews&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development Tools&lt;/strong&gt;: Get comfortable with an IDE (like VS Code), command line, and debugging tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Development Fundamentals&lt;/strong&gt;: HTML, CSS, and JavaScript if you’re interested in web development&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Learn about unit tests, integration tests, and test-driven development&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Databases&lt;/strong&gt;: Start with SQL basics, then learn about database design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical steps you can take:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Choose learning resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Free platforms like freeCodeCamp, The Odin Project, or CS50x&lt;/li&gt;
&lt;li&gt;Paid platforms like Udemy, Coursera, or boot camps&lt;/li&gt;
&lt;li&gt;Technical books and documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with small, achievable projects&lt;/li&gt;
&lt;li&gt;Gradually increase complexity&lt;/li&gt;
&lt;li&gt;Create a portfolio to showcase your work&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Join the community:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Participate in coding forums like Stack Overflow&lt;/li&gt;
&lt;li&gt;Join local coding meetups&lt;/li&gt;
&lt;li&gt;Contribute to open source projects&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Would you like me to suggest some specific resources for any of these areas? Or would you prefer to focus on a particular aspect of software engineering?`}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Before vs After: Impact of Tagged Prompts on AI Responses&lt;/h2&gt;
&lt;h2&gt;Step-by-Step Guide to Implementing Tagged Prompts&lt;/h2&gt;
&lt;p&gt;Tagged prompts can be implemented in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;As a system prompt for ongoing AI interactions&lt;/li&gt;
&lt;li&gt;As part of individual conversation messages&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Basic Implementation&lt;/h3&gt;
&lt;p&gt;Here’s a simple but effective system prompt structure:&lt;/p&gt;
&lt;p&gt;&amp;lt;SystemPromptUI&lt;br /&gt;
prompt={`When answering queries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Think through your reasoning in  tags&lt;/li&gt;
&lt;li&gt;Provide your answer in  tags&lt;/li&gt;
&lt;li&gt;If you spot an error, explain and correct it in  tags`}&lt;br /&gt;
/&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Tagged Response Example&lt;/h3&gt;
&lt;p&gt;&amp;lt;ChatUI&lt;br /&gt;
question=“I want to learn software engineering. What should I do?”&lt;br /&gt;
answer={`&lt;br /&gt;
Let me break this down systematically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Software engineering is a broad field - need to cover both fundamentals and practical skills&lt;/li&gt;
&lt;li&gt;Should suggest a structured learning path that’s manageable&lt;/li&gt;
&lt;li&gt;Important to emphasize both theoretical knowledge and hands-on practice&lt;/li&gt;
&lt;li&gt;Need to include guidance on building a portfolio and joining the community&lt;/li&gt;
&lt;li&gt;Should consider both formal and self-learning options&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here’s a comprehensive path to get started with software engineering:&lt;br /&gt;
Start with Programming Fundamentals:&lt;br /&gt;
Begin with Python or JavaScript - they’re beginner-friendly and widely used. Focus on basics like variables, control flow, functions, and data structures. Platforms like freeCodeCamp or The Odin Project offer free structured courses.&lt;br /&gt;
Build Computer Science Foundation:&lt;br /&gt;
Learn core concepts like:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Data structures (arrays, linked lists, trees)&lt;br /&gt;
Basic algorithms and their complexity&lt;br /&gt;
How computers work at a basic level&lt;br /&gt;
Version control with Git&lt;/p&gt;
&lt;p&gt;Practice Project-Based Learning:&lt;/p&gt;
&lt;p&gt;Start with small projects that teach fundamentals&lt;br /&gt;
Gradually increase complexity as you learn&lt;br /&gt;
Build projects that solve real problems you encounter&lt;br /&gt;
Document your code and maintain a GitHub portfolio&lt;/p&gt;
&lt;p&gt;Join the Community:&lt;/p&gt;
&lt;p&gt;Participate in coding communities on Discord or Reddit&lt;br /&gt;
Contribute to open source projects&lt;br /&gt;
Attend local tech meetups or virtual events&lt;br /&gt;
Find a mentor or study group&lt;/p&gt;
&lt;p&gt;Recommended Learning Path:&lt;/p&gt;
&lt;p&gt;Month 1-2: Programming basics and Git&lt;br /&gt;
Month 3-4: Web development fundamentals (HTML, CSS, JavaScript)&lt;br /&gt;
Month 5-6: Choose a specialization (backend, frontend, mobile, etc.)&lt;br /&gt;
Month 7-8: Build portfolio projects&lt;br /&gt;
Month 9+: Advanced topics and job preparation&lt;/p&gt;
&lt;p&gt;Resources to Consider:&lt;/p&gt;
&lt;p&gt;CS50 from Harvard (free on edX) for computer science basics&lt;br /&gt;
freeCodeCamp for web development&lt;br /&gt;
“Clean Code” by Robert Martin for best practices&lt;br /&gt;
LeetCode for coding practice&lt;/p&gt;
&lt;p&gt;Would you like me to elaborate on any of these areas or suggest resources for a specific aspect you’re most interested in?&lt;/p&gt;


After reviewing my response, I note that while comprehensive, I could have:
1. Emphasized the importance of learning testing early in the process
2. Mentioned the value of understanding databases
3. Included information about different learning styles and approaches
However, these can be addressed in follow-up discussion based on the human&apos;s interests and needs.
`}
/&amp;gt;
&lt;h2&gt;Advanced Techniques: Taking Tagged Prompts Further&lt;/h2&gt;
&lt;p&gt;For more sophisticated applications, we can add quality metrics and step tracking:&lt;/p&gt;
&lt;p&gt;&amp;lt;SystemPromptUI&lt;br /&gt;
prompt={`Use  tags to explore different approaches and viewpoints&lt;br /&gt;
Use  tags to break down the solution (20 step budget, request more if needed)&lt;br /&gt;
Add  tags after each step to track remaining budget&lt;br /&gt;
Use  tags to evaluate progress and be self-critical&lt;br /&gt;
Rate quality with  tags (0.0-1.0):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Greater than or equal to 0.8: Continue approach&lt;/li&gt;
&lt;li&gt;Between 0.5 and 0.7: Minor adjustments&lt;/li&gt;
&lt;li&gt;Less than 0.5: Try new approach&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Show all work and calculations explicitly&lt;br /&gt;
Explore multiple solutions when possible&lt;br /&gt;
Summarize final answer in  tags&lt;br /&gt;
End with final reflection and reward score&lt;br /&gt;
Adjust strategy based on reward scores and intermediate results`}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Tagged Prompts in Production: v0 by Vercel Case Study&lt;/h2&gt;
&lt;p&gt;Vercel’s AI assistant v0 demonstrates how tagged prompts work in production. Their implementation, revealed through a &lt;a href=&quot;https://www.reddit.com/r/LocalLLaMA/comments/1gwwyia/leaked_system_prompts_from_v0_vercels_ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;leaked prompt on Reddit&lt;/a&gt;, shows the power of structured prompts in professional tools.&lt;/p&gt;
&lt;h2&gt;Essential Resources for Mastering Tagged Prompts&lt;/h2&gt;
&lt;p&gt;For deeper exploration of tagged prompts and related concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Claude Documentation on Structured Outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.promptingguide.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Prompt Engineering Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Key Takeaways: Getting Started with Tagged Prompts&lt;/h2&gt;
&lt;p&gt;This was just a quick overview to explain the basic idea of tagged prompts.&lt;br /&gt;
I would suggest trying out this technique for your specific use case.&lt;br /&gt;
Compare responses with tags and without tags to see the difference.&lt;/p&gt;

          </content:encoded><category>ai</category><category>llm</category><category>prompt-engineering</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Use the Variant Props Pattern in Vue</title><link>https://alexop.dev/posts/vue-typescript-variant-props-type-safe-props/</link><guid isPermaLink="true">https://alexop.dev/posts/vue-typescript-variant-props-type-safe-props/</guid><description>Learn how to create type-safe Vue components where prop types depend on other props using TypeScript discriminated unions. A practical guide with real-world examples.</description><pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Building Vue components that handle multiple variations while maintaining type safety can be tricky. Let’s dive into the Variant Props Pattern (VPP) - a powerful approach that uses TypeScript’s discriminated unions with Vue’s composition API to create truly type-safe component variants.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;The Variant Props Pattern in Vue combines TypeScript’s discriminated unions with Vue’s prop system to create type-safe component variants. Instead of using complex type utilities, we explicitly mark incompatible props as never to prevent prop mixing at compile time:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Define base props&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;BaseProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Success variant prevents error props&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;SuccessProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; BaseProps &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  errorCode&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Prevents mixing&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Error variant prevents success props&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ErrorProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; BaseProps &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  errorCode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  message&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Prevents mixing&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Props&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; SuccessProps &lt;span&gt;|&lt;/span&gt; ErrorProps&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern provides compile-time safety, excellent IDE support, and reliable vue-tsc compatibility. Perfect for components that need multiple, mutually exclusive prop combinations.&lt;/p&gt;
&lt;h2&gt;The Problem: Mixed Props Nightmare&lt;/h2&gt;
&lt;p&gt;Picture this: You’re building a notification component that needs to handle both success and error states. Each state has its own specific properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Success notifications need a &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;duration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Error notifications need an &lt;code&gt;errorCode&lt;/code&gt; and a &lt;code&gt;retryable&lt;/code&gt; flag&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without proper type safety, developers might accidentally mix these props:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&amp;lt;!-- This should fail! --&amp;gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;NotificationAlert&lt;/span&gt;
  &lt;span&gt;variant&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;primary&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;title&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;Data Saved&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;Success!&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;errorCode&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;UPLOAD_001&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;  &lt;span&gt;&amp;lt;!--&lt;/span&gt; &lt;span&gt;🚨&lt;/span&gt; &lt;span&gt;Mixing&lt;/span&gt; &lt;span&gt;success&lt;/span&gt; &lt;span&gt;and&lt;/span&gt; &lt;span&gt;error&lt;/span&gt; &lt;span&gt;props&lt;/span&gt; &lt;span&gt;--&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  :duration=&quot;5000&quot;
  @close=&quot;handleClose&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Simple Solution That Doesn’t Work&lt;/h2&gt;
&lt;p&gt;Your first instinct might be to define separate interfaces:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;SuccessProps&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;primary&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  duration&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ErrorProps&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;danger&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;warning&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  errorCode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  retryable&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 🚨 This allows mixing both types!&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Props&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; SuccessProps &lt;span&gt;&amp;amp;&lt;/span&gt; ErrorProps&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem? This approach allows developers to use both success and error props simultaneously - definitely not what we want!&lt;/p&gt;
&lt;h2&gt;Using Discriminated Unions with &lt;code&gt;never&lt;/code&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TypeScript Tip&lt;/strong&gt;: The &lt;code&gt;never&lt;/code&gt; type is a special type in TypeScript that represents values that never occur. When a property is marked as &lt;code&gt;never&lt;/code&gt;, TypeScript ensures that value can never be assigned to that property. This makes it perfect for creating mutually exclusive props, as it prevents developers from accidentally using props that shouldn’t exist together.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;never&lt;/code&gt; type commonly appears in TypeScript in several scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Functions that never return (throw errors or have infinite loops)&lt;/li&gt;
&lt;li&gt;Exhaustive type checking in switch statements&lt;/li&gt;
&lt;li&gt;Impossible type intersections (e.g., &lt;code&gt;string &amp;amp; number&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Making properties mutually exclusive, as we do in this pattern&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The main trick to make it work with the current implmenation of defineProps is to use &lt;code&gt;never&lt;/code&gt; to explicitly mark unused variant props.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Base props shared between variants&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;BaseProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Success variant&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;SuccessProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; BaseProps &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;primary&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  duration&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// Explicitly mark error props as never&lt;/span&gt;
  errorCode&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  retryable&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Error variant&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ErrorProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; BaseProps &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;danger&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;warning&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  errorCode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  retryable&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// Explicitly mark success props as never&lt;/span&gt;
  message&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  duration&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Final props type - only one variant allowed!&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Props&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; SuccessProps &lt;span&gt;|&lt;/span&gt; ErrorProps&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Important Note About Vue Components&lt;/h2&gt;
&lt;p&gt;When implementing this pattern, you’ll need to make your component generic due to a current type restriction in &lt;code&gt;defineComponent&lt;/code&gt;.&lt;br /&gt;
By making the component generic, we can bypass &lt;code&gt;defineComponent&lt;/code&gt; and define the component as a functional component:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot; generic=&quot;T&quot;&amp;gt;
// Now our discriminated union props will work correctly
type BaseProps = {
  title: string;
};

type SuccessProps = BaseProps &amp;amp; {
  variant: &quot;primary&quot; | &quot;secondary&quot;;
  message: string;
  duration: number;
  errorCode?: never;
  retryable?: never;
};

// ... rest of the types
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach allows TypeScript to properly enforce our prop variants at compile time.&lt;/p&gt;
&lt;h2&gt;Putting It All Together&lt;/h2&gt;
&lt;p&gt;Here’s our complete notification component using the Variant Props Pattern:&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Variant Props Pattern (VPP) provides a robust approach for building type-safe Vue components. While the Vue team is working on improving native support for discriminated unions &lt;a href=&quot;https://github.com/vuejs/core/issues/8952&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;in vuejs/core#8952&lt;/a&gt;, this pattern offers a practical solution today:&lt;/p&gt;
&lt;p&gt;Unfortunately, what currently is not working is using helper utility types like Xor so that we don’t have to&lt;br /&gt;
manually mark unused variant props as never. When you do that, you will get an error from vue-tsc.&lt;/p&gt;
&lt;p&gt;Example of a helper type like Xor:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Without&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; Exclude&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;&lt;span&gt;XOR&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;U&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;object&lt;/span&gt;
  &lt;span&gt;?&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;Without&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;Without&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;U&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;U&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Success notification properties&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;SuccessProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;primary&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  duration&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Error notification properties&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ErrorProps&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  title&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  variant&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;danger&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;warning&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  errorCode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  retryable&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Final props type - only one variant allowed! ✨&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Props&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;XOR&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;SuccessProps&lt;span&gt;,&lt;/span&gt; ErrorProps&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Video Reference&lt;/h2&gt;
&lt;p&gt;If you also prefer to learn this in video format, check out this tutorial:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

          </content:encoded><category>vue</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>SQLite in Vue: Complete Guide to Building Offline-First Web Apps</title><link>https://alexop.dev/posts/sqlite-vue3-offline-first-web-apps-guide/</link><guid isPermaLink="true">https://alexop.dev/posts/sqlite-vue3-offline-first-web-apps-guide/</guid><description>Learn how to build offline-capable Vue 3 apps using SQLite and WebAssembly in 2024. Step-by-step tutorial includes code examples for database operations, query playground implementation, and best practices for offline-first applications.</description><pubDate>Mon, 25 Nov 2024 07:44:12 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;TLDR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Set up SQLite WASM in a Vue 3 application for offline data storage&lt;/li&gt;
&lt;li&gt;Learn how to use Origin Private File System (OPFS) for persistent storage&lt;/li&gt;
&lt;li&gt;Build a SQLite query playground with Vue composables&lt;/li&gt;
&lt;li&gt;Implement production-ready offline-first architecture&lt;/li&gt;
&lt;li&gt;Compare SQLite vs IndexedDB for web applications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking to add offline capabilities to your Vue application? While browsers offer IndexedDB, SQLite provides a more powerful solution for complex data operations. This comprehensive guide shows you how to integrate SQLite with Vue using WebAssembly for robust offline-first applications.&lt;/p&gt;
&lt;h2&gt;📚 What We’ll Build&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Vue 3 app with SQLite that works offline&lt;/li&gt;
&lt;li&gt;A simple query playground to test SQLite&lt;/li&gt;
&lt;li&gt;Everything runs in the browser - no server needed!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/sqlite-vue/sqlite-playground.png&quot; alt=&quot;Screenshot Sqlite Playground&quot; loading=&quot;lazy&quot; /&gt;&lt;br /&gt;
&lt;em&gt;Try it out: Write and run SQL queries right in your browser&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🚀 &lt;strong&gt;Want the code?&lt;/strong&gt; Get the complete example at &lt;a href=&quot;https://github.com/alexanderop/sqlite-vue-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/alexanderop/sqlite-vue-example&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;🗃️ Why SQLite?&lt;/h2&gt;
&lt;p&gt;Browser storage like IndexedDB is okay, but SQLite is better because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s a real SQL database in your browser&lt;/li&gt;
&lt;li&gt;Your data stays safe even when offline&lt;/li&gt;
&lt;li&gt;You can use normal SQL queries&lt;/li&gt;
&lt;li&gt;It handles complex data relationships well&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;🛠️ How It Works&lt;/h2&gt;
&lt;p&gt;We’ll use three main technologies:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SQLite Wasm&lt;/strong&gt;: SQLite converted to run in browsers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Workers&lt;/strong&gt;: Runs database code without freezing your app&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Origin Private File System&lt;/strong&gt;: A secure place to store your database&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s how they work together:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;📝 Implementation Guide&lt;/h2&gt;
&lt;p&gt;Let’s build this step by step, starting with the core SQLite functionality and then creating a playground to test it.&lt;/p&gt;
&lt;h3&gt;Step 1: Install Dependencies&lt;/h3&gt;
&lt;p&gt;First, install the required SQLite WASM package:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; @sqlite.org/sqlite-wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Configure Vite&lt;/h3&gt;
&lt;p&gt;Create or update your &lt;code&gt;vite.config.ts&lt;/code&gt; file to support WebAssembly and cross-origin isolation:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  server&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    headers&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;Cross-Origin-Opener-Policy&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;same-origin&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;Cross-Origin-Embedder-Policy&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;require-corp&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  optimizeDeps&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    exclude&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;@sqlite.org/sqlite-wasm&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration is crucial for SQLite WASM to work properly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cross-Origin Headers&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Embedder-Policy&lt;/code&gt; headers enable “cross-origin isolation”&lt;/li&gt;
&lt;li&gt;This is required for using SharedArrayBuffer, which SQLite WASM needs for optimal performance&lt;/li&gt;
&lt;li&gt;Without these headers, the WebAssembly implementation might fail or perform poorly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dependency Optimization&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;optimizeDeps.exclude&lt;/code&gt; tells Vite not to pre-bundle the SQLite WASM package&lt;/li&gt;
&lt;li&gt;This is necessary because the WASM files need to be loaded dynamically at runtime&lt;/li&gt;
&lt;li&gt;Pre-bundling would break the WASM initialization process&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 3: Add TypeScript Types&lt;/h3&gt;
&lt;p&gt;Since &lt;code&gt;@sqlite.org/sqlite-wasm&lt;/code&gt; doesn’t include TypeScript types for Sqlite3Worker1PromiserConfig, we need to create our own. Create a new file &lt;code&gt;types/sqlite-wasm.d.ts&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;Define this as a d.ts file so that TypeScript knows about it.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;declare&lt;/span&gt; &lt;span&gt;module&lt;/span&gt; &lt;span&gt;&quot;@sqlite.org/sqlite-wasm&quot;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;OnreadyFunction&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;Sqlite3Worker1PromiserConfig&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    onready&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; OnreadyFunction&lt;span&gt;;&lt;/span&gt;
    worker&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Worker &lt;span&gt;|&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; Worker&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    generateMessageId&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;messageObject&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    debug&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;args&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    onunhandled&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;event&lt;span&gt;:&lt;/span&gt; MessageEvent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;DbId&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;undefined&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PromiserMethods&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;&quot;config-get&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      args&lt;span&gt;:&lt;/span&gt; Record&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        dbID&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
        version&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          libVersion&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
          sourceId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
          libVersionNumber&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
          downloadVersion&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        bigIntEnabled&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        opfsEnabled&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        vfsList&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    open&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      args&lt;span&gt;:&lt;/span&gt; Partial&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
        filename&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        vfs&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        dbId&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
        filename&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        persistent&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        vfs&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    exec&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      args&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        sql&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        dbId&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
        bind&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        returnValue&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        dbId&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
        sql&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        bind&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        returnValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        resultRows&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PromiserResponseSuccess&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; PromiserMethods&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    result&lt;span&gt;:&lt;/span&gt; PromiserMethods&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;result&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    messageId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    dbId&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
    workerReceivedTime&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    workerRespondTime&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    departureTime&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PromiserResponseError&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      operation&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      message&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      errorClass&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      input&lt;span&gt;:&lt;/span&gt; object&lt;span&gt;;&lt;/span&gt;
      stack&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    messageId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    dbId&lt;span&gt;:&lt;/span&gt; DbId&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;PromiserResponse&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; PromiserMethods&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt;
    &lt;span&gt;|&lt;/span&gt; PromiserResponseSuccess&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
    &lt;span&gt;|&lt;/span&gt; PromiserResponseError&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;type&lt;/span&gt; &lt;span&gt;Promiser&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; PromiserMethods&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    messageType&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    messageArguments&lt;span&gt;:&lt;/span&gt; PromiserMethods&lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;args&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;PromiserResponse&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;sqlite3Worker1Promiser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    config&lt;span&gt;?&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Sqlite3Worker1PromiserConfig &lt;span&gt;|&lt;/span&gt; OnreadyFunction
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Promiser&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Create the SQLite Composable&lt;/h3&gt;
&lt;p&gt;The core of our implementation is the &lt;code&gt;useSQLite&lt;/code&gt; composable. This will handle all database operations:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; databaseConfig &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  filename&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;file:mydb.sqlite3?vfs=opfs&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  tables&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    test&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;test_table&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      schema&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;
        CREATE TABLE IF NOT EXISTS test_table (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
      &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useSQLite&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isLoading &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isInitialized &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;let&lt;/span&gt; promiser&lt;span&gt;:&lt;/span&gt; ReturnType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; sqlite3Worker1Promiser&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; dbId&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;isInitialized&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;// Initialize the SQLite worker&lt;/span&gt;
      promiser &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;&lt;span&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;const&lt;/span&gt; _promiser &lt;span&gt;=&lt;/span&gt; &lt;span&gt;sqlite3Worker1Promiser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;onready&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;resolve&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;_promiser&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;promiser&lt;span&gt;)&lt;/span&gt; &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Failed to initialize promiser&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Get configuration and open database&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;promiser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;config-get&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; openResponse &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;promiser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;open&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        filename&lt;span&gt;:&lt;/span&gt; databaseConfig&lt;span&gt;.&lt;/span&gt;filename&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;openResponse&lt;span&gt;.&lt;/span&gt;type &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;openResponse&lt;span&gt;.&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;

      dbId &lt;span&gt;=&lt;/span&gt; openResponse&lt;span&gt;.&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;dbId &lt;span&gt;as&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;// Create initial tables&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;promiser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;exec&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        dbId&lt;span&gt;,&lt;/span&gt;
        sql&lt;span&gt;:&lt;/span&gt; databaseConfig&lt;span&gt;.&lt;/span&gt;tables&lt;span&gt;.&lt;/span&gt;test&lt;span&gt;.&lt;/span&gt;schema&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      isInitialized&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; err &lt;span&gt;instanceof&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; err &lt;span&gt;:&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Unknown error&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;throw&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;executeQuery&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sql&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; params&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;dbId &lt;span&gt;||&lt;/span&gt; &lt;span&gt;!&lt;/span&gt;promiser&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;await&lt;/span&gt; &lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;

    isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; promiser&lt;span&gt;!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;exec&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        dbId&lt;span&gt;:&lt;/span&gt; dbId &lt;span&gt;as&lt;/span&gt; DbId&lt;span&gt;,&lt;/span&gt;
        sql&lt;span&gt;,&lt;/span&gt;
        bind&lt;span&gt;:&lt;/span&gt; params&lt;span&gt;,&lt;/span&gt;
        returnValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;resultRows&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;type &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;

      &lt;span&gt;return&lt;/span&gt; result&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt;
        err &lt;span&gt;instanceof&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; err &lt;span&gt;:&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Query execution failed&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;throw&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      isLoading&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    isLoading&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;,&lt;/span&gt;
    isInitialized&lt;span&gt;,&lt;/span&gt;
    executeQuery&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Create a SQLite Playground Component&lt;/h3&gt;
&lt;p&gt;Now let’s create a component to test our SQLite implementation:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { isLoading, error, executeQuery } = useSQLite();
const sqlQuery = ref(&quot;SELECT * FROM test_table&quot;);
const queryResult = ref&amp;lt;any[]&amp;gt;([]);
const queryError = ref&amp;lt;string | null&amp;gt;(null);

// Predefined example queries for testing
const exampleQueries = [
  { title: &quot;Select all&quot;, query: &quot;SELECT * FROM test_table&quot; },
  {
    title: &quot;Insert&quot;,
    query: &quot;INSERT INTO test_table (name) VALUES (&apos;New Test Item&apos;)&quot;,
  },
  {
    title: &quot;Update&quot;,
    query: &quot;UPDATE test_table SET name = &apos;Updated Item&apos; WHERE name LIKE &apos;New%&apos;&quot;,
  },
  {
    title: &quot;Delete&quot;,
    query: &quot;DELETE FROM test_table WHERE name = &apos;Updated Item&apos;&quot;,
  },
];

async function runQuery() {
  queryError.value = null;
  queryResult.value = [];

  try {
    const result = await executeQuery(sqlQuery.value);
    const isSelect = sqlQuery.value.trim().toLowerCase().startsWith(&quot;select&quot;);

    if (isSelect) {
      queryResult.value = result?.result.resultRows || [];
    } else {
      // After mutation, fetch updated data
      queryResult.value =
        (await executeQuery(&quot;SELECT * FROM test_table&quot;))?.result.resultRows ||
        [];
    }
  } catch (err) {
    queryError.value = err instanceof Error ? err.message : &quot;An error occurred&quot;;
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;mx-auto max-w-7xl px-4 py-6&quot;&amp;gt;
    &amp;lt;h2 class=&quot;text-2xl font-bold&quot;&amp;gt;SQLite Playground&amp;lt;/h2&amp;gt;

    &amp;lt;!-- Example queries --&amp;gt;
    &amp;lt;div class=&quot;mt-4&quot;&amp;gt;
      &amp;lt;h3 class=&quot;text-sm font-medium&quot;&amp;gt;Example Queries:&amp;lt;/h3&amp;gt;
      &amp;lt;div class=&quot;mt-2 flex gap-2&quot;&amp;gt;
        &amp;lt;button
          v-for=&quot;example in exampleQueries&quot;
          :key=&quot;example.title&quot;
          class=&quot;rounded-full bg-gray-100 px-3 py-1 text-sm hover:bg-gray-200&quot;
          @click=&quot;sqlQuery = example.query&quot;
        &amp;gt;
          {{ example.title }}
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- Query input --&amp;gt;
    &amp;lt;div class=&quot;mt-6&quot;&amp;gt;
      &amp;lt;textarea
        v-model=&quot;sqlQuery&quot;
        rows=&quot;4&quot;
        class=&quot;w-full rounded-lg px-4 py-3 font-mono text-sm&quot;
        :disabled=&quot;isLoading&quot;
      /&amp;gt;
      &amp;lt;button
        :disabled=&quot;isLoading&quot;
        class=&quot;mt-2 rounded-lg bg-blue-600 px-4 py-2 text-white&quot;
        @click=&quot;runQuery&quot;
      &amp;gt;
        {{ isLoading ? &quot;Running...&quot; : &quot;Run Query&quot; }}
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- Error display --&amp;gt;
    &amp;lt;div
      v-if=&quot;error || queryError&quot;
      class=&quot;mt-4 rounded-lg bg-red-50 p-4 text-red-600&quot;
    &amp;gt;
      {{ error?.message || queryError }}
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- Results table --&amp;gt;
    &amp;lt;div v-if=&quot;queryResult.length&quot; class=&quot;mt-4&quot;&amp;gt;
      &amp;lt;h3 class=&quot;text-lg font-semibold&quot;&amp;gt;Results:&amp;lt;/h3&amp;gt;
      &amp;lt;div class=&quot;mt-2 overflow-x-auto&quot;&amp;gt;
        &amp;lt;table class=&quot;w-full&quot;&amp;gt;
          &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;th
                v-for=&quot;column in Object.keys(queryResult[0])&quot;
                :key=&quot;column&quot;
                class=&quot;px-4 py-2 text-left&quot;
              &amp;gt;
                {{ column }}
              &amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/thead&amp;gt;
          &amp;lt;tbody&amp;gt;
            &amp;lt;tr v-for=&quot;(row, index) in queryResult&quot; :key=&quot;index&quot;&amp;gt;
              &amp;lt;td
                v-for=&quot;column in Object.keys(row)&quot;
                :key=&quot;column&quot;
                class=&quot;px-4 py-2&quot;
              &amp;gt;
                {{ row[column] }}
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🎯 Real-World Example: Notion’s SQLite Implementation&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.notion.com/blog/how-we-sped-up-notion-in-the-browser-with-wasm-sqlite&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Notion recently shared&lt;/a&gt; how they implemented SQLite in their web application, providing some valuable insights:&lt;/p&gt;
&lt;h3&gt;Performance Improvements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;20% faster page navigation across all modern browsers&lt;/li&gt;
&lt;li&gt;Even greater improvements for users with slower connections:&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Multi-Tab Architecture&lt;/h3&gt;
&lt;p&gt;Notion solved the challenge of handling multiple browser tabs with an innovative approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Each tab has its own Web Worker for SQLite operations&lt;/li&gt;
&lt;li&gt;A SharedWorker manages which tab is “active”&lt;/li&gt;
&lt;li&gt;Only one tab can write to SQLite at a time&lt;/li&gt;
&lt;li&gt;Queries from all tabs are routed through the active tab’s Worker&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Key Learnings from Notion&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Async Loading&lt;/strong&gt;: They load the WASM SQLite library asynchronously to avoid blocking initial page load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;: They implemented a “racing” system between SQLite and API requests to handle slower devices&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPFS Handling&lt;/strong&gt;: They discovered that Origin Private File System (OPFS) doesn’t handle concurrency well out of the box&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Origin Isolation&lt;/strong&gt;: They opted for OPFS SyncAccessHandle Pool VFS to avoid cross-origin isolation requirements&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This real-world implementation demonstrates both the potential and challenges of using SQLite in production web applications. Notion’s success shows that with careful architecture choices, SQLite can significantly improve web application performance.&lt;/p&gt;
&lt;h2&gt;🎯 Conclusion&lt;/h2&gt;
&lt;p&gt;You now have a solid foundation for building offline-capable Vue applications using SQLite. This approach offers significant advantages over traditional browser storage solutions, especially for complex data requirements.&lt;/p&gt;

          </content:encoded><category>vue</category><category>local-first</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Create Dark Mode-Compatible Technical Diagrams in Astro with Excalidraw: A Complete Guide</title><link>https://alexop.dev/posts/excalidraw-dark-mode-astro-diagrams/</link><guid isPermaLink="true">https://alexop.dev/posts/excalidraw-dark-mode-astro-diagrams/</guid><description>Learn how to create and integrate theme-aware Excalidraw diagrams into your Astro blog. This step-by-step guide shows you how to build custom components that automatically adapt to light and dark modes, perfect for technical documentation and blogs</description><pubDate>Sun, 27 Oct 2024 07:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Why You Need Theme-Aware Technical Diagrams in Your Astro Blog&lt;/h2&gt;
&lt;p&gt;Technical bloggers often face a common challenge: creating diagrams seamlessly integrating with their site’s design system. While tools like Excalidraw make it easy to create beautiful diagrams, maintaining their visual consistency across different theme modes can be frustrating. This is especially true when your Astro blog supports light and dark modes.&lt;br /&gt;
This tutorial will solve this problem by building a custom solution that automatically adapts your Excalidraw diagrams to match your site’s theme.&lt;/p&gt;
&lt;h2&gt;Common Challenges with Technical Diagrams in Web Development&lt;/h2&gt;
&lt;p&gt;When working with Excalidraw, we face several issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exported SVGs come with fixed colors&lt;/li&gt;
&lt;li&gt;Diagrams don’t automatically adapt to dark mode&lt;/li&gt;
&lt;li&gt;Maintaining separate versions for different themes is time-consuming&lt;/li&gt;
&lt;li&gt;Lack of interactive elements and smooth transitions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Before vs After: The Impact of Theme-Aware Diagrams&lt;/h2&gt;
&lt;div&gt;
  &lt;div&gt;
    &lt;h4&gt;Standard Export&lt;/h4&gt;
    &lt;p&gt;Here&apos;s how a typical Excalidraw diagram looks without any customization:&lt;/p&gt;
    
  &lt;/div&gt;
  &lt;div&gt;
    &lt;h4&gt;With Our Solution&lt;/h4&gt;
    &lt;p&gt;And here&apos;s the same diagram using our custom component:&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2&gt;Building a Theme-Aware Excalidraw Component for Astro&lt;/h2&gt;
&lt;p&gt;We’ll create an Astro component that transforms static Excalidraw exports into dynamic, theme-aware diagrams. Our solution will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Automatically adapt to light and dark modes&lt;/li&gt;
&lt;li&gt;Support your custom design system colors&lt;/li&gt;
&lt;li&gt;Add interactive elements and smooth transitions&lt;/li&gt;
&lt;li&gt;Maintain accessibility standards&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;💡 Quick Start: Need an Astro blog first? Use &lt;a href=&quot;https://github.com/satnaing/astro-paper&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AstroPaper&lt;/a&gt; as your starter or build from scratch. This tutorial focuses on the diagram component itself.&lt;/p&gt;
&lt;h2&gt;Step-by-Step Implementation Guide&lt;/h2&gt;
&lt;h3&gt;1. Implementing the Theme System&lt;/h3&gt;
&lt;p&gt;First, let’s define the color variables that will power our theme-aware diagrams:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;html[data-theme=&quot;light&quot;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--color-fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 250&lt;span&gt;,&lt;/span&gt; 252&lt;span&gt;,&lt;/span&gt; 252&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-text-base&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 34&lt;span&gt;,&lt;/span&gt; 46&lt;span&gt;,&lt;/span&gt; 54&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-accent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 211&lt;span&gt;,&lt;/span&gt; 0&lt;span&gt;,&lt;/span&gt; 106&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-card&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 234&lt;span&gt;,&lt;/span&gt; 206&lt;span&gt;,&lt;/span&gt; 219&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-card-muted&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 241&lt;span&gt;,&lt;/span&gt; 186&lt;span&gt;,&lt;/span&gt; 212&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-border&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 227&lt;span&gt;,&lt;/span&gt; 169&lt;span&gt;,&lt;/span&gt; 198&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;html[data-theme=&quot;dark&quot;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--color-fill&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 33&lt;span&gt;,&lt;/span&gt; 39&lt;span&gt;,&lt;/span&gt; 55&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-text-base&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 234&lt;span&gt;,&lt;/span&gt; 237&lt;span&gt;,&lt;/span&gt; 243&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-accent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 255&lt;span&gt;,&lt;/span&gt; 107&lt;span&gt;,&lt;/span&gt; 237&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-card&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 52&lt;span&gt;,&lt;/span&gt; 63&lt;span&gt;,&lt;/span&gt; 96&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-card-muted&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 138&lt;span&gt;,&lt;/span&gt; 51&lt;span&gt;,&lt;/span&gt; 123&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--color-border&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 171&lt;span&gt;,&lt;/span&gt; 75&lt;span&gt;,&lt;/span&gt; 153&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Creating Optimized Excalidraw Diagrams&lt;/h3&gt;
&lt;p&gt;Follow these steps to prepare your diagrams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create your diagram at &lt;a href=&quot;https://excalidraw.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Excalidraw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Export the diagram:
&lt;ul&gt;
&lt;li&gt;Select your diagram&lt;/li&gt;
&lt;li&gt;Click the export button&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/excalidraw-astro/how-to-click-export-excalidraw.png&quot; alt=&quot;How to export Excalidraw diagram as SVG&quot; loading=&quot;lazy&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configure export settings:
&lt;ul&gt;
&lt;li&gt;Uncheck “Background”&lt;/li&gt;
&lt;li&gt;Choose SVG format&lt;/li&gt;
&lt;li&gt;Click “Save”&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/excalidraw-astro/save-as-svg.png&quot; alt=&quot;How to hide background and save as SVG&quot; loading=&quot;lazy&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. Building the ExcalidrawSVG Component&lt;/h3&gt;
&lt;p&gt;Here’s our custom Astro component that handles the theme-aware transformation:&lt;/p&gt;
&lt;pre class=&quot;language-astro&quot;&gt;&lt;code class=&quot;language-astro&quot;&gt;---
interface Props {
  src: ImageMetadata | string;
  alt: string;
  caption?: string;
}

const { src, alt, caption } = Astro.props;

const svgUrl = typeof src === &quot;string&quot; ? src : src.src;
---

&amp;lt;figure class=&quot;excalidraw-figure&quot;&amp;gt;
  &amp;lt;div class=&quot;excalidraw-svg&quot; data-svg-url={svgUrl} aria-label={alt}&amp;gt;
    
  &amp;lt;/div&amp;gt;
  {caption &amp;amp;&amp;amp; &amp;lt;figcaption&amp;gt;{caption}&amp;lt;/figcaption&amp;gt;}
&amp;lt;/figure&amp;gt;

&amp;lt;script&amp;gt;
  function modifySvg(svgString: string): string {
    const parser = new DOMParser();
    const doc = parser.parseFromString(svgString, &quot;image/svg+xml&quot;);
    const svg = doc.documentElement;

    svg.setAttribute(&quot;width&quot;, &quot;100%&quot;);
    svg.setAttribute(&quot;height&quot;, &quot;100%&quot;);
    svg.classList.add(&quot;w-full&quot;, &quot;h-auto&quot;);

    doc.querySelectorAll(&quot;text&quot;).forEach(text =&amp;gt; {
      text.removeAttribute(&quot;fill&quot;);
      text.classList.add(&quot;fill-skin-base&quot;);
    });

    doc.querySelectorAll(&quot;rect&quot;).forEach(rect =&amp;gt; {
      rect.removeAttribute(&quot;fill&quot;);
      rect.classList.add(&quot;fill-skin-soft&quot;);
    });

    doc.querySelectorAll(&quot;path&quot;).forEach(path =&amp;gt; {
      path.removeAttribute(&quot;stroke&quot;);
      path.classList.add(&quot;stroke-skin-accent&quot;);
    });

    doc.querySelectorAll(&quot;g&quot;).forEach(g =&amp;gt; {
      g.classList.add(&quot;excalidraw-element&quot;);
    });

    return new XMLSerializer().serializeToString(doc);
  }

  function initExcalidrawSVG() {
    const svgContainers =
      document.querySelectorAll&amp;lt;HTMLElement&amp;gt;(&quot;.excalidraw-svg&quot;);
    svgContainers.forEach(async container =&amp;gt; {
      const svgUrl = container.dataset.svgUrl;
      if (svgUrl) {
        try {
          const response = await fetch(svgUrl);
          if (!response.ok) {
            throw new Error(`Failed to fetch SVG: ${response.statusText}`);
          }
          const svgData = await response.text();
          const modifiedSvg = modifySvg(svgData);
          container.innerHTML = modifiedSvg;
        } catch (error) {
          console.error(&quot;Error in ExcalidrawSVG component:&quot;, error);
          container.innerHTML = `&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 100 100&quot;&amp;gt;
            &amp;lt;text x=&quot;10&quot; y=&quot;50&quot; fill=&quot;red&quot;&amp;gt;Error loading SVG&amp;lt;/text&amp;gt;
          &amp;lt;/svg&amp;gt;`;
        }
      }
    });
  }

  // Run on initial page load
  document.addEventListener(&quot;DOMContentLoaded&quot;, initExcalidrawSVG);

  // Run on subsequent navigation
  document.addEventListener(&quot;astro:page-load&quot;, initExcalidrawSVG);
&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
  .excalidraw-figure {
    @apply my-8 w-full max-w-full overflow-hidden;
  }
  .excalidraw-svg {
    @apply w-full max-w-full overflow-hidden;
  }
  :global(.excalidraw-svg svg) {
    @apply h-auto w-full;
  }
  :global(.excalidraw-svg .fill-skin-base) {
    @apply fill-[rgb(34,46,54)] dark:fill-[rgb(234,237,243)];
  }
  :global(.excalidraw-svg .fill-skin-soft) {
    @apply fill-[rgb(234,206,219)] dark:fill-[rgb(52,63,96)];
  }
  :global(.excalidraw-svg .stroke-skin-accent) {
    @apply stroke-[rgb(211,0,106)] dark:stroke-[rgb(255,107,237)];
  }
  :global(.excalidraw-svg .excalidraw-element) {
    @apply transition-all duration-300;
  }
  :global(.excalidraw-svg .excalidraw-element:hover) {
    @apply opacity-80;
  }
  figcaption {
    @apply mt-4 text-center text-sm italic text-skin-base;
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Using the Component&lt;/h3&gt;
&lt;p&gt;Integrate the component into your MDX blog posts:&lt;/p&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; We need to use MDX so that we can use the &lt;code&gt;ExcalidrawSVG&lt;/code&gt; component in our blog posts. You can read more about MDX &lt;a href=&quot;https://mdxjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-mdx&quot;&gt;&lt;code class=&quot;language-mdx&quot;&gt;---
---

# My Technical Blog Post

&amp;lt;ExcalidrawSVG
  src={myDiagram}
  alt=&quot;Architecture diagram&quot;
  caption=&quot;System architecture overview&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Best Practices and Tips for Theme-Aware Technical Diagrams&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Simplicity and Focus&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep diagrams simple and focused for better readability&lt;/li&gt;
&lt;li&gt;Avoid cluttering with unnecessary details&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Consistent Styling&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use consistent styling across all diagrams&lt;/li&gt;
&lt;li&gt;Maintain a uniform look and feel throughout your documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Thorough Testing&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test thoroughly in both light and dark modes&lt;/li&gt;
&lt;li&gt;Ensure diagrams are clear and legible in all color schemes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Accessibility Considerations&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consider accessibility when choosing colors and contrast&lt;/li&gt;
&lt;li&gt;Ensure diagrams are understandable for users with color vision deficiencies&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Smooth Transitions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement smooth transitions for theme changes&lt;/li&gt;
&lt;li&gt;Provide a seamless experience when switching between light and dark modes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With this custom component, you can now create technical diagrams that seamlessly integrate with your Astro blog’s design system.&lt;br /&gt;
This solution eliminates the need for maintaining multiple versions of diagrams while providing a superior user experience through smooth transitions and interactive elements.&lt;/p&gt;

          </content:encoded><category>astro</category><category>excalidraw</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Frontend Testing Guide: 10 Essential Rules for Naming Tests</title><link>https://alexop.dev/posts/frontend-testing-guide-10-essential-rules/</link><guid isPermaLink="true">https://alexop.dev/posts/frontend-testing-guide-10-essential-rules/</guid><description>Learn how to write clear and maintainable frontend tests with 10 practical naming rules. Includes real-world examples showing good and bad practices for component testing across any framework.</description><pubDate>Sat, 26 Oct 2024 08:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The path to better testing starts with something surprisingly simple: how you name your tests. Good test names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make your test suite more maintainable&lt;/li&gt;
&lt;li&gt;Guide you toward writing tests that focus on user behavior&lt;/li&gt;
&lt;li&gt;Improve clarity and readability for your team&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this blog post, we’ll explore 10 essential rules for writing better tests that will transform your approach to testing. These principles are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Framework-agnostic&lt;/li&gt;
&lt;li&gt;Applicable across the entire testing pyramid&lt;/li&gt;
&lt;li&gt;Useful for various testing tools:
&lt;ul&gt;
&lt;li&gt;Unit tests (Jest, Vitest)&lt;/li&gt;
&lt;li&gt;Integration tests&lt;/li&gt;
&lt;li&gt;End-to-end tests (Cypress, Playwright)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By following these rules, you’ll create a more robust and understandable test suite, regardless of your chosen testing framework or methodology.&lt;/p&gt;
&lt;h2&gt;Rule 1: Always Use “should” + Verb&lt;/h2&gt;
&lt;p&gt;Every test name should start with “should” followed by an action verb.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;displays the error message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;modal visibility&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;form validation working&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should display error message when validation fails&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show modal when trigger button is clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should validate form when user submits&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [verb] [expected outcome]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 2: Include the Trigger Event&lt;/h2&gt;
&lt;p&gt;Specify what causes the behavior you’re testing.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update counter&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should validate email&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show dropdown&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should increment counter when plus button is clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show error when email format is invalid&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should open dropdown when toggle is clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [verb] [expected outcome] when [trigger event]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 3: Group Related Tests with Descriptive Contexts&lt;/h2&gt;
&lt;p&gt;Use describe blocks to create clear test hierarchies.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;AuthForm&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should test empty state&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should test invalid state&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should test success state&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;AuthForm&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;when form is empty&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should disable submit button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should not show any validation errors&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;when submitting invalid data&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show validation errors&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should keep submit button disabled&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;[Component/Feature]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;when [specific condition]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should [expected behavior]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rule 4: Name State Changes Explicitly&lt;/h2&gt;
&lt;p&gt;Clearly describe the before and after states in your test names.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should change status&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update todo&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should modify permissions&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should change status from pending to approved&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should mark todo as completed when checkbox clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should upgrade user from basic to premium&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should change [attribute] from [initial state] to [final state]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 5: Describe Async Behavior Clearly&lt;/h2&gt;
&lt;p&gt;Include loading and result states for asynchronous operations.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should load data&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle API call&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should fetch user&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show skeleton while loading data&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should display error message when API call fails&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should render profile after user data loads&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [verb] [expected outcome] [during/after] [async operation]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 6: Name Error Cases Specifically&lt;/h2&gt;
&lt;p&gt;Be explicit about the type of error and what causes it.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show error&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle invalid input&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should validate form&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;should show &quot;Invalid Card&quot; when card number is wrong&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;should display &quot;Required&quot; when password is empty&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show network error when API is unreachable&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should show [specific error message] when [error condition]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 7: Use Business Language, Not Technical Terms&lt;/h2&gt;
&lt;p&gt;Write tests using domain language rather than implementation details.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update state&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should dispatch action&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should modify DOM&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should save customer order&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update cart total&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should mark order as delivered&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [business action] [business entity]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 8: Include Important Preconditions&lt;/h2&gt;
&lt;p&gt;Specify conditions that affect the behavior being tested.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should enable button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should apply discount&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should enable checkout when cart has items&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show free shipping when total exceeds $100&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should apply discount when user is premium member&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [expected behavior] when [precondition]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 9: Name UI Feedback Tests from User Perspective&lt;/h2&gt;
&lt;p&gt;Describe visual changes as users would perceive them.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should set error class&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should toggle visibility&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update styles&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should highlight search box in red when empty&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show green checkmark when password is strong&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should disable submit button while processing&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt; &lt;code&gt;should [visual change] when [user action/condition]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rule 10: Structure Complex Workflows Step by Step&lt;/h2&gt;
&lt;p&gt;Break down complex processes into clear steps.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Checkout&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should process checkout&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle shipping&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should complete order&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Checkout Process&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should first validate items are in stock&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should then collect shipping address&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should finally process payment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;after successful payment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should display order confirmation&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should send confirmation email&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generic Pattern:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;[Complex Process]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should first [initial step]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should then [next step]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should finally [final step]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;after [key milestone]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should [follow-up action]&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Complete Example&lt;/h2&gt;
&lt;p&gt;Here’s a comprehensive example showing how to combine all these rules:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;// ❌ Bad&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;ShoppingCart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;test adding item&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;check total&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;handle checkout&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// ✅ Good&lt;/span&gt;
&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;ShoppingCart&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;when adding items&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should add item to cart when add button is clicked&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should update total price immediately&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show item count badge&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;when cart is empty&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should display empty cart message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should disable checkout button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;during checkout process&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should validate stock before proceeding&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should show loading indicator while processing payment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should display success message after completion&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Test Name Checklist&lt;/h2&gt;
&lt;p&gt;Before committing your test, verify that its name:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Starts with “should”&lt;/li&gt;
&lt;li&gt;[ ] Uses a clear action verb&lt;/li&gt;
&lt;li&gt;[ ] Specifies the trigger condition&lt;/li&gt;
&lt;li&gt;[ ] Uses business language&lt;/li&gt;
&lt;li&gt;[ ] Describes visible behavior&lt;/li&gt;
&lt;li&gt;[ ] Is specific enough for debugging&lt;/li&gt;
&lt;li&gt;[ ] Groups logically with related tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Thoughtful test naming is a fundamental building block in the broader landscape of writing better tests. To maintain consistency across your team:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Document your naming conventions in detail&lt;/li&gt;
&lt;li&gt;Share these guidelines with all team members&lt;/li&gt;
&lt;li&gt;Integrate the guidelines into your development workflow&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For teams using AI tools like GitHub Copilot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incorporate these guidelines into your project documentation&lt;/li&gt;
&lt;li&gt;Link the markdown file containing these rules to Copilot&lt;/li&gt;
&lt;li&gt;This integration allows Copilot to suggest test names aligned with your conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more information on linking documentation to Copilot, see:&lt;br /&gt;
&lt;a href=&quot;https://visualstudiomagazine.com/Articles/2024/09/09/VS-Code-Experiments-Boost-AI-Copilot-Functionality.aspx&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;VS Code Experiments Boost AI Copilot Functionality&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By following these steps, you can ensure consistent, high-quality test naming across your entire project.&lt;/p&gt;

          </content:encoded><category>testing</category><category>vitest</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Create a Native-Like App in 4 Steps: PWA Magic with Vue 3 and Vite</title><link>https://alexop.dev/posts/create-pwa-vue3-vite-4-steps/</link><guid isPermaLink="true">https://alexop.dev/posts/create-pwa-vue3-vite-4-steps/</guid><description>Transform your Vue 3 project into a powerful Progressive Web App in just 4 steps. Learn how to create offline-capable, installable web apps using Vite and modern PWA techniques.</description><pubDate>Sun, 20 Oct 2024 07:44:12 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Progressive Web Apps (PWAs) have revolutionized our thoughts on web applications. PWAs offer a fast, reliable, and engaging user experience by combining the best web and mobile apps. They work offline, can be installed on devices, and provide a native app-like experience without app store distribution.&lt;/p&gt;
&lt;p&gt;This guide will walk you through creating a Progressive Web App using Vue 3 and Vite. By the end of this tutorial, you’ll have a fully functional PWA that can work offline, be installed on users’ devices, and leverage modern web capabilities.&lt;/p&gt;
&lt;h2&gt;Understanding the Basics of Progressive Web Apps (PWAs)&lt;/h2&gt;
&lt;p&gt;Before diving into the development process, it’s crucial to grasp the fundamental concepts of PWAs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-platform Compatibility&lt;/strong&gt;: PWAs are designed for applications that can function across multiple platforms, not just the web.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Once, Deploy Everywhere&lt;/strong&gt;: With PWAs, you can develop an application once and deploy it on Android, iOS, Desktop, and Web platforms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced User Experience&lt;/strong&gt;: PWAs offer features like offline functionality, push notifications, and home screen installation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a more in-depth understanding of PWAs, refer to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MDN Web Docs on Progressive Web Apps&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Prerequisites for Building a PWA with Vue 3 and Vite&lt;/h2&gt;
&lt;p&gt;Before you start, make sure you have the following tools installed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Node.js installed on your system&lt;/li&gt;
&lt;li&gt;Package manager: pnpm, npm, or yarn&lt;/li&gt;
&lt;li&gt;Basic familiarity with Vue 3&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Step 1: Setting Up the Vue Project&lt;/h2&gt;
&lt;p&gt;First, we’ll set up a new Vue project using the latest Vue CLI. This will give us a solid foundation to build our PWA upon.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a new Vue project by running the following command in your terminal:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;pnpm&lt;/span&gt; create vue@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Follow the prompts to configure your project. Here’s an example configuration:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;✔ Project name: … local-first-example
✔ Add TypeScript? … Yes
✔ Add JSX Support? … Yes
✔ Add Vue Router &lt;span&gt;for&lt;/span&gt; Single Page Application development? … Yes
✔ Add Pinia &lt;span&gt;for&lt;/span&gt; state management? … Yes
✔ Add Vitest &lt;span&gt;for&lt;/span&gt; Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint &lt;span&gt;for&lt;/span&gt; code quality? … Yes
✔ Add Prettier &lt;span&gt;for&lt;/span&gt; code formatting? … Yes
✔ Add Vue DevTools &lt;span&gt;7&lt;/span&gt; extension &lt;span&gt;for&lt;/span&gt; debugging? &lt;span&gt;(&lt;/span&gt;experimental&lt;span&gt;)&lt;/span&gt; … Yes
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once the project is created, navigate to your project directory and install dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;cd&lt;/span&gt; local-first-example
&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt;
&lt;span&gt;pnpm&lt;/span&gt; run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Great! You now have a basic Vue 3 project up and running. Let’s move on to adding PWA functionality.&lt;/p&gt;
&lt;h2&gt;Step 2: Create the needed assets for the PWA&lt;/h2&gt;
&lt;p&gt;We need to add specific assets and configurations to transform our Vue app into a PWA.&lt;br /&gt;
PWAs can be installed on various devices, so we must prepare icons and other assets for different platforms.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, let’s install the necessary packages:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;pnpm&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; &lt;span&gt;-D&lt;/span&gt; vite-plugin-pwa @vite-pwa/assets-generator
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a high-resolution icon (preferably an SVG or a PNG with at least 512x512 pixels) for your PWA and place it in your &lt;code&gt;public&lt;/code&gt; directory. Name it something like &lt;code&gt;pwa-icon.svg&lt;/code&gt; or &lt;code&gt;pwa-icon.png&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generate the PWA assets by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx pwa-asset-generator &lt;span&gt;--preset&lt;/span&gt; minimal public/pwa-icon.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This command will automatically generate a set of icons and a web manifest file in your &lt;code&gt;public&lt;/code&gt; directory. The &lt;code&gt;minimal&lt;/code&gt; preset will create:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;favicon.ico (48x48 transparent icon for browser tabs)&lt;/li&gt;
&lt;li&gt;favicon.svg (SVG icon for modern browsers)&lt;/li&gt;
&lt;li&gt;apple-touch-icon-180x180.png (Icon for iOS devices when adding to home screen)&lt;/li&gt;
&lt;li&gt;maskable-icon-512x512.png (Adaptive icon that fills the entire shape on Android devices)&lt;/li&gt;
&lt;li&gt;pwa-64x64.png (Small icon for various UI elements)&lt;/li&gt;
&lt;li&gt;pwa-192x192.png (Medium-sized icon for app shortcuts and tiles)&lt;/li&gt;
&lt;li&gt;pwa-512x512.png (Large icon for high-resolution displays and splash screens)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Output will look like this:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; vue3-pwa-timer@0.0.0 generate-pwa-assets /Users/your user/git2/vue3-pwa-example
&lt;span&gt;&amp;gt;&lt;/span&gt; pwa-assets-generator &lt;span&gt;&quot;--preset&quot;&lt;/span&gt; &lt;span&gt;&quot;minimal-2023&quot;&lt;/span&gt; &lt;span&gt;&quot;public/pwa-icon.svg&quot;&lt;/span&gt;

Zero Config PWA Assets Generator v0.2.6
◐ Preparing to generate PWA assets&lt;span&gt;..&lt;/span&gt;.
◐ Resolving instructions&lt;span&gt;..&lt;/span&gt;.
✔ PWA assets ready to be generated, instructions resolved
◐ Generating PWA assets from public/pwa-icon.svg image
◐ Generating assets &lt;span&gt;for&lt;/span&gt; public/pwa-icon.svg&lt;span&gt;..&lt;/span&gt;.
✔ Generated PNG file: /Users/your user/git2/vue3-pwa-example/public/pwa-64x64.png
✔ Generated PNG file: /Users/your user/git2/vue3-pwa-example/public/pwa-192x192.png
✔ Generated PNG file: /Users/your user/git2/vue3-pwa-example/public/pwa-512x512.png
✔ Generated PNG file: /Users/your user/git2/vue3-pwa-example/public/maskable-icon-512x512.png
✔ Generated PNG file: /Users/your user/git2/vue3-pwa-example/public/apple-touch-icon-180x180.png
✔ Generated ICO file: /Users/your user/git2/vue3-pwa-example/public/favicon.ico
✔ Assets generated &lt;span&gt;for&lt;/span&gt; public/pwa-icon.svg
◐ Generating Html Head Links&lt;span&gt;..&lt;/span&gt;.
&lt;span&gt;&amp;lt;&lt;/span&gt;link &lt;span&gt;rel&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;icon&quot;&lt;/span&gt; &lt;span&gt;href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/favicon.ico&quot;&lt;/span&gt; &lt;span&gt;sizes&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;48x48&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;link &lt;span&gt;rel&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;icon&quot;&lt;/span&gt; &lt;span&gt;href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/pwa-icon.svg&quot;&lt;/span&gt; &lt;span&gt;sizes&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;any&quot;&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;image/svg+xml&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;link &lt;span&gt;rel&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;apple-touch-icon&quot;&lt;/span&gt; &lt;span&gt;href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/apple-touch-icon-180x180.png&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
✔ Html Head Links generated
◐ Generating PWA web manifest icons entry&lt;span&gt;..&lt;/span&gt;.
&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&quot;icons&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-64x64.png&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;64x64&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;,
    &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-192x192.png&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;192x192&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;,
    &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-512x512.png&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;512x512&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;,
    &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;maskable-icon-512x512.png&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;512x512&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;,
      &lt;span&gt;&quot;purpose&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;maskable&quot;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
✔ PWA web manifest icons entry generated
✔ PWA assets generated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These steps will ensure your PWA has all the necessary icons and assets to function correctly across different devices and platforms.&lt;br /&gt;
The minimal-2023 preset provides a modern, optimized set of icons that meet the latest PWA requirements.&lt;/p&gt;
&lt;h2&gt;Step 3: Configuring Vite for PWA Support&lt;/h2&gt;
&lt;p&gt;With our assets ready, we must configure Vite to enable PWA functionality. This involves setting up the manifest and other PWA-specific options.&lt;/p&gt;
&lt;p&gt;First, update your main HTML file (usually &lt;code&gt;index.html&lt;/code&gt;) to include important meta tags in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, update your &lt;code&gt;vite.config.ts&lt;/code&gt; file with the following configuration:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;vue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;VitePWA&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      registerType&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;autoUpdate&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      includeAssets&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
        &lt;span&gt;&quot;favicon.ico&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;apple-touch-icon-180x180.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;&quot;maskable-icon-512x512.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      manifest&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;My Awesome PWA&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        short_name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;MyPWA&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;A PWA built with Vue 3&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        theme_color&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;#ffffff&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        icons&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-64x64.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;64x64&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-192x192.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;192x192&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-512x512.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;512x512&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            purpose&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;any&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;maskable-icon-512x512.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;512x512&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            purpose&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;maskable&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      devOptions&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        enabled&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  The `devOptions: { enabled: true }` setting is crucial for testing your PWA on localhost. Normally, PWAs require HTTPS, but this setting allows the PWA features to work on `http://localhost` during development. Remember to remove or set this to `false` for production builds.
&lt;/aside&gt;
&lt;p&gt;This configuration generates a Web App Manifest, a JSON file that tells the browser about your Progressive Web App and how it should behave when installed on the user’s desktop or mobile device. The manifest includes the app’s name, icons, and theme colors.&lt;/p&gt;
&lt;h2&gt;PWA Lifecycle and Updates&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;registerType: &apos;autoUpdate&apos;&lt;/code&gt; option in our configuration sets up automatic updates for our PWA. Here’s how it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When a user visits your PWA, the browser downloads and caches the latest version of your app.&lt;/li&gt;
&lt;li&gt;On subsequent visits, the service worker checks for updates in the background.&lt;/li&gt;
&lt;li&gt;If an update is available, it’s downloaded and prepared for the next launch.&lt;/li&gt;
&lt;li&gt;The next time the user opens or refreshes the app, they’ll get the latest version.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This ensures that users always have the most up-to-date version of your app without manual intervention.&lt;/p&gt;
&lt;h2&gt;Step 4: Implementing Offline Functionality with Service Workers&lt;/h2&gt;
&lt;p&gt;The real power of PWAs comes from their ability to work offline. We’ll use the &lt;code&gt;vite-plugin-pwa&lt;/code&gt; to integrate Workbox, which will handle our service worker and caching strategies.&lt;/p&gt;
&lt;p&gt;Before we dive into the configuration, let’s understand the runtime caching strategies we’ll be using:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;StaleWhileRevalidate&lt;/strong&gt; for static resources (styles, scripts, and workers):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This strategy serves cached content immediately while fetching an update in the background.&lt;/li&gt;
&lt;li&gt;It’s ideal for frequently updated resources that aren’t 100% up-to-date.&lt;/li&gt;
&lt;li&gt;We’ll limit the cache to 50 entries and set an expiration of 30 days.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CacheFirst&lt;/strong&gt; for images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This strategy serves cached images immediately without network requests if they’re available.&lt;/li&gt;
&lt;li&gt;It’s perfect for static assets that don’t change often.&lt;/li&gt;
&lt;li&gt;We’ll limit the image cache to 100 entries and set an expiration of 60 days.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These strategies ensure that your PWA remains functional offline while efficiently managing cache storage.&lt;/p&gt;
&lt;p&gt;Now, let’s update your &lt;code&gt;vite.config.ts&lt;/code&gt; file to include service worker configuration with these advanced caching strategies:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  plugins&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
    &lt;span&gt;vue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;VitePWA&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
      devOptions&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        enabled&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      registerType&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;autoUpdate&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      includeAssets&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;favicon.ico&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;apple-touch-icon.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;masked-icon.svg&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      manifest&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Vue 3 PWA Timer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        short_name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;PWA Timer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        description&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;A customizable timer for Tabata and EMOM workouts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        theme_color&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;#ffffff&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        icons&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-192x192.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;192x192&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            src&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;pwa-512x512.png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            sizes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;512x512&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            type&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;image/png&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      workbox&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        runtimeCaching&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;urlPattern&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; request &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt;
              request&lt;span&gt;.&lt;/span&gt;destination &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;style&quot;&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
              request&lt;span&gt;.&lt;/span&gt;destination &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;script&quot;&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
              request&lt;span&gt;.&lt;/span&gt;destination &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;worker&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            handler&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;StaleWhileRevalidate&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            options&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
              cacheName&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;static-resources&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
              expiration&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                maxEntries&lt;span&gt;:&lt;/span&gt; &lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
                maxAgeSeconds&lt;span&gt;:&lt;/span&gt; &lt;span&gt;30&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;24&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;60&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;60&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 30 days&lt;/span&gt;
              &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;urlPattern&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; request &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; request&lt;span&gt;.&lt;/span&gt;destination &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;image&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            handler&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;CacheFirst&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            options&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
              cacheName&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;images&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
              expiration&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
                maxEntries&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
                maxAgeSeconds&lt;span&gt;:&lt;/span&gt; &lt;span&gt;60&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;24&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;60&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;60&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 60 days&lt;/span&gt;
              &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Testing Your PWA&lt;/h2&gt;
&lt;p&gt;Now that we’ve set up our PWA, it’s time to test its capabilities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Test your PWA locally:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;pnpm&lt;/span&gt; run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open Chrome DevTools and navigate to the Application tab.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the “Manifest” section to ensure your Web App Manifest is loaded correctly.&lt;/li&gt;
&lt;li&gt;In the “Service Workers” section, verify that your service worker is registered and active.&lt;br /&gt;
[&lt;img src=&quot;https://alexop.dev//../../assets/images/pwa/serviceWorker.png&quot; alt=&quot;PWA Service Worker&quot; loading=&quot;lazy&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test offline functionality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to the Network tab in DevTools and check the “Offline” box to simulate offline conditions.&lt;/li&gt;
&lt;li&gt;Refresh the page and verify that your app still works without an internet connection.&lt;/li&gt;
&lt;li&gt;Uncheck the “Offline” box and refresh to ensure the app works online.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test caching:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the Application tab, go to “Cache Storage” to see the caches created by your service worker.&lt;/li&gt;
&lt;li&gt;Verify that assets are being cached according to your caching strategies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test installation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;On desktop: Look for the install icon in the address bar or the three-dot menu.&lt;br /&gt;
&lt;a href=&quot;https://alexop.dev//../../assets/images/pwa/desktopInstall.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/pwa/desktopInstall.png&quot; alt=&quot;PWA Install Icon&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://alexop.dev//../../assets/images/pwa/installApp.png&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/pwa/installApp.png&quot; alt=&quot;PWA Install Icon&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On mobile: You should see a prompt to “Add to Home Screen”.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test updates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make a small change to your app and redeploy.&lt;/li&gt;
&lt;li&gt;Revisit the app and check if the service worker updates (you can monitor this in the Application tab).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By thoroughly testing these aspects, you can ensure that your PWA functions correctly across various scenarios and platforms.&lt;/p&gt;
&lt;aside&gt;
  If you want to see a full-fledged PWA in action, check out
  [Elk](https://elk.zone/), a nimble Mastodon web client. It&apos;s built with Nuxt
  and is anexcellent example of a production-ready PWA. You can also explore its
  open-source code on [GitHub](https://github.com/elk-zone/elk) to see how
  they&apos;ve implemented various PWA features.
&lt;/aside&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Congratulations! You’ve successfully created a Progressive Web App using Vue 3 and Vite.&lt;br /&gt;
Your app can now work offline, be installed on users’ devices, and provide a native-like experience.&lt;/p&gt;
&lt;p&gt;Refer to the &lt;a href=&quot;https://vite-pwa-org.netlify.app/workbox/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vite PWA Workbox documentation&lt;/a&gt; for more advanced Workbox configurations and features.&lt;/p&gt;
&lt;p&gt;The more challenging part is building suitable components with a native-like feel on all the devices you want to support.&lt;br /&gt;
PWAs are also a main ingredient in building local-first applications.&lt;br /&gt;
If you are curious about what I mean by that, check out the following: &lt;a href=&quot;https://alexop.dev//../what-is-local-first-web-development&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;What is Local First Web Development&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a complete working example of this Vue 3 PWA, you can check out the complete source code at &lt;a href=&quot;https://github.com/alexanderop/vue3-pwa-example&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;full example&lt;/a&gt;.&lt;br /&gt;
This repository contains the finished project, allowing you to see how all the pieces come together in a real-world application.&lt;/p&gt;

          </content:encoded><category>vue</category><category>pwa</category><category>vite</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Atomic Architecture: Structuring Vue and Nuxt Projects</title><link>https://alexop.dev/posts/atomic-design-vue-or-nuxt/</link><guid isPermaLink="true">https://alexop.dev/posts/atomic-design-vue-or-nuxt/</guid><description>Learn how to implement Atomic Design principles in Vue or Nuxt projects. Improve your code structure and maintainability with this guide</description><pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Clear writing requires clear thinking. The same is valid for coding.&lt;br /&gt;
Throwing all components into one folder may work when starting a personal project.&lt;br /&gt;
But as projects grow, especially with larger teams, this approach leads to problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Duplicated code&lt;/li&gt;
&lt;li&gt;Oversized, multipurpose components&lt;/li&gt;
&lt;li&gt;Difficult-to-test code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Atomic Design offers a solution. Let’s examine how to apply it to a Nuxt project.&lt;/p&gt;
&lt;h2&gt;What is Atomic Design&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/atomic/diagram.svg&quot; alt=&quot;atomic design diagram brad Frost&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Brad Frost developed Atomic Design as a methodology for creating design systems. He structured it into five levels inspired by chemistry:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Atoms: Basic building blocks (e.g. form labels, inputs, buttons)&lt;/li&gt;
&lt;li&gt;Molecules: Simple groups of UI elements (e.g. search forms)&lt;/li&gt;
&lt;li&gt;Organisms: Complex components made of molecules/atoms (e.g. headers)&lt;/li&gt;
&lt;li&gt;Templates: Page-level layouts&lt;/li&gt;
&lt;li&gt;Pages: Specific instances of templates with content&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;
  Read Brad Frost&apos;s original post for the full picture: [Atomic Web
  Design](https://bradfrost.com/blog/post/atomic-web-design/)
&lt;/aside&gt;
&lt;p&gt;For Nuxt, we can adapt these definitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Atoms: Pure, single-purpose components&lt;/li&gt;
&lt;li&gt;Molecules: Combinations of atoms with minimal logic&lt;/li&gt;
&lt;li&gt;Organisms: Larger, self-contained, reusable components&lt;/li&gt;
&lt;li&gt;Templates: Nuxt layouts defining page structure&lt;/li&gt;
&lt;li&gt;Pages: Components handling data and API calls&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;
&lt;p&gt;Molecules and organisms can be confusing. Think of it like LEGO:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Molecules are small and simple. Individual bricks that snap together.&lt;br /&gt;
Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A search bar (input + button)&lt;/li&gt;
&lt;li&gt;A login form (username input + password input + submit button)&lt;/li&gt;
&lt;li&gt;A star rating (5 star icons + rating number)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Organisms are bigger and more complex. Pre-built sets.&lt;br /&gt;
Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A full website header (logo + navigation menu + search bar)&lt;/li&gt;
&lt;li&gt;A product card (image + title + price + add to cart button)&lt;/li&gt;
&lt;li&gt;A comment section (comment form + list of comments)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remember: Molecules are parts of organisms, but organisms can work independently.&lt;/p&gt;
&lt;/aside&gt;
&lt;h3&gt;Code Example: Before and After&lt;/h3&gt;
&lt;h4&gt;Consider this non-Atomic Design todo app component:&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/atomic/screenshot-example-app.png&quot; alt=&quot;Screenshot of ToDo App&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;container mx-auto p-4&quot;&amp;gt;
    &amp;lt;h1 class=&quot;mb-4 text-2xl font-bold text-gray-800 dark:text-gray-200&quot;&amp;gt;
      Todo App
    &amp;lt;/h1&amp;gt;

    &amp;lt;!-- Add Todo Form --&amp;gt;
    &amp;lt;form @submit.prevent=&quot;addTodo&quot; class=&quot;mb-4&quot;&amp;gt;
      &amp;lt;input
        v-model=&quot;newTodo&quot;
        type=&quot;text&quot;
        placeholder=&quot;Enter a new todo&quot;
        class=&quot;mr-2 rounded border bg-white p-2 text-gray-800 dark:bg-gray-700 dark:text-gray-200&quot;
      /&amp;gt;
      &amp;lt;button
        type=&quot;submit&quot;
        class=&quot;rounded bg-blue-500 p-2 text-white transition duration-300 hover:bg-blue-600&quot;
      &amp;gt;
        Add Todo
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;!-- Todo List --&amp;gt;
    &amp;lt;ul class=&quot;space-y-2&quot;&amp;gt;
      &amp;lt;li
        v-for=&quot;todo in todos&quot;
        :key=&quot;todo.id&quot;
        class=&quot;flex items-center justify-between rounded bg-gray-100 p-3 shadow-sm dark:bg-gray-700&quot;
      &amp;gt;
        &amp;lt;span class=&quot;text-gray-800 dark:text-gray-200&quot;&amp;gt;{{ todo.text }}&amp;lt;/span&amp;gt;
        &amp;lt;button
          @click=&quot;deleteTodo(todo.id)&quot;
          class=&quot;rounded bg-red-500 p-1 text-white transition duration-300 hover:bg-red-600&quot;
        &amp;gt;
          Delete
        &amp;lt;/button&amp;gt;
      &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Todo {
  id: number;
  text: string;
}

const newTodo = ref(&quot;&quot;);
const todos = ref&amp;lt;Todo[]&amp;gt;([]);

const fetchTodos = async () =&amp;gt; {
  // Simulating API call
  todos.value = [
    { id: 1, text: &quot;Learn Vue.js&quot; },
    { id: 2, text: &quot;Build a Todo App&quot; },
    { id: 3, text: &quot;Study Atomic Design&quot; },
  ];
};

const addTodo = async () =&amp;gt; {
  if (newTodo.value.trim()) {
    // Simulating API call
    const newTodoItem: Todo = {
      id: Date.now(),
      text: newTodo.value,
    };
    todos.value.push(newTodoItem);
    newTodo.value = &quot;&quot;;
  }
};

const deleteTodo = async (id: number) =&amp;gt; {
  // Simulating API call
  todos.value = todos.value.filter(todo =&amp;gt; todo.id !== id);
};

onMounted(fetchTodos);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach leads to large, difficult-to-maintain components. Let’s refactor using Atomic Design:&lt;/p&gt;
&lt;h3&gt;This will be the refactored structure&lt;/h3&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;📐 Template &lt;span&gt;(&lt;/span&gt;Layout&lt;span&gt;)&lt;/span&gt;
   │
   └─── 📄 Page &lt;span&gt;(&lt;/span&gt;TodoApp&lt;span&gt;)&lt;/span&gt;
        │
        └─── 📦 Organism &lt;span&gt;(&lt;/span&gt;TodoList&lt;span&gt;)&lt;/span&gt;
             │
             ├─── 🧪 Molecule &lt;span&gt;(&lt;/span&gt;TodoForm&lt;span&gt;)&lt;/span&gt;
             │    │
             │    ├─── ⚛️ Atom &lt;span&gt;(&lt;/span&gt;BaseInput&lt;span&gt;)&lt;/span&gt;
             │    └─── ⚛️ Atom &lt;span&gt;(&lt;/span&gt;BaseButton&lt;span&gt;)&lt;/span&gt;
             │
             └─── 🧪 Molecule &lt;span&gt;(&lt;/span&gt;TodoItems&lt;span&gt;)&lt;/span&gt;
                  │
                  └─── 🧪 Molecule &lt;span&gt;(&lt;/span&gt;TodoItem&lt;span&gt;)&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;multiple instances&lt;span&gt;]&lt;/span&gt;
                       │
                       ├─── ⚛️ Atom &lt;span&gt;(&lt;/span&gt;BaseText&lt;span&gt;)&lt;/span&gt;
                       └─── ⚛️ Atom &lt;span&gt;(&lt;/span&gt;BaseButton&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Refactored Components&lt;/h3&gt;
&lt;h4&gt;Template Default&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div
    class=&quot;min-h-screen bg-gray-100 text-gray-900 transition-colors duration-300 dark:bg-gray-900 dark:text-gray-100&quot;
  &amp;gt;
    &amp;lt;header class=&quot;bg-white shadow dark:bg-gray-800&quot;&amp;gt;
      &amp;lt;nav
        class=&quot;container mx-auto flex items-center justify-between px-4 py-4&quot;
      &amp;gt;
        &amp;lt;NuxtLink to=&quot;/&quot; class=&quot;text-xl font-bold&quot;&amp;gt;Todo App&amp;lt;/NuxtLink&amp;gt;
        
      &amp;lt;/nav&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;main class=&quot;container mx-auto px-4 py-8&quot;&amp;gt;
      
    &amp;lt;/main&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pages&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Todo {
  id: number;
  text: string;
}

const todos = ref&amp;lt;Todo[]&amp;gt;([]);

const fetchTodos = async () =&amp;gt; {
  // Simulating API call
  todos.value = [
    { id: 1, text: &quot;Learn Vue.js&quot; },
    { id: 2, text: &quot;Build a Todo App&quot; },
    { id: 3, text: &quot;Study Atomic Design&quot; },
  ];
};

const addTodo = async (text: string) =&amp;gt; {
  // Simulating API call
  const newTodoItem: Todo = {
    id: Date.now(),
    text,
  };
  todos.value.push(newTodoItem);
};

const deleteTodo = async (id: number) =&amp;gt; {
  // Simulating API call
  todos.value = todos.value.filter(todo =&amp;gt; todo.id !== id);
};

onMounted(fetchTodos);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;container mx-auto p-4&quot;&amp;gt;
    &amp;lt;h1 class=&quot;mb-4 text-2xl font-bold text-gray-800 dark:text-gray-200&quot;&amp;gt;
      Todo App
    &amp;lt;/h1&amp;gt;
    
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Organism (TodoList)&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Todo {
  id: number;
  text: string;
}

defineProps&amp;lt;{
  todos: Todo[];
}&amp;gt;();

defineEmits&amp;lt;{
  (e: &quot;add-todo&quot;, value: string): void;
  (e: &quot;delete-todo&quot;, id: number): void;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    
    &amp;lt;ul class=&quot;space-y-2&quot;&amp;gt;
      &amp;lt;TodoItem
        v-for=&quot;todo in todos&quot;
        :key=&quot;todo.id&quot;
        :todo=&quot;todo&quot;
        @delete-todo=&quot;$emit(&apos;delete-todo&apos;, $event)&quot;
      /&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Molecules (TodoForm and TodoItem)&lt;/h4&gt;
&lt;h5&gt;TodoForm.vue:&lt;/h5&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const newTodo = ref(&quot;&quot;);
const emit = defineEmits&amp;lt;{
  (e: &quot;add-todo&quot;, value: string): void;
}&amp;gt;();

const addTodo = () =&amp;gt; {
  if (newTodo.value.trim()) {
    emit(&quot;add-todo&quot;, newTodo.value);
    newTodo.value = &quot;&quot;;
  }
};
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;form @submit.prevent=&quot;addTodo&quot; class=&quot;mb-4&quot;&amp;gt;
    
    &amp;lt;BaseButton type=&quot;submit&quot;&amp;gt;Add Todo&amp;lt;/BaseButton&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;TodoItem.vue:&lt;/h5&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
interface Todo {
  id: number;
  text: string;
}

const props = defineProps&amp;lt;{
  todo: Todo;
}&amp;gt;();

defineEmits&amp;lt;{
  (e: &quot;delete-todo&quot;, id: number): void;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;li class=&quot;flex items-center justify-between&quot;&amp;gt;
    &amp;lt;BaseText&amp;gt;{{ todo.text }}&amp;lt;/BaseText&amp;gt;
    &amp;lt;BaseButton variant=&quot;danger&quot; @click=&quot;$emit(&apos;delete-todo&apos;, todo.id)&quot;&amp;gt;
      Delete
    &amp;lt;/BaseButton&amp;gt;
  &amp;lt;/li&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Atoms (BaseButton, BaseInput, BaseText)&lt;/h4&gt;
&lt;h5&gt;BaseButton.vue:&lt;/h5&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps&amp;lt;{
  variant?: &quot;primary&quot; | &quot;danger&quot;;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button
    :class=&quot;[
      &apos;rounded p-2 transition duration-300&apos;,
      variant === &apos;danger&apos;
        ? &apos;bg-red-500 text-white hover:bg-red-600&apos;
        : &apos;bg-blue-500 text-white hover:bg-blue-600&apos;,
    ]&quot;
  &amp;gt;
    &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;BaseInput.vue:&lt;/h4&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps&amp;lt;{
  modelValue: string;
  placeholder?: string;
}&amp;gt;();
defineEmits&amp;lt;{
  (e: &quot;update:modelValue&quot;, value: string): void;
}&amp;gt;();
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input
    :value=&quot;modelValue&quot;
    @input=&quot;
      $emit(&apos;update:modelValue&apos;, ($event.target as HTMLInputElement).value)
    &quot;
    type=&quot;text&quot;
    :placeholder=&quot;placeholder&quot;
    class=&quot;mr-2 rounded border bg-white p-2 text-gray-800 dark:bg-gray-700 dark:text-gray-200&quot;
  /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;aside&gt;
  Want to check out the full example yourself? [click
  me](https://github.com/alexanderop/todo-app-example)
&lt;/aside&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component Level&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Atoms&lt;/td&gt;
&lt;td&gt;Pure, single-purpose components&lt;/td&gt;
&lt;td&gt;BaseButton BaseInput BaseIcon BaseText&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Molecules&lt;/td&gt;
&lt;td&gt;Combinations of atoms with minimal logic&lt;/td&gt;
&lt;td&gt;SearchBar LoginForm StarRating Tooltip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organisms&lt;/td&gt;
&lt;td&gt;Larger, self-contained, reusable components. Can perform side effects and complex operations.&lt;/td&gt;
&lt;td&gt;TheHeader ProductCard CommentSection NavigationMenu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates&lt;/td&gt;
&lt;td&gt;Nuxt layouts defining page structure&lt;/td&gt;
&lt;td&gt;DefaultLayout BlogLayout DashboardLayout AuthLayout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages&lt;/td&gt;
&lt;td&gt;Components handling data and API calls&lt;/td&gt;
&lt;td&gt;HomePage UserProfile ProductList CheckoutPage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Atomic Design gives you a clear code structure and works well as a starting point.&lt;br /&gt;
As complexity grows, other architectures may fit better.&lt;br /&gt;
My post on &lt;a href=&quot;https://alexop.dev//../how-to-structure-vue-projects&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to structure vue Projects&lt;/a&gt; covers approaches beyond Atomic Design for larger projects.&lt;/p&gt;

          </content:encoded><category>vue</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Bolt Your Presentations: AI-Powered Slides</title><link>https://alexop.dev/posts/create-ai-presentations-fast/</link><guid isPermaLink="true">https://alexop.dev/posts/create-ai-presentations-fast/</guid><description>Elevate your dev presentations with AI-powered tools. Learn to leverage Bolt, Slidev, and WebContainers for rapid, code-friendly slide creation. This guide walks developers through 7 steps to build impressive tech presentations using Markdown and browser-based Node.js. Master efficient presentation development with instant prototyping and one-click deployment to Netlify</description><pubDate>Sat, 05 Oct 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Presentations plague the middle-class professional. Most bore audiences with wordy slides. But AI tools promise sharper results, faster. Let’s explore how.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/venn.svg&quot; alt=&quot;Bolt landingpage&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The Birth of Bolt&lt;/h2&gt;
&lt;p&gt;StackBlitz unveiled Bolt at ViteConf 2024. This browser-based coding tool lets developers build web apps without local setup. Pair it with Slidev, a Markdown slide creator, for rapid presentation development.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=knLe8zzwNRA&quot; title=&quot;WebContainers &amp;amp; AI: Introducing bolt.new&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;img src=&quot;http://img.youtube.com/vi/knLe8zzwNRA/0.jpg&quot; alt=&quot;Image Presentation WebContainers &amp;amp; AI: Introducing bolt.new&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Tools Breakdown&lt;/h2&gt;
&lt;p&gt;Three key tools enable this approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Bolt: AI-powered web app creation in the browser&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/bolt-desc.png&quot; alt=&quot;Bolt landingpage&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Slidev: Markdown-based slides with code support&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/slidev-desc.png&quot; alt=&quot;Slidev Landing page&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Webcontainers: Browser-based Node.js for instant prototyping&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/webcontainers-interface.png&quot; alt=&quot;WebContainers landing page&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Seven Steps to AI Presentation Mastery&lt;/h2&gt;
&lt;p&gt;Follow these steps to craft AI-powered presentations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open bolt.new in your browser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tell Bolt to make a presentation on your topic. Be specific. (add use Slidedev for it)&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/initial-interface.png&quot; alt=&quot;Screenshot for chat Bolt&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Review the Bolt-generated slides. Check content and flow.&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/presentation.png&quot; alt=&quot;Screenshot presentation result of bolt&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Edit and refine.&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/code-overview.png&quot; alt=&quot;Screenshot for code from Bolt&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ask AI for help with new slides, examples, or transitions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add code snippets and diagrams.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deploy to Netlify with one click.&lt;br /&gt;
&lt;img src=&quot;https://alexop.dev//../../assets/images/create-ai-presentations-fast/deploy-netlify.png&quot; alt=&quot;Screenshot deploy bolt&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Why This Method Works&lt;/h2&gt;
&lt;p&gt;This approach delivers key advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Speed: Bolt jumpstarts content creation.&lt;/li&gt;
&lt;li&gt;Ease: No software to install.&lt;/li&gt;
&lt;li&gt;Flexibility: Make real-time adjustments.&lt;/li&gt;
&lt;li&gt;Collaboration: Share works-in-progress.&lt;/li&gt;
&lt;li&gt;Quality: Built-in themes ensure polish.&lt;/li&gt;
&lt;li&gt;Version control: Combine it with GitHub.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Try this approach for your next talk. You’ll create polished slides that engage your audience.&lt;/p&gt;

          </content:encoded><category>productivity</category><category>ai</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>10 Rules for Better Writing from the Book Economical Writing</title><link>https://alexop.dev/posts/10-rules-better-writing-economical-writing-book/</link><guid isPermaLink="true">https://alexop.dev/posts/10-rules-better-writing-economical-writing-book/</guid><description>Master 10 key writing techniques from Deirdre McCloskey&apos;s &apos;Economical Writing.&apos; Learn to use active verbs, write clearly, and avoid common mistakes. Ideal for students, researchers, and writers aiming to communicate more effectively.</description><pubDate>Sat, 28 Sep 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I always look for ways to &lt;code&gt;improve my writing&lt;/code&gt;. Recently, I found Deirdre McCloskey’s book &lt;code&gt;Economical Writing&lt;/code&gt; through an Instagram reel. In this post, I share &lt;code&gt;10 useful rules&lt;/code&gt; from the book, with examples and quotes from McCloskey.&lt;/p&gt;
&lt;h2&gt;Rules&lt;/h2&gt;
&lt;h3&gt;Rule 1: Be Thou Clear; but Seek Joy, Too&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Clarity is a matter of speed directed at The Point.&lt;br /&gt;
Bad writing makes slow reading.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;McCloskey emphasizes that &lt;code&gt;clarity is crucial above all&lt;/code&gt;. When writing about complex topics, give your reader every help possible. I’ve noticed that even if a text has good content, bad writing makes it hard to understand.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 2: You Will Need Tools&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The next most important tool is a dictionary, or nowadays a site on the internet that is itself a good dictionary. Googling a word is a bad substitute for a good dictionary site. You have to choose the intelligent site over the dreck such as Wiktionary, Google, and &lt;a href=&quot;http://Dictionary.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dictionary.com&lt;/a&gt;, all useless.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Writer highlights the significance of &lt;code&gt;tools&lt;/code&gt; that everyone who is serious about writing should use. The tools could be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href=&quot;https://www.grammarly.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;
  Spell Checker
&lt;/a&gt;
(Grammarly for example)
&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://www.oed.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;
  OED
&lt;/a&gt;
(a real dictionary to look up the origin of words)
&lt;/li&gt;
&lt;li&gt;
&lt;a href=&quot;https://www.thesaurus.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;
  Thesaurus
&lt;/a&gt;
(shows you similar words)
&lt;/li&gt;
&lt;li&gt;&amp;lt;a&lt;br /&gt;
href=“&lt;a href=&quot;https://www.hemingwayapp.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.hemingwayapp.com&lt;/a&gt;”&lt;br /&gt;
target=“_blank”&lt;br /&gt;
rel=“noopener noreferrer”
&lt;blockquote&gt;&lt;/blockquote&gt;
Hemingway Editor&lt;br /&gt;
&lt;br /&gt;
(improves readability and highlights complex sentences)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rule 3: Avoid Boilerplate&lt;/h3&gt;
&lt;p&gt;McCloskey warns against using &lt;code&gt;filler language&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Never start a paper with that all-purpose filler for the bankrupt imagination, ‘This paper . . .’&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 4: A Paragraph Should Have a Point&lt;/h3&gt;
&lt;p&gt;Each paragraph should &lt;code&gt;focus&lt;/code&gt; on a single topic:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The paragraph should be a more or less complete discussion of one topic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 5: Make Your Writing Cohere&lt;/h3&gt;
&lt;p&gt;Coherence is crucial for readability:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Make writing hang together. The reader can understand writing that hangs together,&lt;br /&gt;
from the level of phrases up to entire books.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 6: Avoid Elegant Variation&lt;/h3&gt;
&lt;p&gt;McCloskey emphasizes that &lt;code&gt;clarity trumps elegance&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;People who write so seem to mistake the purpose of writing, believing it to be an opportunity for empty display. The seventh grade, they should realize, is over.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 7: Watch Punctuation&lt;/h3&gt;
&lt;p&gt;Proper punctuation is more complex than it seems:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another detail is punctuation. You might think punctuation would be easy, since English has only seven marks.&quot;&lt;br /&gt;
After a comma (,), semicolon (;), or colon (:), put one space before you start something new. After a period (.), question mark (?), or exclamation point (!), put two spaces.&lt;br /&gt;
The colon (:) means roughly “to be specific.” The semicolon (;) means roughly “likewise” or “also.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 8: Watch The Order Around Switch Until It Good Sounds&lt;/h3&gt;
&lt;p&gt;McCloskey advises ending sentences with the main point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You should cultivate the habit of mentally rearranging the order of words and phrases of every sentence you write. Rules, as usual, govern the rewriting. One rule or trick is to use so-called auxiliary verbs (should, can, might, had, is, etc.) to lessen clotting in the sentence. “Looking through a lens-shape magnified what you saw.” Tough to read. “Looking through a lens-shape would magnify what you saw” is easier.&lt;br /&gt;
The most important rule of rearrangement of sentences is that the end is the place of emphasis. I wrote the sentence first as “The end of the sentence is the emphatic location,” which put the emphasis on the word location. The reader leaves the sentence with the last word ringing in her mental ears.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 9: Use Verbs, Active Ones&lt;/h3&gt;
&lt;p&gt;Active verbs make writing more engaging:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use active verbs: not “Active verbs should be used,” which is cowardice, hiding the user in the passive voice. Rather: “You should use active verbs.”&lt;br /&gt;
Verbs make English. If you pick out active, accurate, and lively verbs, you will write in an active, accurate, and lively style.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Rule 10: Avoid This, That, These, Those&lt;/h3&gt;
&lt;p&gt;Vague demonstrative pronouns can obscure meaning:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Often the plain the will do fine and keep the reader reading. The formula in revision is to ask of every this, these, those whether it might better be replaced by ether plain old the (the most common option) or it, or such (a).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I quickly finished the book, thanks to its excellent writing style. Its most important lesson was that much of what I learned about &lt;code&gt;good writing&lt;/code&gt; in school is incorrect. Good writing means expressing your thoughts &lt;code&gt;clearly&lt;/code&gt;. Avoid using complicated words. &lt;code&gt;Write the way you speak&lt;/code&gt;. The book demonstrates that using everyday words is a strength, not a weakness. I suggest everyone read this book. Think about how you can improve your writing by using its ideas.&lt;/p&gt;

          </content:encoded><category>book-summary</category><category>productivity</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>TypeScript Tutorial: Extracting All Keys from Nested Object</title><link>https://alexop.dev/posts/typescript-extract-all-keys-nested-objects/</link><guid isPermaLink="true">https://alexop.dev/posts/typescript-extract-all-keys-nested-objects/</guid><description>Learn how to extract all keys, including nested ones, from TypeScript objects using advanced type manipulation techniques. Improve your TypeScript skills and write safer code.</description><pubDate>Mon, 23 Sep 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;What’s the Problem?&lt;/h2&gt;
&lt;p&gt;Let’s say you have a big TypeScript object. It has objects inside objects. You want to get all the keys, even the nested ones. But TypeScript doesn’t provide this functionality out of the box.&lt;/p&gt;
&lt;p&gt;Look at this User object:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;User&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  address&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    street&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    city&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You want “id”, “name”, and “address.street”. The standard approach is insufficient:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// little helper to prettify the type on hover&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Pretty&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;UserKeys&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; User&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;PrettyUserKeys&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Pretty&lt;span&gt;&amp;lt;&lt;/span&gt;UserKeys&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach returns the top-level keys, missing nested properties like “address.street”.&lt;/p&gt;
&lt;p&gt;We need a more sophisticated solution using TypeScript’s advanced features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Conditional Types (if-then for types)&lt;/li&gt;
&lt;li&gt;Mapped Types (change each part of a type)&lt;/li&gt;
&lt;li&gt;Template Literal Types (make new string types)&lt;/li&gt;
&lt;li&gt;Recursive Types (types that refer to themselves)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s our solution:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ExtractKeys&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;object&lt;/span&gt;
  &lt;span&gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
        &lt;span&gt;|&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;
        &lt;span&gt;|&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;object&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;ExtractKeys&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s break down this type definition:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We check if T is an object.&lt;/li&gt;
&lt;li&gt;For each key in the object:&lt;/li&gt;
&lt;li&gt;We either preserve the key as-is, or&lt;/li&gt;
&lt;li&gt;If the key’s value is another object, we combine the key with its nested keys&lt;/li&gt;
&lt;li&gt;We apply this to the entire type structure&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now let’s use it:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;UserKeys&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; ExtractKeys&lt;span&gt;&amp;lt;&lt;/span&gt;User&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us all keys, including nested ones.&lt;/p&gt;
&lt;p&gt;The practical benefits become clear in this example:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; user&lt;span&gt;:&lt;/span&gt; User &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;123&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;John Doe&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  address&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    street&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Main St&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    city&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Berlin&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;getProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;obj&lt;span&gt;:&lt;/span&gt; User&lt;span&gt;,&lt;/span&gt; key&lt;span&gt;:&lt;/span&gt; UserKeys&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; keys &lt;span&gt;=&lt;/span&gt; key&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; obj&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; k &lt;span&gt;of&lt;/span&gt; keys&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    result &lt;span&gt;=&lt;/span&gt; result&lt;span&gt;[&lt;/span&gt;k&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; result&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// This works&lt;/span&gt;
&lt;span&gt;getProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;address.street&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// This gives an error&lt;/span&gt;
&lt;span&gt;getProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;address.country&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript detects potential errors during development.&lt;/p&gt;
&lt;p&gt;Important Considerations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;This type implementation may impact performance with complex nested objects.&lt;/li&gt;
&lt;li&gt;The type system enhances development-time safety without runtime overhead.&lt;/li&gt;
&lt;li&gt;Consider the trade-off between type safety and code readability.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;We’ve explored how to extract all keys from nested TypeScript objects. This technique provides enhanced type safety for your data structures. Consider the performance implications when implementing this in your projects.&lt;/p&gt;

          </content:encoded><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>TypeScript Snippets in Astro: Show, Don&apos;t Tell</title><link>https://alexop.dev/posts/typescript-snippets-astro-show-dont-tell/</link><guid isPermaLink="true">https://alexop.dev/posts/typescript-snippets-astro-show-dont-tell/</guid><description>Learn how to add interactive type information and syntax highlighting to TypeScript snippets in your Astro site, enhancing code readability and user experience.</description><pubDate>Sun, 22 Sep 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Elevate Your Astro Code Highlights with TypeScript Snippets&lt;/h2&gt;
&lt;p&gt;Want to take your Astro code highlights to the next level? This guide will show you how to add TypeScript snippets with hover-over type information, making your code examples more interactive and informative.&lt;/p&gt;
&lt;h2&gt;Prerequisites for Astro Code Highlights&lt;/h2&gt;
&lt;p&gt;Start with an Astro project. Follow the &lt;a href=&quot;https://docs.astro.build/en/getting-started/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;official Astro quickstart guide&lt;/a&gt; to set up your project.&lt;/p&gt;
&lt;h2&gt;Configuring Shiki for Enhanced Astro Code Highlights&lt;/h2&gt;
&lt;p&gt;Astro includes Shiki for syntax highlighting. Here’s how to optimize it for TypeScript snippets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update your &lt;code&gt;astro.config.mjs&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;defineConfig&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  markdown&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    shikiConfig&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      themes&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; light&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;min-light&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; dark&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;tokyo-night&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      wrap&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Add a stylish border to your code blocks:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span&gt;pre:has(code)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;@apply&lt;/span&gt; border border-skin-line&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding Type Information to Code Blocks&lt;/h2&gt;
&lt;p&gt;To add type information to your code blocks, you can use TypeScript’s built-in type annotations:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// @errors: 2322&lt;/span&gt;
&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;User&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; user&lt;span&gt;:&lt;/span&gt; User &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;John Doe&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;30&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// Type error: Type &apos;string&apos; is not assignable to type &apos;number&apos;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;user&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also show type information inline:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;User&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; user&lt;span&gt;:&lt;/span&gt; User &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;John Doe&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;30&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// The type of user.name is &apos;string&apos;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; name &lt;span&gt;=&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benefits of Enhanced Astro Code Highlights&lt;/h2&gt;
&lt;p&gt;Your Astro site now includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced syntax highlighting&lt;/li&gt;
&lt;li&gt;Type information in code blocks&lt;/li&gt;
&lt;li&gt;Adaptive light and dark mode code blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features enhance code readability and user experience, making your code examples more valuable to readers.&lt;/p&gt;

          </content:encoded><category>astro</category><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Vue 3.5&apos;s onWatcherCleanup: Mastering Side Effect Management in Vue Applications</title><link>https://alexop.dev/posts/vue-35s-onwatchercleanup-mastering-side-effect-management-in-vue-applications/</link><guid isPermaLink="true">https://alexop.dev/posts/vue-35s-onwatchercleanup-mastering-side-effect-management-in-vue-applications/</guid><description>Discover how Vue 3.5&apos;s new onWatcherCleanup function revolutionizes side effect management in Vue applications</description><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;My team and I discussed Vue 3.5’s new features, focusing on the &lt;code&gt;onWatcherCleanup&lt;/code&gt; function. The insights proved valuable enough to share in this blog post.&lt;/p&gt;
&lt;h2&gt;The Side Effect Challenge in Vue&lt;/h2&gt;
&lt;p&gt;Managing side effects in Vue presents challenges when dealing with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API calls&lt;/li&gt;
&lt;li&gt;Timer operations&lt;/li&gt;
&lt;li&gt;Event listener management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These side effects become complex during frequent value changes.&lt;/p&gt;
&lt;h2&gt;A Common Use Case: Fetching User Data&lt;/h2&gt;
&lt;p&gt;To illustrate the power of &lt;code&gt;onWatcherCleanup&lt;/code&gt;, let’s compare the old and new ways of fetching user data.&lt;/p&gt;
&lt;h3&gt;The Old Way&lt;/h3&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const userId = ref&amp;lt;string&amp;gt;(&quot;&quot;);
const userData = ref&amp;lt;any | null&amp;gt;(null);
let controller: AbortController | null = null;

watch(userId, async (newId: string) =&amp;gt; {
  if (controller) {
    controller.abort();
  }
  controller = new AbortController();

  try {
    const response = await fetch(`https://api.example.com/users/${newId}`, {
      signal: controller.signal,
    });
    if (!response.ok) {
      throw new Error(&quot;User not found&quot;);
    }
    userData.value = await response.json();
  } catch (error) {
    if (error instanceof Error &amp;amp;&amp;amp; error.name !== &quot;AbortError&quot;) {
      console.error(&quot;Fetch error:&quot;, error);
      userData.value = null;
    }
  }
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    
    &amp;lt;div v-if=&quot;userData&quot;&amp;gt;
      &amp;lt;h2&amp;gt;User Data&amp;lt;/h2&amp;gt;
      &amp;lt;pre&amp;gt;{{ JSON.stringify(userData, null, 2) }}&amp;lt;/pre&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-else-if=&quot;userId &amp;amp;&amp;amp; !userData&quot;&amp;gt;User not found&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Problems with this method:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;External controller management&lt;/li&gt;
&lt;li&gt;Manual request abortion&lt;/li&gt;
&lt;li&gt;Cleanup logic separate from effect&lt;/li&gt;
&lt;li&gt;Easy to forget proper cleanup&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The New Way: onWatcherCleanup&lt;/h2&gt;
&lt;p&gt;Here’s how &lt;code&gt;onWatcherCleanup&lt;/code&gt; improves the process:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const userId = ref&amp;lt;string&amp;gt;(&quot;&quot;);
const userData = ref&amp;lt;any | null&amp;gt;(null);

watch(userId, async (newId: string) =&amp;gt; {
  const controller = new AbortController();

  onWatcherCleanup(() =&amp;gt; {
    controller.abort();
  });

  try {
    const response = await fetch(`https://api.example.com/users/${newId}`, {
      signal: controller.signal,
    });
    if (!response.ok) {
      throw new Error(&quot;User not found&quot;);
    }
    userData.value = await response.json();
  } catch (error) {
    if (error instanceof Error &amp;amp;&amp;amp; error.name !== &quot;AbortError&quot;) {
      console.error(&quot;Fetch error:&quot;, error);
      userData.value = null;
    }
  }
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    
    &amp;lt;div v-if=&quot;userData&quot;&amp;gt;
      &amp;lt;h2&amp;gt;User Data&amp;lt;/h2&amp;gt;
      &amp;lt;pre&amp;gt;{{ JSON.stringify(userData, null, 2) }}&amp;lt;/pre&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-else-if=&quot;userId &amp;amp;&amp;amp; !userData&quot;&amp;gt;User not found&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Benefits of onWatcherCleanup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Clearer code: Cleanup logic is right next to the effect&lt;/li&gt;
&lt;li&gt;Automatic execution&lt;/li&gt;
&lt;li&gt;Fewer memory leaks&lt;/li&gt;
&lt;li&gt;Simpler logic&lt;/li&gt;
&lt;li&gt;Consistent with Vue API&lt;/li&gt;
&lt;li&gt;Fits seamlessly into Vue’s reactivity system&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;When to Use onWatcherCleanup&lt;/h2&gt;
&lt;p&gt;Use it to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cancel API requests&lt;/li&gt;
&lt;li&gt;Clear timers&lt;/li&gt;
&lt;li&gt;Remove event listeners&lt;/li&gt;
&lt;li&gt;Free resources&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Advanced Techniques&lt;/h2&gt;
&lt;h3&gt;Multiple Cleanups&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dependency&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; timer1 &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;/* ... */&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; timer2 &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;/* ... */&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;5000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;onWatcherCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer1&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;onWatcherCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;timer2&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// More logic...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional Cleanup&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dependency&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;condition&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; resource &lt;span&gt;=&lt;/span&gt; &lt;span&gt;acquireResource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;onWatcherCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;releaseResource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resource&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;// More code...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With watchEffect&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;onCleanup &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; data &lt;span&gt;=&lt;/span&gt; &lt;span&gt;fetchSomeData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;onCleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;cleanupData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;data&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How onWatcherCleanup Works&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/onWatcherCleanup.png&quot; alt=&quot;Image description&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Vue uses a WeakMap to manage cleanup functions efficiently. This approach connects cleanup functions with their effects and triggers them at the right time.&lt;/p&gt;
&lt;h3&gt;Executing Cleanup Functions&lt;/h3&gt;
&lt;p&gt;The system triggers cleanup functions in two scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Before the effect re-runs&lt;/li&gt;
&lt;li&gt;When the watcher stops&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This ensures proper resource management and side effect cleanup.&lt;/p&gt;
&lt;h3&gt;Under the Hood&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;onWatcherCleanup&lt;/code&gt; function integrates with Vue’s reactivity system. It uses the current active watcher to associate cleanup functions with the correct effect, triggering cleanups in the right context.&lt;/p&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;onWatcherCleanup&lt;/code&gt; implementation prioritizes efficiency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The system creates cleanup arrays on demand&lt;/li&gt;
&lt;li&gt;WeakMap usage optimizes memory management&lt;/li&gt;
&lt;li&gt;Adding cleanup functions happens instantly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These design choices enhance your Vue applications’ performance when handling watchers and side effects.&lt;/p&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Register cleanups at the start of your effect function&lt;/li&gt;
&lt;li&gt;Keep cleanup functions simple and focused&lt;/li&gt;
&lt;li&gt;Avoid creating new side effects within cleanup functions&lt;/li&gt;
&lt;li&gt;Handle potential errors in your cleanup logic&lt;/li&gt;
&lt;li&gt;Thoroughly test your effects and their associated cleanups&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Vue 3.5’s &lt;code&gt;onWatcherCleanup&lt;/code&gt; strengthens the framework’s toolset for managing side effects. It enables cleaner, more maintainable code by unifying setup and teardown logic. This feature helps create robust applications that handle resource management effectively and prevent side effect-related bugs.&lt;/p&gt;
&lt;p&gt;As you incorporate &lt;code&gt;onWatcherCleanup&lt;/code&gt; into your projects, you’ll discover how it simplifies common patterns and prevents bugs related to unmanaged side effects.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Build Your Own Vue-like Reactivity System from Scratch</title><link>https://alexop.dev/posts/how-to-build-your-own-vue-like-reactivity-system-from-scratch/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-build-your-own-vue-like-reactivity-system-from-scratch/</guid><description>Learn to build a Vue-like reactivity system from scratch, implementing your own ref() and watchEffect(). </description><pubDate>Sun, 04 Aug 2024 00:00:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Understanding the core of modern Frontend frameworks is crucial for every web developer. Vue, known for its reactivity system, offers a seamless way to update the DOM based on state changes. But have you ever wondered how it works under the hood?&lt;/p&gt;
&lt;p&gt;In this tutorial, we’ll demystify Vue’s reactivity by building our own versions of &lt;code&gt;ref()&lt;/code&gt; and &lt;code&gt;watchEffect()&lt;/code&gt;. By the end, you’ll have a deeper understanding of reactive programming in frontend development.&lt;/p&gt;
&lt;h2&gt;What is Reactivity in Frontend Development?&lt;/h2&gt;
&lt;p&gt;Before we dive in, let’s define reactivity:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reactivity: A declarative programming model for updating based on state changes.&lt;/strong&gt;[^1]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;[^1]: &lt;a href=&quot;https://www.pzuraq.com/blog/what-is-reactivity&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;What is Reactivity&lt;/a&gt; by Pzuraq&lt;/p&gt;
&lt;p&gt;This concept is at the heart of modern frameworks like Vue, React, and Angular. Let’s see how it works in a simple Vue component:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const counter = ref(0);

const incrementCounter = () =&amp;gt; {
  counter.value++;
};
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Counter: {{ counter }}&amp;lt;/h1&amp;gt;
    &amp;lt;button @click=&quot;incrementCounter&quot;&amp;gt;Increment&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;State Management:&lt;/strong&gt; &lt;code&gt;ref&lt;/code&gt; creates a reactive reference for the counter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Declarative Programming:&lt;/strong&gt; The template uses &lt;code&gt;{{ counter }}&lt;/code&gt; to display the counter value. The DOM updates automatically when the state changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Building Our Own Vue-like Reactivity System&lt;/h2&gt;
&lt;p&gt;To create a basic reactivity system, we need three key components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A method to store data&lt;/li&gt;
&lt;li&gt;A way to track changes&lt;/li&gt;
&lt;li&gt;A mechanism to update dependencies when data changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Key Components of Our Reactivity System&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;A store for our data and effects&lt;/li&gt;
&lt;li&gt;A dependency tracking system&lt;/li&gt;
&lt;li&gt;An effect runner that activates when data changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Understanding Effects in Reactive Programming&lt;/h3&gt;
&lt;p&gt;An &lt;code&gt;effect&lt;/code&gt; is a function that executes when a reactive state changes. Effects can update the DOM, make API calls, or perform calculations.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Effect&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;Effect&lt;/code&gt; type represents a function that runs when a reactive state changes.&lt;/p&gt;
&lt;h3&gt;The Store&lt;/h3&gt;
&lt;p&gt;We’ll use a Map to store our reactive dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; depMap&lt;span&gt;:&lt;/span&gt; Map&lt;span&gt;&amp;lt;&lt;/span&gt;object&lt;span&gt;,&lt;/span&gt; Map&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Set&lt;span&gt;&amp;lt;&lt;/span&gt;Effect&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementing Key Reactivity Functions&lt;/h2&gt;
&lt;h3&gt;The Track Function: Capturing Dependencies&lt;/h3&gt;
&lt;p&gt;This function records which effects depend on specific properties of reactive objects. It builds a dependency map to keep track of these relationships.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Effect&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;let&lt;/span&gt; activeEffect&lt;span&gt;:&lt;/span&gt; Effect &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; depMap&lt;span&gt;:&lt;/span&gt; Map&lt;span&gt;&amp;lt;&lt;/span&gt;object&lt;span&gt;,&lt;/span&gt; Map&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Set&lt;span&gt;&amp;lt;&lt;/span&gt;Effect&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;track&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;:&lt;/span&gt; object&lt;span&gt;,&lt;/span&gt; key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;activeEffect&lt;span&gt;)&lt;/span&gt; &lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;let&lt;/span&gt; dependenciesForTarget &lt;span&gt;=&lt;/span&gt; depMap&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;dependenciesForTarget&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    dependenciesForTarget &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Map&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; Set&lt;span&gt;&amp;lt;&lt;/span&gt;Effect&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    depMap&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;,&lt;/span&gt; dependenciesForTarget&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;let&lt;/span&gt; dependenciesForKey &lt;span&gt;=&lt;/span&gt; dependenciesForTarget&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;dependenciesForKey&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    dependenciesForKey &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Set&lt;span&gt;&amp;lt;&lt;/span&gt;Effect&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    dependenciesForTarget&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; dependenciesForKey&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  dependenciesForKey&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;activeEffect&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Trigger Function: Activating Effects&lt;/h3&gt;
&lt;p&gt;When a reactive property changes, this function activates all the effects that depend on that property. It uses the dependency map created by the track function.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;:&lt;/span&gt; object&lt;span&gt;,&lt;/span&gt; key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;symbol&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; depsForTarget &lt;span&gt;=&lt;/span&gt; depMap&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;target&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;depsForTarget&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; depsForKey &lt;span&gt;=&lt;/span&gt; depsForTarget&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;depsForKey&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      depsForKey&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;effect &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;effect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implementing ref: Creating Reactive References&lt;/h3&gt;
&lt;p&gt;This creates a reactive reference to a value. It wraps the value in an object with getter and setter methods that track access and trigger updates when the value changes.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;RefImpl&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;private&lt;/span&gt; _value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;_value &lt;span&gt;=&lt;/span&gt; value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;get&lt;/span&gt; &lt;span&gt;value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;track&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;_value&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;set&lt;/span&gt; &lt;span&gt;value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;newValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;newValue &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;_value&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;_value &lt;span&gt;=&lt;/span&gt; newValue&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;trigger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;value&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; RefImpl&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;RefImpl&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Creating watchEffect: Reactive Computations&lt;/h3&gt;
&lt;p&gt;This function creates a reactive computation. It executes the provided effect function and re-runs it whenever any reactive values used within the effect change.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;effect&lt;span&gt;:&lt;/span&gt; Effect&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;wrappedEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    activeEffect &lt;span&gt;=&lt;/span&gt; wrappedEffect&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;effect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    activeEffect &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;wrappedEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Putting It All Together: A Complete Example&lt;/h2&gt;
&lt;p&gt;Let’s see our reactivity system in action:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; countRef &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; doubleCountRef &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Ref count is: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;countRef&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;watchEffect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  doubleCountRef&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; countRef&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;*&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Double count is: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;doubleCountRef&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

countRef&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
countRef&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
countRef&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Final depMap:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; depMap&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Diagram for the complete workflow&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/refFromScratch.png&quot; alt=&quot;diagram for reactive workflow&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;check out the full example -&amp;gt; &lt;a href=&quot;https://www.typescriptlang.org/play/?#code/C4TwDgpgBAogZnCBjYUC8UAUBKdA+KANwHsBLAEwG4BYAKDoBsJUBDFUwieRFALlgTJUAHygA7AK4MG6cVIY16tAPTKoAYQBOEFsGgsoAWRZgowYlADO57VHIRIY+2KSkIlqHGKaoOpAAsoYgAjACshADo6VSgAFX9oY1MAd1JpKH9iBnIgsKEPYH9dKABbEygAawgQAotLZg9iOF9BFEso2iRiMWs7ByT+JIAeEPCUABojEyHrTVIxAHMoUUsQEuCsyYBlZiHuITxD2TEIZKmwHEU6OAkXYFJus002CsxgFk0F5n5RoUmqkD8WbzJYrNYbBjYfgkChQADedCgUFIzUwbHunH2KFwCNoSKRXR6WQgEQYxAWmAA5LFnkgKiCWjxgLxKZN0RwuK1gNgrnj8UxUPYwJYAGLeWIfL6oDBCpIRKVvSXMHmI-EorAAQiFovFSu58NV+L6wrFmgln2Yx1O5xmwDmi2WVnBmygO2Aey5h0uhvxspMEXqwEVFuAk21pvNUpVfKRAF86D6BcadZoANLVWTh3Uh+XMTAA6NG9WYLUOFPpkA4n1IrNpjMYE5nN0epl4b0x31liN6gN5gFhrveCuF-HxpRG2sViIscjkNHsTFc6OqsdjhO0G53B5iJ6kBZfTTBqU-PITSrVIF2hlg9ZZKFEMg5XEE7qWYmk8lUml7g8MiBcjwvB8d4QxZSYQKlSZKQBMDz0rXkXx6QVBzNPVM36f0FQg5VFCRYta0jZUDQ7Qleknetk27HMFQLXC1VRcjK2Io0axQqcgJgNh-Ewf8mXwZiWMQt8mA-ClKRgAAPZAJHuB1eKEWD5OxOjBKUoMRyNWMNKgMc4zoNdOgYFhLA8AAlf8AEkSjABghliAhnygMA5kIXRoAAfVchgJAgfhYgQqBSLtCQUG8TAvJ8vyqw7QpSHaTyWG86AMAiiA6IMpEpSIRKfJwPyBKRO0Xjefw4qg1LKW00j3zJMSAHFmFkpZUtg2L4tS7TtGACRNB3NqIgSpL0vXJFA2ypLMEbAA1HLfLiaKi1RabZqgDU0AwfrBp8haWM21KrWSGahurQLXxqz9KTdJrxsi1lxFOI7tpU-Er33CBDza8rZsq57dJ0-T103dhHm0OA7LbeZSHuRLHrm2J73MuArJs8GBK6nqd0bKBEeRhhMEh6GGFh6MDKB+5HmSXQAixIM1P4Gn7xhJ9VTJ7coGSZ4wEgcgaZwAqoHZRc+IwDmTG5mnnrU9sjUFzlhbkaRhvHdnOfFrl2wMmJJJYaymCgCRLBYL5eHXILTtuYBEdkUHMAABmXTpX0FYgJGCJh1BdsRLf-a3-zth2ta5KAAEZ+AAGXJAoEhu6AmnNr3EboSngGp9W+bQBzVWqkTaswAADK2ugt5FLH4AASOEi4T-8IlS2M85Jh310DviACZ+DdDxyBdt2IA9i2rfMKBgmgbvXb1wpoH2uOq+9uAk6p-xefTzO+TH3v++ruBa5WjBZ8RnekqgAAqKBW7o7OSVzvOABEe712eS-LuF1-dz258Pnz68b3kYm-N77RLEnoyfIdB94132hgYOihwHb0gWfGB78D7wIAMy8nXKbM6OcLoinmIlY0Aw7p+jANGIAA&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;click&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Beyond the Basics: What’s Missing?&lt;/h2&gt;
&lt;p&gt;While our implementation covers the core concepts, production-ready frameworks like Vue offer more advanced features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Handling of nested objects and arrays&lt;/li&gt;
&lt;li&gt;Efficient cleanup of outdated effects&lt;/li&gt;
&lt;li&gt;Performance optimizations for large-scale applications&lt;/li&gt;
&lt;li&gt;Computed properties and watchers&lt;/li&gt;
&lt;li&gt;Much more…&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion: Mastering Frontend Reactivity&lt;/h2&gt;
&lt;p&gt;By building our own &lt;code&gt;ref&lt;/code&gt; and &lt;code&gt;watchEffect&lt;/code&gt; functions, we’ve gained valuable insights into the reactivity systems powering modern frontend frameworks. We’ve covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating reactive data stores with &lt;code&gt;ref&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tracking changes using the &lt;code&gt;track&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;Updating dependencies with the &lt;code&gt;trigger&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;Implementing reactive computations via &lt;code&gt;watchEffect&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This knowledge empowers you to better understand, debug, and optimize reactive systems in your frontend projects.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>What is Local-first Web Development?</title><link>https://alexop.dev/posts/what-is-local-first-web-development/</link><guid isPermaLink="true">https://alexop.dev/posts/what-is-local-first-web-development/</guid><description>What is local-first software and why does it matter? This guide covers local-first architecture, offline-capable apps with automatic sync, data ownership, and how to build a local-first web app with Vue step by step.</description><pubDate>Wed, 29 May 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Imagine having complete control over your data in every web app, from social media platforms to productivity tools. Picture using these apps offline with automatic synchronization when you’re back online. This is the essence of local-first web development – a revolutionary approach that puts users in control of their digital experience.&lt;/p&gt;
&lt;p&gt;As browsers and devices become more powerful, we can now create web applications that minimize backend dependencies, eliminate loading delays, and overcome network errors. In this comprehensive guide, we’ll dive into the fundamentals of local-first web development and explore its numerous benefits for users and developers alike.&lt;/p&gt;
&lt;h2&gt;The Limitations of Traditional Web Applications&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/what-is-local-first/tradidonal-web-app.png&quot; alt=&quot;Traditional Web Application&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Traditional web applications rely heavily on backend servers for most operations. This dependency often results in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frequent loading spinners during data saves&lt;/li&gt;
&lt;li&gt;Potential errors when the backend is unavailable&lt;/li&gt;
&lt;li&gt;Limited or no functionality when offline&lt;/li&gt;
&lt;li&gt;Data storage primarily in the cloud, reducing user ownership&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While modern frameworks like Nuxt have improved initial load times through server-side rendering, many apps still suffer from performance issues post-load. Moreover, users often face challenges in accessing or exporting their data if an app shuts down.&lt;/p&gt;
&lt;h2&gt;Core Principles of Local-First Development&lt;/h2&gt;
&lt;p&gt;Local-first development shares similarities with offline-first approaches but goes further in prioritizing user control and data ownership. Here are the key principles that define a true local-first web application:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Instant Access:&lt;/strong&gt; Users can immediately access their work without waiting for data to load or sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device Independence:&lt;/strong&gt; Data is accessible across multiple devices seamlessly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Independence:&lt;/strong&gt; Basic tasks function without an internet connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effortless Collaboration:&lt;/strong&gt; The app supports easy collaboration, even in offline scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future-Proof Data:&lt;/strong&gt; User data remains accessible and usable over time, regardless of software changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-In Security:&lt;/strong&gt; Security and privacy are fundamental design considerations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Control:&lt;/strong&gt; Users have full ownership and control over their data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s important to note that some features, such as account deletion, may still require real-time backend communication to maintain data integrity.&lt;/p&gt;
&lt;p&gt;For a deeper dive into local-first software principles, check out &lt;a href=&quot;https://www.inkandswitch.com/local-first/#seven-ideals-for-local-first-software&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ink &amp;amp; Switch: Seven Ideals for Local-First Software&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Cloud vs Local-First Software Comparison&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Cloud Software 🌥️&lt;/th&gt;
&lt;th&gt;Local-First Software 💻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time Collaboration&lt;/td&gt;
&lt;td&gt;😟 Hard to implement&lt;/td&gt;
&lt;td&gt;😊 Built for real-time sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline Support&lt;/td&gt;
&lt;td&gt;😟 Does not work offline&lt;/td&gt;
&lt;td&gt;😊 Works offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service Reliability&lt;/td&gt;
&lt;td&gt;😟 Service shuts down? Lose everything!&lt;/td&gt;
&lt;td&gt;😊 Users can continue using local copy of software + data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service Implementation&lt;/td&gt;
&lt;td&gt;😟 Custom service for each app (infra, ops, on-call rotation, …)&lt;/td&gt;
&lt;td&gt;😊 Sync service is generic → outsource to cloud vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Local-First Software Fit Guide&lt;/h2&gt;
&lt;h3&gt;✅ Good Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;File Editing&lt;/strong&gt; 📝 - text editors, word processors, spreadsheets, slides, graphics, video, music, CAD, Jupyter notebooks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Productivity&lt;/strong&gt; 📋 - notes, tasks, issues, calendar, time tracking, messaging, bookkeeping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Ideal for apps where users freely manipulate their data&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Bad Fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Money&lt;/strong&gt; 💰 - banking, payments, ad tracking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Physical Resources&lt;/strong&gt; 📦 - e-commerce, inventory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vehicles&lt;/strong&gt; 🚗 - car sharing, freight, logistics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Better with centralized cloud/server model for real-world resource management&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Types of Local-First Applications&lt;/h2&gt;
&lt;p&gt;Local-first applications can be categorized into two main types:&lt;/p&gt;
&lt;h3&gt;1. Local-Only Applications&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/what-is-local-first/local-only.png&quot; alt=&quot;Local-Only Applications&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;While often mistakenly categorized as local-first, these are actually offline-first applications. They store data exclusively on the user’s device without cloud synchronization, and data transfer between devices requires manual export and import processes. This approach, while simpler to implement, doesn’t fulfill the core local-first principles of device independence and effortless collaboration. It’s more accurately described as an offline-first architecture.&lt;/p&gt;
&lt;h3&gt;2. Sync-Enabled Applications&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/what-is-local-first/sync-enabled-applications.png&quot; alt=&quot;Sync-Enabled Applications&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;These applications automatically synchronize user data with a cloud database, enhancing the user experience but introducing additional complexity for developers.&lt;/p&gt;
&lt;h2&gt;Challenges in Implementing Sync-Enabled Local-First Apps&lt;/h2&gt;
&lt;p&gt;Developing sync-enabled local-first applications presents unique challenges, particularly in managing data conflicts. For example, in a collaborative note-taking app, offline edits by multiple users can lead to merge conflicts upon synchronization. Resolving these conflicts requires specialized algorithms and data structures, which we’ll explore in future posts in this series.&lt;/p&gt;
&lt;p&gt;Even for single-user applications, synchronizing local data with cloud storage demands careful consideration and additional logic.&lt;/p&gt;
&lt;h2&gt;Building Local-First Web Apps: A Step-by-Step Approach&lt;/h2&gt;
&lt;p&gt;To create powerful local-first web applications, consider the following key steps, with a focus on Vue.js:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transform Your Vue SPA into a PWA&lt;/strong&gt;&lt;br /&gt;
Convert your Vue Single Page Application (SPA) into a Progressive Web App (PWA) to enable native app-like interactions. For a detailed guide, see &lt;a href=&quot;https://alexop.dev//../create-pwa-vue3-vite-4-steps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Create a Native-Like App in 4 Steps: PWA Magic with Vue 3 and Vite&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implement Robust Storage Solutions&lt;/strong&gt;&lt;br /&gt;
Move beyond simple localStorage to more sophisticated storage mechanisms that support offline functionality and data persistence. Learn more in &lt;a href=&quot;https://alexop.dev//../sqlite-vue3-offline-first-web-apps-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;How to Use SQLite in Vue 3: Complete Guide to Offline-First Web Apps&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Develop Syncing and Authentication Systems&lt;/strong&gt;&lt;br /&gt;
For sync-enabled apps, implement user authentication and secure data synchronization across devices. Learn how to implement syncing and conflict resolution in Building Local-First Apps with Vue and Dexie.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prioritize Security Measures&lt;/strong&gt;&lt;br /&gt;
Employ encryption techniques to protect sensitive user data stored in the browser.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We’ll delve deeper into each of these topics throughout this series on local-first web development.&lt;/p&gt;
&lt;h2&gt;Additional Resources for Local-First Development&lt;/h2&gt;
&lt;p&gt;To further your understanding of local-first applications, explore these valuable resources:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href=&quot;https://localfirstweb.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Local First Web&lt;/a&gt; - An excellent starting point with comprehensive follow-up topics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Podcast:&lt;/strong&gt; &lt;a href=&quot;https://www.localfirst.fm/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Local First FM&lt;/a&gt; - An insightful podcast dedicated to local-first development.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community:&lt;/strong&gt; Join the &lt;a href=&quot;https://discord.com/invite/ZRrwZxn4rW&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Local First Discord&lt;/a&gt; to connect with fellow developers and enthusiasts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource:&lt;/strong&gt; &lt;a href=&quot;https://www.localfirst.fm/landscape&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Local-First Landscape&lt;/a&gt; - A comprehensive overview of local-first technologies, frameworks, and tools to help developers navigate the ecosystem.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;p&gt;This article sparked an interesting discussion on Hacker News, where developers shared their experiences and insights about local-first development. You can join the conversation and read different perspectives on the topic in the &lt;a href=&quot;https://news.ycombinator.com/item?id=43577285&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Hacker News discussion thread&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion: Embracing the Local-First Revolution&lt;/h2&gt;
&lt;p&gt;Local-first web development represents a paradigm shift in how we create and interact with web applications. By prioritizing user control, data ownership, and offline capabilities, we can build more resilient, user-centric apps that adapt to the evolving needs of modern users.&lt;/p&gt;
&lt;p&gt;This introductory post marks the beginning of an exciting journey into the world of local-first development. Stay tuned for more in-depth articles exploring various aspects of building powerful, local-first web applications with Vue and other modern web technologies.&lt;/p&gt;

          </content:encoded><category>local-first</category><category>architecture</category><category>vue</category><category>offline</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Vue Accessibility Blueprint: 8 Steps</title><link>https://alexop.dev/posts/vue-accessibility-blueprint-8-steps/</link><guid isPermaLink="true">https://alexop.dev/posts/vue-accessibility-blueprint-8-steps/</guid><description>Master Vue accessibility with our comprehensive guide. Learn 8 crucial steps to create inclusive, WCAG-compliant web applications that work for all users.</description><pubDate>Sat, 18 May 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Creating accessible Vue components is crucial for building inclusive web applications that work for everyone, including people with disabilities. This guide outlines 8 essential steps to improve the accessibility of your Vue projects and align them with Web Content Accessibility Guidelines (WCAG) standards.&lt;/p&gt;
&lt;h2&gt;Why Accessibility Matters&lt;/h2&gt;
&lt;p&gt;Implementing accessible design in Vue apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expands your potential user base&lt;/li&gt;
&lt;li&gt;Enhances user experience&lt;/li&gt;
&lt;li&gt;Boosts SEO performance&lt;/li&gt;
&lt;li&gt;Reduces legal risks&lt;/li&gt;
&lt;li&gt;Demonstrates social responsibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let’s explore the 8 key steps for building accessible Vue components:&lt;/p&gt;
&lt;h2&gt;1. Master Semantic HTML&lt;/h2&gt;
&lt;p&gt;Using proper HTML structure and semantics provides a solid foundation for assistive technologies. Key actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use appropriate heading levels (h1-h6)&lt;/li&gt;
&lt;li&gt;Add ARIA attributes&lt;/li&gt;
&lt;li&gt;Ensure form inputs have associated labels&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Accessible Blog&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;nav&lt;/span&gt; &lt;span&gt;aria-label&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;Main&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span&gt;href&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;#home&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Home&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span&gt;href&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;#about&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;About&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;nav&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;header&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;main&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;article&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Latest Post&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;h2&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Content goes here...&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;article&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span&gt;for&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;comment&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Comment:&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span&gt;id&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;comment&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;comment&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;textarea&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;submit&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Post&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;main&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resource: &lt;a href=&quot;https://vuejs.org/guide/best-practices/accessibility.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Accessibility Guide&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2. Use eslint-plugin-vue-a11y&lt;/h2&gt;
&lt;p&gt;Add this ESLint plugin to detect accessibility issues during development:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; eslint-plugin-vuejs-accessibility --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automated a11y checks&lt;/li&gt;
&lt;li&gt;Consistent code quality&lt;/li&gt;
&lt;li&gt;Less manual testing needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Resource: &lt;a href=&quot;https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;eslint-plugin-vue-a11y GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;3. Test with Vue Testing Library&lt;/h2&gt;
&lt;p&gt;Adopt Vue Testing Library to write accessibility-focused tests:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;renders accessible button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;MyComponent&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; button &lt;span&gt;=&lt;/span&gt; screen&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;button&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeInTheDocument&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resource: &lt;a href=&quot;https://testing-library.com/docs/vue-testing-library/intro/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue Testing Library Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;4. Use Screen Readers&lt;/h2&gt;
&lt;p&gt;Test your app with screen readers like NVDA, VoiceOver or JAWS to experience it as visually impaired users do.&lt;/p&gt;
&lt;h2&gt;5. Run Lighthouse Audits&lt;/h2&gt;
&lt;p&gt;Use Lighthouse in Chrome DevTools or CLI to get comprehensive accessibility assessments.&lt;/p&gt;
&lt;p&gt;Resource: &lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google Lighthouse Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;6. Consult A11y Experts&lt;/h2&gt;
&lt;p&gt;Partner with accessibility specialists to gain deeper insights and recommendations.&lt;/p&gt;
&lt;h2&gt;7. Integrate A11y in Workflows&lt;/h2&gt;
&lt;p&gt;Make accessibility a core part of planning and development:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include a11y requirements in user stories&lt;/li&gt;
&lt;li&gt;Set a11y acceptance criteria&lt;/li&gt;
&lt;li&gt;Conduct team WCAG training&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8. Automate Testing with Cypress&lt;/h2&gt;
&lt;p&gt;Use Cypress with axe-core for automated a11y testing:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Home Page Accessibility&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;beforeEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    cy&lt;span&gt;.&lt;/span&gt;&lt;span&gt;visit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    cy&lt;span&gt;.&lt;/span&gt;&lt;span&gt;injectAxe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Has no detectable a11y violations&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    cy&lt;span&gt;.&lt;/span&gt;&lt;span&gt;checkA11y&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resource: &lt;a href=&quot;https://docs.cypress.io/app/guides/accessibility-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cypress Accessibility Testing Guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By following these 8 steps, you will enhance the accessibility of your Vue applications and create more inclusive web experiences. Remember that accessibility is an ongoing process - continually learn, test, and strive to make your apps usable by everyone.&lt;/p&gt;

          </content:encoded><category>vue</category><category>accessibility</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Structure Vue Projects</title><link>https://alexop.dev/posts/how-to-structure-vue-projects/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-structure-vue-projects/</guid><description>Discover best practices for structuring Vue projects of any size, from simple apps to complex enterprise solutions.</description><pubDate>Sun, 12 May 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Quick Summary&lt;/h2&gt;
&lt;p&gt;This post covers specific Vue project structures suited for different project sizes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flat structure for small projects&lt;/li&gt;
&lt;li&gt;Atomic Design for scalable applications&lt;/li&gt;
&lt;li&gt;Modular approach for larger projects&lt;/li&gt;
&lt;li&gt;Feature Sliced Design for complex applications&lt;/li&gt;
&lt;li&gt;Micro frontends for enterprise-level solutions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When starting a Vue project, one of the most critical decisions you’ll make is how to structure it. The right structure enhances scalability, maintainability, and collaboration within your team. This consideration aligns with &lt;strong&gt;Conway’s Law&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.”&lt;br /&gt;
— Mel Conway&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In essence, your Vue application’s architecture will reflect your organization’s structure, influencing how you should plan your project’s layout.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-structure-vue/conway.png&quot; alt=&quot;Diagram of Conway&apos;s Law&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Whether you’re building a small app or an enterprise-level solution, this guide covers specific project structures suited to different scales and complexities.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Flat Structure: Perfect for Small Projects&lt;/h2&gt;
&lt;p&gt;Are you working on a small-scale Vue project or a proof of concept? A simple, flat folder structure might be the best choice to keep things straightforward and avoid unnecessary complexity.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “BaseButton.vue” },&lt;br /&gt;
{ name: “BaseCard.vue” },&lt;br /&gt;
{ name: “PokemonList.vue” },&lt;br /&gt;
{ name: “PokemonCard.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “composables”,&lt;br /&gt;
children: [{ name: “usePokemon.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “utils”,&lt;br /&gt;
children: [{ name: “validators.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “layout”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “DefaultLayout.vue” },&lt;br /&gt;
{ name: “AdminLayout.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “plugins”,&lt;br /&gt;
children: [{ name: “translate.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
children: [{ name: “Home.vue” }, { name: “PokemonDetail.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “router”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “store”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “assets”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “images”, children: [] },&lt;br /&gt;
{ name: “styles”, children: [] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “…”, comment: “test files” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “App.vue” },&lt;br /&gt;
{ name: “main.js” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Pros and Cons&lt;/h3&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;✅ Pros&lt;/th&gt;
        &lt;th&gt;❌ Cons&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Easy to implement&lt;/td&gt;
        &lt;td&gt;Not scalable&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Minimal setup&lt;/td&gt;
        &lt;td&gt;Becomes cluttered as the project grows&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Ideal for small teams or solo developers&lt;/td&gt;
        &lt;td&gt;Lack of clear separation of concerns&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Atomic Design: Scalable Component Organization&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/atomic/diagram.svg&quot; alt=&quot;Atomic Design Diagram&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For larger Vue applications, Atomic Design provides a clear structure. This approach organizes components into a hierarchy from simplest to most complex.&lt;/p&gt;
&lt;h3&gt;The Atomic Hierarchy&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Atoms:&lt;/strong&gt; Basic elements like buttons and icons.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Molecules:&lt;/strong&gt; Groups of atoms forming simple components (e.g., search bars).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organisms:&lt;/strong&gt; Complex components made up of molecules and atoms (e.g., navigation bars).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Templates:&lt;/strong&gt; Page layouts that structure organisms without real content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages:&lt;/strong&gt; Templates filled with real content to form actual pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method ensures scalability and maintainability, facilitating a smooth transition between simple and complex components.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “atoms”,&lt;br /&gt;
children: [{ name: “AtomButton.vue” }, { name: “AtomIcon.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “molecules”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “MoleculeSearchInput.vue” },&lt;br /&gt;
{ name: “MoleculePokemonThumbnail.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “organisms”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “OrganismPokemonCard.vue” },&lt;br /&gt;
{ name: “OrganismHeader.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “templates”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “TemplatePokemonList.vue” },&lt;br /&gt;
{ name: “TemplatePokemonDetail.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “pages”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “PageHome.vue” },&lt;br /&gt;
{ name: “PagePokemonDetail.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “composables”,&lt;br /&gt;
children: [{ name: “usePokemon.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “utils”,&lt;br /&gt;
children: [{ name: “validators.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “layout”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “LayoutDefault.vue” },&lt;br /&gt;
{ name: “LayoutAdmin.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “plugins”,&lt;br /&gt;
children: [{ name: “translate.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “router”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “store”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “assets”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “images”, children: [] },&lt;br /&gt;
{ name: “styles”, children: [] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “…”, comment: “test files” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “App.vue” },&lt;br /&gt;
{ name: “main.js” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Pros and Cons&lt;/h3&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;✅ Pros&lt;/th&gt;
        &lt;th&gt;❌ Cons&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Highly scalable&lt;/td&gt;
        &lt;td&gt;Can introduce overhead in managing layers&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Organized component hierarchy&lt;/td&gt;
        &lt;td&gt;Initial complexity in setting up&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Reusable components&lt;/td&gt;
        &lt;td&gt;Might be overkill for smaller projects&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Improves collaboration among teams&lt;/td&gt;
        &lt;td&gt;&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;aside&gt;
  Check out my detailed blog post on [Atomic Design in Vue and
  Nuxt](../atomic-design-vue-or-nuxt).
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Modular Approach: Feature-Based Organization&lt;/h2&gt;
&lt;p&gt;As your project scales, consider a &lt;strong&gt;Modular Monolithic Architecture&lt;/strong&gt;. This structure encapsulates each feature or domain, enhancing maintainability and preparing for potential evolution towards microservices.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “core”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [{ name: “BaseButton.vue” }, { name: “BaseIcon.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “models”, children: [] },&lt;br /&gt;
{ name: “store”, children: [] },&lt;br /&gt;
{ name: “services”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “DefaultLayout.vue” },&lt;br /&gt;
{ name: “AdminLayout.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “utils”,&lt;br /&gt;
children: [{ name: “validators.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “modules”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “pokemon”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “PokemonThumbnail.vue” },&lt;br /&gt;
{ name: “PokemonCard.vue” },&lt;br /&gt;
{ name: “PokemonListTemplate.vue” },&lt;br /&gt;
{ name: “PokemonDetailTemplate.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “models”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “store”,&lt;br /&gt;
children: [{ name: “pokemonStore.js” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “services”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
children: [{ name: “PokemonDetailPage.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “pokemonTests.spec.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “search”,&lt;br /&gt;
open: false,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [{ name: “SearchInput.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “models”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “store”,&lt;br /&gt;
children: [{ name: “searchStore.js” }],&lt;br /&gt;
},&lt;br /&gt;
{ name: “services”, children: [] },&lt;br /&gt;
{ name: “views”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “searchTests.spec.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “assets”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “images”, children: [] },&lt;br /&gt;
{ name: “styles”, children: [] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “scss”, children: [] },&lt;br /&gt;
{ name: “App.vue” },&lt;br /&gt;
{ name: “main.ts” },&lt;br /&gt;
{ name: “router.ts” },&lt;br /&gt;
{ name: “store.ts” },&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “…”, comment: “test files” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “plugins”,&lt;br /&gt;
children: [{ name: “translate.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Alternative: Simplified Flat Feature Structure&lt;/h3&gt;
&lt;p&gt;A common pain point in larger projects is excessive folder nesting, which can make navigation and file discovery more difficult. Here’s a simplified, flat feature structure that prioritizes IDE-friendly navigation and reduces cognitive load:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “features”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “project”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “project.composable.ts” },&lt;br /&gt;
{ name: “project.data.ts” },&lt;br /&gt;
{ name: “project.store.ts” },&lt;br /&gt;
{ name: “project.types.ts” },&lt;br /&gt;
{ name: “project.utils.ts” },&lt;br /&gt;
{ name: “project.utils.test.ts” },&lt;br /&gt;
{ name: “ProjectList.vue” },&lt;br /&gt;
{ name: “ProjectItem.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;This structure offers key advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quick Navigation&lt;/strong&gt;: Using IDE features like “Quick Open” (Ctrl/Cmd + P), you can find any project-related file by typing “project…”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced Nesting&lt;/strong&gt;: All feature-related files are at the same level, eliminating deep folder hierarchies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear Ownership&lt;/strong&gt;: Each file’s name indicates its purpose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern Recognition&lt;/strong&gt;: Consistent naming makes it simple to understand each file’s role&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test Colocation&lt;/strong&gt;: Tests live right next to the code they’re testing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Feature-Sliced Design: For Complex Applications&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Feature-Sliced Design&lt;/strong&gt; is ideal for big, long-term projects. This approach breaks the application into different layers, each with a specific role.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-structure-vue/feature-sliced.png&quot; alt=&quot;Feature-Sliced Design Diagram&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Layers of Feature-Sliced Design&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;App:&lt;/strong&gt; Global settings, styles, and providers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processes:&lt;/strong&gt; Global business processes, like user authentication flows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages:&lt;/strong&gt; Full pages built using entities, features, and widgets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Widgets:&lt;/strong&gt; Combines entities and features into cohesive UI blocks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Features:&lt;/strong&gt; Handles user interactions that add value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entities:&lt;/strong&gt; Represents core business models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared:&lt;/strong&gt; Reusable utilities and components unrelated to specific business logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “app”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “App.vue” },&lt;br /&gt;
{ name: “main.js” },&lt;br /&gt;
{ name: “app.scss” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{ name: “processes”, children: [] },&lt;br /&gt;
{&lt;br /&gt;
name: “pages”,&lt;br /&gt;
children: [{ name: “Home.vue” }, { name: “PokemonDetailPage.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “widgets”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “UserProfile.vue” },&lt;br /&gt;
{ name: “PokemonStatsWidget.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “features”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “pokemon”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “CatchPokemon.vue” },&lt;br /&gt;
{ name: “PokemonList.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “user”,&lt;br /&gt;
children: [{ name: “Login.vue” }, { name: “Register.vue” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “entities”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “user”,&lt;br /&gt;
children: [{ name: “userService.js” }, { name: “userModel.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “pokemon”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “pokemonService.js” },&lt;br /&gt;
{ name: “pokemonModel.js” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “shared”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “ui”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “BaseButton.vue” },&lt;br /&gt;
{ name: “BaseInput.vue” },&lt;br /&gt;
{ name: “Loader.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “lib”,&lt;br /&gt;
children: [{ name: “api.js” }, { name: “helpers.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “assets”,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “images”, children: [] },&lt;br /&gt;
{ name: “styles”, children: [] },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “router”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “store”,&lt;br /&gt;
children: [{ name: “index.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
children: [{ name: “featureTests.spec.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Pros and Cons&lt;/h3&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;✅ Pros&lt;/th&gt;
        &lt;th&gt;❌ Cons&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;High cohesion and clear separation&lt;/td&gt;
        &lt;td&gt;Initial complexity in understanding the layers&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Scalable and maintainable&lt;/td&gt;
        &lt;td&gt;Requires thorough planning&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Facilitates team collaboration&lt;/td&gt;
        &lt;td&gt;Needs consistent enforcement of conventions&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;aside&gt;
  Visit the [official Feature-Sliced Design
  documentation](https://feature-sliced.design/) for an in-depth understanding.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Micro Frontends: Enterprise-Level Solution&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Micro frontends&lt;/strong&gt; apply the microservices concept to frontend development. Teams can work on distinct sections of a web app independently, enabling flexible development and deployment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-structure-vue/microfrontend.png&quot; alt=&quot;Micro Frontend Diagram&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Key Components&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application Shell:&lt;/strong&gt; The main controller handling basic layout and routing, connecting all micro frontends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decomposed UIs:&lt;/strong&gt; Each micro frontend focuses on a specific part of the application using its own technology stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pros and Cons&lt;/h3&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;✅ Pros&lt;/th&gt;
        &lt;th&gt;❌ Cons&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Independent deployments&lt;/td&gt;
        &lt;td&gt;High complexity in orchestration&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Scalability across large teams&lt;/td&gt;
        &lt;td&gt;Requires robust infrastructure&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;Technology-agnostic approach&lt;/td&gt;
        &lt;td&gt;Potential inconsistencies in user experience&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;aside&gt;
  Micro frontends are best suited for large, complex projects with multiple
  development teams. This approach can introduce significant complexity and is
  usually not necessary for small to medium-sized applications.
&lt;/aside&gt;
&lt;aside&gt;
  Want to learn how to actually implement microfrontends with Vue? Check out my
  comprehensive guide: [How to build Microfrontends with Module Federation and
  Vue](../how-to-build-microfrontends-with-module-federation-and-vue) - includes
  working code, architectural decisions, and a complete reference project.
&lt;/aside&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-structure-vue/conclusion.png&quot; alt=&quot;Conclusion&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Selecting the right project structure depends on your project’s size, complexity, and team organization. The more complex your team or project is, the more you should aim for a structure that facilitates scalability and maintainability.&lt;/p&gt;
&lt;p&gt;Your project’s architecture should grow with your organization, providing a solid foundation for future development.&lt;/p&gt;
&lt;h3&gt;Comparison Chart&lt;/h3&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;Approach&lt;/th&gt;
        &lt;th&gt;Description&lt;/th&gt;
        &lt;th&gt;✅ Pros&lt;/th&gt;
        &lt;th&gt;❌ Cons&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;strong&gt;Flat Structure&lt;/strong&gt;
        &lt;/td&gt;
        &lt;td&gt;Simple structure for small projects&lt;/td&gt;
        &lt;td&gt;Easy to implement&lt;/td&gt;
        &lt;td&gt;Not scalable, can become cluttered&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;strong&gt;Atomic Design&lt;/strong&gt;
        &lt;/td&gt;
        &lt;td&gt;Hierarchical component-based structure&lt;/td&gt;
        &lt;td&gt;Scalable, organized, reusable components&lt;/td&gt;
        &lt;td&gt;Overhead in managing layers, initial complexity&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;strong&gt;Modular Approach&lt;/strong&gt;
        &lt;/td&gt;
        &lt;td&gt;Feature-based modular structure&lt;/td&gt;
        &lt;td&gt;Scalable, encapsulated features&lt;/td&gt;
        &lt;td&gt;Potential duplication, requires discipline&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;strong&gt;Feature-Sliced Design&lt;/strong&gt;
        &lt;/td&gt;
        &lt;td&gt;Functional layers and slices for large projects&lt;/td&gt;
        &lt;td&gt;High cohesion, clear separation&lt;/td&gt;
        &lt;td&gt;Initial complexity, requires thorough planning&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;strong&gt;Micro Frontends&lt;/strong&gt;
        &lt;/td&gt;
        &lt;td&gt;Independent deployments of frontend components&lt;/td&gt;
        &lt;td&gt;Independent deployments, scalable&lt;/td&gt;
        &lt;td&gt;High complexity, requires coordination between teams&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;General Rules and Best Practices&lt;/h2&gt;
&lt;p&gt;Before concluding, let’s highlight some general rules you can apply to every structure. These guidelines are important for maintaining consistency and readability in your codebase.&lt;/p&gt;
&lt;h3&gt;Base Component Names&lt;/h3&gt;
&lt;p&gt;Use a prefix for your UI components to distinguish them from other components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “MyButton.vue” },&lt;br /&gt;
{ name: “VueTable.vue” },&lt;br /&gt;
{ name: “Icon.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “BaseButton.vue” },&lt;br /&gt;
{ name: “BaseTable.vue” },&lt;br /&gt;
{ name: “BaseIcon.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Related Component Names&lt;/h3&gt;
&lt;p&gt;Group related components together by naming them accordingly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “TodoList.vue” },&lt;br /&gt;
{ name: “TodoItem.vue” },&lt;br /&gt;
{ name: “TodoButton.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “TodoList.vue” },&lt;br /&gt;
{ name: “TodoListItem.vue” },&lt;br /&gt;
{ name: “TodoListItemButton.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Order of Words in Component Names&lt;/h3&gt;
&lt;p&gt;Component names should start with the highest-level words and end with descriptive modifiers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “ClearSearchButton.vue” },&lt;br /&gt;
{ name: “ExcludeFromSearchInput.vue” },&lt;br /&gt;
{ name: “LaunchOnStartupCheckbox.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “SearchButtonClear.vue” },&lt;br /&gt;
{ name: “SearchInputExclude.vue” },&lt;br /&gt;
{ name: “SettingsCheckboxLaunchOnStartup.vue” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Organizing Tests&lt;/h3&gt;
&lt;p&gt;Decide whether to keep your tests in a separate folder or alongside your components. Both approaches are valid, but consistency is key.&lt;/p&gt;
&lt;h4&gt;Approach 1: Separate Test Folder&lt;/h4&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “vue-project”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [{ name: “MyComponent.vue” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
children: [{ name: “HomeView.vue” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “tests”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
children: [{ name: “MyComponent.spec.js” }],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
children: [{ name: “HomeView.spec.js” }],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;h4&gt;Approach 2: Inline Test Files&lt;/h4&gt;
&lt;p&gt;&amp;lt;FileTree&lt;br /&gt;
tree={[&lt;br /&gt;
{&lt;br /&gt;
name: “vue-project”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “src”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{&lt;br /&gt;
name: “components”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “MyComponent.vue” },&lt;br /&gt;
{ name: “MyComponent.spec.js” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
{&lt;br /&gt;
name: “views”,&lt;br /&gt;
open: true,&lt;br /&gt;
children: [&lt;br /&gt;
{ name: “HomeView.vue” },&lt;br /&gt;
{ name: “HomeView.spec.js” },&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
],&lt;br /&gt;
},&lt;br /&gt;
]}&lt;br /&gt;
/&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/style-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Official Vue.js Style Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://micro-frontends.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Micro Frontends - Extending Microservice Ideas to Frontend Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/micro-frontends.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Martin Fowler on Micro Frontends&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://feature-sliced.design/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Official Feature-Sliced Design Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;

          </content:encoded><category>vue</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Persist User Data with LocalStorage in Vue</title><link>https://alexop.dev/posts/how-to-persist-user-data-with-localstorage-in-vue/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-persist-user-data-with-localstorage-in-vue/</guid><description>Learn how to efficiently store and manage user preferences like dark mode in Vue applications using LocalStorage. This guide covers basic operations, addresses common challenges, and provides type-safe solutions for robust development.</description><pubDate>Sun, 21 Apr 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When developing apps, there’s often a need to store data. Consider a simple scenario where your application features a dark mode, and users want to save their preferred setting. Most users might prefer dark mode, but some will want light mode. This raises the question: where should we store this preference? We could use an API with a backend to store the setting. For configurations that affect the client’s experience, persisting this data locally makes more sense. LocalStorage offers a straightforward solution. In this blog post, I’ll guide you through using LocalStorage in Vue and show you how to handle this data in an elegant and type-safe manner.&lt;/p&gt;
&lt;h2&gt;Understanding LocalStorage&lt;/h2&gt;
&lt;p&gt;LocalStorage is a web storage API that lets JavaScript sites store and access data directly in the browser indefinitely. This data remains saved across browser sessions. LocalStorage is straightforward, using a key-value store model where both the key and the value are strings.&lt;/p&gt;
&lt;p&gt;Here’s how you can use LocalStorage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To &lt;strong&gt;store&lt;/strong&gt; data: &lt;code&gt;localStorage.setItem(&apos;myKey&apos;, &apos;myValue&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;To &lt;strong&gt;retrieve&lt;/strong&gt; data: &lt;code&gt;localStorage.getItem(&apos;myKey&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;To &lt;strong&gt;remove&lt;/strong&gt; an item: &lt;code&gt;localStorage.removeItem(&apos;myKey&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;To &lt;strong&gt;clear&lt;/strong&gt; all storage: &lt;code&gt;localStorage.clear()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/localstorage-vue/diagram.png&quot; alt=&quot;Diagram that explains LocalStorage&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Using LocalStorage for Dark Mode Settings&lt;/h2&gt;
&lt;p&gt;In Vue, you can use LocalStorage to save a user’s preference for dark mode in a component.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/localstorage-vue/picture-dark-mode.png&quot; alt=&quot;Picture that shows a button where user can toggle dark mode&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;button class=&quot;dark-mode-toggle&quot; @click=&quot;toggleDarkMode&quot;&amp;gt;
    {{ isDarkMode ? &quot;Switch to Light Mode&quot; : &quot;Switch to Dark Mode&quot; }}
    
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const isDarkMode = ref(JSON.parse(localStorage.getItem(&quot;darkMode&quot;) ?? &quot;false&quot;));

const styleProperties = computed(() =&amp;gt; ({
  &quot;--background-color&quot;: isDarkMode.value ? &quot;#333&quot; : &quot;#FFF&quot;,
  &quot;--text-color&quot;: isDarkMode.value ? &quot;#FFF&quot; : &quot;#333&quot;,
}));

const sunIcon = `&amp;lt;svg some svg &amp;lt;/svg&amp;gt;`;

const moonIcon = `&amp;lt;svg some svg &amp;lt;/svg&amp;gt;`;

function applyStyles() {
  for (const [key, value] of Object.entries(styleProperties.value)) {
    document.documentElement.style.setProperty(key, value);
  }
}

function toggleDarkMode() {
  isDarkMode.value = !isDarkMode.value;
  localStorage.setItem(&quot;darkMode&quot;, JSON.stringify(isDarkMode.value));
  applyStyles();
}

// On component mount, apply the stored or default styles
onMounted(applyStyles);
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
.dark-mode-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  font-size: 16px;
  color: var(--text-color);
  background-color: var(--background-color);
  border: 1px solid var(--text-color);
  border-radius: 5px;
  cursor: pointer;
}

.icon {
  display: inline-block;
  margin-left: 10px;
}

:root {
  --background-color: #fff;
  --text-color: #333;
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
  transition:
    background-color 0.3s,
    color 0.3s;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Addressing Issues with Initial Implementation&lt;/h2&gt;
&lt;p&gt;The basic approach works well for simple cases, but larger applications face these key challenges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Type Safety and Key Validation&lt;/strong&gt;: Always check and handle data from LocalStorage to prevent errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoupling from LocalStorage&lt;/strong&gt;: Avoid direct LocalStorage interactions in your components. Instead, use a utility service or state management for better code maintenance and testing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: Manage exceptions like browser restrictions or storage limits properly as LocalStorage operations can fail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Synchronization Across Components&lt;/strong&gt;: Use event-driven communication or shared state to keep all components updated with changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serialization Constraints&lt;/strong&gt;: LocalStorage stores data as strings, making serialization and deserialization challenging with complex data types.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Solutions and Best Practices for LocalStorage&lt;/h2&gt;
&lt;p&gt;To overcome these challenges, consider these solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type Definitions&lt;/strong&gt;: Use TypeScript to enforce type safety and help with autocompletion.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// types/localStorageTypes.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;UserSettings&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;LocalStorageValues&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  darkMode&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  userSettings&lt;span&gt;:&lt;/span&gt; UserSettings&lt;span&gt;;&lt;/span&gt;
  lastLogin&lt;span&gt;:&lt;/span&gt; Date&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;type&lt;/span&gt; &lt;span&gt;LocalStorageKeys&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; LocalStorageValues&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Utility Classes&lt;/strong&gt;: Create a utility class to manage all LocalStorage operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// utils/LocalStorageHandler.ts&lt;/span&gt;
&lt;span&gt;// export class LocalStorageHandler {&lt;/span&gt;
  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; LocalStorageKeys&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; LocalStorageValues&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; item &lt;span&gt;=&lt;/span&gt; localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; item &lt;span&gt;?&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;item&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; LocalStorageValues&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Error retrieving item from localStorage: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;error&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;K&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; LocalStorageKeys&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;K&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    value&lt;span&gt;:&lt;/span&gt; LocalStorageValues&lt;span&gt;[&lt;/span&gt;&lt;span&gt;K&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; item &lt;span&gt;=&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; item&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;Error setting item in localStorage: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;error&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;:&lt;/span&gt; LocalStorageKeys&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;static&lt;/span&gt; &lt;span&gt;clear&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clear&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Composables&lt;/strong&gt;: Extract logic into Vue composables for better reusability and maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// composables/useDarkMode.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useDarkMode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isDarkMode &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;LocalStorageHandler&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;darkMode&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isDarkMode&lt;span&gt;,&lt;/span&gt; newValue &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    LocalStorageHandler&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;darkMode&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; newValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; isDarkMode &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/localstorage-vue/diagram-local-storage-and-component.png&quot; alt=&quot;Diagram that shows how component and localStorage work together&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can check the full refactored example out here&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://play.vuejs.org/#eNq9WG1v20YS/itz6gGSAXFFUu88O7m0lyBpnKY49/qlKhCaXEmMKS6xXEqWff7vfXZJSiSluAkKVLEYcnZenpmdnRnqsfMqTdk25x2vc6n4Jo19xV8sEqLL21wpkVAQ+1l2teiEvryzNiLklhKrVcwXHfp3EEfBHdYKyn/A8QEMi45RQPT4SFFWUekldW92kQrWpARdR6u1Ik3vkldf0Owl/empUHOZpf4RRxSIBLa31lptYv1ct7ARInkHBujMcnMH1kHhz6BwCA+Xg5qneMwCGaWKMq7ylGI/WWmXMuNGtEmFVPRIgdikueJhn0TyQeQJbumJllJsqIvwdWuseXYIxYGFDWrU7r+0WYDLNHvNgSe6qkv3Lo58mdrH/GcpUi5VxDMwVoh6vQu6ekG9R+1l17Ju/eBuJQExtAIRC9n1aibY1o9zsxffDYdDE/vv3rx50+2Xworfq+fFNLcR0/KL5OmiDrKIOcB9usy2K7rfxInes7VSqTcY7HY7thsyIVcD17btAViwPbsoVGswuSM8rLlOjOppGcV6i4NcSp6oHzQsUKtMuI3oNrJgU6dDxHffi3tQbbLJmeCvTMPL1FdrCrHyYUYjNnL8IRvPyVzAiX82TZkzKyglWS/YliY/QMrx2ZiNS7K+3TpsXKNZYP4VpFc1Nkg9bHDjfoXs1mrSwGex8cNmYk3a0usWJ75vnVFTYyltOS7ZdUguzd62pC3n7QnAh82cDecTGjPHbtqf2jOyY4fZCC8u1RpiaOk1/Y3hij0xl6YhvfjwYcic0QRBno1Hp5qvR2zujGB3fFb1dSEMZycNzKVuoNZa5sydN10qdCNIGjYSoG7523C3pfE9yp4NibmYiJ2oLnA9LDq6PF3qs/Di0/EkHQrZ33mUtNGvPUs66YbkOAh3wGY28piNXBcb61oIFoLqTF1rxCbOyGKT6VxnuAnCfDSxXDaezsA2moxxPx/W9gsBH09mzJ06r8bMdofIBn01SzTH7k7HATAx22HD6Qg38yGbT4fksok7q6lBJk+mUGPSaTgrr8XSiLnjKQzbE/hwtOtOptiu+emOLPMkUBH2wk/TeH+jC3FGKLqm4C6FpF6xZ7/d8X2fTKX8ncSSPt5+5oFiCLdExe61KnhRUi9KNUShCPINeFl18zrm5tnIMTSnUnbfO9pB7SVCm8RfDWezHR+gnpTzK/pHm6b5YhH48Y0S0l8Zu+/QLXtd3f9N8+rTjzcffwIsGSWraLnvtXXojkD1YOk+ZhAOBvQRnRyNSyRwDVmORtovWEmtObqckGisiGnIl34el30vWySHrturKYZe7JPp3mUn12TKAgQqBIW1h5YiEGGUofvvPVrG/B69GGDjaJVYERzNPAoAjUtD/5xnCi6iI4KUKEwVqR9w65arHeeJYUn9MEQgPHLs9J5cXAx5CQkrix44FiYlzfRVDzsne/VOe2EW22274mvTS24hQw4eBzYzEUfhl7QaPkv6YZTDtXGFJJeZNpGKqPTV7A/Tw1UrRlESRwlcRlbcGdmNL1dRYsV8iXhopytpTwqBgUbznML2SE8ORkEdJciYIyoNtyLcFwq+LRrPBlZJP8kifTC8E7Vks2HWL+TNfYEESaUTCRnU6WMSRFCW0Yp9zkSCMdngQyVFFkcxlx9TrRrToled5EXHj2Ox+9HQlMy5Ga6MzJoHd2fon7N7TVt0fpY843KLEfqwphBurorl1zc/wbnaIlI716P4M4v/5ciPXGMs2L6H84Bd4zNo35npFYn8S/b6HrmeVU5poKbKGP5FB8PuD8+4foSL+l5VJ0TxulZUftmnqH8qQzD5vRmaFSj0P7h+w5UGoefbx8Tf4PQUdcakR525ru9XXXWMSFlKy3KE/RYi5n5SuorR+mDAa5grGdAN1bVAdnt4D1F6f561+57vtVWUY1T7U0Atr9/6JvCF34eXhba+/jnPjm8RJ2Es3iVKhKadNxSURqvIZMpXUUDYIV3UL98TMoYnYVNGw3ihm4xH7y+8M3h+e/87/Z+SPI4rvfqjZHl2j5+iL+qyijA12kqJQFspTunxI/EaJpNC6mXRa1JfZrynKRfkN8EeEXkGUU3ZEwW+fqvscSlRDM6BQ3Yws9r79Fr/p42jWW+REwUAE/c6co/++Wgknj59AXgbRXFrEqm2BWVf/YotKFv9FzYCG7QVKP/fsBGt9l307JYvZ2cAM3eYXfiLUYZCfewKQFHyNQH+Qhgl34gtr9A1Y6SDeCY8Ddea8pXBtpUARUT2/kxXyXXUoete7XW+dfIlX/ZpZ2JX/yEB4meLQ3WSz9eCcrVRDQ7zYOMnhQp+mRLHHx+uNKLeuYpVHdbjDHhBL1/S0o8zkziFQuNKbRjsUy/hO5Oo5geKWtjOGTk3aB7kq5gerZWHrfnzie7enac/AMi2358=&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Play with Vue on Vue Playground&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This post explained the effective use of LocalStorage in Vue to manage user settings such as dark mode. We covered its basic operations, addressed common issues, and provided solutions to ensure robust and efficient application development. With these strategies, developers can create more responsive applications that effectively meet user needs.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Write Clean Vue Components</title><link>https://alexop.dev/posts/how-to-write-clean-vue-components/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-write-clean-vue-components/</guid><description>There are many ways to write better Vue components. One of my favorite ways is to separate business logic into pure functions.</description><pubDate>Sun, 28 Jan 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Writing code that’s both easy to test and easy to read can be a challenge, with Vue components. In this blog post, I’m going to share a design idea that will make your Vue components better. This method won’t speed up your code, but it will make it simpler to test and understand. Think of it as a big-picture way to improve your Vue coding style. It’s going to make your life easier when you need to fix or update your components.&lt;/p&gt;
&lt;p&gt;Whether you’re new to Vue or have been using it for some time, this tip will help you make your Vue components cleaner and more straightforward.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Understanding Vue Components&lt;/h2&gt;
&lt;p&gt;A Vue component is like a reusable puzzle piece in your app. It has three main parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;View&lt;/strong&gt;: This is the template section where you design the user interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reactivity&lt;/strong&gt;: Here, Vue’s features like &lt;code&gt;ref&lt;/code&gt; make the interface interactive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business Logic&lt;/strong&gt;: This is where you process data or manage user actions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-write-clean-vue-components/architecture.png&quot; alt=&quot;Architecture&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Case Study: &lt;code&gt;snakeGame.vue&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Let’s look at a common Vue component, &lt;code&gt;snakeGame.vue&lt;/code&gt;. It mixes the view, reactivity, and business logic, which can make it complex and hard to work with.&lt;/p&gt;
&lt;h3&gt;Code Sample: Traditional Approach&lt;/h3&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;game-container&quot;&amp;gt;
    &amp;lt;canvas ref=&quot;canvas&quot; width=&quot;400&quot; height=&quot;400&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const canvas = ref&amp;lt;HTMLCanvasElement | null&amp;gt;(null);
const ctx = ref&amp;lt;CanvasRenderingContext2D | null&amp;gt;(null);
let snake = [{ x: 200, y: 200 }];
let direction = { x: 0, y: 0 };
let lastDirection = { x: 0, y: 0 };
let food = { x: 0, y: 0 };
const gridSize = 20;
let gameInterval: number | null = null;

onMounted(() =&amp;gt; {
  if (canvas.value) {
    ctx.value = canvas.value.getContext(&quot;2d&quot;);
    resetFoodPosition();
    gameInterval = window.setInterval(gameLoop, 100);
  }
  window.addEventListener(&quot;keydown&quot;, handleKeydown);
});

onUnmounted(() =&amp;gt; {
  if (gameInterval !== null) {
    window.clearInterval(gameInterval);
  }
  window.removeEventListener(&quot;keydown&quot;, handleKeydown);
});

function handleKeydown(e: KeyboardEvent) {
  e.preventDefault();
  switch (e.key) {
    case &quot;ArrowUp&quot;:
      if (lastDirection.y !== 0) break;
      direction = { x: 0, y: -gridSize };
      break;
    case &quot;ArrowDown&quot;:
      if (lastDirection.y !== 0) break;
      direction = { x: 0, y: gridSize };
      break;
    case &quot;ArrowLeft&quot;:
      if (lastDirection.x !== 0) break;
      direction = { x: -gridSize, y: 0 };
      break;
    case &quot;ArrowRight&quot;:
      if (lastDirection.x !== 0) break;
      direction = { x: gridSize, y: 0 };
      break;
  }
}

function gameLoop() {
  updateSnakePosition();
  if (checkCollision()) {
    endGame();
    return;
  }
  checkFoodCollision();
  draw();
  lastDirection = { ...direction };
}

function updateSnakePosition() {
  for (let i = snake.length - 2; i &amp;gt;= 0; i--) {
    snake[i + 1] = { ...snake[i] };
  }
  snake[0].x += direction.x;
  snake[0].y += direction.y;
}

function checkCollision() {
  return (
    snake[0].x &amp;lt; 0 ||
    snake[0].x &amp;gt;= 400 ||
    snake[0].y &amp;lt; 0 ||
    snake[0].y &amp;gt;= 400 ||
    snake
      .slice(1)
      .some(segment =&amp;gt; segment.x === snake[0].x &amp;amp;&amp;amp; segment.y === snake[0].y)
  );
}

function checkFoodCollision() {
  if (snake[0].x === food.x &amp;amp;&amp;amp; snake[0].y === food.y) {
    snake.push({ ...snake[snake.length - 1] });
    resetFoodPosition();
  }
}

function resetFoodPosition() {
  food = {
    x: Math.floor(Math.random() * 20) * gridSize,
    y: Math.floor(Math.random() * 20) * gridSize,
  };
}

function draw() {
  if (!ctx.value) return;
  ctx.value.clearRect(0, 0, 400, 400);
  drawGrid();
  drawSnake();
  drawFood();
}

function drawGrid() {
  if (!ctx.value) return;
  ctx.value.strokeStyle = &quot;#ddd&quot;;
  for (let i = 0; i &amp;lt;= 400; i += gridSize) {
    ctx.value.beginPath();
    ctx.value.moveTo(i, 0);
    ctx.value.lineTo(i, 400);
    ctx.value.stroke();
    ctx.value.moveTo(0, i);
    ctx.value.lineTo(400, i);
    ctx.value.stroke();
  }
}

function drawSnake() {
  if (!ctx.value) return;
  ctx.value.fillStyle = &quot;green&quot;;
  snake.forEach(segment =&amp;gt; {
    ctx.value?.fillRect(segment.x, segment.y, gridSize, gridSize);
  });
}

function drawFood() {
  if (!ctx.value) return;
  ctx.value.fillStyle = &quot;red&quot;;
  ctx.value.fillRect(food.x, food.y, gridSize, gridSize);
}

function endGame() {
  if (gameInterval !== null) {
    window.clearInterval(gameInterval);
  }
  alert(&quot;Game Over&quot;);
}
&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
.game-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Screenshot from the game&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//./../../assets/images/how-to-write-clean-vue-components/snakeGameImage.png&quot; alt=&quot;Snake Game Screenshot&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Challenges with the Traditional Approach&lt;/h3&gt;
&lt;p&gt;When you mix the view, reactivity, and business logic all in one file, the component becomes bulky and hard to maintain. Unit tests become more complex, requiring integration tests for comprehensive coverage.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Introducing the Functional Core, Imperative Shell Pattern&lt;/h2&gt;
&lt;p&gt;To solve these problems in Vue, we use the “Functional Core, Imperative Shell” pattern. This pattern is key in software architecture and helps you structure your code better:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Functional Core, Imperative Shell Pattern&lt;/strong&gt;: In this design, the main logic of your app (the ‘Functional Core’) stays pure and without side effects, making it testable. The ‘Imperative Shell’ handles the outside world, like the UI or databases, and talks to the pure core.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//./../../assets/images/how-to-write-clean-vue-components/functional-core-diagram.png&quot; alt=&quot;Functional core Diagram&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;What Are Pure Functions?&lt;/h3&gt;
&lt;p&gt;In this pattern, &lt;strong&gt;pure functions&lt;/strong&gt; are at the heart of the ‘Functional Core’. A pure function is a concept from functional programming, and it has two key characteristics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Predictability&lt;/strong&gt;: If you give a pure function the same inputs, it always gives back the same output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Side Effects&lt;/strong&gt;: Pure functions don’t change anything outside them. They don’t alter external variables, call APIs, or do any input/output.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pure functions simplify testing, debugging, and code comprehension. They form the foundation of the Functional Core, keeping your app’s business logic clean and manageable.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Applying the Pattern in Vue&lt;/h3&gt;
&lt;p&gt;In Vue, this pattern has two parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Imperative Shell&lt;/strong&gt; (&lt;code&gt;useGameSnake.ts&lt;/code&gt;): This part handles the Vue-specific reactive bits. It’s where your components interact with Vue, managing operations like state changes and events.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functional Core&lt;/strong&gt; (&lt;code&gt;pureGameSnake.ts&lt;/code&gt;): This is where your pure business logic lives. It’s separate from Vue, which makes it easier to test and think about your app’s main functions, independent of the UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Implementing &lt;code&gt;pureGameSnake.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;pureGameSnake.ts&lt;/code&gt; file encapsulates the game’s business logic without any Vue-specific reactivity. This separation means easier testing and clearer logic.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; gridSize &lt;span&gt;=&lt;/span&gt; &lt;span&gt;20&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Position&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  y&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Snake&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Position&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;initializeSnake&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Snake &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; y&lt;span&gt;:&lt;/span&gt; &lt;span&gt;200&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;moveSnake&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;:&lt;/span&gt; Snake&lt;span&gt;,&lt;/span&gt; direction&lt;span&gt;:&lt;/span&gt; Position&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Snake &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; snake&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;segment&lt;span&gt;,&lt;/span&gt; index&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;index &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; x&lt;span&gt;:&lt;/span&gt; segment&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;+&lt;/span&gt; direction&lt;span&gt;.&lt;/span&gt;x&lt;span&gt;,&lt;/span&gt; y&lt;span&gt;:&lt;/span&gt; segment&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;+&lt;/span&gt; direction&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;snake&lt;span&gt;[&lt;/span&gt;index &lt;span&gt;-&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;isCollision&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;:&lt;/span&gt; Snake&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; head &lt;span&gt;=&lt;/span&gt; snake&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
    head&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
    head&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;&amp;gt;=&lt;/span&gt; &lt;span&gt;400&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
    head&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
    head&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;&amp;gt;=&lt;/span&gt; &lt;span&gt;400&lt;/span&gt; &lt;span&gt;||&lt;/span&gt;
    snake&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slice&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;segment &lt;span&gt;=&amp;gt;&lt;/span&gt; segment&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;===&lt;/span&gt; head&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; segment&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;===&lt;/span&gt; head&lt;span&gt;.&lt;/span&gt;y&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;randomFoodPosition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Position &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    x&lt;span&gt;:&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;floor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;random&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; gridSize&lt;span&gt;,&lt;/span&gt;
    y&lt;span&gt;:&lt;/span&gt; Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;floor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Math&lt;span&gt;.&lt;/span&gt;&lt;span&gt;random&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; gridSize&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;isFoodEaten&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;:&lt;/span&gt; Snake&lt;span&gt;,&lt;/span&gt; food&lt;span&gt;:&lt;/span&gt; Position&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;boolean&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; head &lt;span&gt;=&lt;/span&gt; snake&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; head&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;===&lt;/span&gt; food&lt;span&gt;.&lt;/span&gt;x &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; head&lt;span&gt;.&lt;/span&gt;y &lt;span&gt;===&lt;/span&gt; food&lt;span&gt;.&lt;/span&gt;y&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implementing &lt;code&gt;useGameSnake.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;useGameSnake.ts&lt;/code&gt;, we manage the Vue-specific state and reactivity, leveraging the pure functions from &lt;code&gt;pureGameSnake.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;Position&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  y&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Snake&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Position&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;GameState&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  snake&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Snake&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  direction&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Position&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  food&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Position&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  gameState&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;over&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;playing&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useGameSnake&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; GameState &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; snake&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Snake&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;initializeSnake&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; direction&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Position&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; x&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; y&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; food&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;Position&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;randomFoodPosition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; gameState&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;over&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;playing&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;playing&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; gameInterval&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; startGame &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    gameInterval &lt;span&gt;=&lt;/span&gt; window&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      snake&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;moveSnake&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt; direction&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

      &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isCollision&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        gameState&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;over&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;gameInterval &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;gameInterval&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isFoodEaten&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt; food&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;[&lt;/span&gt;snake&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;-&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
        food&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; GameLogic&lt;span&gt;.&lt;/span&gt;&lt;span&gt;randomFoodPosition&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;onMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;startGame&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;onUnmounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;gameInterval &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;clearInterval&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;gameInterval&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; snake&lt;span&gt;,&lt;/span&gt; direction&lt;span&gt;,&lt;/span&gt; food&lt;span&gt;,&lt;/span&gt; gameState &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Refactoring &lt;code&gt;gameSnake.vue&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Now, our &lt;code&gt;gameSnake.vue&lt;/code&gt; is more focused, using &lt;code&gt;useGameSnake.ts&lt;/code&gt; for managing state and reactivity, while the view remains within the template.&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;game-container&quot;&amp;gt;
    &amp;lt;canvas ref=&quot;canvas&quot; width=&quot;400&quot; height=&quot;400&quot;&amp;gt;&amp;lt;/canvas&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const { snake, direction, food, gameState } = useGameSnake();
const canvas = ref&amp;lt;HTMLCanvasElement | null&amp;gt;(null);
const ctx = ref&amp;lt;CanvasRenderingContext2D | null&amp;gt;(null);
let lastDirection = { x: 0, y: 0 };

onMounted(() =&amp;gt; {
  if (canvas.value) {
    ctx.value = canvas.value.getContext(&quot;2d&quot;);
    draw();
  }
  window.addEventListener(&quot;keydown&quot;, handleKeydown);
});

onUnmounted(() =&amp;gt; {
  window.removeEventListener(&quot;keydown&quot;, handleKeydown);
});

watch(gameState, state =&amp;gt; {
  if (state === &quot;over&quot;) {
    alert(&quot;Game Over&quot;);
  }
});

function handleKeydown(e: KeyboardEvent) {
  e.preventDefault();
  switch (e.key) {
    case &quot;ArrowUp&quot;:
      if (lastDirection.y !== 0) break;
      direction.value = { x: 0, y: -gridSize };
      break;
    case &quot;ArrowDown&quot;:
      if (lastDirection.y !== 0) break;
      direction.value = { x: 0, y: gridSize };
      break;
    case &quot;ArrowLeft&quot;:
      if (lastDirection.x !== 0) break;
      direction.value = { x: -gridSize, y: 0 };
      break;
    case &quot;ArrowRight&quot;:
      if (lastDirection.x !== 0) break;
      direction.value = { x: gridSize, y: 0 };
      break;
  }
  lastDirection = { ...direction.value };
}

watch(
  [snake, food],
  () =&amp;gt; {
    draw();
  },
  { deep: true }
);

function draw() {
  if (!ctx.value) return;
  ctx.value.clearRect(0, 0, 400, 400);
  drawGrid();
  drawSnake();
  drawFood();
}

function drawGrid() {
  if (!ctx.value) return;
  ctx.value.strokeStyle = &quot;#ddd&quot;;
  for (let i = 0; i &amp;lt;= 400; i += gridSize) {
    ctx.value.beginPath();
    ctx.value.moveTo(i, 0);
    ctx.value.lineTo(i, 400);
    ctx.value.stroke();
    ctx.value.moveTo(0, i);
    ctx.value.lineTo(400, i);
    ctx.value.stroke();
  }
}

function drawSnake() {
  ctx.value.fillStyle = &quot;green&quot;;
  snake.value.forEach(segment =&amp;gt; {
    ctx.value.fillRect(segment.x, segment.y, gridSize, gridSize);
  });
}

function drawFood() {
  ctx.value.fillStyle = &quot;red&quot;;
  ctx.value.fillRect(food.value.x, food.value.y, gridSize, gridSize);
}
&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
.game-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Advantages of the Functional Core, Imperative Shell Pattern&lt;/h2&gt;
&lt;p&gt;The Functional Core, Imperative Shell pattern enhances the &lt;strong&gt;testability&lt;/strong&gt; and &lt;strong&gt;maintainability&lt;/strong&gt; of Vue components. By separating the business logic from the framework-specific code, this pattern offers key advantages:&lt;/p&gt;
&lt;h3&gt;Simplified Testing&lt;/h3&gt;
&lt;p&gt;Business logic combined with Vue’s reactivity and component structure makes testing complex. Traditional unit testing becomes challenging, leading to integration tests that lack precision. By extracting the core logic into pure functions (as in &lt;code&gt;pureGameSnake.ts&lt;/code&gt;), we write focused unit tests for each function. This isolation streamlines testing, as each piece of logic operates independently of Vue’s reactivity system.&lt;/p&gt;
&lt;h3&gt;Enhanced Maintainability&lt;/h3&gt;
&lt;p&gt;The Functional Core, Imperative Shell pattern creates a clear &lt;strong&gt;separation of concerns&lt;/strong&gt;. Vue components focus on the user interface and reactivity, while the pure business logic lives in separate, framework-agnostic files. This separation improves code readability and understanding. Maintenance becomes straightforward as the application grows.&lt;/p&gt;
&lt;h3&gt;Framework Agnosticism&lt;/h3&gt;
&lt;p&gt;A key advantage of this pattern is the &lt;strong&gt;portability&lt;/strong&gt; of your business logic. The pure functions in the Functional Core remain independent of any UI framework. If you need to switch from Vue to another framework, or if Vue changes, your core logic remains intact. This flexibility protects your code against changes and shifts in technology.&lt;/p&gt;
&lt;h2&gt;Testing Complexities in Traditional Vue Components vs. Functional Core, Imperative Shell Pattern&lt;/h2&gt;
&lt;h3&gt;Challenges in Testing Traditional Components&lt;/h3&gt;
&lt;p&gt;Testing traditional Vue components, where view, reactivity, and business logic combine, presents specific challenges. In such components, unit tests face these obstacles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests function more like integration tests, reducing precision&lt;/li&gt;
&lt;li&gt;Vue’s reactivity system creates complex mocking requirements&lt;/li&gt;
&lt;li&gt;Test coverage must span reactive behavior and side effects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These challenges reduce confidence in tests and component stability.&lt;/p&gt;
&lt;h3&gt;Simplified Testing with Functional Core, Imperative Shell Pattern&lt;/h3&gt;
&lt;p&gt;The Functional Core, Imperative Shell pattern transforms testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolated Business Logic&lt;/strong&gt;: Pure functions in the Functional Core enable direct unit tests without Vue’s reactivity or component states.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable Outcomes&lt;/strong&gt;: Pure functions deliver consistent outputs for given inputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear Separation&lt;/strong&gt;: The reactive and side-effect code stays in the Imperative Shell, enabling focused testing of Vue interactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach creates a modular, testable codebase where each component undergoes thorough testing, improving reliability.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The Functional Core, Imperative Shell pattern strengthens Vue applications through improved testing and maintenance. It prepares your code for future changes and growth. While restructuring requires initial effort, the pattern delivers long-term benefits, making it valuable for Vue developers aiming to enhance their application’s architecture and quality.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//./../../assets/images/how-to-write-clean-vue-components/conclusionDiagram.png&quot; alt=&quot;Blog Conclusion Diagram&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;

          </content:encoded><category>vue</category><category>architecture</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>The Problem with as in TypeScript: Why It&apos;s a Shortcut We Should Avoid</title><link>https://alexop.dev/posts/the-problem-with-as-in-typescript-why-its-a-shortcut-we-should-avoid/</link><guid isPermaLink="true">https://alexop.dev/posts/the-problem-with-as-in-typescript-why-its-a-shortcut-we-should-avoid/</guid><description>Learn why as can be a Problem in Typescript</description><pubDate>Sun, 21 Jan 2024 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h3&gt;Introduction: Understanding TypeScript and Its Challenges&lt;/h3&gt;
&lt;p&gt;TypeScript enhances JavaScript by adding stricter typing rules. While JavaScript’s flexibility enables rapid development, it can also lead to runtime errors such as “undefined is not a function” or type mismatches. TypeScript aims to catch these errors during development.&lt;/p&gt;
&lt;p&gt;The as keyword in TypeScript creates specific challenges with type assertions. It allows developers to override TypeScript’s type checking, reintroducing the errors TypeScript aims to prevent. When developers assert an any type with a specific interface, runtime errors occur if the object doesn’t match the interface. In codebases, frequent use of as indicates underlying design issues or incomplete type definitions.&lt;/p&gt;
&lt;p&gt;The article will examine the pitfalls of overusing as and provide guidelines for more effective TypeScript development, helping developers leverage TypeScript’s strengths while avoiding its potential drawbacks. Readers will explore alternatives to as, such as type guards and generics, and learn when type assertions make sense.&lt;/p&gt;
&lt;h3&gt;Easy Introduction to TypeScript’s &lt;code&gt;as&lt;/code&gt; Keyword&lt;/h3&gt;
&lt;p&gt;TypeScript is a special version of JavaScript. It adds rules to make coding less error-prone and clearer. But there’s a part of TypeScript, called the &lt;code&gt;as&lt;/code&gt; keyword, that’s tricky. In this article, I’ll talk about why &lt;code&gt;as&lt;/code&gt; can be a problem.&lt;/p&gt;
&lt;h4&gt;What is &lt;code&gt;as&lt;/code&gt; in TypeScript?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;as&lt;/code&gt; in TypeScript changes data types. For example:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;let&lt;/span&gt; unknownInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Hello, TypeScript!&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; asString &lt;span&gt;=&lt;/span&gt; unknownInput &lt;span&gt;as&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;//  ^?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The Problem with &lt;code&gt;as&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The best thing about TypeScript is that it finds mistakes in your code before you even run it. But when you use &lt;code&gt;as&lt;/code&gt;, you can skip these checks. It’s like telling the computer, “I’m sure this is right,” even if we might be wrong.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;as&lt;/code&gt; too much is risky. It can cause errors in parts of your code where TypeScript could have helped. Imagine driving with a blindfold; that’s what it’s like.&lt;/p&gt;
&lt;h4&gt;Why Using &lt;code&gt;as&lt;/code&gt; Can Be Bad&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skipping Checks&lt;/strong&gt;: TypeScript is great because it checks your code. Using &lt;code&gt;as&lt;/code&gt; means you skip these helpful checks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Making Code Unclear&lt;/strong&gt;: When you use &lt;code&gt;as&lt;/code&gt;, it can make your code hard to understand. Others (or even you later) might not know why you used &lt;code&gt;as&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Errors Happen&lt;/strong&gt;: If you use &lt;code&gt;as&lt;/code&gt; wrong, your program will crash.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Better Ways Than &lt;code&gt;as&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type Guards&lt;/strong&gt;: TypeScript has type guards. They help you check types.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// Let&apos;s declare a variable of unknown type&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; unknownInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Now we&apos;ll use a type guard with typeof&lt;/span&gt;
&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt; unknownInput &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;string&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// TypeScript now knows unknownInput is a string&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;unknownInput&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUpperCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Here, TypeScript still considers it unknown&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;unknownInput&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Better Type Definitions&lt;/strong&gt;: Developers reach for &lt;code&gt;as&lt;/code&gt; because of incomplete type definitions. Improving type definitions eliminates this need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your Own Type Guards&lt;/strong&gt;: For complicated types, you can make your own checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;// @errors: 2345&lt;/span&gt;

&lt;span&gt;// Define our type guard function&lt;/span&gt;
&lt;span&gt;function&lt;/span&gt; &lt;span&gt;isValidString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;unknownInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; unknownInput &lt;span&gt;is&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;typeof&lt;/span&gt; unknownInput &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;string&quot;&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; unknownInput&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Example usage&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; someInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Hello, World!&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; emptyInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; numberInput&lt;span&gt;:&lt;/span&gt; &lt;span&gt;unknown&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;42&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isValidString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;someInput&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;someInput&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUpperCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Input is not a valid string&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isValidString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;emptyInput&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;This won&apos;t be reached&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Empty input is not a valid string&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;isValidString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;numberInput&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;This won&apos;t be reached&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Number input is not a valid string&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Hover over `result` to see the inferred type&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;someInput&lt;span&gt;,&lt;/span&gt; emptyInput&lt;span&gt;,&lt;/span&gt; numberInput&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;isValidString&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;//    ^?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cases Where Using &lt;code&gt;as&lt;/code&gt; is Okay&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;as&lt;/code&gt; keyword fits specific situations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integrating with Non-Typed Code&lt;/strong&gt;: When working with JavaScript libraries or external APIs without types, &lt;code&gt;as&lt;/code&gt; helps assign types to external data. Type guards remain the better choice, offering more robust type checking that aligns with TypeScript’s goals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Casting in Tests&lt;/strong&gt;: In unit tests, when mocking or setting up test data, &lt;code&gt;as&lt;/code&gt; helps shape data into the required form.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In these situations, verify that &lt;code&gt;as&lt;/code&gt; solves a genuine need rather than masking improper type handling.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/asTypescript.png&quot; alt=&quot;Diagram as typescript inference&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;as&lt;/code&gt; serves a purpose in TypeScript, but better alternatives exist. By choosing proper type handling over shortcuts, we create clearer, more reliable code. Let’s embrace TypeScript’s strengths and write better code.&lt;/p&gt;

          </content:encoded><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Exploring the Power of Square Brackets in TypeScript</title><link>https://alexop.dev/posts/exploring-the-power-of-square-brackets-in-typescript/</link><guid isPermaLink="true">https://alexop.dev/posts/exploring-the-power-of-square-brackets-in-typescript/</guid><description>TypeScript, a statically-typed superset of JavaScript, implements square brackets [] for specific purposes. This post details the essential applications of square brackets in TypeScript, from array types to complex type manipulations, to help you write type-safe code.</description><pubDate>Tue, 19 Dec 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;TypeScript, the popular statically-typed superset of JavaScript, offers advanced type manipulation features that enhance development with strong typing. Square brackets &lt;code&gt;[]&lt;/code&gt; serve distinct purposes in TypeScript. This post details how square brackets work in TypeScript, from array types to indexed access types and beyond.&lt;/p&gt;
&lt;h2&gt;1. Defining Array Types&lt;/h2&gt;
&lt;p&gt;Square brackets in TypeScript define array types with precision.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;let&lt;/span&gt; numbers&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; strings&lt;span&gt;:&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;hello&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;world&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This syntax specifies that &lt;code&gt;numbers&lt;/code&gt; contains numbers, and &lt;code&gt;strings&lt;/code&gt; contains strings.&lt;/p&gt;
&lt;h2&gt;2. Tuple Types&lt;/h2&gt;
&lt;p&gt;Square brackets define tuples - arrays with fixed lengths and specific types at each index.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Point&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; coordinates&lt;span&gt;:&lt;/span&gt; Point &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;12.34&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;56.78&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, &lt;code&gt;Point&lt;/code&gt; represents a 2D coordinate as a tuple.&lt;/p&gt;
&lt;h2&gt;3. The &lt;code&gt;length&lt;/code&gt; Property&lt;/h2&gt;
&lt;p&gt;Every array in TypeScript includes a &lt;code&gt;length&lt;/code&gt; property that the type system recognizes.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;LengthArr&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;Array&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;length&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; &lt;span&gt;foo&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; LengthArr&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;2&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript recognizes &lt;code&gt;length&lt;/code&gt; as the numeric size of the array.&lt;/p&gt;
&lt;h2&gt;4. Indexed Access Types&lt;/h2&gt;
&lt;p&gt;Square brackets access specific index or property types.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Point&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;FirstElement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Point&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;FirstElement&lt;/code&gt; represents the first element in the &lt;code&gt;Point&lt;/code&gt; tuple: &lt;code&gt;number&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;5. Creating Union Types from Tuples&lt;/h2&gt;
&lt;p&gt;Square brackets help create union types from tuples efficiently.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Statuses&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;&quot;active&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;inactive&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;pending&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;CurrentStatus&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Statuses&lt;span&gt;[&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Statuses[number]&lt;/code&gt; creates a union from all tuple elements.&lt;/p&gt;
&lt;h2&gt;6. Generic Array Types and Constraints&lt;/h2&gt;
&lt;p&gt;Square brackets define generic constraints and types.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;logArrayElements&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;elements&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  elements&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;element &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;element&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function accepts any array type through the generic constraint &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;7. Mapped Types with Index Signatures&lt;/h2&gt;
&lt;p&gt;Square brackets in mapped types define index signatures to create dynamic property types.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;StringMap&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;let&lt;/span&gt; map&lt;span&gt;:&lt;/span&gt; StringMap&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;2&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;StringMap&lt;/code&gt; creates a type with string keys and values of type &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;8. Advanced Tuple Manipulation&lt;/h2&gt;
&lt;p&gt;Square brackets enable precise tuple manipulation for extracting or omitting elements.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;WithoutFirst&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;...&lt;/span&gt;&lt;span&gt;infer&lt;/span&gt; Rest&lt;span&gt;]&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; Rest &lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Tail&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; WithoutFirst&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WithoutFirst&lt;/code&gt; removes the first element from a tuple.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Square brackets in TypeScript provide essential functionality, from basic array definitions to complex type manipulations. These features make TypeScript code reliable and maintainable. The growing adoption of TypeScript demonstrates the practical benefits of its robust type system.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/intro.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeScript Handbook&lt;/a&gt; provides comprehensive documentation of these features. &lt;a href=&quot;https://typehero.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeHero&lt;/a&gt; offers hands-on practice through interactive challenges to master TypeScript concepts, including square bracket techniques for type manipulation. These resources will strengthen your command of TypeScript and expand your programming capabilities.&lt;/p&gt;

          </content:encoded><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Test Vue Composables: A Comprehensive Guide with Vitest</title><link>https://alexop.dev/posts/how-to-test-vue-composables/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-test-vue-composables/</guid><description>Learn how to effectively test Vue composables using Vitest. Covers independent and dependent composables, with practical examples and best practices.</description><pubDate>Sat, 25 Nov 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Hello, everyone; in this blog post, I want to help you better understand how to test a composable in Vue. Nowadays, much of our business logic or UI logic is often encapsulated in composables, so I think it’s important to understand how to test them.&lt;/p&gt;
&lt;h2&gt;Definitions&lt;/h2&gt;
&lt;p&gt;Before discussing the main topic, it’s important to understand some basic concepts regarding testing. This foundational knowledge will help clarify where testing Vue compostables fits into the broader landscape of software testing.&lt;/p&gt;
&lt;h3&gt;Composables&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Composables&lt;/strong&gt; in Vue are reusable composition functions that encapsulate and manage reactive states and logic. They allow a flexible way to organize and reuse code across components, enhancing modularity and maintainability.&lt;/p&gt;
&lt;h3&gt;Testing Pyramid&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Testing Pyramid&lt;/strong&gt; is a conceptual metaphor that illustrates the ideal&lt;br /&gt;
balance of different types of testing. It recommends a large base of unit tests,&lt;br /&gt;
supplemented by a smaller set of integration tests and capped with an even&lt;br /&gt;
smaller set of end-to-end tests. This structure ensures efficient and effective&lt;br /&gt;
test coverage.&lt;/p&gt;
&lt;h3&gt;Unit Testing and How Testing a Composable Would Be a Unit Test&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Unit testing&lt;/strong&gt; refers to the practice of testing individual units of code in isolation. In the context of Vue, testing a composable is a form of unit testing. It involves rigorously verifying the functionality of these isolated, reusable code blocks, ensuring they function correctly without external dependencies.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Testing Composables&lt;/h2&gt;
&lt;p&gt;Composables in Vue are essentially functions, leveraging Vue’s reactivity system. Given this unique nature, we can categorize composables into different types. On one hand, there are &lt;code&gt;Independent Composables&lt;/code&gt;, which can be tested directly due to their standalone nature. On the other hand, we have &lt;code&gt;Dependent Composables&lt;/code&gt;, which only function correctly when integrated within a &lt;a href=&quot;http://component.In&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;component.In&lt;/a&gt; the sections that follow, I’ll delve into these distinct types, provide examples for each, and guide you through effective testing strategies for both.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Independent Composables&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;An Independent Composable exclusively uses Vue’s Reactivity APIs. These composables operate independently of Vue component instances, making them straightforward to test.&lt;/p&gt;
&lt;h4&gt;Example &amp;amp; Testing Strategy&lt;/h4&gt;
&lt;p&gt;Here is an example of an independent composable that calculates the sum of two reactive values:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;useSum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ComputedRef&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; a&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test this composable, you would directly invoke it and assert its returned state:&lt;/p&gt;
&lt;p&gt;Test with Vitest:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;useSum&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;correctly computes the sum of two numbers&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; num1 &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; num2 &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; sum &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useSum&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;num1&lt;span&gt;,&lt;/span&gt; num2&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;sum&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test directly checks the functionality of useSum by passing reactive references and asserting the computed result.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Dependent Composables&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Dependent Composables&lt;/code&gt; are distinguished by their reliance on Vue’s component instance. They often leverage features like lifecycle hooks or context for their operation. These composables are an integral part of a component and necessitate a distinct approach for testing, as opposed to Independent Composables.&lt;/p&gt;
&lt;h4&gt;Example &amp;amp; Usage&lt;/h4&gt;
&lt;p&gt;An exemplary Dependent Composable is &lt;code&gt;useLocalStorage&lt;/code&gt;. This composable facilitates interaction with the browser’s localStorage and harnesses the &lt;code&gt;onMounted&lt;/code&gt; lifecycle hook for initialization:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useLocalStorage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; initialValue&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;ref&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;initialValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;function&lt;/span&gt; &lt;span&gt;loadFromLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; storedValue &lt;span&gt;=&lt;/span&gt; localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;storedValue &lt;span&gt;!==&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      value&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;storedValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;onMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;loadFromLocalStorage&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;,&lt;/span&gt; newValue &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;newValue&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; value &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; useLocalStorage&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This composable can be utilised within a component, for instance, to create a persistent counter:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alexop.dev//../../assets/images/how-to-test-vue-composables/counter-ui.png&quot; alt=&quot;Counter Ui&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// ... script content ...
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Counter: {{ count }}&amp;lt;/h1&amp;gt;
    &amp;lt;button @click=&quot;increment&quot;&amp;gt;Increment&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The primary benefit here is the seamless synchronization of the reactive &lt;code&gt;count&lt;/code&gt; property with localStorage, ensuring persistence across sessions.&lt;/p&gt;
&lt;h3&gt;Testing Strategy&lt;/h3&gt;
&lt;p&gt;To effectively test &lt;code&gt;useLocalStorage&lt;/code&gt;, especially considering the &lt;code&gt;onMounted&lt;/code&gt; lifecycle, we initially face a challenge. Let’s start with a basic test setup:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;useLocalStorage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should load the initialValue&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; value &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;testKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;initValue&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;initValue&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should load from localStorage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;testKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;fromStorage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; value &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;testKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;initialValue&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;fromStorage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the first test will pass, asserting that the composable initialises with the given &lt;code&gt;initialValue&lt;/code&gt;. However, the second test, which expects the composable to load a pre-existing value from localStorage, fails. The challenge arises because the &lt;code&gt;onMounted&lt;/code&gt; lifecycle hook is not triggered during testing. To address this, we need to refactor our composable or our test setup to simulate the component mounting process.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Enhancing Testing with the &lt;code&gt;withSetup&lt;/code&gt; Helper Function&lt;/h3&gt;
&lt;p&gt;To facilitate easier testing of composables that rely on Vue’s lifecycle hooks, we’ve developed a higher-order function named &lt;code&gt;withSetup&lt;/code&gt;. This utility allows us to create a Vue component context programmatically, focusing primarily on the setup lifecycle function where composables are typically used.&lt;/p&gt;
&lt;h4&gt;Introduction to &lt;code&gt;withSetup&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;withSetup&lt;/code&gt; is designed to simulate a Vue component’s setup function, enabling us to test composables in an environment that closely mimics their real-world use. The function accepts a composable and returns both the composable’s result and a Vue app instance. This setup allows for comprehensive testing, including lifecycle and reactivity features.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;withSetup&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;composable&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; App&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; result&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; app &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;composable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;result&lt;span&gt;,&lt;/span&gt; app&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this implementation, &lt;code&gt;withSetup&lt;/code&gt; mounts a minimal Vue app and executes the provided composable function during the setup phase. This approach allows us to capture and return the composable’s output alongside the app instance for further testing.&lt;/p&gt;
&lt;h4&gt;Utilizing &lt;code&gt;withSetup&lt;/code&gt; in Tests&lt;/h4&gt;
&lt;p&gt;With &lt;code&gt;withSetup&lt;/code&gt;, we can enhance our testing strategy for composables like &lt;code&gt;useLocalStorage&lt;/code&gt;, ensuring they behave as expected even when they depend on lifecycle hooks:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should load the value from localStorage if it was set before&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  localStorage&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;testKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;valueFromLocalStorage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;result&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;withSetup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;useLocalStorage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;testKey&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;testValue&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;valueFromLocalStorage&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test demonstrates how &lt;code&gt;withSetup&lt;/code&gt; enables the composable to execute as if it were part of a regular Vue component, ensuring the &lt;code&gt;onMounted&lt;/code&gt; lifecycle hook is triggered as expected. Additionally, the robust TypeScript support enhances the development experience by providing clear type inference and error checking.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Testing Composables with Inject&lt;/h3&gt;
&lt;p&gt;Another common scenario is testing composables that rely on Vue’s dependency injection system using &lt;code&gt;inject&lt;/code&gt;. These composables present unique challenges as they expect certain values to be provided by ancestor components. Let’s explore how to effectively test such composables.&lt;/p&gt;
&lt;h4&gt;Example Composable with Inject&lt;/h4&gt;
&lt;p&gt;Here’s an example of a composable that uses inject:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; MessageKey&lt;span&gt;:&lt;/span&gt; InjectionKey&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;Symbol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; message &lt;span&gt;=&lt;/span&gt; &lt;span&gt;inject&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;MessageKey&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;throw&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Message must be provided&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;getUpperCase&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUpperCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;getReversed&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; message&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reverse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    message&lt;span&gt;,&lt;/span&gt;
    getUpperCase&lt;span&gt;,&lt;/span&gt;
    getReversed&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Creating a Test Helper&lt;/h4&gt;
&lt;p&gt;To test composables that use inject, we need a helper function that creates a testing environment with the necessary providers. Here’s a utility function that makes this possible:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;InstanceType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;V&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;arg&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;infer&lt;/span&gt; &lt;span&gt;X&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;X&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;&lt;span&gt;VM&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; InstanceType&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;unmount&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;InjectionConfig&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  key&lt;span&gt;:&lt;/span&gt; InjectionKey&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;useInjectedSetup&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;TResult&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; TResult&lt;span&gt;,&lt;/span&gt;
  injections&lt;span&gt;:&lt;/span&gt; InjectionConfig&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; TResult &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;unmount&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;let&lt;/span&gt; result&lt;span&gt;!&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; TResult&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; Comp &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;h&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; Provider &lt;span&gt;=&lt;/span&gt; &lt;span&gt;defineComponent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      injections&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; key&lt;span&gt;,&lt;/span&gt; value &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;provide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;key&lt;span&gt;,&lt;/span&gt; value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      &lt;span&gt;return&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;h&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Comp&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; mounted &lt;span&gt;=&lt;/span&gt; &lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Provider&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;...&lt;/span&gt;result&lt;span&gt;,&lt;/span&gt;
    unmount&lt;span&gt;:&lt;/span&gt; mounted&lt;span&gt;.&lt;/span&gt;unmount&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; TResult &lt;span&gt;&amp;amp;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;unmount&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;void&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;&lt;span&gt;mount&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Comp&lt;span&gt;:&lt;/span&gt; &lt;span&gt;V&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; el &lt;span&gt;=&lt;/span&gt; document&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; app &lt;span&gt;=&lt;/span&gt; &lt;span&gt;createApp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;Comp &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;unmount&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;unmount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; comp &lt;span&gt;=&lt;/span&gt; app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;mount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;el&lt;span&gt;)&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;any&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;VM&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;V&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  comp&lt;span&gt;.&lt;/span&gt;unmount &lt;span&gt;=&lt;/span&gt; unmount&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; comp&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Writing Tests&lt;/h4&gt;
&lt;p&gt;With our helper function in place, we can now write comprehensive tests for our inject-dependent composable:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;useMessage&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should handle injected message&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; wrapper &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useInjectedSetup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;useMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;[&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; key&lt;span&gt;:&lt;/span&gt; MessageKey&lt;span&gt;,&lt;/span&gt; value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;hello world&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;wrapper&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;hello world&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUpperCase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;HELLO WORLD&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getReversed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;dlrow olleh&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    wrapper&lt;span&gt;.&lt;/span&gt;&lt;span&gt;unmount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;should throw error when message is not provided&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;useInjectedSetup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;useMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toThrow&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Message must be provided&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;useInjectedSetup&lt;/code&gt; helper creates a testing environment that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Simulates a component hierarchy&lt;/li&gt;
&lt;li&gt;Provides the necessary injection values&lt;/li&gt;
&lt;li&gt;Executes the composable in a proper Vue context&lt;/li&gt;
&lt;li&gt;Returns the composable’s result along with an unmount function&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach allows us to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test composables that depend on inject&lt;/li&gt;
&lt;li&gt;Verify error handling when required injections are missing&lt;/li&gt;
&lt;li&gt;Test the full functionality of methods that use injected values&lt;/li&gt;
&lt;li&gt;Properly clean up after tests by unmounting the test component&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remember to always unmount the test component after each test to prevent memory leaks and ensure test isolation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Independent Composables 🔓&lt;/th&gt;
&lt;th&gt;Dependent Composables 🔗&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;- ✅ can be tested directly&lt;/td&gt;
&lt;td&gt;- 🧪 need a component to test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- 🛠️ uses everything beside of lifecycles and provide / inject&lt;/td&gt;
&lt;td&gt;- 🔄 uses Lifecycles or Provide / Inject&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In our exploration of testing Vue composables, we uncovered two distinct categories: &lt;strong&gt;Independent Composables&lt;/strong&gt; and &lt;strong&gt;Dependent Composables&lt;/strong&gt;. Independent Composables stand alone and can be tested akin to regular functions, showcasing straightforward testing procedures. Meanwhile, Dependent Composables, intricately tied to Vue’s component system and lifecycle hooks, require a more nuanced approach. For these, we learned the effectiveness of utilizing a helper function, such as &lt;code&gt;withSetup&lt;/code&gt;, to simulate a component context, enabling comprehensive testing.&lt;/p&gt;
&lt;p&gt;I hope this blog post has been insightful and useful in enhancing your understanding of testing Vue composables. I’m also keen to learn about your experiences and methods in testing composables within your projects. Your insights and approaches could provide valuable perspectives and contribute to the broader Vue community’s knowledge.&lt;/p&gt;

          </content:encoded><category>vue</category><category>testing</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Robust Error Handling in TypeScript: A Journey from Naive to Rust-Inspired Solutions</title><link>https://alexop.dev/posts/robust-error-handling-in-typescript-a-journey-from-naive-to-rust-inspired-solutions/</link><guid isPermaLink="true">https://alexop.dev/posts/robust-error-handling-in-typescript-a-journey-from-naive-to-rust-inspired-solutions/</guid><description>Learn to write robust, predictable TypeScript code using Rust&apos;s Result pattern. This post demonstrates practical examples and introduces the ts-results library, implementing Rust&apos;s powerful error management approach in TypeScript.</description><pubDate>Sat, 18 Nov 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In software development, robust error handling forms the foundation of reliable software. Even the best-written code encounters unexpected challenges in production. This post explores how to enhance TypeScript error handling with Rust’s Result pattern—creating more resilient and explicit error management.&lt;/p&gt;
&lt;h2&gt;The Pitfalls of Overlooking Error Handling&lt;/h2&gt;
&lt;p&gt;Consider this TypeScript division function:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; a &lt;span&gt;/&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function appears straightforward but fails when &lt;code&gt;b&lt;/code&gt; is zero, returning &lt;code&gt;Infinity&lt;/code&gt;. Such overlooked cases can lead to illogical outcomes:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; a &lt;span&gt;/&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;// ---cut---&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;calculateAverageSpeed&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;distance&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; time&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; averageSpeed &lt;span&gt;=&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;distance&lt;span&gt;,&lt;/span&gt; time&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;averageSpeed&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; km/h&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// will be &quot;Infinity km/h&quot;&lt;/span&gt;
&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Average Speed: &quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;calculateAverageSpeed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Embracing Explicit Error Handling&lt;/h2&gt;
&lt;p&gt;TypeScript provides powerful error management techniques. The Rust-inspired approach enhances code safety and predictability.&lt;/p&gt;
&lt;h3&gt;Result Type Pattern: A Rust-Inspired Approach in TypeScript&lt;/h3&gt;
&lt;p&gt;Rust excels at explicit error handling through the &lt;code&gt;Result&lt;/code&gt; type. Here’s the pattern in TypeScript:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Success&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; kind&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; value&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Failure&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; kind&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;failure&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;E&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Result&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;E&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Success&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; Failure&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;function&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; Result&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;b &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; kind&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;failure&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Cannot divide by zero&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; kind&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; value&lt;span&gt;:&lt;/span&gt; a &lt;span&gt;/&lt;/span&gt; b &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handling the Result in TypeScript&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;handleDivision&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;result&lt;span&gt;:&lt;/span&gt; Result&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;result&lt;span&gt;.&lt;/span&gt;kind &lt;span&gt;===&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Division result:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Division error:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;handleDivision&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Native Rust Implementation for Comparison&lt;/h3&gt;
&lt;p&gt;In Rust, the &lt;code&gt;Result&lt;/code&gt; type is an enum with variants for success and error:&lt;/p&gt;
&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;
&lt;span&gt;fn&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;i32&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;i32&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&amp;gt;&lt;/span&gt; &lt;span&gt;std&lt;span&gt;::&lt;/span&gt;result&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;i32&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;String&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; b &lt;span&gt;==&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;std&lt;span&gt;::&lt;/span&gt;result&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Err&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Cannot divide by zero&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;to_string&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;std&lt;span&gt;::&lt;/span&gt;result&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Ok&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a &lt;span&gt;/&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;fn&lt;/span&gt; &lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;match&lt;/span&gt; &lt;span&gt;divide&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;std&lt;span&gt;::&lt;/span&gt;result&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Ok&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;println!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Division result: {}&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;std&lt;span&gt;::&lt;/span&gt;result&lt;span&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Err&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;println!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Error: {}&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why the Rust Way?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Explicit Handling&lt;/strong&gt;: Forces handling of both outcomes, enhancing code robustness.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clarity&lt;/strong&gt;: Makes code intentions clear.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety&lt;/strong&gt;: Reduces uncaught exceptions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functional Approach&lt;/strong&gt;: Aligns with TypeScript’s functional programming style.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Leveraging ts-results for Rust-Like Error Handling&lt;/h2&gt;
&lt;p&gt;For TypeScript developers, the &lt;a href=&quot;https://github.com/vultix/ts-results&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ts-results&lt;/a&gt; library is a great tool to apply Rust’s error handling pattern, simplifying the implementation of Rust’s &lt;code&gt;Result&lt;/code&gt; type in TypeScript.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Implementing Rust’s &lt;code&gt;Result&lt;/code&gt; pattern in TypeScript, with tools like ts-results, enhances error handling strategies. This approach creates robust applications that handle errors while maintaining code integrity and usability.&lt;/p&gt;
&lt;p&gt;Let’s embrace these practices to craft software that withstands the tests of time and uncertainty.&lt;/p&gt;

          </content:encoded><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Mastering Vue 3 Composables: A Comprehensive Style Guide</title><link>https://alexop.dev/posts/mastering-vue-3-composables-a-comprehensive-style-guide/</link><guid isPermaLink="true">https://alexop.dev/posts/mastering-vue-3-composables-a-comprehensive-style-guide/</guid><description>Did you ever struggle how to write better composables in Vue? In this Blog post I try to give some tips how to do that</description><pubDate>Sat, 16 Sep 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The release of Vue 3 brought a transformational change, moving from the Options API to the Composition API. At the heart of this transition lies the concept of “composables” — modular functions that leverage Vue’s reactive features. This change enhanced the framework’s flexibility and code reusability. The inconsistent implementation of composables across projects often leads to convoluted and hard-to-maintain codebases.&lt;/p&gt;
&lt;p&gt;This style guide harmonizes coding practices around composables, focusing on producing clean, maintainable, and testable code. While composables represent a new pattern, they remain functions at their core. The guide bases its recommendations on time-tested principles of good software design.&lt;/p&gt;
&lt;p&gt;This guide serves as a comprehensive resource for both newcomers to Vue 3 and experienced developers aiming to standardize their team’s coding style.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;File Naming&lt;/h2&gt;
&lt;h3&gt;Rule 1.1: Prefix with &lt;code&gt;use&lt;/code&gt; and Follow PascalCase&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good&lt;/span&gt;
useCounter&lt;span&gt;.&lt;/span&gt;ts&lt;span&gt;;&lt;/span&gt;
useApiRequest&lt;span&gt;.&lt;/span&gt;ts&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;
counter&lt;span&gt;.&lt;/span&gt;ts&lt;span&gt;;&lt;/span&gt;
APIrequest&lt;span&gt;.&lt;/span&gt;ts&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Composable Naming&lt;/h2&gt;
&lt;h3&gt;Rule 2.1: Use Descriptive Names&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Folder Structure&lt;/h2&gt;
&lt;h3&gt;Rule 3.1: Place in composables Directory&lt;/h3&gt;
&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;src/
└── composables/
    ├── useCounter.ts
    └── useUserData.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Argument Passing&lt;/h2&gt;
&lt;h3&gt;Rule 4.1: Use Object Arguments for Four or More Parameters&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good: For Multiple Parameters&lt;/span&gt;
&lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt; id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; fetchOnMount&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; token&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;abc&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; locale&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;en&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Also Good: For Fewer Parameters&lt;/span&gt;
&lt;span&gt;useCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;session&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;
&lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;abc&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;en&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Error Handling&lt;/h2&gt;
&lt;h3&gt;Rule 5.1: Expose Error State&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Do something&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; err&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; error &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;
&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Do something&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;An error occurred:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Avoid Mixing UI and Business Logic&lt;/h2&gt;
&lt;h3&gt;Rule 6.2: Decouple UI from Business Logic in Composables&lt;/h3&gt;
&lt;p&gt;Composables should focus on managing state and business logic, avoiding UI-specific behavior like toasts or alerts. Keeping UI logic separate from business logic will ensure that your composable is reusable and testable.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fetchUser&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; axios&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userId&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; e&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;,&lt;/span&gt; fetchUser &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// In component&lt;/span&gt;
&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;,&lt;/span&gt; fetchUser &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;error&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;newValue&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;newValue&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;showToast&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;An error occurred.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;  &lt;span&gt;// UI logic in component&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; fetchUser &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fetchUser&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; axios&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userId&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;showToast&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;An error occurred.&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// UI logic inside composable&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; fetchUser &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Anatomy of a Composable&lt;/h2&gt;
&lt;h3&gt;Rule 7.2: Structure Your Composables Well&lt;/h3&gt;
&lt;p&gt;A well-structured composable improves understanding, usage, and maintenance. It consists of these components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Primary State&lt;/strong&gt;: The main reactive state that the composable manages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Metadata&lt;/strong&gt;: States that hold values like API request status or errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Methods&lt;/strong&gt;: Functions that update the Primary State and State Metadata. These functions can call APIs, manage cookies, or integrate with other composables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Following this structure makes your composables more intuitive and improves code quality across your project.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good Example: Anatomy of a Composable&lt;/span&gt;
&lt;span&gt;// Well-structured according to Anatomy of a Composable&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Primary State&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Supportive State&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; status &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;idle&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Methods&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fetchUser&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    status&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;loading&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; axios&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userId&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
      status&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;success&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      status&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;error&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; e&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; status&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;,&lt;/span&gt; fetchUser &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Bad Example: Anatomy of a Composable&lt;/span&gt;
&lt;span&gt;// Lacks well-defined structure and mixes concerns&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserDataAndMore&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Muddled State: Not clear what&apos;s Primary or Supportive&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; message &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Initializing...&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Methods: Multiple responsibilities and side-effects&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fetchUserAndIncrement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    message&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Fetching user and incrementing count...&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; axios&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userId&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;e&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      message&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failed to fetch user.&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Incrementing count, unrelated to user fetching&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// More Methods: Different kind of task entirely&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;setMessage&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; newMessage &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    message&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; newMessage&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; message&lt;span&gt;,&lt;/span&gt; fetchUserAndIncrement&lt;span&gt;,&lt;/span&gt; setMessage &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Functional Core, Imperative Shell&lt;/h2&gt;
&lt;h3&gt;Rule 8.2: (optional) use functional core imperative shell pattern&lt;/h3&gt;
&lt;p&gt;Structure your composable such that the core logic is functional and devoid of side effects, while the imperative shell handles the Vue-specific or side-effecting operations. Following this principle makes your composable easier to test, debug, and maintain.&lt;/p&gt;
&lt;h4&gt;Example: Functional Core, Imperative Shell&lt;/h4&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// good&lt;/span&gt;
&lt;span&gt;// Functional Core&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; &lt;span&gt;calculate&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// Imperative Shell&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCalculatorGood&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    result&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;calculate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;// Using the functional core&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Other side-effecting code can go here, e.g., logging, API calls&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; result&lt;span&gt;,&lt;/span&gt; add &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// wrong&lt;/span&gt;
&lt;span&gt;// Mixing core logic and side effects&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCalculatorBad&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; result &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;add&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Side-effect within core logic&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Adding:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    result&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; result&lt;span&gt;,&lt;/span&gt; add &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Single Responsibility Principle&lt;/h2&gt;
&lt;h3&gt;Rule 9.1: Use SRP for composables&lt;/h3&gt;
&lt;p&gt;A composable should follow the Single Responsibility Principle: one reason to change. This means each composable handles one specific task. Following this principle creates composables that are clear, maintainable, and testable.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Good&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;decrement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;--&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; increment&lt;span&gt;,&lt;/span&gt; decrement &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// Bad&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useUserAndCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userId&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; user &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;fetchUser&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; axios&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/api/users/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;userId&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      user&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;data&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;An error occurred while fetching user data:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;decrement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;--&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; user&lt;span&gt;,&lt;/span&gt; fetchUser&lt;span&gt;,&lt;/span&gt; count&lt;span&gt;,&lt;/span&gt; increment&lt;span&gt;,&lt;/span&gt; decrement &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;File Structure of a Composable&lt;/h2&gt;
&lt;h3&gt;Rule 10.1: Rule: Consistent Ordering of Composition API Features&lt;/h3&gt;
&lt;p&gt;Your team should establish and follow a consistent order for Composition API features throughout the codebase.&lt;/p&gt;
&lt;p&gt;Here’s a recommended order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Initializing: Setup logic&lt;/li&gt;
&lt;li&gt;Refs: Reactive references&lt;/li&gt;
&lt;li&gt;Computed: Computed properties&lt;/li&gt;
&lt;li&gt;Methods: Functions for state manipulation&lt;/li&gt;
&lt;li&gt;Lifecycle Hooks: onMounted, onUnmounted, etc.&lt;/li&gt;
&lt;li&gt;Watch&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pick an order that works for your team and apply it consistently across all composables.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;// Example in useCounter.ts&lt;/span&gt;
&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;useCounter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Initializing&lt;/span&gt;
  &lt;span&gt;// Initialize variables, make API calls, or any setup logic&lt;/span&gt;
  &lt;span&gt;// For example, using a router&lt;/span&gt;
  &lt;span&gt;// ...&lt;/span&gt;

  &lt;span&gt;// Refs&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; count &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Computed&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; isEven &lt;span&gt;=&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; count&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;%&lt;/span&gt; &lt;span&gt;2&lt;/span&gt; &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Methods&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;increment&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;decrement&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;--&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;// Lifecycle&lt;/span&gt;
  &lt;span&gt;onMounted&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Counter is mounted&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    count&lt;span&gt;,&lt;/span&gt;
    isEven&lt;span&gt;,&lt;/span&gt;
    increment&lt;span&gt;,&lt;/span&gt;
    decrement&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These guidelines provide best practices for writing clean, testable, and efficient Vue 3 composables. They combine established software design principles with practical experience, though they aren’t exhaustive.&lt;/p&gt;
&lt;p&gt;Programming blends art and science. As you develop with Vue, you’ll discover patterns that match your needs. Focus on maintaining a consistent, scalable, and maintainable codebase. Adapt these guidelines to fit your project’s requirements.&lt;/p&gt;
&lt;p&gt;Share your ideas, improvements, and real-world examples in the comments. Your input helps evolve these guidelines into a better resource for the Vue community.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Best Practices for Error Handling in Vue Composables</title><link>https://alexop.dev/posts/best-practices-for-error-handling-in-vue-composables/</link><guid isPermaLink="true">https://alexop.dev/posts/best-practices-for-error-handling-in-vue-composables/</guid><description>Error handling can be complex, but it&apos;s crucial for composables to manage errors consistently. This post explores an effective method for implementing error handling in composables.</description><pubDate>Thu, 18 May 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Navigating the complex world of composables presented a significant challenge. Understanding this powerful paradigm required effort when determining the division of responsibilities between a composable and its consuming component. The strategy for error handling emerged as a critical aspect that demanded careful consideration.&lt;/p&gt;
&lt;p&gt;In this blog post, we aim to clear the fog surrounding this intricate topic. We’ll delve into the concept of &lt;strong&gt;Separation of Concerns&lt;/strong&gt;, a fundamental principle in software engineering, and how it provides guidance for proficient error handling within the scope of composables. Let’s delve into this critical aspect of Vue composables and demystify it together.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Separation of Concerns, even if not perfectly possible, is yet the only available technique for effective ordering of one’s thoughts, that I know of.” – Edsger W. Dijkstra&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The &lt;code&gt;usePokemon&lt;/code&gt; Composable&lt;/h2&gt;
&lt;p&gt;Our journey begins with the creation of a custom composable, aptly named &lt;code&gt;usePokemon&lt;/code&gt;. This particular composable acts as a liaison between our application and the Pokémon API. It boasts three core methods — &lt;code&gt;load&lt;/code&gt;, &lt;code&gt;loadSpecies&lt;/code&gt;, and &lt;code&gt;loadEvolution&lt;/code&gt; — each dedicated to retrieving distinct types of data.&lt;/p&gt;
&lt;p&gt;A straightforward approach would allow these methods to propagate errors directly. Instead, we take a more robust approach. Each method catches potential exceptions internally and exposes them via a dedicated error object. This strategy enables more sophisticated and context-sensitive error handling within the components that consume this composable.&lt;/p&gt;
&lt;p&gt;Without further ado, let’s delve into the TypeScript code for our &lt;code&gt;usePokemon&lt;/code&gt; composable:&lt;/p&gt;
&lt;h2&gt;Dissecting the &lt;code&gt;usePokemon&lt;/code&gt; Composable&lt;/h2&gt;
&lt;p&gt;Let’s break down our &lt;code&gt;usePokemon&lt;/code&gt; composable step by step, to fully grasp its structure and functionality.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;ErrorRecord&lt;/code&gt; Interface and &lt;code&gt;errorsFactory&lt;/code&gt; Function&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ErrorRecord&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  load&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  loadSpecies&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  loadEvolution&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; errorsFactory &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ErrorRecord &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  load&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  loadSpecies&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  loadEvolution&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we define a &lt;code&gt;ErrorRecord&lt;/code&gt; interface that encapsulates potential errors from our three core methods. This interface ensures that each method can store a &lt;code&gt;Error&lt;/code&gt; object or &lt;code&gt;null&lt;/code&gt; if no error has occurred.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;errorsFactory&lt;/code&gt; function creates these ErrorRecord objects. It returns an ErrorRecord with all values set to null, indicating no errors have occurred yet.&lt;/p&gt;
&lt;h3&gt;Initialising Refs&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; species&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; evolution&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;ErrorRecord&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;errorsFactory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we create the &lt;code&gt;Ref&lt;/code&gt; objects that store our data (&lt;code&gt;pokemon&lt;/code&gt;, &lt;code&gt;species&lt;/code&gt;, and &lt;code&gt;evolution&lt;/code&gt;) and our error information (error). We use the errorsFactory function to set up the initial error-free state.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;load&lt;/code&gt;, &lt;code&gt;loadSpecies&lt;/code&gt;, and &lt;code&gt;loadEvolution&lt;/code&gt; Methods&lt;/h3&gt;
&lt;p&gt;Each of these methods performs a similar set of operations: it fetches data from a specific endpoint of the Pokémon API, assigns the returned data to the appropriate &lt;code&gt;Ref&lt;/code&gt; object, and handles any potential errors.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;const&lt;/span&gt; &lt;span&gt;load&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;https://pokeapi.co/api/v2/pokemon/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    pokemon&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;load &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;load &lt;span&gt;=&lt;/span&gt; err &lt;span&gt;as&lt;/span&gt; Error&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, in the &lt;code&gt;load&lt;/code&gt; method, we fetch data from the &lt;code&gt;pokemon&lt;/code&gt; endpoint using the provided ID. A successful fetch updates &lt;code&gt;pokemon.value&lt;/code&gt; with the returned data and clears any previous error by setting &lt;code&gt;error.value.load&lt;/code&gt; to null. When an error occurs during the fetch, we catch it and store it in error.value.load.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;loadSpecies&lt;/code&gt; and &lt;code&gt;loadEvolution&lt;/code&gt; methods operate similarly, but they fetch from different endpoints and store their data and errors in different Ref objects.&lt;/p&gt;
&lt;h3&gt;The Return Object&lt;/h3&gt;
&lt;p&gt;The composable returns an object providing access to the Pokémon, species, and evolution data, as well as the three load methods. It exposes the error object as a computed property. This computed property updates whenever any of the methods sets an error, allowing consumers of the composable to react to errors.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  pokemon&lt;span&gt;,&lt;/span&gt;
  species&lt;span&gt;,&lt;/span&gt;
  evolution&lt;span&gt;,&lt;/span&gt;
  load&lt;span&gt;,&lt;/span&gt;
  loadSpecies&lt;span&gt;,&lt;/span&gt;
  loadEvolution&lt;span&gt;,&lt;/span&gt;
  error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Full Code&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span&gt;interface&lt;/span&gt; &lt;span&gt;ErrorRecord&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  load&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  loadSpecies&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  loadEvolution&lt;span&gt;:&lt;/span&gt; Error &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; errorsFactory &lt;span&gt;=&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; ErrorRecord &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
  load&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  loadSpecies&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  loadEvolution&lt;span&gt;:&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;default&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;usePokemon&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; pokemon&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; species&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; evolution&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;any&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;const&lt;/span&gt; error&lt;span&gt;:&lt;/span&gt; Ref&lt;span&gt;&amp;lt;&lt;/span&gt;ErrorRecord&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;ref&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;errorsFactory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;load&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;https://pokeapi.co/api/v2/pokemon/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      pokemon&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;load &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;load &lt;span&gt;=&lt;/span&gt; err &lt;span&gt;as&lt;/span&gt; Error&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;loadSpecies&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;https://pokeapi.co/api/v2/pokemon-species/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      species&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;loadSpecies &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;loadSpecies &lt;span&gt;=&lt;/span&gt; err &lt;span&gt;as&lt;/span&gt; Error&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;const&lt;/span&gt; &lt;span&gt;loadEvolution&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;const&lt;/span&gt; response &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;https://pokeapi.co/api/v2/evolution-chain/&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;id&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
      &lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      evolution&lt;span&gt;.&lt;/span&gt;value &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; response&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;loadEvolution &lt;span&gt;=&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;.&lt;/span&gt;loadEvolution &lt;span&gt;=&lt;/span&gt; err &lt;span&gt;as&lt;/span&gt; Error&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;return&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    pokemon&lt;span&gt;,&lt;/span&gt;
    species&lt;span&gt;,&lt;/span&gt;
    evolution&lt;span&gt;,&lt;/span&gt;
    load&lt;span&gt;,&lt;/span&gt;
    loadSpecies&lt;span&gt;,&lt;/span&gt;
    loadEvolution&lt;span&gt;,&lt;/span&gt;
    error&lt;span&gt;:&lt;/span&gt; &lt;span&gt;computed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;value&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Pokémon Component&lt;/h2&gt;
&lt;p&gt;Next, let’s look at a Pokémon component that uses our &lt;code&gt;usePokemon&lt;/code&gt; composable:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;div v-if=&quot;pokemon&quot;&amp;gt;
      &amp;lt;h2&amp;gt;Pokemon Data:&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Name: {{ pokemon.name }}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div v-if=&quot;species&quot;&amp;gt;
      &amp;lt;h2&amp;gt;Species Data:&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Name: {{ species.base_happiness }}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div v-if=&quot;evolution&quot;&amp;gt;
      &amp;lt;h2&amp;gt;Evolution Data:&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Name: {{ evolution.evolutionName }}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div v-if=&quot;loadError&quot;&amp;gt;
      An error occurred while loading the pokemon: {{ loadError.message }}
    &amp;lt;/div&amp;gt;

    &amp;lt;div v-if=&quot;loadSpeciesError&quot;&amp;gt;
      An error occurred while loading the species:
      {{ loadSpeciesError.message }}
    &amp;lt;/div&amp;gt;

    &amp;lt;div v-if=&quot;loadEvolutionError&quot;&amp;gt;
      An error occurred while loading the evolution:
      {{ loadEvolutionError.message }}
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script lang=&quot;ts&quot; setup&amp;gt;
const { load, loadSpecies, loadEvolution, pokemon, species, evolution, error } =
  usePokemon();

const loadError = computed(() =&amp;gt; error.value.load);
const loadSpeciesError = computed(() =&amp;gt; error.value.loadSpecies);
const loadEvolutionError = computed(() =&amp;gt; error.value.loadEvolution);

const pokemonId = ref(1);
const speciesId = ref(1);
const evolutionId = ref(1);

load(pokemonId.value);
loadSpecies(speciesId.value);
loadEvolution(evolutionId.value);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above code uses the usePokemon composable to fetch and display Pokémon, species, and evolution data. The component shows errors to users when fetch operations fail.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Wrapping the &lt;code&gt;fetch&lt;/code&gt; operations in a try-catch block in the &lt;code&gt;composable&lt;/code&gt; and surfacing errors through a reactive error object keeps the component clean and focused on its core responsibilities - presenting data and handling user interaction.&lt;/p&gt;
&lt;p&gt;This approach promotes &lt;code&gt;separation of concerns&lt;/code&gt; - the composable manages error handling logic independently, while the component responds to the provided state. The component remains focused on presenting the data effectively.&lt;/p&gt;
&lt;p&gt;The error object’s reactivity integrates seamlessly with Vue’s template system. The system tracks changes automatically, updating relevant template sections when the error state changes.&lt;/p&gt;
&lt;p&gt;This pattern offers a robust approach to error handling in composables. By centralizing error-handling logic in the composable, you create components that maintain clarity, readability, and maintainability.&lt;/p&gt;

          </content:encoded><category>vue</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>How to Improve Accessibility with Testing Library and jest-axe for Your Vue Application</title><link>https://alexop.dev/posts/how-to-improve-accessibility-with-testing-library-and-jest-axe-for-your-vue-application/</link><guid isPermaLink="true">https://alexop.dev/posts/how-to-improve-accessibility-with-testing-library-and-jest-axe-for-your-vue-application/</guid><description>Use Jest axe to have automatic tests for your vue application</description><pubDate>Wed, 12 Apr 2023 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;p&gt;Accessibility is a critical aspect of web development that ensures your application serves everyone, including people with disabilities. Making your Vue apps accessible fulfills legal requirements and enhances the experience for all users. In this post, we’ll explore how to improve accessibility in Vue applications using Testing Library and jest-axe.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before we dive in, make sure you have the following installed in your Vue project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@testing-library/vue&lt;/li&gt;
&lt;li&gt;jest-axe&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can add them with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span&gt;npm&lt;/span&gt; &lt;span&gt;install&lt;/span&gt; --save-dev @testing-library/vue jest-axe
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Example Component&lt;/h2&gt;
&lt;p&gt;Let’s look at a simple Vue component that displays an image and some text:&lt;/p&gt;
&lt;pre class=&quot;language-vue&quot;&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;{{ title }}&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;{{ description }}&amp;lt;/p&amp;gt;
    
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps({
  title: String,
  description: String,
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Developers should include alt text for images to ensure accessibility, but how can we verify this in production?&lt;/p&gt;
&lt;h2&gt;Testing with jest-axe&lt;/h2&gt;
&lt;p&gt;This is where jest-axe comes in. Axe is a leading accessibility testing toolkit used by major tech companies.&lt;/p&gt;
&lt;p&gt;To test our component, we can create a test file like this:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;expect&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extend&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;toHaveNoViolations&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;describe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;MyComponent&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;it&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;has no accessibility violations&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;const&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; container &lt;span&gt;}&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;MyComponent&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;props&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Sample Title&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;&quot;Sample Description&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;const&lt;/span&gt; results &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;axe&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;container&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;results&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toHaveNoViolations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we run this test, we’ll get an error like:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;FAIL  src/components/MyComponent.spec.ts &lt;span&gt;&amp;gt;&lt;/span&gt; MyComponent &lt;span&gt;&amp;gt;&lt;/span&gt; has no accessibility violations

Error: expect&lt;span&gt;(&lt;/span&gt;received&lt;span&gt;)&lt;/span&gt;.toHaveNoViolations&lt;span&gt;(&lt;/span&gt;expected&lt;span&gt;)&lt;/span&gt;

Expected the HTML found at &lt;span&gt;&lt;span&gt;$(&lt;/span&gt;&apos;img&apos;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt; to have no violations:

&lt;span&gt;&amp;lt;&lt;/span&gt;img &lt;span&gt;src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;sample_image.jpg&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;

Received:

&lt;span&gt;&quot;Images must have alternate text (image-alt)&quot;&lt;/span&gt;

Fix any of the following:
  Element does not have an alt attribute
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that &lt;span&gt;do&lt;/span&gt; not exist or references elements that are empty
  Element has no title attribute
  Element&apos;s default semantics were not overridden with &lt;span&gt;role&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;none&quot;&lt;/span&gt; or &lt;span&gt;role&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;presentation&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells us we need to add an alt attribute to our image. We can fix the component and re-run the test until it passes.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By integrating accessibility testing with tools like Testing Library and jest-axe, we catch accessibility issues during development. This ensures our Vue applications remain usable for everyone. Making accessibility testing part of our CI pipeline maintains high standards and delivers a better experience for all users.&lt;/p&gt;

          </content:encoded><category>vue</category><category>accessibility</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item><item><title>Mastering TypeScript: Looping with Types</title><link>https://alexop.dev/posts/mastering-typescript-looping-with-types/</link><guid isPermaLink="true">https://alexop.dev/posts/mastering-typescript-looping-with-types/</guid><description>Did you know that TypeScript is Turing complete? In this post, I will show you how you can loop with TypeScript.</description><pubDate>Fri, 23 Sep 2022 15:22:00 GMT</pubDate><content:encoded>
            
            &lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Loops play a pivotal role in programming, enabling code execution without redundancy. JavaScript developers might be familiar with &lt;code&gt;foreach&lt;/code&gt; or &lt;code&gt;do...while&lt;/code&gt; loops, but TypeScript offers unique looping capabilities at the type level. This blog post delves into three advanced TypeScript looping techniques, demonstrating their importance and utility.&lt;/p&gt;
&lt;h2&gt;Mapped Types&lt;/h2&gt;
&lt;p&gt;Mapped Types in TypeScript allow the transformation of object properties. Consider an object requiring immutable properties:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;User&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  id&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  email&lt;span&gt;:&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  age&lt;span&gt;:&lt;/span&gt; &lt;span&gt;number&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We traditionally hardcode it to create an immutable version of this type. To maintain adaptability with the original type, Mapped Types come into play. They use generics to map each property, offering flexibility to transform property characteristics. For instance:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ReadonlyUser&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;readonly&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This technique is extensible. For example, adding nullability:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Nullable&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or filtering out certain types:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;ExcludeStrings&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt; &lt;span&gt;as&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;&lt;span&gt;string&lt;/span&gt;&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;never&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Understanding the core concept of Mapped Types opens doors to creating diverse, reusable types.&lt;/p&gt;
&lt;h2&gt;Recursion&lt;/h2&gt;
&lt;p&gt;Recursion is fundamental in TypeScript’s type-level programming since state mutation is not an option. Consider applying immutability to all nested properties:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;DeepReadonly&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;readonly&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;keyof&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;object&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; DeepReadonly&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;T&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;P&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, TypeScript’s compiler recursively ensures every property is immutable, demonstrating the language’s depth in handling complex types.&lt;/p&gt;
&lt;h2&gt;Union Types&lt;/h2&gt;
&lt;p&gt;Union Types represent a set of distinct types, such as:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;const&lt;/span&gt; hi &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Hello&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; msg &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;hi&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;, world&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating structured types from unions involves looping over each union member. For instance, constructing a type where each status is an object:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span&gt;type&lt;/span&gt; &lt;span&gt;Status&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&quot;Failure&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;&quot;Success&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;type&lt;/span&gt; &lt;span&gt;StatusObject&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; Status &lt;span&gt;extends&lt;/span&gt; &lt;span&gt;&lt;span&gt;infer&lt;/span&gt;&lt;/span&gt; &lt;span&gt;S&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; status&lt;span&gt;:&lt;/span&gt; &lt;span&gt;S&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;never&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;TypeScript’s advanced type system transcends static type checking, providing sophisticated tools for type transformation and manipulation. Mapped Types, Recursion, and Union Types are not mere features but powerful instruments that enhance code maintainability, type safety, and expressiveness. These techniques underscore TypeScript’s capability to handle complex programming scenarios, affirming its status as more than a JavaScript superset but a language that enriches our development experience.&lt;/p&gt;

          </content:encoded><category>typescript</category><author>alex.opalic.dev@gmail.com (Alexander Opalic)</author></item></channel></rss>