Skip to content
On this page

Astro

WARNING

You will need to update your application to use Vite ^3.1.0 and latest vite-plugin-pwa 0.13.1+.

Astro Integration

vite-plugin-pwa provides the new @vite-pwa/astro integration that will allow you to use vite-plugin-pwa in your Astro applications.

You will need to install @vite-pwa/astro using:

bash
pnpm add -D @vite-pwa/astro
pnpm add -D @vite-pwa/astro
bash
yarn add -D @vite-pwa/astro
yarn add -D @vite-pwa/astro
bash
npm install -D @vite-pwa/astro
npm install -D @vite-pwa/astro

To update your project to use the new vite-plugin-pwa integration for Astro, you only need to change the Astro config file removing the PWA plugin if present:

ts
import { defineConfig } from 'astro/config'
import AstroPWA from '@vite-pwa/astro'

// https://astro.build/config
export default defineConfig({
  integrations: [
    AstroPWA({
      /* your pwa options */
    })
  ]
})
import { defineConfig } from 'astro/config'
import AstroPWA from '@vite-pwa/astro'

// https://astro.build/config
export default defineConfig({
  integrations: [
    AstroPWA({
      /* your pwa options */
    })
  ]
})

Importing Virtual Modules

WARNING

Since Astro will not inject any script in your application when using Astro components, you will need to use/import a PWA virtual module.

You can also enable Development Support to test your PWA webmanifest and debug your custom service worker logic as you develop your Astro application.

Auto Update

The best place to use/import the PWA virtual module will be in the main layout of the application (you should register it in any layout):

src/layouts/Layout.astro
astro
---
import { pwaInfo } from 'virtual:pwa-info';

export interface Props {
	title: string;
}

const { title } = Astro.props as Props;
---
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" href="/favicon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/pwa-192x192.png">
    <link rel="mask-icon" href="/favicon.svg" color="#FFFFFF">
    <meta name="msapplication-TileColor" content="#FFFFFF">
    <meta name="theme-color" content="#ffffff">
    <title>{title}</title>
    <meta name="description" content={title}>
    <style>
    main, footer {
        text-align: center;
    }
    </style>
    <script src="/src/pwa.ts"></script>
    { pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }
  </head>
  <body>
    <main>
        <article>
            <slot />
        </article>
    </main>
  </body>
</html>
---
import { pwaInfo } from 'virtual:pwa-info';

export interface Props {
	title: string;
}

const { title } = Astro.props as Props;
---
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" href="/favicon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/pwa-192x192.png">
    <link rel="mask-icon" href="/favicon.svg" color="#FFFFFF">
    <meta name="msapplication-TileColor" content="#FFFFFF">
    <meta name="theme-color" content="#ffffff">
    <title>{title}</title>
    <meta name="description" content={title}>
    <style>
    main, footer {
        text-align: center;
    }
    </style>
    <script src="/src/pwa.ts"></script>
    { pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }
  </head>
  <body>
    <main>
        <article>
            <slot />
        </article>
    </main>
  </body>
</html>
src/pwa.ts
ts
import { registerSW } from 'virtual:pwa-register'

registerSW({
  immediate: true,
  onRegisteredSW(swScriptUrl) {
    console.log('SW registered: ', swScriptUrl)
  },
  onOfflineReady() {
    console.log('PWA application ready to work offline')
  },
})
import { registerSW } from 'virtual:pwa-register'

registerSW({
  immediate: true,
  onRegisteredSW(swScriptUrl) {
    console.log('SW registered: ', swScriptUrl)
  },
  onOfflineReady() {
    console.log('PWA application ready to work offline')
  },
})

Prompt for Update

The best place to register the ReloadPrompt component will be in the main layout of the application (you should register it in any layout):

src/layouts/Layout.astro
astro
---
import { pwaInfo } from 'virtual:pwa-info';
import ReloadPrompt from '../components/ReloadPrompt.astro';

