NeuroAgent

Fix Workbox Double Slashes in Vite PWA GitHub Pages

Resolve Workbox cache key double slashes issue in Vite PWA deployments on GitHub Pages. Learn configuration fixes and best practices for proper URL normalization.

Workbox setup in Vite PWA (GitHub Pages hosted) causing double forward slashes (//) in cache entries?

I’m experiencing an issue where my Workbox cache is being populated with double forward slashes in the cache keys. This occurs when my Vite PWA is hosted on GitHub Pages.

I’ve already tried:

  • Adjusting the base configuration in Vite (both with and without trailing slashes)
  • Clearing site data and unregistering service workers during testing

The generated service worker doesn’t include the URL suffix from GitHub Actions, which suggests this might be an internal Workbox behavior.

My GitHub Actions workflow includes:

yaml
- name: Production Build
  run: bun run build:prod -- --base=/${{ github.event.repository.name }}/

I’ve created a reproducible example with artifacts from GitHub deployments, though the issue seems to manifest specifically when hosted on GitHub Pages or when not served from the root folder of a server.

What configuration options should I examine or alternative methods should I consider to ensure proper cache keys without double slashes?

NeuroAgent

Workbox in Vite PWA can generate double forward slashes in cache keys when hosted on GitHub Pages due to how the base URL is handled during service worker registration and cache key generation. This typically occurs because the service worker script is loaded with a different base path than the actual cache entries it’s trying to precache or intercept, causing Workbox to normalize URLs incorrectly.

Contents

Understanding the Double Slash Issue

The double forward slash (//) issue in Workbox cache keys manifests when URLs are incorrectly normalized during the caching process. This typically appears as cache keys containing something like //assets// instead of /assets/ or assets/.

When your Vite PWA is hosted on GitHub Pages with a non-root path configuration (using base=/${{ github.event.repository.name }}/), several factors can contribute to this problem:

  1. Service Worker Registration Path: The service worker file gets registered with a specific path, but the precache manifest may contain URLs with different base paths
  2. Runtime Caching: During runtime, requests may be processed with incorrect URL normalization
  3. Cache Key Strategy: Workbox’s default cache key strategies may not handle the base URL transformation correctly

This issue specifically affects GitHub Pages deployments because GitHub serves content from subdirectories, and the URL structure differs from local development or root-hosted deployments.

Root Causes of the Problem

1. Vite Base Configuration Mismatch

The base configuration in Vite directly impacts how assets are referenced and how the service worker generates cache keys. When you set base=/${{ github.event.repository.name }}/, this affects:

  • Asset references in HTML files
  • Service worker script loading
  • Precache manifest generation

However, the service worker itself may not be aware of this base path transformation when it processes requests.

2. Service Worker File Location

Service workers run in a different context than the main page. They’re registered from a specific path but intercept requests from the entire origin. This disconnect can cause URL normalization issues.

3. Workbox URL Normalization

Workbox uses its own URL normalization logic that may not account for custom base paths. The urlManipulation option in Workbox can sometimes create unintended double slashes when processing URLs.

4. GitHub Pages Specific Behavior

GitHub Pages serves content from subdirectories and may handle trailing slashes differently than other hosting platforms. This can cause the service worker to perceive URLs differently than the browser.

Configuration Solutions

1. Modify Vite PWA Configuration

Update your vite.config.js to include proper URL handling:

javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
      manifest: {
        name: 'Your App Name',
        short_name: 'App',
        description: 'Description',
        theme_color: '#ffffff',
      },
      workbox: {
        // Customize workbox configuration
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/.*\.(?:js|css|png|jpg|jpeg|svg|gif|woff|woff2|ttf)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'static-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
              },
              cacheableResponse: {
                statuses: [0, 200],
              },
              // Add URL manipulation to prevent double slashes
              urlManipulation: ({ url }) => {
                const pathname = url.pathname.replace(/\/+/g, '/')
                url.pathname = pathname.startsWith('/') ? pathname : `/${pathname}`
                return [url]
              },
            },
          },
        ],
      },
    }),
  ],
  base: process.env.NODE_ENV === 'production' 
    ? `/${process.env.VITE_APP_NAME}/` 
    : '/',
})

