Tech-blog migrates to Next.js
As winter fades and snowdrops herald the arrival of spring, so too does the landscape of web development shift and evolve with the emergence of new frameworks. Four years have passed since our tech-blog embraced Gatsby in the fall of 2020, a choice that then seemed solid and served its purpose admirably (💌Gatsby). In our Architecture Decision Record of 2020, we opted against Next.js, favoring the straightforward, all-inclusive static site generator Gatsby, built on React, rather than continuing options like Jekyll or Hugo or Next.js (it missed dynamic routing at that time).
Yet, here we are once more, as our tech-blog now transitions to Next.js.
Why the Move?
Before we dive into the intricacies of our transition, let's first explore the rationale behind our decision to switch frameworks. Gatsby has undoubtedly been a reliable choice, boasting a powerful static site generator complemented by a vast array of plugins. However, over the past four years, the landscape has evolved. Gatsby Inc. shifted its focus towards a cloud service, redirecting attention and resources away from the core framework. In another project beside tech-blog we were missing those features promised in the cloud service. Gatsby Inc competed with Netlify and we witnessed downsizing of the developer team as part of cost-cutting measures. Subsequently, in the beginning of 2023, Gatsby was acquired by Netlify, resulting in a noticeable quietness surrounding the project ever since.
Enter Next.js. With its hybrid approach combining SSR, static generation, and client-side rendering, Next.js seemed like the natural progression for this tech-blog and future projects. Its built-in support for React and seamless integration with APIs made it an enticing choice.
Similarities & differences
Next.js and Gatsby share significant similarities due to their foundation in React. React components seamlessly transition between the two frameworks without requiring modifications, preserving knowledge of modern web standards. Key elements such as stylesheets, layouts, and adherence to web accessibility guidelines remain consistent across both platforms. Even the navigation mechanism, facilitated by the Link component, undergoes only minor namespace and parameter changes.
However, the approach to data fetching diverges slightly. Gatsby relies on a GraphQL schema, offering a structured querying system, whereas Next.js adopts a more flexible approach. In Next.js, it is possible to handle data access right inside a component thanks to it's novel approach using React Server Components, providing greater flexibility and clarity.
Transitioning Pain Points
Moving from Gatsby to Next.js wasn't without its challenges. One of the primary hurdles was adjusting to Next.js's file-based routing system compared to Gatsby's convention-based routing. While Gatsby's structure never felt intuitive for dynamic routes, Next.js's file-based approach did look limiting. However, once you understand the the concept, it is has more clarity and gives a better overview to the project structure.
Another pain point was migrating some plugins and dependencies. Gatsby's extensive plugin ecosystem provided solutions for various requirements, from image optimization to SEO, sitemap.xml etc. Adapting these functionalities to Next.js required research and sometimes custom implementation (if it wasn't just a sitemap.xml). However, most plugins did not met our requirements and needed custom code anyway. So this process allowed us to reassess the project's dependencies and optimize for performance and maintainability.
Embracing Next.js Features
Despite the initial challenges, embracing Next.js's features was a game-changer. The ability to choose between server side rendering (SSR), static generation (SSG), or client-side rendering (CSR) based on page requirements empowered us to optimize performance and user experience effectively. And we did use all styles with ease but wanted to pull as much as possible on the server side.
Feature | Rendering style |
---|---|
Article | SSG |
Carousel | CSR |
Search | SSR |
Moreover, one of Next.js's standout features is its built-in API routes. This, perhaps, is my personal favorite architectural change: the elimination of the need for a typical frontend-API for single-page applications (SPAs). Instead, data fetching can be seamlessly hidden on the server side within a clean API class. This translates to a reduced payload, as there are fewer dependencies on the client side. Additionally, it alleviates security concerns, as there is no exposed attack vector. Finally, debugging become effortless in comparison to Gatsby, contributing to a smoother development experience overall.
Code comparison
As an example let's have a look into the blog post template.
Gatsby
File gatsby-config.js
In Gatsby we will define the source in the gatsby-config.js using a plugin, that will read the data and make it available as graphql schema.
module.exports = { pathPrefix: `/tech-blog`, siteMetadata: { title: `Tech Blog`, }, plugins: [ `gatsby-plugin-image`, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content/blog`, name: `blog`, }, }, ...
File gatsby-node.js
The gatsby-node.js file will use the graphql schema to fetch the data at build time and issue createPage actions.
const blogPost = path.resolve(`./src/templates/blog-post.js`) /** * @type {import('gatsby').GatsbyNode['createPages']} */ exports.createPages = async ({ graphql, actions, reporter }) => { const { createPage } = actions // Get all markdown blog posts sorted by date const result = await graphql(` { allMarkdownRemark(sort: { frontmatter: { date: ASC } }, limit: 1000) { nodes { id fields { slug } } } } `) if (result.errors) { reporter.panicOnBuild( `There was an error loading your blog posts`, result.errors ) return } const posts = result.data.allMarkdownRemark.nodes // Create blog posts pages // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js) // `context` is available in the template as a prop and as a variable in GraphQL if (posts.length > 0) { posts.forEach((post, index) => { const previousPostId = index === 0 ? null : posts[index - 1].id const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id createPage({ path: post.fields.slug, component: blogPost, context: { id: post.id, previousPostId, nextPostId, }, }) }) } }
File src/templates/blog-post.js
The final step is rendering each single blog post in the used template blog-post.js.
import React from "react" import { graphql, HeadFC } from "gatsby" import Layout from "../components/layout" import Footer from "../components/footer" import Seo from "../components/seo" import BlogPost from "../components/blogpost" import BannerHeader from "../components/bannerheader" export const pageQuery = graphql`query BlogPostBySlug($id: String!, $previousPostId: String, $nextPostId: String) { site { siteMetadata { title siteUrl } } markdownRemark(id: {eq: $id}) { id excerpt(pruneLength: 160) html fields { slug } frontmatter { title date(formatString: "MMMM DD, YYYY") description tags description160 author coverImage { childImageSharp { gatsbyImageData(width: 600, height: 600, quality: 100, layout: FIXED) fixed { src } } } coverImageUrl } } previous: markdownRemark(id: {eq: $previousPostId}) { fields { slug } frontmatter { title date(formatString: "MMMM DD, YYYY") description description160 tags author } } next: markdownRemark(id: {eq: $nextPostId}) { fields { slug } frontmatter { title date(formatString: "MMMM DD, YYYY") description description160 tags author } } }` const BlogPostTemplate = ({ data, location }) => { const post = data.markdownRemark const siteTitle = data.site.siteMetadata?.title || `Title` const { previous, next } = data return ( <Layout location={location} title={siteTitle}> <BannerHeader post={post} shareLinks={true} detailPage={true} /> <section> <div className="blog-container container"> <div className=""> <div className="row"> <div className="col-12 article-body m-x-auto"> <div class="blog-post-content" dangerouslySetInnerHTML={{ __html: post.html }} itemProp="articleBody" ></div> </div> </div> </div> </div> </section> <footer> {(next || previous) && ( <div className="next-post"> <div className="blog-container container"> <div className="row"> <div className="col-12 m-x-auto"> <h2>More From Tech Blog</h2> <BlogPost noSeparator={true} post={next ? next : previous} /> </div> </div> </div> </div> )} <Footer /> </footer> </Layout> ); } export default BlogPostTemplate export const Head = ({ data, location }) => ( <Seo title={data.markdownRemark.frontmatter.title} description160={data.markdownRemark.frontmatter.description || data.markdownRemark.excerpt} image={data.markdownRemark.frontmatter.coverImage?.childImageSharp.gatsbyImageData.src} postAuthor={data.markdownRemark.frontmatter.author} datePublished={data.markdownRemark.frontmatter.date} keywords={data.markdownRemark.frontmatter.tags.join(',')} articleBody={data.markdownRemark.html} /> )
Next.js
File: src/app/(standard)/posts/[...slug]/page.tsx
In Next.js the blog post template composes the visual aspects and the data access in one file and its folders name.
- Using the (parentheses) in the folders name will create a group. The group path will not be mapped to the URL path and allows organizing segments and project files into logical groups. We use another layout for blog posts than the hero page for example.
- Using the [square brackets] on a folders name to create dynamic segments that are passed to our page and generateMetadata functions
- The function
generateStaticParams
is used for the static site generation (SSG) to pass the slug to the template in order to render the page at build time. - The function
generateMetadata
is used to return the dynamic data. We use it to export openGraph information per blog post, such es the title.
import { PostBody } from '@/app/_components/post-body'; import { PostFooter } from '@/app/_components/post-footer'; import { PostHeader } from '@/app/_components/post-header'; import { getAllPosts, getPostBySlug } from '@/lib/api'; import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { ArticleJsonLd } from 'next-seo'; export default async function Post({ params }: Params) { const post = await getPostBySlug(params.slug); if (!post) { return notFound(); } const allPosts = await getAllPosts(); const postIndex = allPosts.findIndex( (item) => { return item.title === post.title; }); return ( <> <ArticleJsonLd useAppDir={true} url={`https://www.exxcellent.de/tech-blog/${post.slug}`} title={post.title} images={[ 'https://www.exxcellent.de/tech-blog/', ]} datePublished={new Date(post.date).toISOString()} dateModified={new Date(post.date).toISOString()} authorName={[ { name: post.author, url: 'https://www.exxcellent.de', }, ]} publisherName="eXXcellent solutions GmbH" publisherLogo="https://www.exxcellent.de/wp-content/uploads/2021/10/Startseite-Feature.jpg" description={post.description160 ?? ''} isAccessibleForFree={true} /> <article className="mb-32 mt-16 px-2"> <PostHeader title={post.title} coverImage={post.coverImage} date={post.date} author={post.author} tags={post.tags}/> <PostBody markdown={post.content} slug={post.slug} /> </article> <PostFooter posts={allPosts} postIndex={postIndex}/> </> ); } type Params = { params: { slug: string[]; }; }; export async function generateMetadata({ params }: Params): Promise<Metadata> { const post = await getPostBySlug(params.slug); if (!post) { return notFound(); } const title = `${post.title} | eXXcellent solutions`; return { openGraph: { title, images: [post.ogImage?.url], }, }; } export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((post) => ({ slug: post.slug.split('/'), })); }
File: src/lib/api.ts
The api.ts is called from the template and loading data from the filesystem.
import { Post } from '@/interfaces/post'; import fs from 'node:fs/promises'; import matter from 'gray-matter'; import { join } from 'path'; import Fuse, { IFuseOptions } from 'fuse.js'; import { logger } from "./logger"; const postsDirectory = join(process.cwd(), 'public/blog'); const fuseOptions: IFuseOptions<Post> = { keys: ['title', 'content', 'tags'] satisfies Array<keyof Post>, }; export async function getPostDirListing() { return await fs.readdir(postsDirectory, { recursive: true }); } export async function getPostBySlug(slug: string[]):Promise<Post|undefined> { const realSlug = join(...slug).replace(/\.md$/, ''); const fullPath = join(postsDirectory, `${realSlug}.md`); try { const fileContents = await fs.readFile(fullPath, 'utf8'); const { data, content } = matter(fileContents); logger.debug(`Read blog post from path=${fullPath}`); return { ...data, slug: realSlug, content } as Post; } catch (Exception) { logger.error("Could not read the blog post with slug", slug); return undefined; } } export async function getAllPosts() { const postDirElements = await getPostDirListing(); logger.debug(`Reading ${postDirElements.length} blog posts from directory.`); const posts = await Promise.all( postDirElements .filter((element) => element.includes('.md')) .map((slug) => getPostBySlug((slug as string).split('/'))), ); const cleanedPosts:Post[] = (posts.filter( (post) => post !== undefined) as Post[]) // sort posts by date in descending order return cleanedPosts.sort((post1, post2) => (post1.date > post2.date ? -1 : 1)); } ...
Debugging
When it comes to developer experience, Gatsby presents its challenges. Debugging code occurring at build time or within lifecycle phases, such as post-build plugins or client-side operations, can be cumbersome. This often entails connecting to the Chrome debug port and painstakingly deciphering when certain actions are triggered. The process has never been a pleasant developer experience, often resorting to console output debugging reminiscent of techniques from the 1960s.
Debugging in Next.js in comparison is straight forward. Just start the application from your vscode instance in debug mode (all launch scripts are already included) and start debugging the server side. You just open the URL from the browser and boom, the breakpoint stops (including hot reload).
Conclusion
Transitioning from Gatsby to Next.js proved to be highly rewarding. The process unfolded smoothly, thanks to the striking similarities and shared objectives between the two frameworks. Alongside this technical transition, we also took the opportunity to refresh our theme with some vibrant new colors.
The light house scores of the new tech-blog are en par with the old Gatsby version.
Next.js stands shoulder to shoulder with Gatsby in terms of flexibility, performance, and developer experience. However, it introduces a fresh approach to tackling web development projects. Whether you're contemplating a similar transition or delving into new frameworks, we urge you to consider Next.js for your upcoming project. Its versatility and comprehensive ecosystem position it as a compelling option for modern web development.
Our latest Architectural Decision Record has now favored Next.js over Gatsby, Astro, and Remix. But just as surely as the spring will arrive with its snowdrops 2025, new web frameworks will continue to emerge and fade. For At this moment, opting for Next.js feels like the most fitting decision.
Image sources
The cover image used in this post was created by pixabay under the following license. All other images on this page were created by eXXcellent solutions under the terms of the Creative Commons Attribution 4.0 International License