From 5ad0c2c563b005b2f004006987c1b6884efe92ab Mon Sep 17 00:00:00 2001
From: epifaniofrancisco <epifaniofrancisco03@gmail.com>
Date: Tue, 10 Mar 2026 12:53:04 +0100
Subject: [PATCH 1/6] feat(blog): add BackButton component to blog header

---
 app/components/BackButton.vue | 28 ++++++++++++++++++++++++++++
 app/pages/blog/index.vue      |  9 ++++++---
 2 files changed, 34 insertions(+), 3 deletions(-)
 create mode 100644 app/components/BackButton.vue

diff --git a/app/components/BackButton.vue b/app/components/BackButton.vue
new file mode 100644
index 0000000000..8b0f0c46d8
--- /dev/null
+++ b/app/components/BackButton.vue
@@ -0,0 +1,28 @@
+<script setup lang="ts">
+const router = useRouter()
+const canGoBack = useCanGoBack()
+
+withDefaults(
+  defineProps<{
+    class?: string
+  }>(),
+  {
+    class: '',
+  },
+)
+</script>
+
+<template>
+  <button
+    v-if="canGoBack"
+    type="button"
+    :class="[
+      'inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0',
+      $props.class,
+    ]"
+    @click="router.back()"
+  >
+    <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
+    <span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
+  </button>
+</template>
diff --git a/app/pages/blog/index.vue b/app/pages/blog/index.vue
index 07e40d2d9d..984eb60c73 100644
--- a/app/pages/blog/index.vue
+++ b/app/pages/blog/index.vue
@@ -21,9 +21,12 @@ useSeoMeta({
   <main class="container w-full flex-1 py-12 sm:py-16 overflow-x-hidden">
     <article class="max-w-2xl mx-auto">
       <header class="mb-12">
-        <h1 class="font-mono text-3xl sm:text-4xl font-medium">
-          {{ $t('blog.heading') }}
-        </h1>
+        <div class="flex items-baseline justify-between gap-4 mb-4">
+          <h1 class="font-mono text-3xl sm:text-4xl font-medium">
+            {{ $t('blog.heading') }}
+          </h1>
+          <BackButton />
+        </div>
         <p class="text-fg-muted text-lg">
           {{ $t('tagline') }}
         </p>

From 63eb4e94c65e4078141fcac9daf8ca1e9f2d3671 Mon Sep 17 00:00:00 2001
From: epifaniofrancisco <epifaniofrancisco03@gmail.com>
Date: Tue, 10 Mar 2026 13:02:00 +0100
Subject: [PATCH 2/6] refactor: replace back button implementation with
 BackButton component across multiple pages

---
 app/pages/about.vue         | 13 +------------
 app/pages/accessibility.vue | 13 +------------
 app/pages/compare.vue       | 12 +-----------
 app/pages/pds.vue           | 13 +------------
 app/pages/privacy.vue       | 12 +-----------
 app/pages/recharging.vue    | 13 +------------
 app/pages/settings.vue      | 11 +----------
 7 files changed, 7 insertions(+), 80 deletions(-)

diff --git a/app/pages/about.vue b/app/pages/about.vue
index e6dde0c851..20e7559b94 100644
--- a/app/pages/about.vue
+++ b/app/pages/about.vue
@@ -3,9 +3,6 @@ import type { Role } from '#server/api/contributors.get'
 import { SPONSORS } from '~/assets/logos/sponsors'
 import { OSS_PARTNERS } from '~/assets/logos/oss-partners'
 
-const router = useRouter()
-const canGoBack = useCanGoBack()
-
 useSeoMeta({
   title: () => `${$t('about.title')} - npmx`,
   ogTitle: () => `${$t('about.title')} - npmx`,
@@ -57,15 +54,7 @@ const roleLabels = computed(
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('about.heading') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="hidden sm:inline">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <p class="text-fg-muted text-lg">
           {{ $t('tagline') }}
diff --git a/app/pages/accessibility.vue b/app/pages/accessibility.vue
index 9e0e6c9dc3..027fdef41a 100644
--- a/app/pages/accessibility.vue
+++ b/app/pages/accessibility.vue
@@ -12,9 +12,6 @@ defineOgImageComponent('Default', {
   title: () => $t('a11y.title'),
   description: () => $t('a11y.welcome', { app: 'npmx' }),
 })
-
-const router = useRouter()
-const canGoBack = useCanGoBack()
 </script>
 
 <template>
@@ -25,15 +22,7 @@ const canGoBack = useCanGoBack()
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('a11y.title') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
       </header>
 
diff --git a/app/pages/compare.vue b/app/pages/compare.vue
index e38a34657d..56be77de6e 100644
--- a/app/pages/compare.vue
+++ b/app/pages/compare.vue
@@ -8,8 +8,6 @@ definePageMeta({
 })
 
 const { locale } = useI18n()
-const router = useRouter()
-const canGoBack = useCanGoBack()
 const { copied, copy } = useClipboard({ copiedDuring: 2000 })
 
 // Sync packages with URL query param (stable ref - doesn't change on other query changes)
@@ -171,15 +169,7 @@ useSeoMeta({
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('compare.packages.title') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="hidden sm:inline">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <p class="text-fg-muted text-lg">
           {{ $t('compare.packages.tagline') }}
diff --git a/app/pages/pds.vue b/app/pages/pds.vue
index ac14a68ceb..c47259a9f4 100644
--- a/app/pages/pds.vue
+++ b/app/pages/pds.vue
@@ -1,9 +1,6 @@
 <script setup lang="ts">
 import type { AtprotoProfile } from '#shared/types/atproto'
 
-const router = useRouter()
-const canGoBack = useCanGoBack()
-
 useSeoMeta({
   title: () => `${$t('pds.title')} - npmx`,
   ogTitle: () => `${$t('pds.title')} - npmx`,
@@ -51,15 +48,7 @@ const totalAccounts = computed(() => pdsUsers.value.length)
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('pds.title') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="hidden sm:inline">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <p class="text-fg-muted text-lg">
           {{ $t('pds.meta_description') }}
diff --git a/app/pages/privacy.vue b/app/pages/privacy.vue
index cd0d2d8bf5..e2492e313d 100644
--- a/app/pages/privacy.vue
+++ b/app/pages/privacy.vue
@@ -13,8 +13,6 @@ defineOgImageComponent('Default', {
   description: () => $t('privacy_policy.welcome', { app: 'npmx' }),
 })
 
-const router = useRouter()
-const canGoBack = useCanGoBack()
 const buildInfo = useAppConfig().buildInfo
 const { locale } = useI18n()
 </script>
@@ -27,15 +25,7 @@ const { locale } = useI18n()
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('privacy_policy.title') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <i18n-t
           keypath="privacy_policy.last_updated"
diff --git a/app/pages/recharging.vue b/app/pages/recharging.vue
index 7082d18e75..b704802db5 100644
--- a/app/pages/recharging.vue
+++ b/app/pages/recharging.vue
@@ -17,9 +17,6 @@ defineOgImageComponent('Default', {
   description: () => $t('vacations.meta_description'),
 })
 
-const router = useRouter()
-const canGoBack = useCanGoBack()
-
 const { data: stats } = useFetch('/api/repo-stats')
 
 /**
@@ -74,15 +71,7 @@ const icons = [
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('vacations.heading') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <i18n-t
           keypath="vacations.subtitle"
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
index 08523f7703..49824c9523 100644
--- a/app/pages/settings.vue
+++ b/app/pages/settings.vue
@@ -1,6 +1,5 @@
 <script setup lang="ts">
 const router = useRouter()
-const canGoBack = useCanGoBack()
 const { settings } = useSettings()
 const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
 const colorMode = useColorMode()
@@ -51,15 +50,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
           <h1 class="font-mono text-3xl sm:text-4xl font-medium">
             {{ $t('settings.title') }}
           </h1>
-          <button
-            type="button"
-            class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
-            @click="router.back()"
-            v-if="canGoBack"
-          >
-            <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
-            <span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
-          </button>
+          <BackButton />
         </div>
         <p class="text-fg-muted text-lg">
           {{ $t('settings.tagline') }}

From 4c602f185b16ec7e86a2cdde6ccfda91d7b9d6e8 Mon Sep 17 00:00:00 2001
From: epifaniofrancisco <epifaniofrancisco03@gmail.com>
Date: Thu, 12 Mar 2026 16:57:16 +0100
Subject: [PATCH 3/6] test(a11y): add accessibility tests for BackButton
 component and normalize file paths

---
 test/nuxt/a11y.spec.ts                    |  9 +++++++++
 test/unit/a11y-component-coverage.spec.ts | 10 +++++++---
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts
index 9a079f91c8..0896cabd45 100644
--- a/test/nuxt/a11y.spec.ts
+++ b/test/nuxt/a11y.spec.ts
@@ -124,6 +124,7 @@ import {
   AboutLogoList,
   AuthorAvatar,
   AuthorList,
+  BackButton,
   BlogPostFederatedArticles,
   BlogPostListCard,
   BlogPostWrapper,
@@ -449,6 +450,14 @@ describe('component accessibility audits', () => {
     })
   })
 
+  describe('BackButton', () => {
+    it('should have no accessibility violations', async () => {
+      const component = await mountSuspended(BackButton)
+      const results = await runAxe(component)
+      expect(results.violations).toEqual([])
+    })
+  })
+
   describe('TagStatic', () => {
     it('should have no accessibility violations', async () => {
       const component = await mountSuspended(TagStatic, {
diff --git a/test/unit/a11y-component-coverage.spec.ts b/test/unit/a11y-component-coverage.spec.ts
index f6f6ea845a..0dda568f4c 100644
--- a/test/unit/a11y-component-coverage.spec.ts
+++ b/test/unit/a11y-component-coverage.spec.ts
@@ -49,6 +49,10 @@ const SKIPPED_COMPONENTS: Record<string, string> = {
   'Button/Group.vue': "Wrapper component, tests wouldn't make much sense here",
 }
 
+function normalizeComponentPath(filePath: string): string {
+  return filePath.replaceAll('\\', '/')
+}
+
 /**
  * Recursively get all Vue component files in a directory.
  */
@@ -62,7 +66,7 @@ function getVueFiles(dir: string, baseDir: string = dir): string[] {
       files.push(...getVueFiles(fullPath, baseDir))
     } else if (entry.isFile() && entry.name.endsWith('.vue')) {
       // Get relative path from base components directory
-      files.push(path.relative(baseDir, fullPath))
+      files.push(normalizeComponentPath(path.relative(baseDir, fullPath)))
     }
   }
 
@@ -87,7 +91,7 @@ function parseComponentsDeclaration(dtsPath: string): Map<string, string[]> {
   let match
   while ((match = exportRegex.exec(content)) !== null) {
     const componentName = match[1]!
-    const filePath = match[2]!
+    const filePath = normalizeComponentPath(match[2]!)
 
     const existing = componentMap.get(componentName) || []
     if (!existing.includes(filePath)) {
@@ -116,7 +120,7 @@ function getTestedComponents(
   let match
 
   while ((match = directImportRegex.exec(testFileContent)) !== null) {
-    tested.add(match[1]!)
+    tested.add(normalizeComponentPath(match[1]!))
   }
 
   // Match #components imports like:

From 2f33f92c6070a7894582865b491674346fef1885 Mon Sep 17 00:00:00 2001
From: "Willow (GHOST)" <git@willow.sh>
Date: Sat, 21 Mar 2026 08:53:27 -0700
Subject: [PATCH 4/6] chore: remove explicit class prop

---
 app/components/BackButton.vue | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/app/components/BackButton.vue b/app/components/BackButton.vue
index 8b0f0c46d8..680e7f751c 100644
--- a/app/components/BackButton.vue
+++ b/app/components/BackButton.vue
@@ -1,25 +1,13 @@
 <script setup lang="ts">
 const router = useRouter()
 const canGoBack = useCanGoBack()
-
-withDefaults(
-  defineProps<{
-    class?: string
-  }>(),
-  {
-    class: '',
-  },
-)
 </script>
 
 <template>
   <button
     v-if="canGoBack"
     type="button"
-    :class="[
-      'inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0',
-      $props.class,
-    ]"
+    class="inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
     @click="router.back()"
   >
     <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />

From f0c41c2ce49d38254b4c457bd9e7d4fe41e6e075 Mon Sep 17 00:00:00 2001
From: "Willow (GHOST)" <git@willow.sh>
Date: Sat, 21 Mar 2026 09:06:11 -0700
Subject: [PATCH 5/6] chore: revert blog back button

---
 app/pages/blog/index.vue | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/app/pages/blog/index.vue b/app/pages/blog/index.vue
index d582c964ca..ef824f0637 100644
--- a/app/pages/blog/index.vue
+++ b/app/pages/blog/index.vue
@@ -21,12 +21,9 @@ useSeoMeta({
   <main class="container w-full flex-1 py-12 sm:py-16 overflow-x-hidden">
     <article class="max-w-2xl mx-auto">
       <header class="mb-12">
-        <div class="flex items-baseline justify-between gap-4 mb-4">
-          <h1 class="font-mono text-3xl sm:text-4xl font-medium">
-            {{ $t('blog.heading') }}
-          </h1>
-          <BackButton />
-        </div>
+        <h1 class="font-mono text-3xl sm:text-4xl font-medium">
+          {{ $t('blog.heading') }}
+        </h1>
         <p class="text-fg-muted text-lg">
           {{ $t('tagline') }}
         </p>

From 476e96f3df07a24b14e8a008b15c62bfb1ebce14 Mon Sep 17 00:00:00 2001
From: "Willow (GHOST)" <git@willow.sh>
Date: Sat, 21 Mar 2026 09:25:29 -0700
Subject: [PATCH 6/6] tests: fix

---
 test/nuxt/a11y.spec.ts | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts
index ea7128d6da..21e9870397 100644
--- a/test/nuxt/a11y.spec.ts
+++ b/test/nuxt/a11y.spec.ts
@@ -113,6 +113,12 @@ vi.mock('vue-data-ui/vue-ui-xy', () => {
   }
 })
 
+vi.mock('~/composables/useCanGoBack', () => {
+  return {
+    useCanGoBack: () => shallowRef(true),
+  }
+})
+
 // Import components from #components where possible
 // For server/client variants, we need to import directly to test the specific variant
 import {
@@ -461,6 +467,7 @@ describe('component accessibility audits', () => {
   describe('BackButton', () => {
     it('should have no accessibility violations', async () => {
       const component = await mountSuspended(BackButton)
+      expect(component.find('button').exists()).toBe(true)
       const results = await runAxe(component)
       expect(results.violations).toEqual([])
     })
