Post-Mortem: Outbreak Database
Modernizing an aging custom PHP website with Craft CMS for content management, and a hybrid Twig/Vue.js + Vuex + Axios + GraphQL on the frontend
Andrew Welch / nystudio107
Related talk: Solving Problems with Modern Tooling
I was contacted to do overflow work for a freelancer who found himself in the enviable position of having too much work booked.
The project was something that will be familiar to most web developers, which was to take an old website OutbreakDatabase.com and modernize it.
This article describes the higher-level decisions made while working on the project; if you want to get into the technical implementation, check out the Using the Craft CMS “headless” with the GraphQL API article.
N.B.: While my role on the project is finished, the project may or may not be live at the time of this writing.
The client wanted a website that was easier for content authors to maintain the hygiene of the data in the outbreak database, and the site just needed an overall refresh to carry it forward for the next 10 years.
The website describes itself thusly:
They just didn’t want the website to look like it dated back to 1993.
The Initial Handoff
The design for the website was already done, and the less interesting (to me anyway) work of data migration to Craft CMS was done already as well.
Bonus for me.
I was given access to the existing site, a CSS file that was being used to style this project and several other “mini-site” projects for the client, and some Twig templates that showed the mocked out design.
The clients goals were:
- Make the outbreak database easier to maintain for the content authors
- Make the frontend easier to use by researchers and journalists
- Modernize the website underpinnings
- Potentially provide an API to allow other parties to access the database directly
Other than that, I was given pretty much free rein to do whatever I thought was best. Which is a level of trust I really enjoy in my relationship with the original freelance developer.
Luckily for me, using Craft CMS as a backend ensures that the first two bullet points are already taken care of by Craft CMS’s excellent content modeling & authoring capabilities.
As I do for any project I work on, I spend a bit of time upfront learning about the client, their goals, etc. The normal stuff.
Then I sit down to think about what technologies and techniques I could apply to help them reach their goals.
GraphQL as an API
While the actual design of the website was not in my control, the technological underpinnings of the website and the user experience definitely was.
I wanted to use GraphQL over the Element API not just because it was less work, but because it provided a self-documented, strictly typed API for us automatically. GraphQL is a documented, widely embraced standard, so plenty of learning materials are available.
Since the client had a stated intention of wanting to be able to provide others access to the database, I immediately thought of GraphQL.
It was a nice, clean, modern way to present standardized access to data, that allows researchers to query for just the data that they are looking for. Since Pixel & Tonic had recently released a first-party GraphQL implementation for Craft CMS 3.3, it seemed like a lock.
There was a rub, however.
At the time, the GraphQL implementation didn’t support querying based on custom fields, which we needed for the faceted search. So we were left with the prospect of:
- Writing a custom Element API implementation
- Using Mark Huot’s CraftQL plugin
- ???
So like any responsible developer, I went with ???. Which in this case meant filing some issues for the Craft CMS developers to see if the concerns could be addressed.
Fortunately, we weren’t the only developers wanting this functionality, so Andris rolled his sleeves up and got it implemented in Craft CMS 3.4.
We were in business.
Adopting Vue + Vuex + Axios
Since we’d already decided on GraphQL as an API, I thought the best way to ensure we were building out an API others could access would be to consume that API ourselves.
So instead of using Craft’s built-in Element Queries for accessing data via Twig, I adopted Vue.js and Axios.
We’d use Vue to help make writing the interactive UI easier to do, and Axios to send along our GraphQL queries to the Craft CMS backend.
Vuex is a global data store that we’d leverage to stash the data fetched via Axios, and make it available to all of our Vue.js components.
Here’s what the original website UX was like for searching:
So pretty typical for an older website design: a form where you blindly enter search criteria, click the Search button, and a results page shows up.
If you make a mistake, or don’t find what you want, you hit the back button, and try again.
The new design and UX handed off to me looked visually nicer:
While this looks better, it operated much the same: enter your search criteria, click a button, go to a search results page. Hit the back button to try again if you don’t get what you want.
I thought we could do better, and Vue.js + Vuex + Axios + GraphQL would make doing that easier.
Doing Better
A great part of my satisfaction working on renovating older sites is the goal of making the world just a little bit better. We don’t always hit the mark dead-on, but striving to improve things is what motivates me.
So here’s what we ended up with:
First I eliminated the “search results page”; instead, the search results would be displayed interactively right below the query. As soon as you start typing, it starts searching (debounced of course), and a little spinner shows you so (thanks, vue-simple-spinner).
Clicking on the Search button or hitting the Return/Enter key would smoothly autoscroll (thanks, vue2-smooth-scroll) to view the search results.
I think the UI should be reworked a bit to make this a little less bulky so we can see more of the search results, but already I think we have a nice improvement.
People can interactively see the results of their search query, and make adjustments as needed without hopping back and forth between pages.
But we didn’t want to lose the ability of being able to copy a search result from the address bar, and send it to colleagues. So a little magic was done to update the address bar with a proper search?keywords= URL.
Next up was to eliminate some of the “I don’t know what to search for” problem. Instead of providing just an empty box where you type what criteria you want, we’d provide an auto-complete lookup of available choices (thanks, @trevoreyre/autocomplete-vue):
I think this helps greatly with the UX, because researchers can just start typing, and they’ll see a list of possible things they can choose from.
This also adds some transparency to the database hygiene, and allows the content authors to easily see duplicated data.
The CSS Problem
Whenever I start on a new project, I greatly look forward to refactoring the site to use Tailwind CSS. If you’re not on-board the Tailwind express yet, do give it a look, I’ve yet to know of anyone who has used it, and moved back to a more traditional BEM approach.
I’d be willing to use some pro-bono hours to do the refactoring myself if it isn’t included in the project. But in this case, the CSS was being used on a number of sites to give them all a similar look.
So even if I did the CSS refactoring to Tailwind CSS on my own time, it wouldn’t mesh well with their goals of having one CSS file for multiple sites.
So I decided to roll their CSS in as legacy/styles.css and use my normal Tailwind CSS + PurgeCSS setup to to override 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 Tailwind CSS’s utility classes for additional styling or to override the base CSS as needed
- The existing legacy styles.css is imported wholesale, so they can update it as they see fit
Hybrid Website
This website is what I’d term a “hybrid” website, in that it uses both Twig and Vue to render content.
It was done this way for practical reasons. The project was already using Twig to render pages, and the budget wasn’t there to redo the tooling to use JAMstack with something like Gridsome. The benefits of doing so were also dubious in this case.
So instead we dropped Vue.js into the mix just for the dynamic components on the page. For example, this is what the homepage 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 template setup described in the An Effective Twig Base Templating Setup article, 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 components are not rendered until Vue & our components are loaded, executed, and mounted. However the resulting website still performs nicely:
So it was done this way in a nod to practicality, but should the client wish to jump to a full JAMstack setup in the future, we’re more than halfway home already.
This technique was described in the Using VueJS 2.0 with Craft CMS and Using VueJS + GraphQL to make Practical Magic articles if you want to learn more.
Final Thoughts
No project is ever perfect, especially software development projects. But I feel like the higher level decisions made helped to improve this project overall.
It’s a good example of how picking the right bits of technology can enable you to create an improved end result.
Further Reading
If you want to be notified about new articles, follow nystudio107 on Twitter.
Copyright ©2020 nystudio107. Designed by nystudio107
Top comments (0)