2. Environment-Specific Build Configuration

Create separate build configurations for different environments:

javascript
// vite.config.js
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())
  
  return {
    base: env.VITE_BASE_URL || '/',
    plugins: [
      // ... your plugins
    ],
  }
})

Then set environment variables in your .env files:

  • .env.production: VITE_BASE_URL=/${process.env.GITHUB_REPOSITORY.replace('github.com/', '').split('/')[1]}/
  • .env.development: VITE_BASE_URL=/

3. Custom Service Worker File

Create a custom service worker that handles URL normalization:

javascript
// public/sw.js
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'

// Get the precache manifest from the injected variable
const precacheManifest = self.__WB_MANIFEST

// Normalize URLs to prevent double slashes
const normalizeUrl = (url) => {
  try {
    const parsedUrl = new URL(url, self.location.href)
    pathname = parsedUrl.pathname.replace(/\/+/g, '/')
    parsedUrl.pathname = pathname.startsWith('/') ? pathname : `/${pathname}`
    return parsedUrl.toString()
  } catch (e) {
    return url
  }
}

// Precache with normalized URLs
const normalizedManifest = precacheManifest.map(entry => ({
  ...entry,
  url: normalizeUrl(entry.url)
}))

precacheAndRoute(normalizedManifest)

// Register runtime caching with URL manipulation
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new CacheFirst({
    cacheName: 'api-cache',
    plugins: [{
      cacheKeyWillBeUsed: async ({ request }) => {
        const normalizedUrl = normalizeUrl(request.url)
        return new Request(normalizedUrl, request)
      }
    }]
  })
)

Alternative Approaches

1. GitHub Actions Path Adjustment

Modify your GitHub Actions workflow to handle the base path more carefully:

yaml
- name: Production Build
  run: |
    REPO_NAME="${{ github.event.repository.name }}"
    bun run build:prod -- --base="/${REPO_NAME}/"
    
- name: Upload to GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./dist
    cname: your-domain.com
    keep_files: true

2. Custom Cache Key Strategy

Implement a custom cache key strategy that handles the base path:

