Come into my web

A progressive web app for Dark Mofo

Dark Mofo is a music and arts festival that delves into centuries-old winter solstice rituals, exploring the links between ancient and contemporary mythology, humans and nature, religious and secular traditions, darkness and light, and birth, death and renewal.

It seemed that the stars aligned, when Apple announced support for much of the technology that powers Progressive Web Apps (PWA) in the months leading up to the festival.

The second coming?

Why we sold our souls to the PWA gods

Easily updatable - No app-store gatekeepers. Updates are automatic for users.

- No app-store gatekeepers. Updates are automatic for users. One code base works consistently across devices.

works consistently across devices. Data-friendly . ~125kb app.js contained all markup, js and css. ~1.4MB JSON payload of all text content.

. contained all markup, js and css. JSON payload of all text content. Flexibility to transition from early announcements, to ticket sales, to an on-ground tool.

💖🖤💖

Caching with Cloudfront and Service Workers

The Dark Mofo PWA is built with Node, Next.js, and React, powered by a REST API. Cloudfront sits on top of both our REST API and our Node server to ensure reliable uptime and speed.

On the frontend, we utilise a combination of both pre and runtime caching via SW Precache. The precaching allows us to cache our app shell, along with dependencies like custom fonts and the core mapbox library. Runtime caching is configured to cache additional API calls (for both content and map functionality).

We can detect when our PWA is installed on a user's homescreen via a combination of the matchMedia API and window.navigator.standalone. The latter is marked as Non-standard with poor cross-browser support, but I found this is the most reliable with iOS PWA detection.

isStandalone() { return ( window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone // iOS ) }

Then, we request a full sitemap from our API. This JSON payload totals ~1.4 MB. Once this cache completes, all of the site's content is available to use in spotty or non-network conditions. This cache can be invalidated if needed when updating the frontend of our app.

Only JSON data is saved into our cache. All imagery, video and audio is ignored - this is to keep cache storage to a minimum, as device storage varies greatly between OS and device. Imagery, video, and audio still work via vanilla network requests when user's have a network connection.

sitemap: [ "/", "/program/", "/program/st-vincent/", "/program/electric-wizard/", "/program/alice-glass-plus-zola-jesus/", "/program/retribution-tanya-tagaq/", "/program/nanook-of-the-north-tanya-tagaq/", "/program/red-bull-music-presents-jagwar-ma/", ... ]

✨✨✨SW trend of 2018✨✨✨

Precincts & Mapbox

Dark Mofo completely engulfs Hobart, and there are 40+ different venues across the city. There is also the concept of precincts, groupings of multiple venues and/or artwork installations.

Mapbox allows an incredible amount of control over how our custom maps look and function. Working with design, we've established a workflow using Mapbox Studio that empowers design to go absolutely crazy with custom line work, shapes, patterns.

These groupings are then imported into our React app, where we can control when and how they display, as well as overlay custom React markers, panel overlays, and other UI elements.

const precincts = ['night-mass', 'dark-park', 'invisible-house', 'art-walk'] this.showPrecinct('night-mass')

Users are able to plot their location in the city by tapping the crosshairs. This triggers the watchPosition() method on the HTML5 Geolocation API, which updates in sync with the device's internal GPS.

Between 3 and 6am, drunk() mode is enabled as a gentle reminder for people to get home safe.

isDrunk = () => { this.setState({ drunk: true }, () => { setInterval(() => { this.map.rotateTo(this.map.getBearing() + 0.5, { duration: 0 }) }, 30) }) return 🍺🍺🍺 }

Planner & Firebase

Users are able to manage the events/performances they are attending with the planner. I was quite excited to give the new React Context API a spin. It is incredibly straightforward and much less of a nightmare than I was expecting!

Our DarkProvider wraps our entire app. DarkConsumer then can be used anywhere we need to share planner state and/or actions.

const Page = () => ( <DarkConsumer> {({ planner, status, actions }) => ( <Planner planner={planner} status={status} actions={actions} /> )} </DarkConsumer> )

Planner data is stored locally, as well as into Firebase, if user's opt in. Planners can be synced from multiple devices using the EventSource API, which comes out of the box with Firebase's real-time databases.

Truly satanic

As of this writing, there are a few things still on shaky ground when it comes to PWA's.

Splash screens

Apple PWA's do not respect icon/splash screen rules as android devices do (via the manifest.json). Android shows icon, background color, and app title. Apple (on right), show's a white screen (Safari's default). I have seen scattered documentation on how to enable iOS splash screens, but had zero luck in implementing it myself.

⚡️🧚‍ BREAKING NEWS 🧚‍⚡️ Thanks to help from the Twittersphere, this issue is now resolved. This post describes how iOS splash screens can be created and linked! Truly a godsend!

App state & routing

Also, Apple PWA's will do a full restart every time the app is minimised then expanded again. This is incredibly annoying as no app state is persisted. We were able to counteract this functionality by storing the last viewed route to a cookie - then on startup of PWA, we could reinstate this route when the app mounts.

Router.push('/', Cookies.get('lastViewed'))

Dark Mofo runs 7-24 of June 2018. Hobart, Tasmania.