Skip to content

Service worker is registered from relative paths instead of absolute in SvelteKit PWA #873

@spwoodcock

Description

@spwoodcock

I'm not 100% sure if this is user error, or a bug, but I have scoured many similar issues such as #665, the documentation, and the code here to work this out 😅 Hopefully someone can help!

Context

  • I am building a PWA that is hosted under https://mapper.fmtm.hotosm.org.
  • Users either access via the main domain, or directly into the project route: e.g. https://mapper.fmtm.hotosm.org/project/1.
  • We are trying to aggressively cache everything on the website, so it can be loaded entirely offline (for field mapping).

Problem

  • Loading directly from the site root works fine, https://mapper.fmtm.hotosm.org, and manifest and sw.js are found.
  • However, if loading from https://mapper.fmtm.hotosm.org/project/1, I get errors:

manifest:

Manifest fetch from https://mapper.dev.fmtm.hotosm.org/project/manifest.webmanifest failed, code 404

sw.js

SW registration error TypeError: Failed to register a ServiceWorker for scope ('https://mapper.dev.fmtm.hotosm.org/') with script ('https://mapper.dev.fmtm.hotosm.org/project/sw.js'): A bad HTTP response code (404) was received when fetching the script.

Both exist on the server under https://mapper.dev.fmtm.hotosm.org/sw.js and https://mapper.dev.fmtm.hotosm.org/manifest.webmanifest

Note we also have some issues related to caching, but one thing at a time!

Files For Debug

vite.config.ts:

const pwaOptions: Partial<VitePWAOptions> = {
	// This ensures that caches are invalidated when the app is updated
	registerType: 'autoUpdate',
	injectRegister: 'script', # originally I was using 'auto', but changed to try and tweak manually
	strategies: 'generateSW',

	// Important to ensure the PWA runs on the **entire** website
	// For example, it's possible to have `scope: '/project'` for a PWA
	// on only part of the website
	scope: '/',

	// Allow testing the PWA during local development
	devOptions: {
		enabled: true,
		// // Don't fallback on document based (e.g. `/some-page`) requests
		// We need this to include /project/ID as well as home page for direct load when offline
		navigateFallbackAllowlist: [/^\/$/, /^\/project\/.+$/],
		// Enable this to disable runtime caching for easier debugging
		// disableRuntimeConfig: true,
	},

	// // Cache all the imports, including favicon
	workbox: {
		// Don't fallback on document based (e.g. `/some-page`) requests
		// Even though this says `null` by default, I had to set this specifically to `null` to make it work
		// We can do this because we are explicitly handling all navigation routes via runtimeCaching
		navigateFallback: null,

		// Cache all imports
		// globPatterns: ["**/*"],
		globPatterns: ['**/*.{js,css,html,wasm,ico,svg,png,jpg,jpeg,gif,webmanifest}'],

		// This is where the magic happens: routes to cache key to cache to
		runtimeCaching: [
			// Handle SPA navigations (e.g., /, /project/123)
			{
				urlPattern: ({ request }: RouteMatchCallbackOptions) => request.mode === 'navigate',
				// Try to get fresh version; fallback when offline
				handler: 'NetworkFirst',
				// // NOTE the below doesn't work yet, so we use NetworkFirst above
				// // This will load the cached version instantly, then update the cache in the
				// // background. On next refresh, the updated app will be loaded.
				// // This is a good tradeoff, as slightly outdated content is only shown temporarily.
				// handler: 'StaleWhileRevalidate',
				options: {
					cacheName: 'field-tm-html-pages',
					networkTimeoutSeconds: 5, // Setting only valid for NetworkFirst, else errors
					expiration: {
						maxEntries: 50,
						maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days
					},
				},
			},
			// Static assets (e.g., JS, CSS, icons, fonts)
			{
				urlPattern: ({ url }: RouteMatchCallbackOptions) => url.origin === self.location.origin,
				// Serve fast from cache; change infrequently
				handler: 'CacheFirst',
				options: {
					cacheName: 'field-tm-static-assets',
					expiration: {
						maxEntries: 300,
						maxAgeSeconds: 60 * 60 * 24 * 14, // 14 days
					},
				},
			},
		],

		// We need to cache files up to 15MB to allow for PGLite to be cached
		maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
	},

	// Cache all the static assets in the static folder
	includeAssets: ['**/*', 'icons/*.svg'],

	manifest: {
		id: 'com.hotosm.field-tm',
		name: 'Field-TM',
		short_name: 'Field-TM',
		description: 'Coordinated field mapping for Open Mapping campaigns.',
		categories: ['mapping', 'humanitarian', 'hotosm', 'field', 'odk'],
		scope: '/',
		start_url: '/',
		orientation: 'portrait',
		dir: 'auto',
		display: 'standalone',
		launch_handler: 'auto',
		theme_color: '#d63f3f',
		background_color: '#d63f3f',
		icons: [
			{
				src: 'pwa-64x64.png',
				sizes: '64x64',
				type: 'image/png',
				purpose: 'any',
			},
			{
				src: 'pwa-192x192.png',
				sizes: '192x192',
				type: 'image/png',
			},
			{
				src: 'pwa-512x512.png',
				sizes: '512x512',
				type: 'image/png',
			},
			{
				src: 'maskable-icon-512x512.png',
				sizes: '512x512',
				type: 'image/png',
				purpose: 'maskable',
			},
		],
		screenshots: [
			{
				src: 'screenshot-mapper.jpeg',
				sizes: '1280x720',
				type: 'image/jpeg',
				form_factor: 'wide',
				label: 'Mapper App',
			},
		],
		related_applications: [
			{
				platform: 'web',
				url: 'https://fmtm.hotosm.org',
			},
		],
	},
};

svelte.config.ts

import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	base: '/',

	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter({
			// default options are shown. On some platforms
			// these options are set automatically — see below
			pages: 'build',
			assets: 'build',
			// This fallback is required to compile as SPA
			// as we don't have any server side rendering here
			fallback: 'index.html',
			precompress: false,
			strict: true,
		}),
		alias: {
			$lib: 'src/lib',
			$components: 'src/components',
			$store: 'src/store',
			$routes: 'src/routes',
			$constants: 'src/constants',
			$styles: 'src/styles',
			$assets: 'src/assets',
			$translations: 'src/translations',
			$static: 'static',
			$migrations: '../migrations',
		},
	},
};

export default config;

I tested with a +layout.svelte file like this:

	import { pwaInfo } from 'virtual:pwa-info';
	const webManifestLink = $derived(pwaInfo ? pwaInfo.webManifest.linkTag : '');

<svelte:head>
	{@html webManifestLink}
</svelte:head>

and also without the above, but adding the manifest and sw.js manually:

<svelte:head>
        <link rel="manifest" href="/manifest.webmanifest">
	<script>
		if ('serviceWorker' in navigator) {
			navigator.serviceWorker.register('/sw.js', { scope: '/' });
		}
	</script>
</svelte:head>

The second approach did fix the loading of the manifest, but the sw.js file was still loaded from the wrong place!

Thanks so much for your time & I appreciate the support 🙏

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions