DEV Community

Cover image for Create a PWA with Sveltekit | Svelte
Navin Kodag
Navin Kodag

Posted on • Updated on

Create a PWA with Sveltekit | Svelte

I've been using sveltekit and svelte for over a year now. I'm also waiting for it to be matured enough to beat NextJs in terms of community. But I like them both.

So, on this weekend I wanted to turn one of my SvelteKit projects into a PWA. When I wanted to do the same with NextJs projects there were a lot of tutorials. But I couldn't find many guides for svelte beginners.

That's because svelte has pwa functionality built into it.

Note !

Basic things for all PWAs

  • A website
  • manifest.json [ basic icons,names,shortcuts]
  • service-worker [ for offline cache ]

So let's get on with it.

lets-go.gif

First:

We'll create a demo Sveltekit project:

npm init svelte@next my-app
Enter fullscreen mode Exit fullscreen mode

create.png

Then we'll choose a simple config in vite for the sake of this article:

config.png

Choose typescript because #typescriptgang:

svelte-kit-config.png

Now we have a demo project set up with typescript, it will be straight-forward from here on:

note.png

Let's get into our directory:

cd my-app
Enter fullscreen mode Exit fullscreen mode

And run:

yarn
Enter fullscreen mode Exit fullscreen mode

yarn-output.png

After that,

  • In the /static directory, We'll create a manifest.json.
  • When svelte compiles the whole application, it copies static files over to the build folder.

manifest.json

Then we'll refer our manifest.json in src/app.html.

app.html

And finally we'll create our src/service-worker.ts

Svelte will automatically detect the service-worker in the src folder's root and then register our service worker during build.
Isn't that neat?

service-worker.ts

Now we just need to build our app using yarn build:
build.png

Now we can preview our build using yarn preview:

preview.png

😯 thats the 'install app' button,

output-button.png

Svelte makes it easy to make PWAs.

The source code of this project lies at:

https://github.com/100lvlmaster/svelte-pwa

You can find me at:

https://100lvlmaster.in

Latest comments (15)

Collapse
 
metropolitan profile image
Eastcoast7624

I tried to follow your tutorial but I got an error:

RollupError: "timestamp" is not exported by ".svelte-kit/generated/service-worker.js", imported by "src/service-worker.ts".

Do you know anything about this? I'm at a loss as how to fix this.

Collapse
 
justingolden21 profile image
Justin Golden

Is there a way to route the user to the page they were most recently on when they reopen the app?

Collapse
 
fxmt2009 profile image
Ade

Navin, used your code but the serviceworker is not being registered. It's giving me a 404 error. I added the serviceworker to src folder, manifest in static as in your code.

Is your code still working after their latest update? In the sveltekit doc, they mentioned that vite register the serviceworker. Is there any configuration for vite registering the service worker?

Collapse
 
justingolden21 profile image
Justin Golden

Hi, wondering if it's possible to simply cache everything rather than just what the user sees. It seems if they install with this sw and then open the app the first time offline, they don't have anything cached. Also, if they navigate to another page or load a font, it fails if offline. My whole app is < 1mb and I'd love to just cache it all on install. Thanks in advance.

Love this article by the way

Collapse
 
swiftwinds profile image
SwiftWinds • Edited

I've modified Navin's service worker to do just that:

import { build, files, version } from "$service-worker";

const worker = self as unknown as ServiceWorkerGlobalScope;
const STATIC_CACHE_NAME = `cache${version}`;
const APP_CACHE_NAME = `offline${version}`;

// hard-coded list of app routes we want to preemptively cache
const routes = ["/"];

// hard-coded list of other assets necessary for page load outside our domain
const customAssets = [
  "https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&display=swap",
  "https://unpkg.com/ress/dist/ress.min.css",
  "https://fonts.gstatic.com/s/inter/v11/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2",
];

// `build` is an array of all the files generated by the bundler,
// `files` is an array of everything in the `static` directory
// `version` is the current version of the app

const addDomain = (assets: string[]) =>
  assets.map((f) => self.location.origin + f);

