Andrew Welch · Post-Mortems · #vuejs #graphql #post-mortem

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

Post-Mortem: Outbreak Database

Mod­ern­iz­ing an aging cus­tom PHP web­site with Craft CMS for con­tent man­age­ment, and a hybrid Twig/Vue.js + Vuex + Axios + GraphQL on the frontend

Disease outbreak database

Relat­ed talk: Solv­ing Prob­lems with Mod­ern Tooling

I was con­tact­ed to do over­flow work for a free­lancer who found him­self in the envi­able posi­tion of hav­ing too much work booked.

The project was some­thing that will be famil­iar to most web devel­op­ers, which was to take an old web­site Out​break​Data​base​.com and mod­ern­ize it.

This arti­cle describes the high­er-lev­el deci­sions made while work­ing on the project; if you want to get into the tech­ni­cal imple­men­ta­tion, check out the Using the Craft CMS head­less” with the GraphQL API article.

N.B.: While my role on the project is fin­ished, the project may or may not be live at the time of this writing.

The custom-built Cake PHP website was starting to show its age, both visually and technologically.

The client want­ed a web­site that was eas­i­er for con­tent authors to main­tain the hygiene of the data in the out­break data­base, and the site just need­ed an over­all refresh to car­ry it for­ward for the next 10 years.

The web­site describes itself thusly:

Outbreak Database is a resource that provides access to food poisoning outbreak data in one easy to search place, dating back to 1993.

They just did­n’t want the web­site to look like it dat­ed back to 1993.

Original outbreak database website

Orig­i­nal Out­break Data­base website

Link The Initial Handoff

The design for the web­site was already done, and the less inter­est­ing (to me any­way) work of data migra­tion to Craft CMS was done already as well.

Bonus for me.

I was giv­en access to the exist­ing site, a CSS file that was being used to style this project and sev­er­al oth­er mini-site” projects for the client, and some Twig tem­plates that showed the mocked out design.

The clients goals were:

  • Make the out­break data­base eas­i­er to main­tain for the con­tent authors
  • Make the fron­tend eas­i­er to use by researchers and journalists
  • Mod­ern­ize the web­site underpinnings
  • Poten­tial­ly pro­vide an API to allow oth­er par­ties to access the data­base directly

Oth­er than that, I was giv­en pret­ty much free rein to do what­ev­er I thought was best. Which is a lev­el of trust I real­ly enjoy in my rela­tion­ship with the orig­i­nal free­lance developer.

Luck­i­ly for me, using Craft CMS as a back­end ensures that the first two bul­let points are already tak­en care of by Craft CMS’s excel­lent con­tent mod­el­ing & author­ing capabilities.

As I do for any project I work on, I spend a bit of time upfront learn­ing about the client, their goals, etc. The nor­mal stuff.

Then I sit down to think about what tech­nolo­gies and tech­niques I could apply to help them reach their goals.

Link GraphQL as an API

While the actu­al design of the web­site was not in my con­trol, the tech­no­log­i­cal under­pin­nings of the web­site and the user expe­ri­ence def­i­nite­ly was.

I want­ed to use GraphQL over the Ele­ment API not just because it was less work, but because it pro­vid­ed a self-doc­u­ment­ed, strict­ly typed API for us auto­mat­i­cal­ly. GraphQL is a doc­u­ment­ed, wide­ly embraced stan­dard, so plen­ty of learn­ing mate­ri­als are available.

Since the client had a stat­ed inten­tion of want­i­ng to be able to pro­vide oth­ers access to the data­base, I imme­di­ate­ly thought of GraphQL.

It was a nice, clean, mod­ern way to present stan­dard­ized access to data, that allows researchers to query for just the data that they are look­ing for. Since Pix­el & Ton­ic had recent­ly released a first-par­ty GraphQL imple­men­ta­tion for Craft CMS 3.3, it seemed like a lock.

There was a rub, however.

At the time, the GraphQL imple­men­ta­tion did­n’t sup­port query­ing based on cus­tom fields, which we need­ed for the faceted search. So we were left with the prospect of:

So like any respon­si­ble devel­op­er, I went with ???. Which in this case meant fil­ing some issues for the Craft CMS devel­op­ers to see if the con­cerns could be addressed.