export interface Props {
  title: string;
}

const { title } = Astro.props as Props;
---
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" href="/favicon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/pwa-192x192.png">
    <link rel="mask-icon" href="/favicon.svg" color="#FFFFFF">
    <meta name="msapplication-TileColor" content="#FFFFFF">
    <meta name="theme-color" content="#ffffff">
    <title>{title}</title>
    <meta name="description" content={title}>
    <style>
    main, footer {
        text-align: center;
    }
    </style>
    { pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }
  </head>
  <body>
    <main>
        <article>
            <slot />
        </article>
    </main>
    <ReloadPrompt />
  </body>
</html>
---
import { pwaInfo } from 'virtual:pwa-info';
import ReloadPrompt from '../components/ReloadPrompt.astro';

export interface Props {
  title: string;
}

const { title } = Astro.props as Props;
---
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" href="/favicon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/pwa-192x192.png">
    <link rel="mask-icon" href="/favicon.svg" color="#FFFFFF">
    <meta name="msapplication-TileColor" content="#FFFFFF">
    <meta name="theme-color" content="#ffffff">
    <title>{title}</title>
    <meta name="description" content={title}>
    <style>
    main, footer {
        text-align: center;
    }
    </style>
    { pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }
  </head>
  <body>
    <main>
        <article>
            <slot />
        </article>
    </main>
    <ReloadPrompt />
  </body>
</html>
src/components/ReloadPrompt.astro
astro
<script src="./pwa.ts"></script>
<div
    id="pwa-toast"
    role="alert"
    aria-labelledby="toast-message"
>
    <div class="message">
      <span id="toast-message"></span>
    </div>
    <button id="pwa-refresh">
      Reload
    </button>
    <button id="pwa-close">
      Close
    </button>
</div>

<style>
#pwa-toast {
  visibility: hidden;
  position: fixed;
  right: 0;
  bottom: 0;
  margin: 16px;
  padding: 12px;
  border: 1px solid #8885;
  border-radius: 4px;
  z-index: 1;
  text-align: left;
  box-shadow: 3px 4px 5px 0 #8885;
}
#pwa-toast .message {
  margin-bottom: 8px;
}
#pwa-toast button {
  border: 1px solid #8885;
  outline: none;
  margin-right: 5px;
  border-radius: 2px;
  padding: 3px 10px;
}
#pwa-toast.show {
  visibility: visible;
}
button#pwa-refresh {
  display: none;
}
#pwa-toast.show.refresh button#pwa-refresh {
  display: inline-block;
}
</style>
<script src="./pwa.ts"></script>
<div
    id="pwa-toast"
    role="alert"
    aria-labelledby="toast-message"
>
    <div class="message">
      <span id="toast-message"></span>
    </div>
    <button id="pwa-refresh">
      Reload
    </button>
    <button id="pwa-close">
      Close
    </button>
</div>

<style>
#pwa-toast {
  visibility: hidden;
  position: fixed;
  right: 0;
  bottom: 0;
  margin: 16px;
  padding: 12px;
  border: 1px solid #8885;
  border-radius: 4px;
  z-index: 1;
  text-align: left;
  box-shadow: 3px 4px 5px 0 #8885;
}
#pwa-toast .message {
  margin-bottom: 8px;
}
#pwa-toast button {
  border: 1px solid #8885;
  outline: none;
  margin-right: 5px;
  border-radius: 2px;
  padding: 3px 10px;
}
#pwa-toast.show {
  visibility: visible;
}
button#pwa-refresh {
  display: none;
}
#pwa-toast.show.refresh button#pwa-refresh {
  display: inline-block;
}
</style>
src/components/pwa.ts
ts
import { registerSW } from 'virtual:pwa-register'

