5 minute read

Why I moved my personal website to Gatsby.js + Github

Why I moved my personal website to Gatsby.js + Github Pages.
gatsbygit
Built with Gatsby.js and Github.

I used to have an old static website (from 10 years ago .. static html files, some jquery etc) for the usual task of keeping track of my work and online presence. Due to how traditional web hosting services work, it has been a challenge (read: not fun) updating it. I recently had some time and decided to make the website more interactive + include a modern development experience (as of december 2019).

Reasons I Moved!

There are many good reasons (quite a bit has been written on this) to use Gatsby + Github Pages, but here are some that made sense for my use case (personal website + blog).

Improved Workflow

With my previous hosting provider, updates were too effortful. Original process -> modify html, test locally, sign in and upload to a hosting server via ftp or upload via hosting provider UI. With Gatsby + Github, updating after the initial setup is more straight forward/familiar and takes advantage of my daily dev workflow -> modify files locally and deploy via a push to Github.

Interactive, Data Driven Views

Building and testing interactive, data driven features where a hassle. The old website used custom html/css built from an era where libraries like React were not a thing. Gatbsy is based on React and brings the power of reactive design (see the filtering on the publications page). Being able to define a data model and render/update views by changing the underlying data model provides an really great workflow. This and a set of other features IMHO distinguishes Gatsby from similar frameworks like Next.js, Hugo, Jekyll and Nuxt.js.

Integrated Website + Blog

I needed a way to consolidate my work into a clear connected story. I have used Github pages to host a few demo applications in the past. The hope is that hosting the website on Github for my website is a chance to connect all these otherwise disjoint dots. By using my domain name victordibia.com as a custom domain name with Github Pages, urls like victordibia.github.io/handtrack.js/#/ now automatically resolve to https://victordibia.com/handtrack.js/#/.

The old website also did not have an integrated blog. In the recent past, I have used medium to write for the datascience audience. However, medium is changing as a platform, and the sheer volume of posts authored there has exploded (hence poor signal to noise ratio). This makes it hard to organize posts and content. By default, Gatsby supports blogs (more like generation of linked pages from static files) and provides quick start examples.

SSL

Github pages provides SSL out of the box and allows you to enforce HTTPS for all connections. This is great as my previous hosting provider did not provide this by default. Gets rid of the annoying connection not secure message my website used to have.

Free

I appreciate tools like Gatsby that are high quality - and free!

React(ive) Components Everywhere!

Did I forget to mention that the nicest part of the experience so far is the ability to build and embed React Components anywhere within the Gatsby website? To explore that, below is a React Component I built as part of the Anomagram demo which is a direct manipulation interface for visually specifying the architecture of an autoencoder.

jsx
import { ComposeModel } from " ../components/composeModel.js";
<ComposeModel />;

