Next.js may be the best React framework for building your blog. Among the benefits are performance, bundle size and SEO. Let’s build one!
Next.js has become one of the most important frameworks for React applications. It helps developers to build better server-side rendering React applications without boilerplate.
There are many features in Next.js that make it one of the best React frameworks out there—a rich developer experience, smart bundling, route pre-fetching, TypeScript support, SEO, etc.
Creating a blog using Next.js is the best option today for those who want to have a simple but powerful blog—without ending up with a lot of code and while increasing our SEO ranking.
SEO (search engine optimization) is the process of improving your application to rank better on search engines. It is very important for any blog that wants to rank better on search engines and bring in more traffic. A good application with a bad SEO ranking will not be productive, effective or successful.
We are going to use Next.js in this article to build a static-generated and production-ready blog. We will walk through how SSG (static site generation) works and end up with a very good blog with an effective SEO.
We are going to create a new Next.js application using the Create Next App CLI tool. It helps us to easily get started with Next.js and create a new application. To get started, we are going to use the following command:
npx create-next-app blog-with-next-js --example --with-typescript
We used the --example
option for creating a new Next.js application using the example name for the Next.js repository. We used the --with-typescript
option for creating a new Next.js application with TypeScript.
Now that we have our new Next.js application, we are going to create our folder structure.
This is how our folder structure is going to look:
-- src
-- pages
-- components
-- articles
-- lib
We are going to remove all folders that come from the Create Next App CLI and create a new folder called src
. Inside the src
folder, we are going to create all the folders that we are going to need to create our blog.
Now, we are going to install all the dependencies that we are going to need.
yarn add @emotion/styled @next/mdx date-fns gray-matter mdx-prism next-mdx-remote next-seo reading-time rehype remark-autolink-headings remark-capitalize remark-code-titles remark-external-links remark-images remark-slug
After we install all of our dependencies, we are going to our pages folder and create a new file called _app.tsx
.
This is how our _app.tsx
file is going to look:
function MyApp({ Component, pageProps }: any) {
return <Component {...pageProps} />;
}
export default MyApp
Now, inside our articles
folder, we are going to create a new file called introducing-blog-with-nextjs.mdx
. All the blog posts of our blog will be written using Markdown and should have some content outlined by --
which is known as front matter. The front matter holds all the information of our blog post.
This is how our first blog post is going to look:
---
title: "Introducing Blog with Next.js"
description: "A new blog using Next.js and Markdown"
date: "14 Apr, 2021"
slug: "introducing-blog-with-nextjs"
ogImage:
url: "/images/articles/introducing-blog-with-nextjs.jpeg"
---
Nulla tortor orci, porttitor in pulvinar sit amet, ultricies sit amet sem. Nullam et posuere felis, sit amet convallis urna. Pellentesque vel ipsum dolor.
Now that we have our first blog post written, we are going to our lib
folder and create some helper functions that we are going to need.
We are going to create a file called lib.ts
and put the following code there:
import fs from "fs";
import { join } from "path";
import matter from "gray-matter";
import readingTime from "reading-time";
import { API, BlogArticleType } from "src/types";
const articlesDirectory = join(process.cwd(), "src/articles");
function getRawArticleBySlug(slug: string): matter.GrayMatterFile<string> {
const fullPath = join(articlesDirectory, `${slug}.mdx`);
const fileContents = fs.readFileSync(fullPath, "utf8");
return matter(fileContents);
}
function getAllSlugs(): Array<string> {
return fs.readdirSync(articlesDirectory);
}
function getArticleBySlug(
slug: string,
fields: string[] = [],
): BlogArticleType {
const realSlug = slug.replace(/\.mdx$/, "");
const { data, content } = getRawArticleBySlug(realSlug);
const timeReading: any = readingTime(content);
const items: BlogArticleType = {};
fields.forEach((field) => {
if (field === "slug") {
items[field] = realSlug;
}
if (field === "content") {
items[field] = content;
}
if (field === "timeReading") {
items[field] = timeReading;
}
if (data[field]) {
items[field] = data[field];
}
});
return items;
}
function getAllArticles(fields: string[] = []): Array<BlogArticleType> {
return getAllSlugs()
.map((slug) => getArticleBySlug(slug, fields))
.sort((article1, article2) => (article1.date > article2.date ? -1 : 1));
}
function getArticlesByTag(
tag: string,
fields: string[] = [],
): Array<BlogArticleType> {
return getAllArticles(fields).filter((article) => {
const tags = article.tags ?? [];
return tags.includes(tag);
});
}
function getAllTags(): Array<string> {
const articles = getAllArticles(["tags"]);
const allTags = new Set<string>();
articles.forEach((article) => {
const tags = article.tags as Array<string>;
tags.forEach((tag) => allTags.add(tag));
});
return Array.from(allTags);
}
export const api: API = {
getRawArticleBySlug,
getAllSlugs,
getAllArticles,
getArticlesByTag,
getArticleBySlug,
getAllTags,
};
A little bit of explanation of what’s happening inside this file and what all functions are doing:
getRawArticleBySlug
is responsible for fetching all of our blog posts by slug. It goes to our articles folder and gets all files by slug and returns our data and the content of our blog post.getArticleBySlug
is responsible for receiving a slug and an array of fields as arguments and returning a blog post.getAllArticles
is responsible for fetching all of our blog posts and returning an array of blog posts.Inside our src
folder, we are going to create a types.ts
file, where we are going to create all of our TypeScript interfaces and types. Inside the file, paste the following code:
import matter from "gray-matter";
export interface AuthorType {
name: string;
picture: string;
}
export interface ArticleType {
slug: string;
title: string;
description: string;
date: string;
coverImage: string;
author: AuthorType;
excerpt: string;
timeReading: {
text: string;
};
ogImage: {
url: string;
};
content: string;
}
export interface BlogArticleType {
[key: string]: string | Array<string>;
}
export interface API {
getRawArticleBySlug: (slug: string) => matter.GrayMatterFile<string>;
getAllSlugs: () => Array<string>;
getAllArticles: (fields: string[]) => Array<BlogArticleType>;
getArticlesByTag: (tag: string, fields: string[]) => Array<BlogArticleType>;
getArticleBySlug: (slug: string, fields: string[]) => BlogArticleType;
getAllTags: () => Array<string>;
}
Inside our components
folder, we are going to create two new folders.
We are going to create a folder called ArticleItem
, which is where we are going to create the component for rendering an article as a preview.
We are going to create a folder called Article
, where we are going to create our component for rendering a specific article.
We are going to start with our ArticleItem
folder. Inside the folder, create a file called ArticleItem.tsx
and a file called ArticleItem.styles.ts
.
Inside our ArticleItem.styles.ts
, we are going to create some simple styling for our component using Emotion. Put the following code there:
import styled from "@emotion/styled";
export const ArticleItemContainer = styled.article`
width: 100%;
max-width: 800px;
height: fit-content;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 250px repeat(auto-fill, max-content);
grid-row-gap: 20px;
align-items: center;
@media screen and (min-width: 1000px) {
grid-template-columns: 340px 1fr;
grid-template-rows: 1fr;
grid-column-gap: 20px;
align-items: center;
}
`;
Now, inside our ArticleItem.tsx
file, we are going to paste the following code:
import React from "react";
import NextLink from "next/link";
import { ArticleItemContainer } from "./ArticleItem.styles";
import { ArticleType } from "src/types";
interface Props {
article: ArticleType;
};
const ArticleItem = ({ article }: Props) => (
<ArticleItemContainer>
<img
src={article.ogImage.url}
alt="Image for article"
style={{ width: "100%", height: 250, borderRadius: 5, objectFit: "cover" }}
lazy="loading"
/>
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
<NextLink as={`/blog/${article.slug}`} href="/blog/[slug]">
<a href="/blog">
{article.title}
</a>
</NextLink>
<p style={{ color: "#6F6F6F, fontSize: 16, fontWeight: 300 }}>
{article.description}
</p>
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
<p style={{ color: "#6F6F6F, fontSize: 16, fontWeight: 300 }}>
{article.timeReading.text}
</p>
<p style={{ color: "#6F6F6F, fontSize: 16, fontWeight: 300 }}>
•
</p>
<p style={{ color: "#6F6F6F, fontSize: 16, fontWeight: 300 }}>
{article.date}
</p>
</div>
<NextLink as={`/blog/${article.slug}`} href="/blog/[slug]">
<a href="/blog" color="#6f6f6f">
Read article
</a>
</NextLink>
</div>
</ArticleItemContainer>
);
export default ArticleItem;
Next, inside our Article
folder, we will create a file called Article.tsx
and two more folders called Header
and Content
.
Inside the Header
folder, we will create a file called Header.tsx
and paste the following code there:
import React from "react";
interface Props {
readingTime: {
text: string;
};
title: string;
description: string;
date: string;
ogImage: {
url: string;
};
}
const Header = ({ title, description, date, ogImage }: Props) => (
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
<p style={{ color: "#6F6F6F", fontWeight: "300", textAlign: "center" }}>
Published on {date}
</p>
<h1 style={{ color: "#101010", fontWeight: "600", textAlign: "center" }}>
{title}
</h1>
<p style={{ color: "#6F6F6F", fontWeight: "300", textAlign: "center" }}>
{description}
</p>
<img
src={ogImage.url}
alt="Post image"
style={{ width: "100%", height: 400, borderRadius: 5, objectFit: "cover" }}
lazy="loading"
/>
</div>
);
export default Header;
Inside the Content folder
, we will create a file called Content.tsx
and paste the following code there:
import React from "react";
interface Props {
content: React.ReactNode;
};
const Content = ({ content }: Props) => (
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
{content}
</div>
);
export default Content;
Now, inside our Article.tsx
file, paste the following code:
import React from "react";
import Header from "./Header/Header";
import Content from "./Content/Content";
interface Props {
readingTime: {
text: string;
};
title: string;
description: string;
date: string;
ogImage: {
url: string;
};
content: React.ReactNode;
slug: string;
};
const Article = ({
readingTime,
title,
description,
date,
ogImage,
content,
}: Props) => (
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
<Header
readingTime={readingTime}
title={title}
description={description}
date={date}
ogImage={ogImage}
/>
<Content content={content} />
<hr />
</div>
);
export default Article;
Now that we have created our components and the helper functions that we are going to need, we are going to actually create the pages for rendering our blog posts.
Inside our pages
folder, we are going to have a file called index.tsx
where we will render all of our blog posts.
Inside the index.tsx
file we are going to import the ArticleItem
component that we created. After that, we are going to import the API from our lib.ts
file to return all of our blog posts.
import React from "react";
import ArticleItem from "src/components/ArticleItem/ArticleItem";
import { api } from "src/lib/lib";
import { BlogArticleType, ArticleType } from "src/types";
interface Props {
articles: Array<ArticleType>;
};
const Index = ({ articles }: Props) => (
<div style={{ display: "flex", direction: "column", alignItems: "center", justifyItems:"center" }}>
{articles.map((article: ArticleType) => (
<ArticleItem key={article.slug} article={article} />
))}
</div>
);
For each blog post that we have, we’re going to render an ArticleItem
component. We’re receiving our articles as a prop but we need to fetch them.
We’re going to use the getStaticProps
function from Next.js to fetch all of our blog posts and pass our articles as a prop to our component.
export const getStaticProps = async () => {
const articles: Array<BlogArticleType> = api.getAllArticles([
"slug",
"title",
"description",
"date",
"coverImage",
"excerpt",
"timeReading",
"ogImage",
"content",
]);
return {
props: { articles },
};
};
We’re now rendering all of our blog posts correctly. Now, inside our pages
folder, we’re going to create a folder called blog
and inside that folder create a file called [slug].tsx
.
Inside this file, we’re going to render a specific blog post. We’re going to import a few dependencies that we’re going to need and again the API from our lib.ts
file.
import React from "react";
import readingTime from "reading-time";
import mdxPrism from "mdx-prism";
import renderToString from "next-mdx-remote/render-to-string";
import hydrate from "next-mdx-remote/hydrate";
import { NextSeo } from "next-seo";
import MDXComponents from "src/components/MDXComponents/MDXComponents";
import Article from "src/components/Article/Article";
import { api } from "src/lib/lib";
import { BlogArticleType } from "src/types";
interface Props {
readingTime: {
text: string;
};
frontMatter: {
title: string;
description: string;
date: string;
content: string;
ogImage: {
url: string;
};
};
slug: string;
source: any;
tags: Array<string>;
};
const Index = ({ readingTime, frontMatter, slug, source }: Props) => {
const content = hydrate(source);
return (
<div>
<NextSeo
title={frontMatter.title}
description={frontMatter.description}
/>
<Article
readingTime={readingTime}
title={frontMatter.title}
description={frontMatter.description}
date={frontMatter.date}
content={content}
ogImage={frontMatter.ogImage}
slug={slug}
/>
</div>
);
};
We’re going to use the getStaticPaths
from Next.js and pass a list of paths that have to be pre-rendered at build time. We’re also going to use the renderToString
function from next-mdx-remote to turn our content into a string and use some plugins for our Markdown text.
type Params = {
params: {
slug: string;
timeReading: {
text: string;
};
};
};
export async function getStaticProps({ params }: Params) {
const { content, data } = api.getRawArticleBySlug(params.slug);
const mdxSource = await renderToString(content, {
components: MDXComponents,
mdxOptions: {
remarkPlugins: [
require("remark-autolink-headings"),
require("remark-slug"),
require("remark-code-titles"),
require("remark-autolink-headings"),
require("remark-capitalize"),
require("remark-code-titles"),
require("remark-external-links"),
require("remark-images"),
require("remark-slug"),
],
rehypePlugins: [mdxPrism],
},
});
const tags = data.tags ?? [];
return {
props: {
slug: params.slug,
readingTime: readingTime(content),
source: mdxSource,
frontMatter: data,
tags,
},
};
}
export async function getStaticPaths() {
const articles: Array<BlogArticleType> = api.getAllArticles(["slug"]);
return {
paths: articles.map((articles) => {
return {
params: {
slug: articles.slug,
},
};
}),
fallback: false,
};
};
We now have a fully production-ready and statically generated blog using Next.js.
Creating a blog using Next.js is very easy and straightforward. The benefits of Next.js, especially for blogs, are huge. Your blog application will have a very good performance, a small bundle and a good SEO score.
Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.