DEV Community

Cover image for Use Next.js to create a blog from Dev.to API
David Morrow
David Morrow

Posted on

4

Use Next.js to create a blog from Dev.to API

So if you have written a few articles on Dev.to, you may want to have your own site with your articles. I had previously just copied and pasted the markdown, but I recently discovered that there is a simple public API for Dev.to. So this article is about how I used the API to leverage Dev.to as a CMS for my personal site.

I am using this for my site NewFinds.com, you can find the full source here on GitHub

Getting started

There are a ton of great articles, and the docs for Next.js are fantastic. So I am not going to go too deep into the usage of Next.js, but I will touch on a few things that are relevant to static site generation from dynamic content.

The API endpoints we will be using for our data.

There are only three endpoints that we need to list posts for a user, post details (which contains the markdown), and the comments for a post. They are as follows.

  • The list of Posts: https://dev.to/api/articles?username=${username}&page=1&per_page=100
  • The comments for a Post: https://dev.to/api/comments?a_id=${postId}
  • The post details: https://dev.to/api/articles/${username}/${slug}

I am using Typescript, which I find really useful when dealing with APIs. The types for the responses from the API are as such.

// lib/types.ts
export type User = {
  name: string;
  username: string;
  twitter_username: string;
  github_username: string;
  user_id: number;
  website_url: string;
  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;
  public_reactions_count: number;
  positive_reactions_count: number;
  collection_id: number | null;
  published_timestamp: string;
  language: string;
  subforem_id: number | null;
  cover_image: string | null;
  social_image: string;
  canonical_url: string;
  created_at: string;
  edited_at: null;
  crossposted_at: string;
  published_at: string;
  last_comment_at: string;
  reading_time_minutes: number;
  tag_list: string[];
  tags: string;
  user: User;
};

export type PostDetails = {
  body_html: string;
  body_markdown: string;
} & Post;

export type Comment = {
  type_of: string;
  id_code: string;
  created_at: string;
  body_html: string;
  user: User;
  children: Comment[];
};
Enter fullscreen mode Exit fullscreen mode

The only difference between Post and PostDetails is body_html and body_markdown. The body_markdown is what we will use for rendering our post.

Getting setup to statically render the site

The great thing about Next.js is that it can be whatever you need. An API with a database, a frontend SPA, an entirely static site, a static site built from dynamic content. This last one is our use case.

In order to let Next.js pre-render everything on build, (generate html files for every page) we have to setup our next.config.ts file. The output = "export" tells Next.js that we want to pre-render everything.

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
  //...
}
Enter fullscreen mode Exit fullscreen mode

So now, when we run npm run build our site will be pre built to a folder called out, which we can host anywhere statically, its just HTML files and client side Javascript now.

Calling the API

Ill put our API calls into a separate file where we will make our API calls. I pulled this out, so that it can be shared with a few of our pages/components.

// app/lib/api.ts
import { Post, PostDetails, Comment } from "./types";
import { username } from "./consts";

export async function getAllPosts(): Promise<Post[]> {
  const res: Response = await fetch(
    `https://dev.to/api/articles?username=${username}&page=1&per_page=100`,
  );
  return await res.json();
}

export async function getPostBySlug(slug: string): Promise<PostDetails> {
  const res: Response = await fetch(
    `https://dev.to/api/articles/${username}/${slug}`,
  );
  return res.json();
}

