Building a high-performance campaign page with Airtable as CMS
How we built the Paystack Gift Guide, a marketplace of unique gifts, using Airtable as a content management system
Every year since 2018, the Marketing and Design teams at Paystack have worked together to publish a Gift Guide that curates unique gifts made by our creator merchants from all of Africa.
From leather travel bags made from Namibian antelope hide to hand-woven bògòlanfini cloaks, the Paystack Gift Guide is a careful curation of thoughtful gifts.
The goal of this project is to deepen our relationship with our merchants, and to help them boost sales during peak gifting seasons. Today, the Gift guide is a reusable static website built with HTML, CSS, and Javascript, and deployed using Netlify. We also manage content efficiently on the site via Airtable. This wasn’t always the case.
In this article, we share the design and technical decisions we’ve made in the last few years to improve the Guide and create a delightful shopping experience.
Iterating towards success
We made our first-ever Gift Guide to help drive sales for merchants during the Valentine’s Day celebrations in 2018. It was built using Craft CMS, a tool the Marketing team already used to manage content on our blog and website.
This was a natural decision, but not the most efficient one. The Marketing team had to first organise product information from merchants inside Airtable, and then re-upload all that information - product images, product descriptions, pricing, merchant information, and more - to Craft. It was a slow, painful, time-consuming experience.
After the success of the 2018 Valentine’s Gift Guide, we decided that we wanted to re-run the campaign in future. But if we wanted to do more Guides, we needed to make several improvements.
We needed to increase the website speed, improve site navigation to make it easy for users to discover products, make it a lot easier for the Marketing team to manage products, and more.
Here’s how we did it.
Designing with modularity in mind
We knew we wanted to re-run the Gift Guide campaign several times in future, around different shopping seasons, and so we set out to redesign and re-implement the campaign page in a way that made it a lot easier to republish.
We designed two reusable screens – a product gallery page and a product overview page - with modularity and re-usability in mind.
Here are some of our low fidelity wireframes and explorations.
This masonry grid approach took inspiration from Pinterest’s wall of beautiful photos. While it provided a beautiful wall of images, it had a few issues. For example, the absence of product names and pricing made it difficult for customers to make quick purchase decisions. Additionally, the masonry approach required a lot more tweaking to ensure that the image was focused directly on the product.
This iteration introduced product names and pricing, which made it easier to make quick purchase decisions, but the filter on the left took up valuable real estate which could have been used to display larger images.
This exploration was an attempt to gain the best of both worlds, but the lack of spacing between images would have required very careful choreography to ensure lighter images next to dark ones.
The final layout gave us maximum flexibility. Product images are large and compelling, the filter is still visible without eating valuable real estate, and it’s a lot easier to skim product details such as the name and price.
After sorting the page layout and information architecture in low-fidelity, we translated the wireframes into high-fidelity designs, using Paystack’s brand elements. We worked closely with the Brand Design team on several elements.
The custom hero image can be reskinned to reflect the relevant shopping season. The images above show how the Valentine’s Gift Guide looked different from the end-of-year Gift Guide.
In 2018 when we began the Guide, Paystack only operated in Nigeria. Several years later, we operate in multiple countries, and the Brand team helped create multiple “stamp” elements to allow us signal that the Guide was a curation of products from all over the continent.
Sell more online
Paystack has all the payment tools you need to start and scale your business.
Create your free accountBuilding the Airtable CMS
Like we mentioned previously, the first Gift Guide was a hardcoded Craft CMS site. While this worked for the first version, the Marketing team shared feedback that copy-pasting the product details from Airtable into Craft was a slog.
What if we didn’t need to move information from Airtable? What if Airtable could serve as both the way we organised product details internally, as well as the CMS for the production site?
We were pleasantly surprised at how easy it was to turn Airtable into a content management system for the Gift Guide. Literally all product information on the Gift Guide page - links, product images, delivery info - is managed from an Airtable base.
The Airtable API is very well-documented, and they go the extra mile to make it extremely easy to understand how to integrate with multiple libraries.
It’s more secure to make requests to Airtable’s API server-side, because there’s sensitive information like API keys, Base ID, etc. that Airtable requires while making these calls, and exposing this data on the browser makes it easy for third-parties to get access to your Airtable base. To get around this issue, we built a NodeJS middleware that sits between the frontend and Airtable’s API.
Sell more online
Paystack has all the payment tools you need to start and scale your business.
Create your free accountNavigating technical challenges in the frontend
As we brought the design to life, we encountered a number of technical challenges that would have prevented us from achieving our goals. Here’s how we solved those problems:
Reducing complexity with a few lines of code
In addition to helping drive sales for Paystack merchants, the Gift Guide provides valuable real estate with which to acquire new Paystack merchants. We introduced a section educating visitors about Paystack Storefronts, a suite of seller tools that allows creators bring ideas to market, beautifully. This call-to-action module is placed on the third row.
Historically, we’ve used a combination of Flexbox and media query walkarounds to get modules like this to fit across multiple device sizes. While this approach usually works for our web projects, using it for the Gift Guide resulted in a more verbose CSS stylesheet which could become hard to maintain.
We solved these layout issues with CSS grid. With CSS grid, we could achieve the same responsiveness goals with barely five lines of code. More importantly, our users were ready for this upgrade. In February 2018, according to Google Analytics, visitors to our website used older versions of Internet Explorer, Firefox and Opera Mini with no CSS grid support. It made no sense to solve layout problems using CSS grid at the time. By 2021, this had changed significantly.
Grid-row-start and grid-column
.c-gallery__products {
padding: 3.2rem 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 2.4rem;
@media screen and (max-width: 980px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 600px) {
grid-template-columns: 1fr;
}
}
.c-gallery__cta {
border-radius: 2.4rem;
width: 100%;
grid-column: 1/-1; // CTA module spans the full width of the third row
grid-row-start: 3; // CTA appears on 3rd row regardless of vw and count
display: flex;
margin-bottom: 8rem;
}
Creating the illusion of speed
When choosing to turn Airtable into the CMS, one of the things we wanted to achieve was seamless content synchronization between the website and the CMS. Changes made to Airtable had to reflect instantly on the website.
To do this on the frontend, we make an API call to Airtable to fetch the latest content every time users visit the page. We put caching logic in place so we only make GET requests to Airtable’s API once every 30 minutes. This prevented us from hitting Airtable's rate limit threshold.
Depending on the users’ internet speed, making this call on the frontend meant waiting a few seconds for a response from Airtable’s API to populate the product list.
At Paystack, we believe that speed is a feature, and the relatively slow page load was a poor experience. We came up with two potential solutions:
- We could hide the webpage behind a preloader and remove the preloader when all the images load successfully on the frontend
- Since the site was already lightweight and fast, we could show the already loaded content, but build a custom image preloader
The latter option was more appealing; it displayed the hero content and helped us engage our users while the page was loading.
We placed three squares on the first row of the product grid, then animated and styled these three boxes, using CSS to create the shimmer effect. This communicated to the user that the image was loading. The image gallery looks like this:
HTML
<div class="c-gallery__products">
<div data-loading class="c-gallery__product">
<div class="c-gallery__product-image"></div>
</div>
<div data-loading class="c-gallery__product">
<div class="c-gallery__product-image"></div>
</div>
<div data-loading class="c-gallery__product">
<div class="c-gallery__product-image"></div>
</div>
</div>
CSS
@keyframes imageLoader {
from {
transform: translateX(-100%)
}
}
.c-gallery__product-image {
position: relative;
width: 100%;
margin-bottom: 2rem;
padding-top: 110%;
background-color: rgba(112, 128, 144, 0.05);
border-radius: 8px;
overflow: hidden;
}
.c-gallery__product-image::after {
content: "";
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
transform: translateX(100%);
background-image: linear-gradient(-80deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0));
animation: imageloader 1s infinite ease-in-out;
}
When all images have successfully loaded, we dispatch a productIsReady event and remove the loader elements from the DOM. With a few abstractions, this is what the Javascript looks like:
let animationIndex = 0;
const removeLoaders = () => {
const loaders = document.querySelectorAll('[data-loading]');
loaders.forEach(elem => elem.remove());
};
// Create products from Airtable
fetch(FETCH_API_URL)
.then(res => res.json())
.then(data => {
// Format product data and create product DOM element
return data.map(product => {
product.isVisible = true;
createProductElement(product, animationDelayIndex);
animationDelayIndex += 1; // Delay element, to create the staggered animation effect
return item;
});
})
.then((productsData) => {
// Store productsData to help us with sort and filter later
// Dispatch an event that tells use the products are read
const event = new Event('productsReady');
productsContainer.dispatchEvent(event);
})
.catch(e => {
// Handle error
})
.finally(removeLoaders);
Improving discoverability
Every time we publish a Gift Guide, we feature a greater number of products. How could we make it easier for visitors to quickly find the perfect gift for the special someone in their life? We invested in sorting and filtering features to make it easier for prospective customers to find the perfect gift.
The first time we built the filter and sort features, we cleared and created new DOM elements every time a user toggled filter or sort. This was far from ideal: we were doing a ridiculous amount of browser painting, making the user re-download the product image every time there was a sort or filter logic.
Thinking through the solution from a first-principles perspective, our logic needed to do two things in this order:
- Remove or hide every product on the page
- Show the Product DOM element, in the order we want the items to show
We decided to achieve this using a hybrid of CSS and Javascript.
We introduced an active CSS class that controlled the stagger entrance of each product card and used CSS to hide non-active elements. Here’s the CSS code:
@keyframes slideIn {
from {
opacity: 0;
transform: translate3d(4rem, 1rem, 0);
}
}
.c-gallery__product:not([data-loading]) {
opacity: 0;
display: none;
}
.c-gallery__product.active {
display: unset;
opacity: unset;
animation: slideIn .6s $easeOutExpo backwards;
animation-delay: calc(var(--delay-index) * 40ms);
}
The Javascript part of the logic involved toggling CSS classes and reassigning CSS variables:
// utils/product-dom.js
export const hideAllProducts = () => {
document.querySelectorAll('.c-gallery__product').forEach(e => {
e.classList.remove('active');
e.style.setProperty('--delay-index', 0);
});
};
export const animateProductIn = ({ id }, animationDelayIndex) => {
const element = document.querySelector(`[data-id=${id}]`);
element.style.setProperty('--delay-index', animationDelayIndex);
const timeout = setTimeout(() => {
element.classList.add('active');
clearTimeout(timeout);
}, 10 * animationDelayIndex);
};
The hideAllProducts function is called when users toggle the sort or filter dropdown. This hides all products on the DOM. We then fetch productsData from local storage, sort the JSON using Javascript array methods, and call the animateProductIn function on the loop to make the product card visible again.
Controlling the order in which items showed up on the DOM was another challenge. What’s the point of a sort function, if you don’t have full control over the order in which items appear on a list?
We solved this by using the CSS order property that comes with CSS Grid.
Here’s what the final code looks like:
CSS
@keyframes slideIn {
from {
opacity: 0;
transform: translate3d(4rem, 1rem, 0);
}
}
.c-gallery__product:not([data-loading]) {
opacity: 0;
display: none;
}
.c-gallery__product.active {
display: unset;
opacity: unset;
animation: slideIn .6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
animation-delay: calc(var(--delay-index) * 40ms);
order: var(--delay-index);
}
Javascript
import { hideAllProducts, animateProductIn } from './utils/product-dom';
const sortSelect = document.querySelector('[data-sort]');
const productsContainer = document.querySelector('.c-gallery__products');
productsContainer.addEventListener('productsAreReady', () => {
sortSelect.addEventListener('change', () => {
hideAllProducts();
const productsData = JSON.parse(localStorage.getItem('productsData'));
let animationDelayIndex = 0;
let sortDirection = 0;
if (sortSelect.value === 'ascending') {
sortDirection = 1;
} else if (sortSelect.value === 'descending') {
sortDirection = -1;
}
const handleSort = direction => {
let productsArray = [];
if (direction === 0) {
productsArray = productsData.filter(item => item.isVisible);
} else {
productsArray = sortProductByCountryAndPrice(productsData.filter(item => item.isVisible), direction)
}
productsArray.forEach(productData => {
animateProductIn(productData, animationDelayIndex);
animationDelayIndex += 1;
})
};
handleSort(sortDirection);
});
});
With these problems solved, we shipped the newest version of the Gift Guide in time for Valentine’s Day in 2021, and reused it later in December for the end-of-year holiday shopping season, where we featured 30 unique, thoughtful gifts from Paystack merchants in Ghana, Nigeria, and South Africa.
Some final notes
As a small team of designers who code and care deeply about the user experience, it can be all too easy to spend a very long time attempting to resolve every single technical challenge before shipping. We’re learning to find a healthy balance between craft and speed. Like the saying goes, “done is better than perfect.”
This campaign, as with several other projects we’ve worked on, showed us that the product is never completely done, and there’s always an opportunity to improve, refine, simplify.
Ever since we shipped the first Paystack Gift Guide in 2018, we’ve continued to iterate on this campaign page, solving several problems for the business along the way. Most memorably, the experience building the Gift Guide over the last three years was extremely valuable when the product delivery team set out to build Paystack Storefronts, a simple, beautiful no-code seller tool.
To sum it up, here are a few lessons from building and improving the Paystack Gift Guide:
- It’s easy to get attached to and comfortable with tools we’re accustomed to, but sometimes, they might not be well-suited for the problems at hand. First-principles thinking helped us make better decisions. It was how we decided to use Airtable as a content management system, instead of working with two tools
- Collaboration is critical. This was a partnership between several design and marketing teams, and we learned that there’s room for different teams to work and learn from each other throughout the building process
- Data is your friend. We were able to make incremental product decisions and measure ROI using Google Analytics
We hope this short dive into the behind-the-scenes of the Paystack Gift Guide is helpful if you’re looking into spinning up a marketing page using Airtable as a CMS!
Would you like to learn more about how we design at Paystack? Come meet the Paystack Web Design team! We’re hosting a short live Q&A to answer any questions you might have about this article and share more info on how we work. Register for the live event here.
Meet the Web Design team
Learn more about how we design at Paystack and get answers to all your questions
Book your free seat