javascript
// In your Workbox configuration
customCacheKey: ({ request, url, params }) => {
  // Remove double slashes and normalize the path
  const normalizedPath = url.pathname.replace(/\/+/g, '/')
  const cleanPath = normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`
  
  // Reconstruct the URL with the normalized path
  const normalizedUrl = new URL(cleanPath, url.origin)
  
  // Add a prefix if needed for GitHub Pages
  const githubPagesPrefix = `/${process.env.VITE_APP_NAME || ''}/`
  if (normalizedUrl.pathname.startsWith(githubPagesPrefix)) {
    normalizedUrl.pathname = githubPagesPrefix + normalizedUrl.pathname.substring(githubPagesPrefix.length)
  }
  
  return normalizedUrl.toString()
}

3. Service Worker Scope Adjustment

Adjust the service worker scope to match your deployment pattern:

javascript
// Register service worker with proper scope
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    const repoName = window.location.pathname.split('/')[1]
    const swUrl = `/sw.js` // or `/your-repo-name/sw.js`
    
    navigator.serviceWorker.register(swUrl, {
      scope: repoName ? `/${repoName}/` : '/'
    }).then(registration => {
      console.log('ServiceWorker registration successful with scope:', registration.scope)
    }).catch(error => {
      console.log('ServiceWorker registration failed:', error)
    })
  })
}

Testing and Verification

1. Cache Inspection Tools

Use browser DevTools to inspect cache contents:

  1. Open Chrome DevTools (F12)
  2. Go to Application > Cache Storage
  3. Select your cache and examine the keys
  4. Look for entries with double slashes

2. Service Worker Debugging

Add debugging to your service worker:

javascript
// Add to your service worker
console.log('Service Worker activated')
console.log('Current scope:', self.scope)
console.log('Current location:', self.location.href)

// Log cache operations
self.addEventListener('install', (event) => {
  console.log('Installing service worker with manifest:', self.__WB_MANIFEST)
})

self.addEventListener('activate', (event) => {
  console.log('Service worker activated with scope:', self.scope)
})

3. URL Testing Script

Create a test page to verify URL handling:

javascript
// test-url-handling.js
const testUrls = [
  '/assets/script.js',
  '//assets/script.js',
  '/assets//script.js',
  'https://your-domain.com/your-repo-name/assets/script.js'
]

testUrls.forEach(url => {
  console.log(`Original: ${url}`)
  try {
    const normalized = new URL(url, window.location.href)
    console.log(`Normalized: ${normalized.pathname}`)
  } catch (e) {
    console.log(`Error: ${e.message}`)
  }
})

Best Practices for GitHub Pages Deployment

1. Consistent Path Configuration

Ensure consistent path handling across all configuration files:

javascript
// Example of centralized path configuration
const GITHUB_PAGES_CONFIG = {
  enabled: process.env.NODE_ENV === 'production',
  repoName: process.env.GITHUB_REPOSITORY?.split('/').pop() || 'my-app',
  baseUrl: process.env.NODE_ENV === 'production' 
    ? `/${process.env.GITHUB_REPOSITORY?.split('/').pop()}/` 
    : '/'
}

export default defineConfig({
  base: GITHUB_PAGES_CONFIG.baseUrl,
  // ... rest of config
})

2. Environment Variable Management

Set up proper environment variables:

bash
# .env.production
VITE_APP_NAME=my-app
VITE_BASE_URL=/my-app/
VITE_API_URL=https://api.example.com

# .env.development  
VITE_APP_NAME=my-app
VITE_BASE_URL=/
VITE_API_URL=http://localhost:3000

3. Testing Pipeline

Add testing to your GitHub Actions workflow:

yaml
- name: Test Build
  run: |
    cd dist
    # Test that all assets are referenced correctly
    grep -r "assets" index.html | head -5
    # Check for double slashes in HTML
    if grep -r "//" index.html | grep -v "http://" | grep -v "https://"; then
      echo "Found potential double slash issues"
      exit 1
    fi

4. Monitoring and Logging

Implement monitoring to catch cache key issues early:

javascript
// Add to your service worker
const CACHE_KEY_LOGGER = {
  logCacheOperation: (operation, url, cacheName) => {
    console.log(`[${new Date().toISOString()}] ${operation}: ${url} in ${cacheName}`)
    // Send to monitoring service if needed
  }
}

// Wrap cache operations with logging
self.addEventListener('fetch', (event) => {
  const url = event.request.url
  const normalizedUrl = url.replace(/\/+/g, '/')
  
  if (url !== normalizedUrl) {
    CACHE_KEY_LOGGER.logCacheOperation('URL_NORMALIZATION', url, normalizedUrl)
  }
})

Sources

  1. Workbox Documentation - Cache Keys and URL Normalization
  2. Vite Plugin PWA Configuration Guide
  3. GitHub Pages Deployment Best Practices
  4. Service Worker Scope and Context
  5. Workbox Runtime Caching Configuration

Conclusion

The double forward slash issue in Workbox cache keys for Vite PWA on GitHub Pages typically stems from URL normalization problems when the application is deployed to a subdirectory. Key takeaways include:

  1. Fix Vite base configuration to match your GitHub Pages deployment path and ensure consistency across all references
  2. Implement custom URL manipulation in Workbox configurations to handle path normalization properly
  3. Use environment-specific builds to handle development and production URL structures differently
  4. Test cache contents regularly using browser DevTools and custom debugging scripts
  5. Monitor service worker operations to catch URL issues early in the deployment process

For immediate resolution, start by implementing the custom URL manipulation in your Workbox configuration and verify that all cache keys are properly normalized before deploying to production. If the issue persists, consider creating a custom service worker that handles the specific URL patterns of your GitHub Pages deployment.

The key is ensuring that the service worker’s understanding of URL paths matches exactly how GitHub Pages serves your application, preventing any normalization conflicts that result in double slashes in cache keys.