Program Planner DOCS & HELP

Setting up the full-screen pages as a PWA

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.

What the plugin provides

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.php is.
  • WordPress sits directly in the root folder. If it is in a subfolder, you need to adapt all paths below.
FilePurposeLocation
manifest.jsonThe app’s “ID card”: name, icon, colour, start pageRoot folder
sw.jsThe service worker: stores content on the device so it is available without a networkRoot folder
offline.htmlNote shown when a page is not available offlineRoot folder
app-icons/The app iconsFolder in the root folder
event-app.phpConnects everything to WordPresswp-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.

manifest.json
{
  "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.

sw.js
// 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);
      });
    }));
  }
});
ContentWhat happens
Program and exhibitor dataAlways 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, imagesCome instantly from storage and are renewed in the background.
Full-screen pagesCurrent with a network, the last visited version without one.
Forms, backend, other addressesStay 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.

offline.html
<!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.

wp-content/mu-plugins/event-app.php
<?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

  1. 1
    Clear the cache

    Clear the cache of your caching plugin. Exclude sw.js and manifest.json from optimization and do not combine the plugin scripts.

  2. 2
    Check

    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.js as active.

  3. 3
    Android

    Open the full-screen page in the browser and choose “Install app” or “Add to Home screen” in the menu.

  4. 4
    iPhone and iPad

    Open the page in Safari, tap “Share”, then “Add to Home Screen”.

  5. 5
    Computer

    In Chrome or Edge, an install icon appears in the address bar.

  6. 6
    Test offline

    Open the app, reload once and go through the pages. Then turn on airplane mode and open everything again.

Why reload once?

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

AreaBehaviour without a network
FavoritesWorks fully, as the list is stored on the device.
Live tickerOnce 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 timetableShow the state of the last retrieval. Changes arrive as soon as the network is back.
SpeakersThe directory is available offline if the page was preloaded. Pop-ups only if they were opened before.
New pagesPages 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

ProblemSolution
No install optionCheck 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 startIs 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 arriveRaise CACHE_NAME and exclude sw.js from caching.
Calendar stays empty offlineOpen the page once with a network, let it load fully, then test again.
Two navigation barsIf your app has its own bar, add &ec_app=0 to the start_url and to START_PAGES.