export async function getComments(postId: number): Promise<Comment[]> {
  const res: Response = await fetch(
    `https://dev.to/api/comments?a_id=${postId}`,
  );
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

The dynamic article page app/articles/[slug]/page.tsx

Ok so now that we have the ability to load data from dev.to, lets use it to create all the static pages generated from all our articles that come on the API.

Next.js uses the file and folder name to generate params, and determine if a page is dynamic. For more info: dynamic routes

// app/articles/[slug]page.tsx
import type { Post, PostDetails } from "@/app/lib/types";
import type { Metadata } from "next";
import type { JSX } from "react";

import Markdown from "react-markdown";
import rehypeHighlight from "rehype-highlight";

import { getAllPosts, getComments, getPostBySlug } from "@/app/lib/api";
import Comments from "@/app/components/comments";

type Params = {
  slug: string;
};

type DynamicArgs = {
  params: Promise<Params>;
};

export async function generateStaticParams(): Promise<Params[]> {
  const posts: Post[] = await getAllPosts();
  return posts.map((post: Post) => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: DynamicArgs): Promise<Metadata> {
  const { slug } = await params;
  const post: PostDetails = await getPostBySlug(slug);

  return {
    title: post.title,
    description: post.description,
  };
}

export default async function Page({ params }: DynamicArgs): Promise<JSX.Element> {
  const { slug } = await params;
  const post: PostDetails = await getPostBySlug(slug);
  const comments = await getComments(post.id);

  return (
    <>
      <article className="md">
        <h1>{post.title}</h1>
        <Markdown rehypePlugins={[rehypeHighlight]}>
          {post.body_markdown}
        </Markdown>
      </article>

      <footer>
        <div className="comments md">
          <h2>Comments ({comments.length})</h2>
          <Comments comments={comments} />
        </div>
      </footer>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Generating the Static Params

The generateStaticParams function tells Next.js how many pages to generate, and supplies the params, (in our case the slug) to the routes.

It is an async function, we load all the posts from our api, and then return an array of Objects, with a key of slug in each.

export async function generateStaticParams(): Promise<Params[]> {
  const posts: Post[] = await getAllPosts();
  return posts.map((post: Post) => ({ slug: post.slug }));
}
Enter fullscreen mode Exit fullscreen mode

Generating the Metadata

Similar to above, generateMetadata function tells Next.js what metadata is for each page. We load the post for the page by slug using our API, and then return title, and description which Next.js will use to populate our <head> on the page.

export async function generateMetadata({ params }: DynamicArgs): Promise<Metadata> {
  const { slug } = await params;
  const post: PostDetails = await getPostBySlug(slug);

  return {
    title: post.title,
    description: post.description,
  };
}
Enter fullscreen mode Exit fullscreen mode

Rendering the Markdown to HTML

First we load react-markdown and rehype-highlight plugin which we will use to syntax highlight code examples in our markdown.

I think being able to render the syntax highlighting (at compile time), rather than on the client at run-time is a pretty neat benefit of pre-rendering our pages.

import Markdown from "react-markdown";
import rehypeHighlight from "rehype-highlight";
Enter fullscreen mode Exit fullscreen mode
<Markdown rehypePlugins={[rehypeHighlight]}>
  {post.body_markdown}
</Markdown>
Enter fullscreen mode Exit fullscreen mode

Rendering our comments

I chose to render the comments, and its pretty simple with the data structure that is returned on the API. We simply render the <Comments> component and pass in the comments for our article that we loaded.

// app/articles/[slug]/page.tsx
<h2>Comments ({comments.length})</h2>
<Comments comments={comments} />
Enter fullscreen mode Exit fullscreen mode

Now if you remember the schema for comments, it supports children. Like responses to a comment and so forth. In order to support that, we can simply recursivly render the <Comments> component, passing children as the comments prop.

I am stripping the HTML here, I assume Dev.to sanitizes user input, but its always a good idea to be careful with displaying user supplied content.

// app/components/comments.tsx

import type { JSX } from "react";
import type { Comment } from "@/app/lib/types";
import { stripHtml } from "string-strip-html";
import Image from "next/image";

type Props = {
  comments: Comment[];
};

export default function Comments({ comments }: Props): JSX.Element {
  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id_code}>
          <h4>
            <Image
              src={comment.user.profile_image_90}
              alt={comment.user.name}
              width={48}
              height={48}
            />
            {comment.user.name}
          </h4>
          <p>{stripHtml(comment.body_html).result}</p>
          {comment.children.length ? (
            <Comments comments={comment.children} />
          ) : null}
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Updating the site

Since the site is completely static, you will need to npm run build the site each time you want to pull down more article, or comments ect. But if you are using Github pages like I am, it is as simple as running build and then pushing your changes.

Top comments (0)