For­tu­nate­ly, we weren’t the only devel­op­ers want­i­ng this func­tion­al­i­ty, so Andris rolled his sleeves up and got it imple­ment­ed in Craft CMS 3.4.

We were in business.

Link Adopting Vue + Vuex + Axios

Since we’d already decid­ed on GraphQL as an API, I thought the best way to ensure we were build­ing out an API oth­ers could access would be to con­sume that API ourselves.

So instead of using Craft’s built-in Ele­ment Queries for access­ing data via Twig, I adopt­ed Vue.js and Axios.

We’d use Vue to help make writ­ing the inter­ac­tive UI eas­i­er to do, and Axios to send along our GraphQL queries to the Craft CMS backend.

Vuex is a glob­al data store that we’d lever­age to stash the data fetched via Axios, and make it avail­able to all of our Vue.js components.

Here’s what the orig­i­nal web­site UX was like for searching:

So pret­ty typ­i­cal for an old­er web­site design: a form where you blind­ly enter search cri­te­ria, click the Search but­ton, and a results page shows up.

If you make a mis­take, or don’t find what you want, you hit the back but­ton, and try again.

The new design and UX hand­ed off to me looked visu­al­ly nicer:

Updated outbreak database design

Updat­ed Out­break Data­base Design

While this looks bet­ter, it oper­at­ed much the same: enter your search cri­te­ria, click a but­ton, go to a search results page. Hit the back but­ton to try again if you don’t get what you want.

I thought we could do bet­ter, and Vue.js + Vuex + Axios + GraphQL would make doing that easier.

Link Doing Better

A great part of my sat­is­fac­tion work­ing on ren­o­vat­ing old­er sites is the goal of mak­ing the world just a lit­tle bit bet­ter. We don’t always hit the mark dead-on, but striv­ing to improve things is what moti­vates me.

So here’s what we end­ed up with:

First I elim­i­nat­ed the search results page”; instead, the search results would be dis­played inter­ac­tive­ly right below the query. As soon as you start typ­ing, it starts search­ing (debounced of course), and a lit­tle spin­ner shows you so (thanks, vue-sim­ple-spin­ner).

Click­ing on the Search but­ton or hit­ting the Return/​Enter key would smooth­ly auto­scroll (thanks, vue2-smooth-scroll) to view the search results.

Graphql debounced search

Spin­ner for the Inter­ac­tive Search

I think the UI should be reworked a bit to make this a lit­tle less bulky so we can see more of the search results, but already I think we have a nice improvement.

Peo­ple can inter­ac­tive­ly see the results of their search query, and make adjust­ments as need­ed with­out hop­ping back and forth between pages.

But we did­n’t want to lose the abil­i­ty of being able to copy a search result from the address bar, and send it to col­leagues. So a lit­tle mag­ic was done to update the address bar with a prop­er search?keywords= URL.

Next up was to elim­i­nate some of the I don’t know what to search for” prob­lem. Instead of pro­vid­ing just an emp­ty box where you type what cri­te­ria you want, we’d pro­vide an auto-com­plete lookup of avail­able choic­es (thanks, @trevoreyre/autocomplete-vue):

Graphql autocomplete search

Auto­com­plete Lookup

I think this helps great­ly with the UX, because researchers can just start typ­ing, and they’ll see a list of pos­si­ble things they can choose from.

This also adds some trans­paren­cy to the data­base hygiene, and allows the con­tent authors to eas­i­ly see dupli­cat­ed data.

Link The CSS Problem

When­ev­er I start on a new project, I great­ly look for­ward to refac­tor­ing the site to use Tail­wind CSS. If you’re not on-board the Tail­wind express yet, do give it a look, I’ve yet to know of any­one who has used it, and moved back to a more tra­di­tion­al BEM approach.

I’d be will­ing to use some pro-bono hours to do the refac­tor­ing myself if it isn’t includ­ed in the project. But in this case, the CSS was being used on a num­ber of sites to give them all a sim­i­lar look.

So even if I did the CSS refac­tor­ing to Tail­wind CSS on my own time, it would­n’t mesh well with their goals of hav­ing one CSS file for mul­ti­ple sites.

So I decid­ed to roll their CSS in as legacy/styles.css and use my nor­mal Tail­wind CSS + PurgeC­SS set­up to to over­ride styles or add new styles:

