Building in Next.js with static JSON
πΆ Intro
Beth designed herself a new portfolio website. From a dev point of view, the build looked simple; a homepage & multiple case study pages. She didn't want a CMS (Content Management System) - she was happy to add new pages in the codebase.
Knowing this, I wanted to keep things as simple as possible for her to maintain. No copy & pasting template files, no setting up new routes. Ideally, one place to manage content on the site.
π·πΌ Building with Next.js
I reached for Next.js because it makes doing something like this easy (and fast!). But, more importantly, it has features that would help Beth manage the content over time.
yarn create next-app --typescript
π₯ Bosh! We have initialised a fresh Next app with Typescript ready to rumble. To celebrate, I will go and stick the kettle on! βοΈ
πΏ Setting up some JSON data
I'm back! Yum. So, if the site had a CMS and the content was elsewhere, I would set up a reusable asynchronous function to fetch that data. The function would live in some sort of library /lib
directory with a helper file inside: api.js
. Here, I would handle the communications between me and the CMS.
However, we don't have a CMS, we can cut out the fetching part and have our own data already available as a static JSON file.
The location of that JSON file would be something like this:
/lib
/projects
data.json
Now it's time to create data we can use for the case studies. (This would be the response if we were using a CMS/API)
π§ Here's one I made earlier! It's an example of the JSON data - an array of objects, each object is a case study.
[
{
"id": 0,
"meta": {
"category": "Branding & Print",
"slug": "slug-example",
},
"hero": {
"title": "Awesome project title"
"subTitle": "Wonderful project sub title",
"image": {
"src": "/images/projects/willo-remedies/product.jpeg",
"alt": "Mind blowing feature image"
}
},
"intro": {
"description": "An awesome description",
"list": [
"Logo creation",
"Packaging design",
"Brand guidelines",
"Social media design"
"Website design"
]
},
"images": [
{
"src": "/images/projects/willo-remedies/product-2.jpeg",
"alt": "Cute puppy"
},
{
"src": "/images/projects/willo-remedies/product-3.jpeg",
"alt": "Beautiful beetroot"
},
]
},
]
Lovely jubbly. All case study data will reside in this one file, and we will dynamically create all the pages for the case studies (more on that in a moment).
𧱠Page structure
In terms of the architecture of the site, it looked something like this:
/pages
index.tsx // the homepage
success.tsx // netlify form success page
/projects
[slug].tsx // dynamic case study pages
..... I don't know what else to say here tbh lol
π¦ΉπΌ Generating pages dynamically
At build time, we need to tell Next.js what pages to generate. Otherwise, how does Next.js know what to do? We will utilise Next.js' getStaticPaths()
inside our dynamic case study page ππΌ
// [slug].tsx
export async function getStaticPaths() {
const data = require("../../lib/projects/data.json");
return {
paths: data.map((project: ProjectInterface) => {
return {
params: {
slug: project.meta.slug,
},
};
}),
fallback: false, // use stock 404
};
}
Inside getStaticPaths()
we are returning an object. Inside this object, we have an array called paths
, in which we are looping through each case study and returning an object params
(which has a slug
inside it) and assign our slug. As a result, this is used on the frontend in the URL. For example: google.com/projects/slug-example.
Nice. We're smashing this. Before we continue, remember to take a break, drink some water, and get some fresh air π
π€ Passing static data to pages
I'm back again! Similar to what I was saying before, but instead of the page slugs, we need to tell Next.js what data to use for each case study. We do this with getStaticProps()
ππΌ
// [slug].tsx
export const getStaticProps = async (context) => {
const { slug } = context.params;
const data = require("../../lib/projects/data.json");
const project = data.find(
(project: ProjectInterface) => project.meta.slug === slug
);
return {
props: {
project,
},
};
};
We deconstruct the slug
from context.params
. We can then use this to loop over each case study and return the correct one. Then, we pass that to our dynamic page as a prop.
Now, we can go to the frontend and see our pages rendering correctly. Love it!
π§Ό Data cleaning layer
Scrub-a-dub-dub!!! π₯ Before we go passing all data everywhere willy-nilly, we need to sanitise this data first per component. Passing unresolved/unsanitised data to a component can drastically hinder performance.
Take this example: on the homepage, there is a carousel slider showing all projects;
We don't need to pass ALL project data into this - there's no need. Each project has an image, a category, and a title. We will still use the static JSON file, but we will create a resolver that returns a new object - a filtered-down version with only the stuff we need.
// index.tsx (the homepage)
export const getStaticProps = async () => {
const data = require("../lib/projects/data.json");
return {
props: {
carousel: data.map((item: ProjectInterface) =>
carouselItemResolver(item)
),
},
};
};
Then, we can create a new data resolving service:
// services/resolvers/carouselItemResolver.ts
export const carouselItemResolver = (data: ProjectInterface) => {
return {
image: {
src: data.hero.image.src,
alt: data.hero.image.alt,
},
category: data.meta.category,
title: data.hero.title,
};
};
Once we pass the carousel
prop (an array of filtered-down projects) to our component, it uses all the data provided. Yay!
I continued this method across the site for all components π
π¨ Case Study Branding
I messaged Beth, raising the idea of tailoring the colours on each case study to the actual case study featured. She loved the sound of it. Each page would look far better if the colours and accents actually matched the branding on the case study. For example: McDonalds would have red and yellow branding on the page itself. (Now I want some McDonald's fries π«)
In the core JSON data, inside the meta key, I added a "colors" object. The colours can be different per project and, when passed through to the case study page, can be used on the front-end ππΌ
// lib/projects/data.json
"meta": {
// ...other stuff
"colors": {
"primary": "#006747",
"secondary": "#D79A31"
}
},
Pass that through to the page ππΌ
// [slug].tsx
<Project // layout component
id={id}
meta={meta} // ππΌ contains the "colors" property
hero={hero}
intro={intro}
images={images}
navigation={breadcrumbs}
/>
I then grabbed { primary, secondary }
from colors
and used them in the Hero component ππΌ
π Hosting & deployment
The site is hosted on Netlify. When creating a new project from a Github repository, Netlify recognises the project is Next.js and automatically sets the build script for you. Nowadays, there's little work involved when deploying a site like this.
Whenever a change is made to the production branch main
, Netlify will redeploy the site with a yarn build
to rebuild our site with the updated content.
π Forms
For the first time, I used Netlify Forms for the contact section. It was so easy to do! I hadn't even heard of it before. I followed the documentation and had it up and running within 10 minutes. It's also free for smaller sites with little traffic.
π₯ Success page
The form posts to a success page, so it was necessary to create a new page to accommodate this.
π‘ Lighthouse reports
Despite my careful data fetching, compressing images, and utilising Next's Image component, I saw a soul-destroying score of 84 come back in the Lighthouse report. The only culprit was the size of the font I was using.
π« Dela Gothic One - 2.5MB.
I noticed the "Dela Gothic One" font was 2.5MB when I downloaded it from Google Fonts... 2.5MB?! For a font?! I was already hosting the font within the project and importing it, rather than using it directly from Google, but 2.5MB is insane.
I converted the font file from .tff to .woff2, but even still it was 1.2MB.. what the hell?!
After some googling, there seemed to be very little complaints from people about the size of some of the fonts on Google. Must be something I'm doing wrong...
π§ Creating a subset of a font
Baffled, confused, quite frankly BAMBOOZLED, I reached out to a Slack community of front-end developers for confirmation that I wasn't being a moron. It turns out the font, by default, supports 188 languages and the amount of glyphs it comes with, that we won't use, is the cause of the enormous file size. One of them recommended "Creating a sub set of the font with FontForge".
So I took to Google and did exactly that. FontForge isn't the nicest of tools to use, but it did exactly what I needed it to do.
- I selected all the glyphs I needed to use - which were conveniently near the top.
- Inverted the selection of glyphs (to select all the other ones)
- Deleted all those glyphs (took a few seconds as there were thousands)
- Exported a new .ttf file.
π€― 2.5MB -> 14KB. That's more like it!
Much better. The 98 for Accessibility is caused by the main font colour blending in too much with the background colour. That's on Beth, not me π!
π€ Summary
I would have advised having a separate CMS much more if Beth wasn't technically-minded (and if she wasn't a great friend of mine!) because it's easier to test required fields, null fields, and so on. Giving her access to the repository and providing a thorough handover explaining how this all works is enough to fulfill the requirements of this project (and fast!). If there's a site-breaking error in the JSON, the deployment will fail rather than deploy and break the site. Beth is really happy with how the site turned out.