A Nuxt PWA to Improve Québec Citizens' Lives

One of the best feelings working in tech?

Using new tools to solve old problems.

When my colleague Gabriel told me how he built a city-wide application with Nuxt.js, I knew I had to write this up.

311 is Québec City’s official communication channel between over 530K citizens and city officials.

Photo by Jaël Vallée on Unsplash.

Need garbagemen to pick up oversized trash? Hit 311.

Flagging broken street lights and potholes? 311.

You get the idea.

311 receives north of 400K citizen requests per year, and only a small percentage of those are transmitted over the Internet.

Historically available via dedicated phone numbers, the platform has slowly made its way to the web. But like many other public instances, it’s been plagued with inefficiencies.

“Recent” advances in web development opened heaps of new possibilities, which eventually exacerbated said inefficiencies on the current platform:

For citizen users

  • No mobile version

  • Lack of mobile-first functionalities—camera + geolocalization

  • Painful latency on mobile connexions

  • Difficulty uploading files (images, videos)

  • Tiring user experience—click fest

  • No offline capabilities—distant areas, intermittent connexions

For city officials

  • Lack of cross-platform support

  • Tech stack lock-in

  • Difficulty maintaining current solution

  • Overhead for multi-lingual content management

  • No push notifications for warnings & follow-ups

Spektrum (Snipcart’s mother agency) was tasked with improving the 311 experience of both citizens and city officials.

Why Nuxt?

Good question.

Well, why the need for statically generated content in the first place?

Cross-platform support.

Here’s Gab with details:

To limit client costs and offer a solid mobile UX, I needed to be able to embed 100% of this app in Cordova. Traditional web apps in .NET, Ruby, or Python require a server to deliver their content. But a static website lives on its own. I must admit, though, that picking the right static generator was a bit… stressful? I mean, there are literally hundreds of them.

That moment when you need to pick a tech stack for your whole city.

At Snipcart, our team has tried dozens of static site generators. I remember Gab playing around with Gatsby in early 2016, before the cool kids got into it.

For 311, he experimented with VuePress, Jekyll, and Hugo. When I asked him why any of these didn’t make the cut, he told me:

Most of these SSGs (Static Site Generators) are kick*ss programs. But I needed dynamic and SEO-friendly features they were lacking. As a more extensive “framework”, Nuxt had everything I wanted.

So why Nuxt over Next, you ask? Simple: both Spektrum and Snipcart are now working with Vue, a lot.

It was only natural for Gab to keep using the tools he loved.

Vue is also a grassroots, independent initiative we prefer supporting.

If you want more info, Gregg Pollack breaks down the why of Nuxt wonderfully here.

For now let’s have a closer look at how Nuxt specifically helped for this project.

We’ll cycle through project goals and past-pains to do so:

  1. Supporting cross-platform

Pains to solve:

  • Lack of cross-platform support

Cross-platform development can be (too) expensive. To limit tech and $ overhead, Gab worked with Cordova.

By allowing us to generate the app statically, Nuxt makes it easy to use simple HTML/JS/CSS files and push them through Cordova. The latter then allows us to easily push native-like versions of the mobile browsers to both app stores (Android/Apple).

That way, we avoid making the City (and let’s be frank: its citizens) pay for one whole web app and two native mobile apps.

Building the two mobile apps with Cordova is rather simple. All of the app data is located inside a config.xml file. It contains all configurations the native app projects will be re-using. At its core, the file looks like this:

<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="30000" 
    <name short="311">
        311 Ville de Quebec

    <author email="311@ville.quebec.qc.ca" href="https://www.ville.quebec.qc.ca">
        Ville de Québec

    <content  src="https://311.ville.quebec.qc.ca/" />
    <allow-intent  href="tel:*"  />
    <allow-intent href="sms:*"  />
    <allow-intent  href="mailto:*"  />
    <allow-intent  href="geo:*"  />
    <preference  name="Orientation"  value="default" />
    <preference  name="Fullscreen"  value="true"  />

