Skip to content

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
bash
yarn add -D @vite-pwa/astro
bash
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 */
    })
  ]
})

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}>
    <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')
  },
})

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>
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>
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)
    }
  })
})

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
AstroPWA({
  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')))

Experimental

Directory and Trailing Slash Handler

Check the problem in the following issue: https://github.com/vite-pwa/astro/issues/23.

You can find a list of hosts and how they handle trailing slash in this repository.

To enable this feature, you need to add the following configuration to your PWA options:

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

// https://astro.build/config
export default defineConfig({
  integrations: [
    AstroPWA({
      experimental: {
        directoryAndTrailingSlashHandler: true,
      }
    })
  ]
})

If you're using injectManifest strategy, you also need to include directoryIndex and optionally cleanURLs in your custom service worker in the precaching controller:

ts
import { precacheAndRoute } from 'workbox-precaching'

precacheAndRoute(self.__WB_MANIFEST, { directoryIndex: 'index.html', cleanURLs: true })

PWA Assets Experimental from v0.3.0

@vite-pwa/astro plugin will configure integration option properly. We suggest you to use external configuration file, Astro dev server will not be restarted when changing the configuration.

To inject the PWA icons links and the theme-color, you can use the virtual:pwa-assets/head virtual module in your layout components:

  • remove all links with rel icon, apple-touch-icon and apple-touch-startup-image from your html head
  • remove the theme-color meta tag from your html head
  • add the virtual import
  • include theme color and icons links using code-snippet shown below
astro
---
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
---
<html lang="en">
  <head>
    { pwaAssetsHead.themeColor && <meta name="theme-color" content={pwaAssetsHead.themeColor.content} /> }
    { pwaAssetsHead.links.map(link => (
        <link {...link} />
    )) }
  </head>
</html>

You can find a working example in the examples folder.

Released under the MIT License.