// we filter the files because we don't want to cache logos for iOS
// (they're big and largely unused)
// also, we add the domain to our assets, so we can differentiate routes of our
// app from those of other apps that we cache
const ourAssets = addDomain([
  ...files.filter((f) => !/\/icons\/(apple.*?|original.png)/.test(f)),
  ...build,
  ...routes,
]);

const toCache = [...ourAssets, ...customAssets];
const staticAssets = new Set(toCache);

worker.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(STATIC_CACHE_NAME)
      .then((cache) => {
        return cache.addAll(toCache);
      })
      .then(() => {
        worker.skipWaiting();
      })
  );
});

worker.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then(async (keys) => {
      // delete old caches
      for (const key of keys) {
        if (key !== STATIC_CACHE_NAME && key !== APP_CACHE_NAME) {
          await caches.delete(key);
        }
      }

      worker.clients.claim();
    })
  );
});

/**
 * Fetch the asset from the network and store it in the cache.
 * Fall back to the cache if the user is offline.
 */
async function fetchAndCache(request: Request) {
  const cache = await caches.open(APP_CACHE_NAME);

  try {
    const response = await fetch(request);
    cache.put(request, response.clone());
    return response;
  } catch (err) {
    const response = await cache.match(request);
    if (response) {
      return response;
    }

    throw err;
  }
}

worker.addEventListener("fetch", (event) => {
  if (event.request.method !== "GET" || event.request.headers.has("range")) {
    return;
  }

  const url = new URL(event.request.url);

  // don't try to handle e.g. data: URIs
  const isHttp = url.protocol.startsWith("http");
  const isDevServerRequest =
    url.hostname === self.location.hostname && url.port !== self.location.port;
  const isStaticAsset = staticAssets.has(url.href);
  const skipBecauseUncached =
    event.request.cache === "only-if-cached" && !isStaticAsset;

  if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
    event.respondWith(
      (async () => {
        // always serve static files and bundler-generated assets from cache.
        // if your application has other URLs with data that will never change,
        // set this variable to true for them, and they will only be fetched once.
        const cachedAsset =
          isStaticAsset && (await caches.match(event.request));

        return cachedAsset || fetchAndCache(event.request);
      })()
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Simply set the routes array to a list of all routes you want to cache and add URLs to fonts, external CSS, etc. that you want to cache to the customAssets array.

Note that I use a cache-only strategy, so upon updating your app, you'll have to refresh the app twice to see the new content. You can fix this by either employing an instant force refresh (see codyanhorn.tech/blog/pwa-reload-pa...) or notify the user that an update is available (see medium.com/progressive-web-apps/pw...)

Collapse
 
justingolden21 profile image
Justin Golden

Awesome! Thanks : )

Thread Thread
 
swiftwinds profile image
SwiftWinds

No prob! :)

Collapse
 
m1212e profile image
m1212e

Hi, are we allowed to use the service worker snippet? Under which license does your project stand?

Collapse
 
100lvlmaster profile image
Navin Kodag

You can use it to your liking

Collapse
 
yogpanjarale profile image
YogPanjarale

also a great tool to make manifest.json
app-manifest.firebaseapp.com/

Collapse
 
tavelli profile image
Dan Tavelli

hey good article. i used this setup for a sveltekit app and it works great but i was having issue with subsequent updates. it would never pick up the new deploy because the index.html was cached i believe? i am also using the static prerender sveltekit adapter so possibly that is related. i ended up having to modify app.html for it to detect a change and then it would pick up the new service worker. have you run into this issue?

Collapse
 
swiftwinds profile image
SwiftWinds

I was having the same issue, but I modified his script slightly and it worked again. See my comment above :)

Collapse
 
100lvlmaster profile image
Navin Kodag • Edited

I use the vercel adapter when deploying. I haven't used ssg with sveltekit yet. But I do have an issue whenever I upgrade the packages. Sveltekit throws module not defined errors if i don't set the version to "next" instead of something like "^1.0.0". Have you joined their discord server?

Collapse
 
jdgamble555 profile image
Jonathan Gamble

I would think sveltekit would do all this automatically.

Collapse
 
100lvlmaster profile image
Navin Kodag

I like your firebase articles