Next.js Setup
Blog listing
The BlogsPageServer component fetches and renders all blogs server-side. The parent page passes search params down.
Theme preview
Listing page layout
A quick visual pass at how the copied template reads in both light and dark interfaces.
Light theme
Our Blog
blogjs
Search articles by title or category...
Product systems4 min read
Designing content workflows that scale with teams.
Short, scannable cards with enough metadata to help readers choose what to open.
Read more ->
Engineering4 min read
Rendering typed content blocks in App Router.
Short, scannable cards with enough metadata to help readers choose what to open.
Read more ->
Dark theme
Our Blog
blogjs
Search articles by title or category...
Product systems4 min read
Designing content workflows that scale with teams.
Short, scannable cards with enough metadata to help readers choose what to open.
Read more ->
Engineering4 min read
Rendering typed content blocks in App Router.
Short, scannable cards with enough metadata to help readers choose what to open.
Read more ->
File structure
Code
app/
blogs/
page.tsx # route handler, reads ?q= param
components/
BlogsPageServer.tsx # async server component
lib/
blogjs.ts # blogjs clientHow it works
- 1lib\blogjs.tsCode
import { BlogClient } from "blogjs-space"; const client = new BlogClient({ apiKey: "xxxxx-xxxx-xxx-xxxx-xxxxxxxxxxxx", }); export default client; - 2components\BlogsPageServer.tsxCode
import client from "@/lib/blogjs"; import { CONSTANTS } from "@/lib/constants"; import Image from "next/image"; import Link from "next/link"; interface BlogsPageServerProps { query?: string; } export default async function BlogsPageServer({ query }: BlogsPageServerProps) { const result = await client.getBlogs({ query, page: 1, limit: 30, }); const blogs = result.data; if (!blogs.length) { return ( <div className="text-center py-20"> <p className="text-muted-foreground text-lg">No articles found.</p> </div> ); } return ( <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> {blogs.map((blog) => ( <Link key={blog.slug} href={`/blogs/${blog.slug}`} className="group block"> <article className="flex flex-col h-full bg-card rounded-3xl p-6 transition-all duration-300 shadow-sm hover:shadow-lg hover:-translate-y-1"> {/* Category */} {blog.categories?.name && ( <div className="flex items-center justify-between mb-4"> <span className="text-xs font-medium px-3 py-1 bg-background border border-border rounded-full text-primary"> {blog.categories.name} </span> {blog.readTime && ( <span className="text-xs text-muted-foreground"> {blog.readTime} min read </span> )} </div> )} {/* Image */} {blog.image?.url && ( <div className="relative w-full h-40 xl:h-44 mb-5"> <Image src={blog.image.url || ""} alt={blog.title} fill className="rounded-2xl object-cover" /> </div> )} {/* Title */} <h2 className="font-semibold text-xl leading-snug mb-3 group-hover:text-primary transition-colors"> {blog.title} </h2> {/* Description */} <p className="text-muted-foreground text-base leading-relaxed flex-1 mb-5"> {blog.description} </p> {/* Footer */} <div className="flex items-center justify-between pt-4 border-t border-border"> {blog.publishedAt && ( <span className="text-xs text-muted-foreground"> {new Date(blog.publishedAt).toLocaleDateString()} </span> )} <span className="text-xs font-medium text-primary flex items-center gap-1 group-hover:gap-2 transition-all"> Read more → </span> </div> </article> </Link> ))} </div> ); } - 3app/blogs/page.tsxCode
import BlogsPageServer from "@/components/BlogsPageServer"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Blogs | blogjs", description: "Insights on UI/UX design, web development, branding, and building digital products.", }; export default async function BlogsPage({ searchParams, }: { searchParams: Promise<{ [key: string]: string | undefined }>; }) { const query = (await searchParams).q ?? ""; return ( <main className="max-w-[1200px] mx-auto px-4 pt-36 pb-20"> <header className="mb-8 text-center"> <h1 className="text-4xl md:text-5xl font-semibold tracking-tight mb-4"> Our Blog </h1> <p className="text-muted-foreground text-lg max-w-xl mx-auto"> Insights on design, development, and building products that matter. </p> </header> <div className="flex justify-center mb-10"> <form action="" method="get" className="relative max-w-lg w-full"> <label htmlFor="blog-search" className="sr-only"> Search articles </label> <svg className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M17.5 17.5L22 22M20 11C20 15.9706 15.9706 20 11 20C6.02944 20 2 15.9706 2 11C2 6.02944 6.02944 2 11 2C15.9706 2 20 6.02944 20 11Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> </svg> <input id="blog-search" name="q" type="search" defaultValue={query} placeholder="Search articles by title, tag, or category..." className="w-full pl-11 pr-14 py-3 bg-card border border-border rounded-full text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all duration-200" /> {query && ( <a href="?" aria-label="Clear search" className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" > <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> </svg> </a> )} </form> </div> <BlogsPageServer query={query} /> </main> ); }