The most interesting properties here are:

  • id="ca.qc.quebec.ville.espacecitoyen"—unique identifier on Google Play and App Store.

  • ios-CFBundleVersion="0.3.0" / version="0.3.0" / android-versionCode="30000"—the app’s current versions.

  • <content src="https://311.ville.quebec.qc.ca/" />—the real PWA & Nuxt magic. Here we order loading of this page’s files, and then make them available offline. Without this line, we’d have an empty app & white page when offline. When the app is online, it re-fetch files on the server only when they’re needed. When the app is offline, it loads cached files.

Finally, apps that can be compiled by Android Studio & XCode must be generated. For Android, these terminal lines do the trick:

$ cordova platform add android
$ cordova build android
$ cordova prepare android

Very similar for iOS:

$ npm install -g ios-deploy --unsafe-perm=true
$ cordova platform add ios
$ cordova build ios
$ cordova prepare ios

And the City now has two mobile applications to submit to Google/Apple app stores!

  1. Creating a mobile-friendly experience

Pains to solve:

  • No mobile or responsive versions

  • Lack of mobile-first functionalities—camera + geolocalization

  • Painful latency on mobile connections

  • Difficulty to upload files (images, videos)

  • No offline capabilities—distant areas, intermittent connexions

  • No push notifications for warnings & follow-ups

Lack of mobile-first functionalities—camera + geolocalization

