ComCord-The hustle free way to build a next.js blog with dev.toThe hustle free way to build a next.js blog with dev.to
ComCord-Lois
LoisOct 25th, 2023

The plain css way. Inspired by @martinp 's Build a Blog using Next.JS and DEV.to, 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, remark-rehype, oh boy, I could write another blog with that.

Next.js also provided a nice utility tool like withMDX to help you build blog using MDX and pointed me towards contentlayer--let's just say the amount of time I was debugging contentlayer consumed most part of my energy, and I already had a blog 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 (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
Enter fullscreen mode Exit fullscreen mode

And check ✅ for everything. And yes, tailwind, app router.

2. Fetch from dev.to

All documentation about dev.to api is here 👈🏻

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"
Enter fullscreen mode Exit fullscreen mode

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

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();
}
Enter fullscreen mode Exit fullscreen mode

For the usage of notFound() and revalidate, check his post for more details 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Card component came from ui.shad.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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

I used the react-wrap-balancer just to help the title align better, for more details check here. If you know 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;
    }
Enter fullscreen mode Exit fullscreen mode

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.