Right here in this blog post (using the import statements above), you can:

  • Add/Remove layers to the model architecture
  • Add/Remove units from each layer. (you can't train the model here in this blog post, but you can do that in the Anomagram demo, right in the browser!)

Go ahead and try it!

Encoder 4 Layers
Decoder 4 Layers
input units
9 units
6 units
4 units
3 units
2 units
3 units
4 units
6 units
9 units
output units

Getting it Done!

Gatsby

I learned to use React sometime in early 2019 (first project here .. cringe) and have found it to be an excellent tool for authoring interactive web experiences. As always, my approach to learning Gatsby was the learn on demand approach where I do a quick survey (quickstart on the documentation) and then deep dive into aspects as I need them (e.g. modifying gatsby-config.js, gatsby-node.js for backend processing such as extracting file modification dates, optimizing images with gatsby-image etc.). I also started out by searching for an open source template which I can customize to fit my needs.

I was able to get a skeleton going over a weekend.

  • Review Gatsby quick start documentation. I really recommend going through this section to understand the basics - how to setup your Gatsby dev environment, setup up pages, use plugins, query for data etc. If you are not familiar with React already, an introduction to react course is in order.
  • Hunt for a teamplate to start with! Gatsby offers a gallery of templates spanning websites for a tonne of use cases such as education, design portfolio, blogs etc. Jeff Jadulco's template worked great here (it is a personal website template and implements dark mode!).
  • Modify template - build custom layouts as needed. I mainly focused on building pages for my publications and projects and an underlying json data model to support them. It took about a week to really polish and finetune these sections.
  • Push to gh-pages!

Github (Pages)

Building a Gatsby project generates a set of static files (usually in the public folder) which can be then be pushed to Github and hosted on Github pages. Gatsby provides some guidance on how to do this. The one thing to note is to ensure you add pathPrefix to gatsby-config.js and correctly specify the name of the Github repo used to host your content if you are hosting on Github pages domain.

json
// package.json ...
"deploy": "gatsby build --prefix-paths && gh-pages -d public"

Final piece (optional) was to map Github to my website's custom domain. If you are using a custom domain, you might have to include a CNAME file in your gh-pages commit to ensure it always points to your custom domain. I do this by modifying the npm run deploy script command in package.json - to always copy my CNAME file to the public folder before deploying the gh-pages branch.

json
// package.json ...
"deploy": "gatsby build && cp CNAME public && gh-pages -d public"

Interactive Filtering Example

A couple of people have reached out to ask about how the publications page is created. I'd like to add some pointers below on the general approach to building this:

  • Primer on how React works! React explores the idea of state - observable variables that are monitored; changes to this variable can then be used to drive behaviours. This simple concept can really simplify how we build interfaces. For example, imagine that you set a state variable that is an array, and you bind this variable to a view/component/ui within your page. To update your page, you update your state array and the view is re-rendered. These two resources on intro to React and React Hooks are recommended.

  • With the above being said, you can think of project filtering as an exercise in managing some state variable. Let's go through some steps

    • We create an array of objects, where each object represents a publication.
    • We create a view that lays out the publication details (e.g. set grids, color position) and map this array to our layout.
    • To filter our view, we write a filter function on the array e.g. when the user makes a selection (e.g. selects a tag or date etc), we filter the array by tag. Remember that the array is bound to the view so the view automatically is re-rendered for each selection.

Let's look at some code!

The snippet below looks at what our data array could look like.

javascript
export default [
{
title:
"NeuralQA: A Usable Library for Question Answering (Contextual Query Expansion + BERT) on Large Datasets",
authors: ["Victor Dibia"],
venue: { slug: "EMNLP", label: "", link: "", location: "Online " },
type: "conference",
year: 2020,
month: 10,
tags: ["visualization", "machine learning", "nlp", "hci"],
award: "",
links: [
{
label: "PDF",
url: "https://arxiv.org/pdf/2007.15211.pdf",
} ...
],
},
...
}

React view for filterable publications

javascript
import React, { useState } from "react";
import publications from "../data/publications";
import { LinksList, ExtLink, SubString, countAndSort } from "./atoms";
import groupBy from "lodash/groupBy";
import { LinkIcon } from "./icons";
import { sortBy } from "lodash";
const PublicationsGrid = ({ tagType, tagValue, showHeading }) => {
const maxPublications = showHeading ? 5 : publications.length;
const [selection, setSelection] = useState({
tag: tagType || "all",
value: tagValue || "all items",
});
let filteredPublications = [];
switch (selection.tag) {
case "award":
filteredPublications = publications.filter(function (publication) {
return publication.award?.label === selection.value;
});
break;
case "type":
filteredPublications = publications.filter(function (publication) {
return publication.type === selection.value;
});
break;
case "tag":
filteredPublications = publications.filter(function (publication) {
return publication.tags.includes(selection.value);
});
break;
case "venue":
filteredPublications = publications.filter(function (publication) {
return publication.venue?.slug === selection.value;
});
break;
case "co-author":
filteredPublications = publications.filter(function (publication) {
return publication.authors.includes(selection.value);
});
break;
case "all":
filteredPublications = publications;
break;
default:
filteredPublications = publications;
break;
}
filteredPublications = sortBy(filteredPublications, "year")
.reverse()
.slice(0, maxPublications);
const pubGroups = groupBy(filteredPublications, "year");
const pubYears = Object.keys(pubGroups);
pubYears.sort().reverse();
const yearList = pubYears.map((year, i) => {
const pubs = pubGroups[year];
return (
<div className=" divide-y" key={i + "year"}>
<h2 className="text-xl mt-2 font-bold text-primary text-accent ">
{year}
</h2>
<ul className="mt-2 grid md:grid-cols-2 gap-4 md:gap-4 ">
{pubs.map((publication) => {
const authorList = publication.authors.map((author, i) => {
const isVictor =
author.toLocaleLowerCase().includes("victor") &&
author.toLocaleLowerCase().includes("dibia");
const isLast = i === publication.authors.length - 1;
return (
<span
className={(isVictor ? " font-semibold " : "") + " mr-1"}
key={i + "authorrow"}
>
{author}
{!isLast && ","}
</span>
);
});
return (
<li key={publication.title} className=" py-3 ">
<div className=" group flex h-full flex-col justify-between sm:items-end space-x-3 sm:space-x-0 ">
<div className="w-full h-full flex flex-col ">
<h4 className="inline w-full">
{publication.award && (
<span className="text-orange-500">
{" "}
<LinkIcon icon={"trophy"} />{" "}
</span>
)}
<span className="capitalize font-semibold text-primary ">
{" "}
{publication.title}
</span>
{publication.award && (
<span className="capitalize text-sm text-orange-500">
{" "}
{publication.award.label} Award @ {publication.award.venue}
</span>
)}
</h4>
<div className="text-sm text-primary w-full">
{authorList}
</div>
<div className="w-full flex-grow ">
<ExtLink
title={
publication.venue.label ? publication.venue.label : ""
}
link={
publication.venue.link ? publication.venue.link : ""
}
// className="text-sm text-primary"
newTab
>
{" "}
{publication.venue?.slug?.toUpperCase()}
{publication.venue?.location
? ", " + publication.venue?.location
: " "}
</ExtLink>
</div>
<div className="mt-1 text-sm text-primary">
<LinksList links={publication.links} />
</div>
</div>
</div>
</li>
);
})}
</ul>
</div>
);
});
const maxCategoryItems = 8;
const maxCategoryString = 20;
function clickCategory(category, title) {
setSelection({ tag: title, value: category.name });
}
function showCategories(categories, categorytitle) {
return categories.slice(0, maxCategoryItems).map((data, i) => {
const isSelected =
selection.value === data.name && selection.tag === categorytitle;
return (
<div
role="button"
aria-hidden="true"
className={
(isSelected ? "border-accent font-semibold border-b-1 " : "") +
" text-sm border hover:text-accent py-0.75 border-t-0 border-r-0 border-l-0"
}
key={i + "catrow"}
onClick={() => clickCategory(data, categorytitle)}
>
<span
className={
(isSelected ? "text-accent translate-x-2" : "") +
" transition duration-300 transform hover:translate-x-2 inline-block"
}
>
{" "}
<SubString text={data.name} textLength={maxCategoryString} />
<span className="opacity opacity-20 text-xs">
{" "}
({data.value})
</span>{" "}
</span>
</div>
);
});
}
let authors = [];
let tags = [];
let types = [];
let awards = [];
let venues = [];
publications.forEach((x) => {
x.authors.forEach((author) => {
if (!author.toLocaleLowerCase().includes("dibia")) {
authors.push(author);
}
});
x.tags.forEach((tag) => {
tags.push(tag);
});
types.push(x.type);
if (!x.venue.slug.toLocaleLowerCase().includes("patent")) {
venues.push(x.venue.slug);
}
if (x.award) {
awards.push(x.award.label);
}
});
authors = countAndSort(authors);
tags = countAndSort(tags);
types = countAndSort(types);
awards = countAndSort(awards);
venues = countAndSort(venues);
const allCats = [types, tags, venues, authors, awards];
const pubCategories = ["type", "tag", "venue", "co-author", "award"];
const pubCategoryList = pubCategories.map((category, i) => {
return (
<div className="capitalize" key={i + "ptyperow"}>
{" "}
<span className="font-semibold"> {category} </span>
<div className="mt-1">{showCategories(allCats[i], category)} </div>
</div>
);
});
return (
<section className="">
{showHeading && (
<h2 className="mt-32 font-normal text-accent tracking-widestest">
RECENT PUBLICATIONS
</h2>
)}
{!showHeading && (
<>
<div className=" mt-2 mb-6">
<div className="borderhidden md:block md:grid md:grid-cols-5 gap-4 md:gap-4">
{" "}
{pubCategoryList}{" "}
</div>
</div>
<div className=" text-xs mt-1 mb-2">
{" "}
<span>
{" "}
{selection.tag} : {selection.value}{" "}
</span>|<span className="ml-1 ">
{filteredPublications.length} items
</span> {selection.tag !== "all" && (
<span
className=""
aria-hidden="true"
role="button"
onClick={() => {
setSelection({ tag: "all", value: "all items" });
}}
>
{" "}
| <span className="ml-1 text-accent">
{" "}
<span className="ml-1">Clear Selection</span>{" "}
</span>{" "}
</span>
)}
</div>
</>
)}
{yearList}
{yearList.length === 0 && (
<div className="border-orange text-sm border p-2">
{" "}
⚠️ There are no items that match your selected filters.
</div>
)}
</section>
);
};
export default PublicationsGrid;

And that's it.

Interested in more articles like this? Subscribe to get a monthly roundup of new posts and other interesting ideas at the intersection of Applied AI and HCI.

RELATED POSTS | web

Read the Newsletter.

I write a monthly newsletter on Applied AI and HCI. Subscribe to get notified on new posts.

Feel free to reach out! Twitter, GitHub, LinkedIn

.