Maybe you didn’t know, but this static site was developed using Gatsby a few years ago - another framework built on top of React. At that time, Gatsby was a tool that I wanted to learn because of its advantages and I can say that I was very happy with the result. Through that framework I built a static and secure multilingual website with a focus on performance and PWA support - besides the use of GraphQL (see all the Gatsby advantages here). How fantastic, isn't it? But I confess that I always thought about switching to Next.js and I'll tell you why.
Why didn't I rewrite my site with Next.js before?
After an experience working with Next.js around 2020, rewriting an entire online course platform for HP, I really understood the power of this framework and how we can move from static to dynamic - and from server-side rendering to client-side - on the same project and I was mesmerised by it. From that moment on, I was sure I wanted to write every React app with Next.js. lol But why didn’t I do it?
The main reason I didn't do this sooner is because Gatsby already provided everything I needed to build fast and secure static apps, and despite the advantages of Next.js, I didn't see much benefit from doing so. So I was focusing on learning new things than spending my time transitioning between these two frameworks.
Deciding to move my site from Gatsby to Next.js 13
Next.js 13 is making waves in the community after its announcement in October 2022 during its conference. It arrived with a lot of powerful changes and that's definitely why I'd like to check it out and see its new features up close. I also believe that this framework tends to be more and more adhered to by the community and also by big companies, as it promises to “create full-stack web applications extending the latest features of React” and “lays the foundations to be dynamic without limits”, as its own words.
In terms of building a static site quickly, Gatsby seems to be the winner with its plugins. However, I was more interested in trying out the new Next.js functionalities and React Server Components while rewriting my site. Yes, it's about learning new front-end stuff.
Pros of rewriting my site with Next.js 13
- Next.js is the most used React framework and seems to be the choice for large projects as shown in NPM trends as well as the State of JS survey.
- Autonomy to work with Rest API. Next.js is agnostic about fetching data, while Gatsby forces us to work with the GraphQL API.
- With Next.js I can have more control of my application instead of depending on plugins.
- Great support for the latest React updates, including beta features like server components.
- I have the opportunity to review my site's code and improve it.
- It was a good time to rethink some functionalities that I no longer want to maintain, like multi languages and PWA for example.
Cons of using Next.js 13 to rewrite my site
- It's not very fast to build a static website like we do with Gatsby and its amazing plugins.
- Next.js 13 has a new way of building the site and right now some important features are not fully supported yet: for example, CSS-in-JS only works on client-side components at the moment, which prevents content from rendering on the server.
Refactoring the code
In terms of look and functionality, the site is practically the same, but I made several changes to the code and below I will list the main ones.
The snippets I'll show are just to give you a general idea and may not be the full implementation. They will might change as I'm improving the code, so I'll try to keep them up to date here.
1- Use of Typescript
This is definitely one of the best changes. My project with Gatsby used PropTypes, which is also really nice, but I love Typescript and would like to have it in this new version. You can check more about Typescript here.
2- Replacing Gatsby’s markdown plugins
Perhaps the biggest change was getting rid of the benefits of Gatsby plugins and creating my own solution for turning markdown files into static pages in Next.js.
In Gatsby I used to configure the plugins in gatsby-config.js
file and manage their APIs to create pages dynamically during build time using the gatsby-node.js
file. Although I did many lines of code, it was easy and fast to manage.
Example of gatsby-config.js
file:
// Gatsby => gatsby-config.js
module.exports = {
plugins: [
`gatsby-plugin-styled-components`,
`gatsby-transformer-json`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
`gatsby-plugin-transition-link`,
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/static/assets/img`,
name: `uploads`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/config/translations`,
name: `translations`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/blog`,
name: `blog`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 1040,
linkImagesToOriginal: false,
},
},
{
resolve: `gatsby-remark-embedder`,
options: {
customTransformers: [],
services: {},
},
},
`gatsby-remark-lazy-load`,
`gatsby-remark-prismjs`,
],
},
},
{
resolve: `gatsby-plugin-google-fonts`,
options: {
fonts: [
`roboto:300,400,500`,
`roboto mono:300,400,500`,
`oswald:300,400,600`,
],
},
},
],
};
Example of gatsby-node.js
file:
// Gatsby => gatsby-node.js
const path = require(`path`);
exports.createPages = async ({ graphql }) => {
const pageTemplate = path.resolve(`./src/templates/page.js`);
const postTemplate = path.resolve(`./src/templates/post.js`);
const projectTemplate = path.resolve(`./src/templates/project.js`);
const result = await graphql(`
{
files: allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
fields {
locale
isDefault
slug
}
frontmatter {
title
is_page
is_portfolio
featuredImage
bannerImage
}
}
}
}
}
`);
const contentMarkdown = result.data.files.edges;
contentMarkdown.forEach(({ node: file }) => {
const { slug } = file.fields;
const { title } = file.frontmatter;
const featuredImage = `${file.frontmatter.featuredImage.split('/')[3]}/${
file.frontmatter.featuredImage.split('/')[4]
}`;
const isPage = file.frontmatter.is_page;
const isPortfolio = file.frontmatter.is_portfolio;
let template;
if (isPage) {
template = pageTemplate;
} else if (isPortfolio) {
template = projectTemplate;
} else {
template = postTemplate;
}
});
};
In Next.js it is a little different to replicate the same process, but not very hard as well, however, it needed much more lines of code without the help of plugins. So I created some functionalities using the file system dependency to read the markdown files and parse their front matter and content.
See examples below on how to read and parse markdown files in Next.js 13.
Create a function to convert the markdown into a string and another function to extract the information from a markdown file - you check out the use of these two scripts in the example below of page using server components (app/(pages)/about/page.tsx
)):
// Next.js => utils/markdownToHml.js
// This utility will be used to convert the MDX content into HTML
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypePrettyCode from 'rehype-pretty-code';
// Utils to get the markdown and applies some conversions
export default async function markdownToHtml(markdown: string) {
const result = await unified()
.use(remarkParse) // parses the markdown into syntax tree
.use(remarkGfm) // enables GitHub extensions to markdown
.use(remarkRehype) // transforms markdown syntax tree into an HTML
.use(rehypePrettyCode, {
// syntax highlighting for code (like this one)
theme: 'min-dark',
keepBackground: false,
})
.use(rehypeStringify) // turns it into serialized HTML
.process(markdown);
return result.toString();
}
// Next.js => lib/markdown.js
// This script will be used to extract the data from MDX
// by passing a slug
import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
type FileTypeProps = 'blog' | 'portfolio' | 'pages';
// First get all file names
export function getAllFileNames(type: FileTypeProps) {
const filesDirectory = join(process.cwd(), type);
const allFiles = fs.readdirSync(filesDirectory);
const filteredFiles = allFiles.filter((file) => !!file);
return filteredFiles;
}
// Create a function to convert the MDX file into JS object
export function getMdxFileContentBySlug(
type: FileTypeProps,
slug: string,
fields: string[] = []
) {
const allFileNames = getAllFileNames(type);
// Create an object to store all the markdown data
const fileData: {
[key: string]: string,
} = {};
// If there's MDX files...
if (allFileNames.length) {
// Filter the file by slug
const file = allFileNames.filter((fileName) => {
let formattedFileName = fileName.toString().replace(/\.md$/, '');
// blog and portfolio start with date in the file name.
// eg. : 2023-06-18-
// so I need to remove it to apply the filter
if (type === 'blog' || type === 'portfolio') {
formattedFileName = formattedFileName.slice(11);
}
return formattedFileName === slug;
});
// If there's a file...
if (file.length > 0) {
const filesDirectory = join(process.cwd(), type);
const filePath = join(filesDirectory, file.toString());
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
fields.forEach((field) => {
if (field === 'slug') {
fileData[field] = slug;
return;
}
if (field === 'content') {
fileData[field] = content;
return;
}
if (data[field]) {
fileData[field] = data[field];
}
});
}
}
// Finally return an object containing the slug,
// content and other data specified in "fields" array
return fileData;
}
3- Replacing Prism.js syntax highlighting to Shiki.
I couldn't find any way to continue using Prism.js in Next.js 13, so I came across another NPM package called rehype-pretty-code, developed by Shiki, for syntax highlighting that has themes accurate to VSCode's (I'm using the min-dark
).
It works at build-time so there’s no bundle being sent to the client. Check an example of its implementation in the code snippet above related to the markdown parser utility: utils/markdownToHml.js
.
4- Separation of pages into server and client components
As React Server Component is still in work-progress and Next.js decided to implement it in its version 13, CSS-in-JS doesn’t work in the server yet. And as I decided to continue using Styled Components, I needed to find a way to make it works having all the data generated in build time.
I needed to have a page built at build time (server component) that takes the data and passes it to a client component, which receives the data and displays it while applying the styles. So for each page I created two different components: one for the server and one for the client.
Page using React server component to get the data in build-time:
// Next.js => app/(pages)/about/page.tsx
// This is the server component, responsible for getting
// the MDX content and converting it to HTML by using the
// scripts I showed you above
import markdownToHtml from 'utils/markdownToHtml';
import buildUrl from 'utils/url';
import { getMarkdownContentBySlug } from 'lib/markdown';
import getAllStaticContent from 'lib/static-content';
// The client component
import AboutComponent from './AboutComponent';
// Getting data...
const {
authorName,
brandsListTitle,
aboutPageSeoTitle,
aboutPageSeoDescription,
} = getAllStaticContent('general');
const allBrands = getAllStaticContent('brands');
const formattedBrands = allBrands.map(
(brand: { img: string, label: string }) => {
const { img, label } = brand;
return {
img: img && buildUrl('images', `brands/${img}`),
label,
};
}
);
const pageMdx = getMarkdownContentBySlug('pages', 'about', [
'title',
'content',
'featuredImage',
'featureImageAlt',
]);
// The server component
export default async function About() {
// Parse the markdown
const content = await markdownToHtml(pageMdx.content || '');
const { featuredImage, featureImageAlt } = pageMdx;
return (
<AboutComponent
featuredImage={
featuredImage && buildUrl('images', `about-me/${featuredImage}`)
}
featureImageAlt={featureImageAlt}
content={content}
brandsListTitle={brandsListTitle}
brandsList={formattedBrands}
/>
);
}
Page using React client component to apply the styles and display the data:
// Next.js => app/(pages)/about/AboutComponent.tsx
// This is the client component responsible for
// displaying the data, apply the styles and any other
// effect using React Hooks
'use client';
import Image from 'next/image';
import MarkdownContent from 'app/components/MarkdownContent';
import BrandList, { BrandItemProps } from 'app/components/BrandList';
import Heading from 'app/components/Heading';
import PageTransition from 'app/components/PageTransiion';
// The styles by styled components
import * as S from './styles';
export type AboutComponentProps = {
content: string,
featuredImage: string,
featureImageAlt?: string,
brandsListTitle: string,
brandsList: BrandItemProps[],
};
export default function AboutComponent({
content,
featuredImage,
featureImageAlt = '',
brandsListTitle,
brandsList,
}: AboutComponentProps) {
return (
<PageTransition>
<S.ContentWrapper>
<S.HeaderWrapper>
{featuredImage && (
<S.ImageWrapper>
<Image
src={featuredImage}
alt={featureImageAlt}
width={360}
height={440}
quality={85}
/>
</S.ImageWrapper>
)}
</S.HeaderWrapper>
<div className="container">
<MarkdownContent content={content} />
<S.BrandsSection>
<S.TitleWrapper>
<Heading as="h2">{brandsListTitle}</Heading>
</S.TitleWrapper>
<BrandList listItems={brandsList} />
</S.BrandsSection>
</div>
</S.ContentWrapper>
</PageTransition>
);
}
5- From Gatsby Image to next/image
This is probably the most painful disadvantage of Next.js when compared to Gatsby. In the latter I had everything my site needed to make the image very optimized, whereas in the former I needed to develop a similar but not as awesome solution. (I'm still working on it to make sure all images render this way).
// Next.js => utils/image.js
import fs from 'node:fs/promises';
import { join } from 'path';
import { getPlaiceholder } from 'plaiceholder';
// Convert an image to base 64 to be used as placeholder
export default async function imageBase64Conversor(imgName: string) {
const file = await fs.readFile(join(process.cwd(), `public/${imgName}`));
const { base64 } = await getPlaiceholder(file);
return base64;
}
// Next.js => any server file
import imageBase64Conversor from 'utils/image';
// ...
const imgUrl = buildUrl('images', `about-me/${featuredImage}`);
const featuredImageBase64 = await imageBase64Conversor(imgUrl);
// Next.js => any client file
import Image from 'next/image';
// ...
<Image
src={imgUrl}
alt=""
width={360}
height={440}
placeholder="blur"
blurDataURL={featuredImageBase64}
quality={85}
/>;
6- From Netlify to Vercel
Last but not least, the new Next.js 13 seems to work better on the Vercel platform (which makes sense since this framework is maintained by Vercel). I faced some problems after publishing on Netlify that I was only able to solve when I made this migration. On Netlify, the 400 page was returning a server error, for example, among other odd issues. But since the dev world is not all flowers, I faced an issue with Vercel related to the size of server components, so I needed to remove some Node packages to make the build work again.
Wrapping up
Gatsby is a great tool and I've used it to build this site before, but I decided to rewrite it using Next.js mainly because I wanted to try out the new React Server Components. In doing so, I also learned a lot of things related to Next.js as well as React, reviewed some code and refactored it.
Although some comparisons are inevitable, the idea of this article was not to compare the two frameworks, but to highlight some positive points that I considered important for me to decide to make this migration.
I hope you liked this post. See you next time. 😁