A Progressive Web App (PWA) is a website that can be installed on a phone like an app: with its own icon, without an address bar and usable on a poor network. No app store is needed. This guide turns the full-screen pages into a PWA in about 30 minutes.
The plugin contains neither a manifest nor a service worker, because both belong to your website rather than to a single plugin. It does detect an installed app automatically, though, and adapts filters and search. You need the full-screen pages from the PRO plan.
What you need
- Your website runs on HTTPS. A PWA does not work without it.
- You can reach the root folder of the website via FTP or your host’s file manager, that is where the file
wp-config.phpis. - WordPress sits directly in the root folder. If it is in a subfolder, you need to adapt all paths below.
| File | Purpose | Location |
|---|---|---|
manifest.json | The app’s “ID card”: name, icon, colour, start page | Root folder |
sw.js | The service worker: stores content on the device so it is available without a network | Root folder |
offline.html | Note shown when a page is not available offline | Root folder |
app-icons/ | The app icons | Folder in the root folder |
event-app.php | Connects everything to WordPress | wp-content/mu-plugins/ |
Step 1: Create the icons
Create the folder app-icons in the root folder and save three PNG files in it: icon-192.png (192 × 192 pixels), icon-512.png (512 × 512 pixels) and icon-maskable-512.png (512 × 512 pixels). In the third icon, the background fills the whole area and the motif keeps about 10 percent distance from the edge, because Android crops the corners depending on the device.
Step 2: Create the manifest
Create the file manifest.json in the root folder. Change the name, the short name (shown under the icon, about 12 characters at most) and the colour.
{
"name": "Seitenwind Convention 2026",
"short_name": "Seitenwind",
"lang": "en",
"start_url": "/?ec_plan&ec_app=1",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#f76b15",
"icons": [
{ "src": "/app-icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/app-icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/app-icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}start_url is the page the app starts with. If you have set up a custom start page, it automatically appears first. The addition &ec_app=1 shows the bottom navigation bar even if app mode is not set for everyone.
Step 3: Create the service worker
The service worker is a small script that runs in the background and mediates between the app and the internet. Create the file sw.js in the root folder. It must be there, because a service worker only acts on addresses below its folder. In START_PAGES, enter only pages you really use. If the exhibitor plugin is running too, add /?eem_map&eem_app=1 and /?eem_list&eem_app=1.
// Raise the number whenever you change this file
var CACHE_NAME = 'event-app-v1';
var OFFLINE_PAGE = '/offline.html';
// Pages that are available offline right after installing
var START_PAGES = ['/?ec_plan&ec_app=1', '/?ec_speakers&ec_app=1'];
// A redirect (for example to your own start page) must not stay stored,
// the browser would reject it offline
function plain(response) {
if (!response.redirected) return response;
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: response.headers });
}
function save(key, response) {
if (response.status === 200) {
var copy = response.clone();
caches.open(CACHE_NAME).then(function (cache) { return cache.put(key, copy); }).catch(function () {});
}
return response;
}
self.addEventListener('install', function (event) {
event.waitUntil(caches.open(CACHE_NAME).then(function (cache) {
return Promise.all(START_PAGES.concat(OFFLINE_PAGE).map(function (page) {
return fetch(page).then(function (response) {
return cache.put(page, plain(response));
}).catch(function () {});
}));
}).then(function () { return self.skipWaiting(); }));
});
self.addEventListener('activate', function (event) {
event.waitUntil(caches.keys().then(function (names) {
return Promise.all(names.filter(function (name) {
return name.indexOf('event-app-') === 0 && name !== CACHE_NAME;
}).map(function (name) { return caches.delete(name); }));
}).then(function () { return self.clients.claim(); }));
});
self.addEventListener('fetch', function (event) {
var request = event.request, url = new URL(request.url);
// Forms, other addresses, videos and the backend stay untouched
if (request.method !== 'GET' || url.origin !== self.location.origin || request.headers.has('range')) return;
if (/^/(wp-admin|wp-login)/.test(url.pathname)) return;
// Program and exhibitor data: network first, offline the last version
if (//wp-json/(event-calendar|event-exhibitor)/v1//.test(url.pathname)) {
url.searchParams.delete('t');
url.searchParams.delete('_t');
var key = url.toString();
event.respondWith(fetch(request).then(function (response) {
return save(key, response);
}).catch(function () {
return caches.match(key, { ignoreVary: true }).then(function (hit) {
return hit || caches.match(key, { ignoreSearch: true, ignoreVary: true });
}).then(function (hit) {
if (hit) return hit;
return url.pathname.indexOf('/event-calendar/') !== -1
? new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } })
: Response.error();
});
}));
return;
}
// Scripts, styles and images: instantly from storage, renewed in the background
if (/^/(wp-content|wp-includes)//.test(url.pathname)) {
event.respondWith(caches.match(request, { ignoreVary: true }).then(function (hit) {
var network = fetch(request).then(function (response) { return save(request, response); });
if (!hit) return network;
event.waitUntil(network.catch(function () {}));
return hit;
}));
return;
}
// Full-screen pages: network first, offline the last version or the note
if (request.mode === 'navigate') {
var isAppPage = /[?&](ec|eem)_/.test(url.search);
event.respondWith(fetch(request).then(function (response) {
return isAppPage ? save(request, response) : response;
}).catch(function () {
return caches.match(request, { ignoreVary: true }).then(function (hit) {
return hit || caches.match(OFFLINE_PAGE);
});
}));
}
});| Content | What happens |
|---|---|
| Program and exhibitor data | Always current with a network, the last loaded version without one. The time stamps t and _t are removed from the address for this, otherwise the version would never be found again. |
| Scripts, styles, images | Come instantly from storage and are renewed in the background. |
| Full-screen pages | Current with a network, the last visited version without one. |
| Forms, backend, other addresses | Stay untouched and are never stored. |
Step 4: Create the offline page
This page appears when a page is not available without a network. Save it as offline.html in the root folder.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>No connection right now</title>
</head>
<body style="font-family: system-ui, sans-serif; text-align: center; padding: 3rem 1rem;">
<h1>No connection right now</h1>
<p>This page has not been opened yet. Pages you have already visited keep working.</p>
<button onclick="location.reload()">Try again</button>
</body>
</html>Step 5: Connect to WordPress
The full-screen pages have no theme but output the usual WordPress areas. Manifest and service worker get into the page through these. Create the file event-app.php in the folder wp-content/mu-plugins/. If the folder does not exist yet, create it. Files there are active automatically and survive theme and plugin updates.
<?php
/**
* Plugin Name: Event App (PWA)
*/
add_action( 'wp_head', function () {
echo '<link rel="manifest" href="/manifest.json">' . "
";
echo '<meta name="theme-color" content="#f76b15">' . "
";
echo '<link rel="apple-touch-icon" href="/app-icons/icon-192.png">' . "
";
} );
add_action( 'wp_footer', function () {
echo "<script>if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); }</script>
";
} );Step 6: Test and install
- 1Clear the cache
Clear the cache of your caching plugin. Exclude
sw.jsandmanifest.jsonfrom optimization and do not combine the plugin scripts. - 2Check
Open the full-screen page in Chrome or Edge on a computer, press F12 and choose “Application”. “Manifest” must show no errors, and “Service Workers” must show
sw.jsas active. - 3Android
Open the full-screen page in the browser and choose “Install app” or “Add to Home screen” in the menu.
- 4iPhone and iPad
Open the page in Safari, tap “Share”, then “Add to Home Screen”.
- 5Computer
In Chrome or Edge, an install icon appears in the address bar.
- 6Test offline
Open the app, reload once and go through the pages. Then turn on airplane mode and open everything again.
On the very first visit, the service worker is only installing itself. It therefore stores this page’s scripts and images from the next visit on. Test while logged out, so the WordPress toolbar is not stored too.
What works offline
| Area | Behaviour without a network |
|---|---|
| Favorites | Works fully, as the list is stored on the device. |
| Live ticker | Once loaded, program items switch on time even without a network. Set the preload time in the “Live-Ticker & Monitor” tab to at least the length of the event. |
| Program and timetable | Show the state of the last retrieval. Changes arrive as soon as the network is back. |
| Speakers | The directory is available offline if the page was preloaded. Pop-ups only if they were opened before. |
| New pages | Pages never opened before show the offline note from step 4. |
Changing something later
If you change sw.js, raise the number in CACHE_NAME every time, for example from event-app-v1 to event-app-v2. The new service worker then cleans up the old storage itself. You maintain content in the plugin as usual. Guests receive it the next time they open the app with a network.
If something doesn’t work
| Problem | Solution |
|---|---|
| No install option | Check HTTPS and see in the developer tools whether manifest and icons are found. If the app is already installed, no option appears. |
| Service worker does not start | Is sw.js in the root folder? Open your-site.com/sw.js directly. An error page points to a wrong location or a block by a security plugin. |
| Changes to sw.js do not arrive | Raise CACHE_NAME and exclude sw.js from caching. |
| Calendar stays empty offline | Open the page once with a network, let it load fully, then test again. |
| Two navigation bars | If your app has its own bar, add &ec_app=0 to the start_url and to START_PAGES. |