window.addEventListener('load', () => {
  const pwaToast = document.querySelector<HTMLDivElement>('#pwa-toast')!
  const pwaToastMessage = pwaToast.querySelector<HTMLDivElement>('.message #toast-message')!
  const pwaCloseBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-close')!
  const pwaRefreshBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-refresh')!

  let refreshSW: ((reloadPage?: boolean) => Promise<void>) | undefined

  const refreshCallback = () => refreshSW?.(true)

  const hidePwaToast = (raf = false) => {
    if (raf) {
      requestAnimationFrame(() => hidePwaToast(false))
      return
    }
    if (pwaToast.classList.contains('refresh'))
      pwaRefreshBtn.removeEventListener('click', refreshCallback)

    pwaToast.classList.remove('show', 'refresh')
  }
  const showPwaToast = (offline: boolean) => {
    if (!offline)
      pwaRefreshBtn.addEventListener('click', refreshCallback)
    requestAnimationFrame(() => {
      hidePwaToast(false)
      if (!offline)
        pwaToast.classList.add('refresh')
      pwaToast.classList.add('show')
    })
  }

  pwaCloseBtn.addEventListener('click', () => hidePwaToast(true))

  refreshSW = registerSW({
    immediate: true,
    onOfflineReady() {
      pwaToastMessage.innerHTML = 'App ready to work offline'
      showPwaToast(true)
    },
    onNeedRefresh() {
      pwaToastMessage.innerHTML = 'New content available, click on reload button to update'
      showPwaToast(false)
    },
    onRegisteredSW(swScriptUrl) {

      console.log('SW registered: ', swScriptUrl)
    },
  })
})
import { registerSW } from 'virtual:pwa-register'

window.addEventListener('load', () => {
  const pwaToast = document.querySelector<HTMLDivElement>('#pwa-toast')!
  const pwaToastMessage = pwaToast.querySelector<HTMLDivElement>('.message #toast-message')!
  const pwaCloseBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-close')!
  const pwaRefreshBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-refresh')!

  let refreshSW: ((reloadPage?: boolean) => Promise<void>) | undefined

  const refreshCallback = () => refreshSW?.(true)

  const hidePwaToast = (raf = false) => {
    if (raf) {
      requestAnimationFrame(() => hidePwaToast(false))
      return
    }
    if (pwaToast.classList.contains('refresh'))
      pwaRefreshBtn.removeEventListener('click', refreshCallback)

    pwaToast.classList.remove('show', 'refresh')
  }
  const showPwaToast = (offline: boolean) => {
    if (!offline)
      pwaRefreshBtn.addEventListener('click', refreshCallback)
    requestAnimationFrame(() => {
      hidePwaToast(false)
      if (!offline)
        pwaToast.classList.add('refresh')
      pwaToast.classList.add('show')
    })
  }

  pwaCloseBtn.addEventListener('click', () => hidePwaToast(true))

  refreshSW = registerSW({
    immediate: true,
    onOfflineReady() {
      pwaToastMessage.innerHTML = 'App ready to work offline'
      showPwaToast(true)
    },
    onNeedRefresh() {
      pwaToastMessage.innerHTML = 'New content available, click on reload button to update'
      showPwaToast(false)
    },
    onRegisteredSW(swScriptUrl) {

      console.log('SW registered: ', swScriptUrl)
    },
  })
})

Using Application UI Framework

If you're using some Application UI Framework in your Astro application, you can use/import the corresponding PWA plugin virtual module:

Check also the documentation for Astro Frameworks Components for more information.

If you have a 404 route, you can use it as the fallback navigation for your service worker.

When using generateSW strategy, configure the 404 route in the workbox pwa integration option:

ts
VitePWA({
  workbox: { navigateFallback: '/404' }
})
VitePWA({
  workbox: { navigateFallback: '/404' }
})

If you are using injectManifest strategy, configure the 404 route in the navigation fallback in your custom service worker:

ts
registerRoute(new NavigationRoute(createHandlerBoundToURL('/404')))
registerRoute(new NavigationRoute(createHandlerBoundToURL('/404')))

Released under the MIT License.