<div class="youtube-embed" data-video="https://youtu.be/7kHJ4ytnxyI"></div>
<script type="application/ld+json">
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": "Nuxt PWA mobile experience (311)",
  "description": "A quick video showing off some of the available functionalities on this Nuxt Progressive Web App (PWA).",
  "thumbnailUrl": [
  "uploadDate": "2020-07-13",
  "duration": "PT0M52S",
  "embedUrl": "https://www.youtube.com/embed/7kHJ4ytnxyI"

With JavaScript powering it, Nuxt makes it easy to access camera functionalities through Cordova’s wrapping. After detecting a native mobile usage of the app, we send a direct call to the device camera, bypassing browser prompts.

  function cameraSuccess(imageUri) {
    // send image to server
  function cameraError(error) {
    // notifiy error....
    quality: 75,
    destinationType: Camera.DestinationType.DATA_URL,
    sourceType: Camera.PictureSourceType.CAMERA,
    encodingType: Camera.EncodingType.PNG,
    mediaType: Camera.MediaType.ALLMEDIA,
    allowEdit: false,
    correctOrientation: true,

These native functionalities enable a better photo taking experience. You get more settings to improve photo quality for example. Such parameters aren’t available when using cameras through browsers like Chrome and Firefox.

No offline capabilities—distant areas, intermittent connexions

A dedicated PWA module for Nuxt makes managing browser caching really effective. Leveraged smartly, these configurations make way for performant offline applications. Here we use Nuxt PWA to create service workers and manifest.json. Basic configs are as simple as:

 manifest: {
    name: '311 - Ville de Québec',
    lang: 'fr'

No push notifications for warnings & follow-ups Note: this feature is in the codebase, but isn’t publicly available yet.

We handle push notifications with a library injected in the Cordova wrapper. Notifications are thus unavailable to citizens using the app on browsers.

// Library initialization

pushRegistration = PushNotification.init({
  android: { senderID: "---HIDDEN---" },

  ios: { alert: "true", badge: "true", sound: "true" },

pushRegistration.on("registration", function (data) {
  // Trigger event to store device identifier

    new CustomEvent("store-device", {
      detail: {
        id: data.registrationId,

        type: device.platform === "iOS" ? "Apple" : "Google",

  PushNotification.hasPermission(function (result) {
    if (result.isEnabled) {
      // Trigger device registration

      window.dispatchEvent(new CustomEvent("register-device"));

pushRegistration.on("notification", function (data, d2) {
  // Notify user using alert function.

  // If the application is closed, it will send native notification.

  1. Creating an easily portable & maintainable app

Pains to solve:

  • Tech stack lock-in

  • Difficulty maintaining current solution

  • Overhead for multi-lingual content management

The City handles tech projects like many other public organizations: it publishes a call for tenders with specs & parameters. Providers craft and submit quotes including costs, tech stacks, and timeline estimates. City officials, after evaluating and debating, settle on one provider.

From the average provider’s POV, maintainability isn’t mandatory. They build quotes around technologies they master—not ones they know to be scalable and maintainable. From the City’s POV, though, the latter are key: projects often go through a round 2 of development, i.e. a new call for tenders.

So what happens if the former provider is the only one fluent in the City’s project stack? Lock-in. With a technology, but also with a provider. Not good—if said provider jacks up prices, the City has its hands tied, and citizens get the bill.

Now the new Nuxt app stack is quite simple: JavaScript (TypeScript), HTML, and SCSS.

These allow the 311 team to improve and maintain the solution without having to learn new languages. And since these are only used for the app’s frontend, changing the backend later on will be easy. The only remaining link between the app and the City’s backend infrastructure is a REST API. Keeping its resources interface will enable simple stack modifications.

Nuxt effectively decouples your files from any type of server-side logic and apps, making it easy to take these statically generated files, and migrate them to another solution later on. So, for the City, hosting the application sources becomes way simpler: they can move all of their static files to any server they’d like (IIS, nginx, Apache, etc.).

Bonjour/Hi! Welcome to Québec

Québec is a bilingual city in a bilingual country.

Hence the need for multilingual support inside the app—in its content management layer, more specifically.

To make sure the Nuxt-driven app could eventually support this, Gab used POEditor.

POEditor is a developer-friendly translation tool.

It works with any type of localization file (JSON,RESX, PO, XML, etc.).

It also uses a language file as source code (JSON, RESX, PO, XML, etc.). Such files are located in a Git repo—hence its appeal here.

Once a translation is ready for deployment, the user hits a button, which triggers a commit to the Git repo. The latter sets off a hook starting a build to the CI (Appveyor, GitLab CI, Travis, etc.). Unit, integration, and acceptation tests roll to make sure translation changes haven’t affected the product’s quality. After that thorough, automated process, translations are deployed live on the platform.

POEditor specifics are out of scope here, but I’ll still leave you with these gorgeous UI screenshots:

Closing thoughts


Public organizations like governments, cities, and NGOs operate under logistical constraints when picking service providers: successive call for tenders, rigid decisional guidelines around specs, budgets, deadlines, etc.

The Jamstack, with its focus on API-driven modularity and static frontends, brings much needed flexibility to this rigid system. The decoupling of building/hosting, of backend data/frontend files, makes it way easier to migrate code from one provider to another, from one stack to another.

I encourage developers and agencies to consider this development paradigm more seriously.

Here is a primer on the Jamstack.


After meeting the founders in Toronto and chatting with my team, I think it’s safe to say Nuxt is going places. That funding announcement will help! Some cool stuff to expect:

  • TypeScript as first-class citizen

  • Better community modules & sample apps (e.g. auth-module could be improved)

  • Stronger, larger community

  • Becoming the “go-to framework” for complex Vue.js applications

For those interested in getting started with Nuxt and PWAs:

Happy coding! 🙇‍♂️

About the author

François Lanthier Nadeau
CEO, Snipcart

Francois has worked in SaaS & digital marketing for over 7 years. He’s been published on Indie Hackers, The Startup, freeCodeCamp, Baremetrics, Wishpond, and Growth.org—among others. He’s spoken at 13+ startup and web development conferences in Canada, U.S.A., and Europe. He's been a vocal bootstrapping and Jamstack proponent for years.

Follow him on Twitter.

Shopify Buy Button vs. Snipcart: The Side-By-Side Comparison

Read next from François
View more

36 000+ geeks are getting our monthly newsletter: join them!