Telerik blogs
JavaScriptT2 Light_1200x303

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.

Getting Started

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

Creating Our Files

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:

  • The function called 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.
  • The function called getArticleBySlug is responsible for receiving a slug and an array of fields as arguments and returning a blog post.
  • The function called 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>;
}

Creating Our Components

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;

Rendering Our Blog Posts

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.

Conclusion

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 Maldonado
About the Author

Leonardo Maldonado

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.

Related Posts

Comments

Comments are disabled in preview mode.