/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
 @import 'tailwindcss/base';

/**
 * This injects any component classes registered by plugins.
 *
 */
@import 'tailwindcss/components';

/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';

/**
 * Legacy CSS used for the project, rather than rewriting it in Tailwind
 */
@import './legacy/styles.css';

/**
 * Include styles for individual pages
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 */
@import './vendor.pcss';

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 */
@import 'tailwindcss/utilities';

/**
 * Forced overrides of the legacy CSS
 */
@import './components/overrides.pcss';

This gives me the best of both worlds:

  • I can use Tail­wind CSS’s util­i­ty class­es for addi­tion­al styling or to over­ride the base CSS as needed
  • The exist­ing lega­cy styles.css is import­ed whole­sale, so they can update it as they see fit

Link Hybrid Website

This web­site is what I’d term a hybrid” web­site, in that it uses both Twig and Vue to ren­der content. 

It was done this way for prac­ti­cal rea­sons. The project was already using Twig to ren­der pages, and the bud­get was­n’t there to redo the tool­ing to use JAM­stack with some­thing like Grid­some. The ben­e­fits of doing so were also dubi­ous in this case.

So instead we dropped Vue.js into the mix just for the dynam­ic com­po­nents on the page. For exam­ple, this is what the home­page looks like:

{% extends "_layouts/generic-page-layout.twig" %}

{% block headLinks %}
    {{ parent() }}
{% endblock headLinks %}

{% block content %}
    <div class="section--grey-pattern section--grey-pattern-solid section--mobile-gutter-none"
         style="min-height: 648px;"
    >
        <div id="component-container">
        </div>
    </div><!-- /.section-/-grey-pattern -->
{% endblock %}

{% block subcontent %}
{% endblock %}

{# -- Any JavaScript that should be included before </body> -- #}
{% block bodyJs %}
    {{ parent() }}
    {{ craft.twigpack.includeJsModule("home.js", true) }}
{% endblock bodyJs %}

This is using the Twig tem­plate set­up described in the An Effec­tive Twig Base Tem­plat­ing Set­up arti­cle, and the <div id="component-container"> is where the Vue instance mounts:

// Home page
import { OutbreakMixins } from '../mixins/outbreak.js';
import { createStore } from '../store/store.js';
import '@trevoreyre/autocomplete-vue/dist/style.css';

// App main
const main = async() => {
    // Async load the vue module
    const [ Vue, VueSmoothScroll ] = await Promise.all([
        import(/* webpackChunkName: "vue" */ 'vue'),
        import(/* webpackChunkName: "vue" */ 'vue2-smooth-scroll'),
    ]);
    const store = await createStore(Vue.default);
    Vue.default.use(VueSmoothScroll.default);
    // Create our vue instance
    const vm = new Vue.default({
        render: (h) => {
            return h('search-form');
        },
        mixins: [OutbreakMixins],
        store,
        components: {
            'search-form': () => import(/* webpackChunkName: "searchform" */ '../../vue/SearchForm.vue'),
        },
    });

    return vm;
};

// Execute async function
main().then((vm) => {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

This means that our Vue com­po­nents are not ren­dered until Vue & our com­po­nents are loaded, exe­cut­ed, and mount­ed. How­ev­er the result­ing web­site still per­forms nicely:

Outbreak database page speed

Out­break Data­base PageSpeed

So it was done this way in a nod to prac­ti­cal­i­ty, but should the client wish to jump to a full JAM­stack set­up in the future, we’re more than halfway home already.

This tech­nique was described in the Using Vue­JS 2.0 with Craft CMS and Using Vue­JS + GraphQL to make Prac­ti­cal Mag­ic arti­cles if you want to learn more.

Anoth­er tech­nique that can be used with Vue­JS specif­i­cal­ly is Inline Tem­plates, which puts the HTML for the tem­plate inline (hence the name) so it’s part of the ren­dered page even before the Vue­JS com­po­nent mounts.

Link Final Thoughts

No project is ever per­fect, espe­cial­ly soft­ware devel­op­ment projects. But I feel like the high­er lev­el deci­sions made helped to improve this project overall.

It’s a good exam­ple of how pick­ing the right bits of tech­nol­o­gy can enable you to cre­ate an improved end result.