The hustle free way to build a next.js blog with dev.to
Tags: webdev, nextjs, tutorial, comcord
Published: 2023-10-25 20:51:25

The plain css way. Inspired by @martinp 's [Build a Blog using Next.JS and DEV.to](https://dev.to/martinp/build-a-blog-using-nextjs-and-devto-15a5), I went on to try it but...(Martin is a fantastic developer, I just need something to fit my needs. If you haven't check his article, I recommend you to read his before coming here.)

Styling the blog, figure out the eco-system within [UnifiedJs](https://unifiedjs.com/), [remark-rehype](https://github.com/remarkjs/remark-rehype#css), oh boy, I could write another blog with that. 

Next.js also provided a nice utility tool like [withMDX](https://nextjs.org/docs/pages/building-your-application/configuring/mdx) to help you build blog using MDX and pointed me towards [contentlayer](https://contentlayer.dev/)--let's just say the amount of time I was debugging contentlayer consumed most part of my energy, and [I already had a blog](https://normal-people.com) built with contentlayer. 

## Use nothing

Time to try something new. No `unifiedJs`. No `remark-rehype`. No `withMdx` if it conflicts with your sentry and any other plugins you need to use in your next.config.mjs

If you want to see the final output of what this is before you continue reading, [check this one](https://comcord.vision/blogs) (forgive me, shameless plug). 

It's honestly dead simple. Everything is almost the same as what @martinp put in. 

To start with--same as what he said

## 1. Create a new next.js app
Whichever package manager of your choice
```
$ npx create-next-app@latest
$ yarn create next-app
$ pnpm create next-app
```
And check  ✅ for everything. And yes, tailwind, app router.

## 2. Fetch from dev.to
All documentation about [dev.to api is here](https://developers.forem.com/api/v1) 👈🏻

Followed the same thing as he did but a little extra, as I want only the blogs with the tag #comcord : 
- Fetching from Posts `/api/articles?username=<username>&tag=<tag>`
- Fetching a specific Post `/api/articles/<username>/<post-slug>`

### Add environment variables
I added dev.to username and tag in `.env.local` 
```
DEVTO_USERNAME="zmzlois"
DEVTO_TAG="comcord"
```
### Add typings
To know what the returns of the our types in use it later in component, add the typings to your `/lib/devto/types.ts` like what he said [here](https://dev.to/martinp/build-a-blog-using-nextjs-and-devto-15a5)


### Fetch from dev.to
```
// src/lib/fetch.ts

import { notFound } from "next/navigation";

import type { Post, PostDetails } from "./types";

export async function fetchPosts(): Promise<Post[]> {
  const res = await fetch(
    `https://dev.to/api/articles?username=${process.env.DEVTO_USERNAME}&tag=${process.env.DEVTO_TAG}`,
    {
      next: { revalidate: 600 },
    },
  );

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

export async function fetchPost(slug: string): Promise<PostDetails> {
  const res = await fetch(
    `https://dev.to/api/articles/${process.env.DEVTO_USERNAME}/${slug}`,
    {
      next: { revalidate: 600 },
    },
  );

  if (!res.ok) notFound();
  return res.json();
}
```
For the usage of `notFound()` and `revalidate`, [check his post for more details](https://dev.to/martinp/build-a-blog-using-nextjs-and-devto-15a5) as I try to copy as little of his content as possible. 

## 4. Create the pages
Yep, skip the render function, we don't need them. 

### Blogs page
```
// src/app/blogs/page.tsx
import Image from "next/image";
import Link from "next/link";
import { cn } from "@packages/ui";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@comcord/ui/card";

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

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

  return (
    <div className={"container mx-auto"}>
      <h1
        className={"my-auto text-4xl font-bold text-antiflash-white md:px-36"}
      >
        Blog
      </h1>
      <div className="grid grid-cols-1 gap-4 py-8 md:grid-cols-2">
        {posts.map((post, index) => (
          <Link
            href={`/blogs/${post.slug}`}
            target="_blank"
            key={post.id}
            className={cn(
              "group   m-0  md:m-4",
              {
                "col-span-1 h-auto md:col-span-2  md:mx-36  ": index === 0,
              },
              {
                "col-span-1 ": index >= 0,
              },
            )}
          >
            <Card
              className={
                " flex h-full flex-col justify-between transition-colors duration-700 group-hover:border-antiflash-white/40"
              }
            >
              <Image
                src={post.cover_image!}
                width={320}
                height={200}
                alt={post.title}
                className={"flex rounded-t-lg md:hidden"}
              />
              <CardHeader>
                <CardTitle className={"leading-8"}>{post.title}</CardTitle>
                <CardDescription
                  className={
                    "transition-colors duration-700 group-hover:text-antiflash-white"
                  }
                >
                  {post.description}
                </CardDescription>
              </CardHeader>
              <CardContent className={"my-2 flex justify-center"}>
                {" "}
                <Image
                  src={post.cover_image!}
                  width={500}
                  height={200}
                  alt={post.title}
                  className={" hidden rounded-md md:flex"}
                />
              </CardContent>
              <CardFooter>
                <span className="flex text-sm text-muted-foreground transition-colors duration-700 group-hover:text-antiflash-white">
                  <Image
                    src={post.user.profile_image_90}
                    width={20}
                    height={20}
                    alt={post.user.name}
                    className={"mr-2 rounded-full"}
                  />{" "}
                  {post.user.name} on {post.readable_publish_date}
                </span>
              </CardFooter>
            </Card>
          </Link>
        ))}
      </div>
    </div>
  );
}
```
The `Card` component came from [ui.shad.com](https://ui.shadcn.com/), dope library, I can see why the whole internet is hyped. 

### The blog page

```

import Image from "next/image";
import { format } from "date-fns";
import Balancer from "react-wrap-balancer";

import { fetchPost } from "~/lib/devto/fetch";

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

  return {
    title,
    description,
  };
}

export default async function Page({
  params,
}: {
  params: {
    slug: string;
  };
}) {
  const { title, user, body_html, cover_image, created_at } =
    await fetchPost(params.slug);

  return (
    <>
      <div className={"my-12  flex flex-col content-center items-center gap-8"}>
        <Image
          src={cover_image!}
          alt={`ComCord-${title}`}
          width={800}
          className={
            "white-boxshadow rounded-md border-[3px] border-smoky-black object-fill"
          }
          height={300}
        />
        <Balancer
          className={
            " text-center text-xl font-extrabold text-antiflash-white md:text-4xl"
          }
        >
          {title}
        </Balancer>
        <div className={"flex gap-8 text-sm font-extralight"}>
          <Image
            src={user.profile_image}
            alt={`ComCord-${user.name}`}
            width={50}
            height={50}
            className={"max-w-8 md:max-w-12 max-h-12 rounded-full md:max-h-12"}
          />
          <div className={"flex flex-col gap-2"}>
            <span className={"text-antiflash-white"}>{user.name}</span>
            <span className={"text-antiflash-white/70"}>
              {format(new Date(created_at), "MMM do, yyyy")}
            </span>
          </div>
        </div>
      </div>
      <article>
        <div
          dangerouslySetInnerHTML={{ __html: body_html }}
          className={"article container mx-auto px-2 md:px-60"}
        />
      </article>
    </>
  );
}
```
I used the `react-wrap-balancer` just to help the title align better, for more details check [here](https://react-wrap-balancer.vercel.app/). If you know [shuding](https://twitter.com/shuding_), you know this is good stuff. 

Dev.to api actually provided a nice return of `body_html` so I don't need to render markdown separately. 

#### The next question might be: so how was this styled? 

## 5. Style it with TailwindCSS

I added `article` in the className when I `dangerouslySetInnerHTML={{ __html: body_html }}`

In `globals.css` file, under the `@layer utilities` I added below 

```
// src/styles/globals.css

@layer utilities {
   
    .article h2 {
        @apply text-2xl font-bold my-4 md:my-6;
    }

    .article h3 {
        @apply text-xl font-bold;
    }

    .article h4 {
        @apply font-bold;
    }

    .article a {
        @apply text-blurple underline underline-offset-4 decoration-blurple ;
    }

    .article img {
        @apply my-8 rounded-sm border;
    }

    .article li {
        @apply list-disc;
    }

    .article p {
        @apply font-light text-sm tracking-wide leading-8 my-4 md:my-6;
    }

    .article blockquote {
        @apply bg-onyx/40 border-l-4 border-gray-500 italic my-8 p-4;
    }

    .article hr {
        @apply my-8;
    }
```

You can copy paste the properties and style them as you wish. 


---

Thanks for Martin's the original post. Wish we all can ship things faster and reduce time to market! 

If you find this useful, follow us with a bunch of extremely talented engineer for the ride to build the most ridiculous team communication tool. 

- [ComCord Sign Up](https://comcord.vision)
- [Discord Server](https://discord.gg/2vxMpkYJHX)
- [Twitter](https://twitter.com/comcord_team)