Skip to content
On this page

SvelteKit

INFO

For Type declarations, Prompt for update and Periodic SW Updates go to Svelte entry.

You should remove all references to SvelteKit service worker module to disable it on your application.

Since SvelteKit uses SSR / SSG, we need to add the ReloadPrompt component using dynamic import. Vite Plugin PWA will only register the service worker on build, it is aligned with the current behavior of SvelteKit service worker module.

The best place to include the ReloadPrompt is on the main layout of the application:

src/routes/__layout.svelte
html
<script>
  import { onMount } from 'svelte'
  import { browser, dev } from '$app/env'

  let ReloadPrompt
  onMount(async () => {
    !dev && browser && (ReloadPrompt = (await import('$lib/components/ReloadPrompt.svelte')).default)
  })
</script>

<svelte:head>
  {#if (!dev && browser)}
    <link rel="manifest" href="/_app/manifest.webmanifest">
  {/if}
</svelte:head>

<main>
  <slot />
</main>

{#if ReloadPrompt}
  <svelte:component this={ReloadPrompt} />
{/if}
<script>
  import { onMount } from 'svelte'
  import { browser, dev } from '$app/env'

  let ReloadPrompt
  onMount(async () => {
    !dev && browser && (ReloadPrompt = (await import('$lib/components/ReloadPrompt.svelte')).default)
  })
</script>

<svelte:head>
  {#if (!dev && browser)}
    <link rel="manifest" href="/_app/manifest.webmanifest">
  {/if}
</svelte:head>

<main>
  <slot />
</main>

{#if ReloadPrompt}
  <svelte:component this={ReloadPrompt} />
{/if}

SvelteKit Adapters

The main problem with the current implementation of the service worker module of SvelteKit is that you don't have access to the result applied by any adapter you have configured on your application. The service worker module of SvelteKit will be called before the adapter logic is applied, and so, inside the service worker module, you don't have access to those resources. Your application will not work when the user is offline, since the pages will not be included on the service worker precache manifest.

When using Vite PWA Plugin with any SvelteKit Adapter you need to provide an additional script to rebuild your pwa once SvelteKit finish building your application, that is, when the adapter configured finish its job.

The biggest difference between this plugin and the SvelteKit service worker module is that this plugin does not require integration into the application logic - just configuration. You can take a look at SvelteKit example to configure the additional scripts on your application, it is quite complex since we use it for multiple behaviors with the same codebase.

Workbox manifestTransforms

We must provide a list of URLs for the service worker to load and precache. We provide these to workbox using the the manifestTransforms option under workbox or injectManifest. The manifest entries provided via this option will contain all the assets specified in the srcDir option.

Since SvelteKit outputs an .html page for each pre-rendered page, you can use manifestTransforms to generate the URL from the prerendered HTML file path. For an example, see the pwa-configuration.js module in the next example using @sveltejs/adapter-static.

Pages which are not prerendered or are generated with a unique adapter will need to be handled separately and the manifestTransforms logic will need to be modified accordingly.

Static Adapter example

As an example, when using @sveltejs/adapter-static with generateSW strategy and Prompt for update behavior, you will need:

1) add pwa.js script
js
import { copyFileSync } from 'node:fs'
import { resolveConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import { pwaConfiguration } from './pwa-configuration.js'

const webmanifestDestinations = [
  './.svelte-kit/output/client/',
  './build/',
]

const swDestinations = [
  './build/',
]

async function buildPwa() {
  const config = await resolveConfig({ plugins: [VitePWA({ ...pwaConfiguration })] }, 'build', 'production')
  // when `vite-plugin-pwa` is present, use it to regenerate SW after rendering
  const pwaPlugin = config.plugins.find(i => i.name === 'vite-plugin-pwa')?.api
  if (pwaPlugin?.generateSW) {
    console.log('Generating PWA...')
    await pwaPlugin.generateSW()
    webmanifestDestinations.forEach((d) => {
      copyFileSync('./.svelte-kit/output/client/_app/manifest.webmanifest', `${d}/manifest.webmanifest`)
    })
    // don't copy workbox, SvelteKit will copy it
    swDestinations.forEach((d) => {
      copyFileSync('./.svelte-kit/output/client/sw.js', `${d}/sw.js`)
    })
    console.log('Generation of PWA complete')
  }
}

buildPwa()
import { copyFileSync } from 'node:fs'
import { resolveConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import { pwaConfiguration } from './pwa-configuration.js'

const webmanifestDestinations = [
  './.svelte-kit/output/client/',
  './build/',
]

const swDestinations = [
  './build/',
]

async function buildPwa() {
  const config = await resolveConfig({ plugins: [VitePWA({ ...pwaConfiguration })] }, 'build', 'production')
  // when `vite-plugin-pwa` is present, use it to regenerate SW after rendering
  const pwaPlugin = config.plugins.find(i => i.name === 'vite-plugin-pwa')?.api
  if (pwaPlugin?.generateSW) {
    console.log('Generating PWA...')
    await pwaPlugin.generateSW()
    webmanifestDestinations.forEach((d) => {
      copyFileSync('./.svelte-kit/output/client/_app/manifest.webmanifest', `${d}/manifest.webmanifest`)
    })
    // don't copy workbox, SvelteKit will copy it
    swDestinations.forEach((d) => {
      copyFileSync('./.svelte-kit/output/client/sw.js', `${d}/sw.js`)
    })
    console.log('Generation of PWA complete')
  }
}

buildPwa()
2) add pwa-configuration.js script
js
const pwaConfiguration = {
  srcDir: './build',
  outDir: './.svelte-kit/output/client',
  includeManifestIcons: false,
  base: '/',
  scope: '/',
  manifest: {
    short_name: '<YOUR APP SHORT NAME>',
    name: '<YOUR APP NAME>',
    scope: '/',
    start_url: '/',
    display: 'standalone',
    theme_color: '#ffffff',
    background_color: '#ffffff',
    icons: [
      {
        src: '/pwa-192x192.png',
        sizes: '192x192',
        type: 'image/png'
      },
      {
        src: '/pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png'
      },
      {
        src: '/pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'any maskable'
      }
    ]
  },
  workbox: {
    // mode: 'development',
    navigateFallback: '/',
    // vite and SvelteKit are not aligned: pwa plugin will use /\.[a-f0-9]{8}\./ by default: #164 optimize workbox work
    dontCacheBustURLsMatching: /-[a-f0-9]{8}\./,
    globDirectory: './build/',
    globPatterns: ['robots.txt', '**/*.{js,css,html,ico,png,svg,webmanifest}'],
    globIgnores: ['**/sw*', '**/workbox-*'],
    manifestTransforms: [async (entries) => {
      // manifest.webmanifest is added always by pwa plugin, so we remove it.
      // EXCLUDE from the sw precache sw and workbox-*
      const manifest = entries.filter(({ url }) =>
        url !== 'manifest.webmanifest' && url !== 'sw.js' && !url.startsWith('workbox-')
      ).map((e) => {
        let url = e.url
        if (url && url.endsWith('.html')) {
          if (url.startsWith('/'))
            url = url.slice(1)

          if (url === 'index.html')
            e.url = '/'
          else if (url.endsWith('index.html'))
            e.url = `/${url.substring(0, url.lastIndexOf('/'))}`
          else if (url.endsWith('.html'))
            e.url = `/${url.substring(0, url.length - '.html'.length)}`
        }

        return e
      })

      return { manifest }
    }]
  }
}

export { pwaConfiguration }
const pwaConfiguration = {
  srcDir: './build',
  outDir: './.svelte-kit/output/client',
  includeManifestIcons: false,
  base: '/',
  scope: '/',
  manifest: {
    short_name: '<YOUR APP SHORT NAME>',
    name: '<YOUR APP NAME>',
    scope: '/',
    start_url: '/',
    display: 'standalone',
    theme_color: '#ffffff',
    background_color: '#ffffff',
    icons: [
      {
        src: '/pwa-192x192.png',
        sizes: '192x192',
        type: 'image/png'
      },
      {
        src: '/pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png'
      },
      {
        src: '/pwa-512x512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'any maskable'
      }
    ]
  },
  workbox: {
    // mode: 'development',
    navigateFallback: '/',
    // vite and SvelteKit are not aligned: pwa plugin will use /\.[a-f0-9]{8}\./ by default: #164 optimize workbox work
    dontCacheBustURLsMatching: /-[a-f0-9]{8}\./,
    globDirectory: './build/',
    globPatterns: ['robots.txt', '**/*.{js,css,html,ico,png,svg,webmanifest}'],
    globIgnores: ['**/sw*', '**/workbox-*'],
    manifestTransforms: [async (entries) => {
      // manifest.webmanifest is added always by pwa plugin, so we remove it.
      // EXCLUDE from the sw precache sw and workbox-*
      const manifest = entries.filter(({ url }) =>
        url !== 'manifest.webmanifest' && url !== 'sw.js' && !url.startsWith('workbox-')
      ).map((e) => {
        let url = e.url
        if (url && url.endsWith('.html')) {
          if (url.startsWith('/'))
            url = url.slice(1)

          if (url === 'index.html')
            e.url = '/'
          else if (url.endsWith('index.html'))
            e.url = `/${url.substring(0, url.lastIndexOf('/'))}`
          else if (url.endsWith('.html'))
            e.url = `/${url.substring(0, url.length - '.html'.length)}`
        }

        return e
      })

      return { manifest }
    }]
  }
}

export { pwaConfiguration }
3) modify your build script
json
{
  "scripts": {
    "build": "svelte-kit build && node ./pwa.js"
  }
}
{
  "scripts": {
    "build": "svelte-kit build && node ./pwa.js"
  }
}
4) add Vite Plugin PWA to svelte.config.js
js
import adapter from '@sveltejs/adapter-static'
import preprocess from 'svelte-preprocess'
import { VitePWA } from 'vite-plugin-pwa'
import { pwaConfiguration } from './pwa-configuration.js'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),

  kit: {
    adapter: adapter(),

    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    vite: {
      plugins: [VitePWA(pwaConfiguration)]
    }
  }
}

export default config
import adapter from '@sveltejs/adapter-static'
import preprocess from 'svelte-preprocess'
import { VitePWA } from 'vite-plugin-pwa'
import { pwaConfiguration } from './pwa-configuration.js'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),

  kit: {
    adapter: adapter(),

    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    vite: {
      plugins: [VitePWA(pwaConfiguration)]
    }
  }
}

export default config

Released under the MIT License.