Lois
Posted on October 25, 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
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"
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();
}
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>
);
}
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>
</>
);
}
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;
}
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.
Posted on October 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.