Building Paul the Octopus

Development / 02.06.18
Mark Vaesen

Paul the Octopus is our World Cup predictions game, created in homage to the mystic mollusc.

It’s a fun little tournament that we ran every other year up to 2018, as both an internal and a wider-reaching contest.

The rules are pretty simple: predict the score for each game. A correct score nets you 3 points, a correct result gets 1 point. There are also a few challenges along the way to earn some bonus points.

Visit the site

Right, now the business is taken care of, let’s get technical.

Nuxt

The frontend of this site is built in Nuxt, a server-side rendering framework for Vue. It’s one of the most impressive frameworks we’ve worked in. All the great parts of Vue: mutable data, scoped components and state management, with all the benefits of server-side rendering: SEO, non-JS support and quick page loads.

Nuxt handles all the routing and data fetching in a really neat fashion. It also plays really nicely with Vuex, so globally shared data is a breeze to work with.

An example of this is the data source for teams. Most, but not all routes need access to that information. So instead of fetching teams from the API at the page-level, we request them from the store:

async asyncData({ store }) {
  const teams = await store.dispatch('getTeams')
  return {
    teams
  }
},

And in the store action, we check the state for teams before the request, preventing unnecessary future requests on other pages:

async getTeams ({ state, commit }) {
  if (state.teams.length) return state.teams
  const { data } = await axios.get('/teams')
  commit('addTeams', data)
  return data
}

API

The data is served from a Node.js/Postgres API with public read routes. The only downside of this project is that we have to update the score information at the end of each game. It’s been difficult to rely on other public APIs at previous tournaments.

Build-time API

Usually, Nuxt sites are hosted on a Node server. But given our recent fascination with static site generators, we decided to build this project using Nuxt’s generate command. This works by pre-rendering all the pages and saving them as .html files at build time. Not only does this reduce the hosting costs, but it seriously helps the site performance.

However, the biggest challenge was getting Nuxt to play ball (pardon the pun) with data fetching on the static site. For a standard nuxt generate build, the data is captured at build time, but only for initial page loads. If you load another page, then visit the first page, it re-requests the data from the live API. This wasn’t static enough for our liking.

Taking Charlie Hield’s Nuxt static project as a starting point, we were able to cache the data properly at build time. It works as follows:

Local development

You request data from the live API as ‘normal’.

At build time

When you run the generate command and axios fetches the data, it saves the response in /dist/data/{{ api_path }}.json before returning it to the page. So a call to the /teams endpoint turns into a JSON file saved in /dist/data/teams.json.

On the live site

When the page/store asks for /fixtures, an axios interceptor rewrites that to /dist/data/fixtures.json.

The up-side is another performance win. The pages no longer rely on a database. The only downside is a minute delay on data updates while the build script fetches the new data, but it’s a small price to pay.

Payloads

Requesting data for every dynamic route can be a bit slow, especially when there’s a good number of them. For the individual fixtures, we opted to create a build-time endpoint that would grab all the fixtures in one go, cutting down the HTTP requests from 60+ to one. The payload data is then available in the context for each page component.

In nuxt.config.js, the generate.routes method gathers all the data and passes it to each page.

generate: {
  async routes () {
    ...
    const { data } = axios.get('https://api.paultheoctop.us/build')
    data.forEach(fixture => routes.push({
      route: `/fixtures/${fixture.id}`,
      payload: fixture
    }))
    ...
  }
}
async asyncData({ params, store, payload }, callback) {
  const fixture = payload || await store.dispatch('getFixture')
  return { fixture }
})

Netlify Identity

Finally, authentication. No one likes building auth systems. Paul the Octopus’ creator, Trys Mudford, has gone into detail on a previous blog post so we won’t dwell on it all that long.

A particularly handy, but not overly documented feature was the webhook notifications. At every user validation & signup event, Netlify POST a webhook to my API where we can either respond with a 400 response code and message, or a 200 success code. This gives us the chance to link the Netlify user ID with the user in the database or reject the user signup.

Netlify Identity makes authentication; dare we say it, fun to work with! It slots into Vuex really well and is perfect for a static/hybrid site like this.

Conclusion

This was a big side project to take on, but it was well worth it. Pushing Nuxt generate to the limits and using Netlify Identity in the wild was a real treat.

And if you’re into football:

Visit paultheoctop.us