Build a Blog using Next.JS and

Build a Blog using Next.JS and

In this article you will learn how to build a Next.JS blog by fetching your Posts directly from

I received an incredible feedback from my Post Use Notion as a database for your Next.JS Blog thanks from all of you 🙌

I even saw the Post on the front page of 😶

Today I wanted to share with you how I built my Personal Blog under an hour by using the API.

Let's get started 🔥

1. Create a new Next.JS App

Start by using the create-next-app utility using your favorite package manager.

$ npx create-next-app@latest
$ yarn create next-app
$ pnpm create next-app

Check ✅ for everything! We want linting, typings and obviously the App Router! I also highly recommend to use the src folder.

2. Install dependencies

You will need 9 dependencies:

  • remark: We will use it to parse our Posts Markdown
  • remark-html: A Remark plugin to transform our Markdown into HTML
  • rehype: A library to process and extend HTML
  • rehype-highlight: A Rehype plugin to plug highlight.js to highlight our code
  • rehype-slug: A Rehype plugin that adds ids to our Post titles (anchors)
  • @jsdevtools/rehype-toc: A Rehype plugin that generates a table of content based on our Post titles
  • rehype-stringify: A Rehype plugin that transform our Rehype output into a String
  • remark-rehype: A Remark plugin to use Remark and Rehype in symbiose
  • unified: The library to make easy to use all thoses plugins together
$ npm install remark remark-html rehype rehype-highlight rehype-slug @jsdevtools/rehype-toc rehype-stringify remark-rehype unified

$ yarn add remark remark-html rehype rehype-highlight rehype-slug @jsdevtools/rehype-toc rehype-stringify remark-rehype unified

3. Fetch from provide a wonderful Public API that does not require any authentication you can find the official documentation here.

In our case we will need two things:

  • Fetching our Posts /api/articles?username=<username>
  • Fetching a specific Post /api/articles/<username>/<post-slug>

Add environment variables

It is a good practice to avoid hardcoding values, in case you want to change your username or open-source your blog.

Add your username in .env.local to avoid hardcoding it:


Add typings

Let's add some typings to type the response of the API in src/lib/devto/types.ts:

// src/lib/devto/types.ts

export type User = {
  user_id: number;
  name: string;
  username: string;
  twitter_username: string | null;
  github_username: string | null;
  website_url: string | null;
  profile_image: string;
  profile_image_90: string;

export type Post = {
  type_of: string;
  id: number;
  title: string;
  description: string;
  readable_publish_date: string;
  slug: string;
  path: string;
  url: string;
  comments_count: number;
  collection_id: number | null;
  published_timestamp: string;
  positive_reactions_count: number;
  cover_image: string | null;
  social_image: string;
  canonical_url: string;
  created_at: string;
  edited_at: string;
  crossposted_at: string | null;
  published_at: string;
  last_comment_at: string;
  reading_time_minutes: number;
  tag_list: string[];
  tags: string;
  user: User;

export type PostDetails = Post & {
  body_html: string;
  body_markdown: string;
  tags: string[];

I manually made thoses types and they maybe does not exactly match the actual API, feel free to update them.

Create the fetching functions

Next create a new file src/lib/devto/fetch.ts, it will contains the functions that will fetch the API. It is a good practice to separate them from your App to make them easily reusable.

// src/lib/devto/fetch.ts

import { notFound } from "next/navigation";
import { Post, PostDetails } from "./types";

export async function fetchPosts(): Promise<Post[]> {
  const res = await fetch(
      next: { revalidate: 3 * 60 * 60 },

  if (!res.ok) notFound();
  return res.json();

export async function fetchPost(slug: string): Promise<PostDetails> {
  const res = await fetch(
      next: { revalidate: 3 * 60 * 60 },

  if (!res.ok) notFound();
  return res.json();

Notice that we add the parameter revalidate: 3 * 60 * 60. By default the fetch function extended by Next.JS will cache everything. It will make your Blog blazingly fast but we also want to keep our Blog up to date. With this parameter we tell Next.JS to revalidate the cache every 3 hours.

notFound() acts like a return and will show the not-found.tsx page. More information here.

4. Create the render function

Now let's create a function to render the content of your Posts by using Remark, Rehype and all the plugins:

// src/lib/markdown.ts

import toc from "@jsdevtools/rehype-toc";
import rehypeHighlight from "rehype-highlight";
import rehypeSlug from "rehype-slug";
import rehypeStringify from "rehype-stringify";
import remarkRehype from "remark-rehype";
import remarkParse from "remark-parse";
import { unified } from "unified";

export function renderMarkdown(markdown: string): Promise<string> {
  return unified()
    .use(rehypeHighlight, { ignoreMissing: true })
    .use(toc, {
      headings: ["h1", "h2", "h3"],
    .then((res) => res.toString());

5. Create the pages

Now that you have everything in place to fetch your posts, it is time to create the pages!

The Posts page

It can't be simpler, simply use your fetchPosts function and show them:

// src/app/blog/page.tsx

import { fetchPosts } from "@/lib/devto/fetch";

export default async function Page() {
  const posts = await fetchPosts();

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-8">
      { => (
        <Link href={`/blog/${post.slug}`}>{post.title}</Link>

The Post page

Create a new page with a dynamic segment for our slug src/app/blog/[slug]/page.tsx.

Use the parameters to fetch the post and use the renderMarkdown function to transform your Markdown into HTML.

You can also add generateMetadata to set the title and the description using the data of your Post.

// src/app/blog/[slug]/page.tsx

import 'highlight.js/styles/github-dark.css'; // Import your favorite highlight.js theme

import { fetchPost, fetchPosts } from "@/lib/devto/fetch";
import { renderMarkdown } from "@/lib/markdown";

export async function generateMetadata({
}: {
  params: { slug: string };
}) {
  const { title, description } = await fetchPost(params.slug);

  return {

export default async function Page({ params }: { params: { slug: string } }) {
  const { body_markdown } = await fetchPost(params.slug);

  const content = await renderMarkdown(body_markdown);

  return (
        <div dangerouslySetInnerHTML={{ __html: content }} />

Notice that you are calling twice the fetchPost method, so are you fetching twice? No! It uses the cache, you can verify it when running the dev server, you should see cache: HIT.

And you know what is reaaaally cool? Navigate to the list of your posts and hover the links, you should see in your console your /blog/[slug] pages pre-rendering to predict the user navigation 🔥

6. Going further

I hope that this post motivated you to build an incredible blog! Share your work in the comment section! 💬

Oh and if you want more content like this, follow me: