In this tutorial, we’ll explore how to build a Next.js blog with Hyvor Blogs: a Headless CMS for blogging.
Before going straight to our Next.js blog tutorial, here is a brief explanation for beginners about blogging with a headless CMS. If you already know this stuff, skip right away to the tutorial. Note that we are using App Router in Next.js in this tutorial.
What is a Headless CMS?
In simple words, a headless CMS is a content management system that lets you create, store, and display your content anywhere you want as the way you like it. If explained from another perspective, it is called “headless” because it does not have a head - the frontend part that your users will see on your next.js blog- so you have to plug any kind of a head you want. All you need is to fetch and render data from your headless CMS’s API and design your front end as you prefer.
Here is simply how your Next.js blog fetches data from the headless CMS.
Using a headless CMS, content creators/writers can benefit from a friendly interface to write and manage content, while developers can build amazing blog frontend and improve experiences using whatever tools and design practices they love. This is perfect for companies or enterprises that want their content to appear in lots of different places without having to copy-paste everything manually. Not only companies, but developers who love hassle-free content creation while building beautiful frontends can try this too.
Here is an infographic that explains the inner workings of a headless CMS in more detail than a traditional headless CMS.
Why Hyvor Blogs as your Headless CMS?
Hyvor Blogs is an all-in-one blogging platform that mainly focuses on blogging: no e-commerce, no portfolios, just blogging. As such, we offer bloggers the following features.
Multi-language blogging including RTL language blogging.
User-friendly, team-friendly console with markdown-friendly rich text editor
Data API for headless cms
Hosting privileges: subdomain, custom domain, sub-directory, or anywhere you like.
Developer-friendly: console API, delivery API, webhooks, etc.
Fast blogging and so much more.
This is the Hyvor Blogs console. This is where you will be writing, editing, collaborating with your team managing all your blog content.
Now let’s move to the Next.js headless blog tutorial!
Step 1: Setup Next.js Blog Project
Create a new Next.js project
For this step, we are using the Automatic installation recommended by Next.js. Run the following command in your terminal.
1npx create-next-app@latest
Once you’ve completed this, you'll see the following prompts in your terminal.
1What is your project named? my-app2Would you like to use TypeScript? No / Yes3Would you like to use ESLint? No / Yes4Would you like to use Tailwind CSS? No / Yes5Would you like your code inside a `src/` directory? No / Yes6Would you like to use App Router? (recommended) No / Yes7Would you like to use Turbopack for `next dev`? No / Yes8Would you like to customize the import alias (`@/*` by default)? No / Yes9What import alias would you like configured? @/*
Answer them as necessary to set up your Next.js project with the required dependencies. Here are my answers to the prompts.
Navigate to the project directory
Move into the newly created project folder:
1cd my-app
Start the development server
1npm run dev
This will open the project in the browser at http://localhost:3000
.
This is the project hierarchy for now.
1my-app/ 2├── public/ # Public assets (images, icons, etc.) 3├── src/ 4│ ├── app/ # App Router directory for routing 5│ │ ├── favicon.ico # Favicon for the app 6│ │ ├── globals.css # Global CSS for the app 7│ │ ├── layout.tsx # Root layout for all pages 8│ │ ├── page.tsx # Homepage - lists all posts 9├── README.md10├── eslint.config.mjs11├── next-env.d.ts12├── next.config.ts13├── node_modules14├── package-lock.json15├── package.json16├── postcss.config.mjs17├── tailwind.config.ts18└── tsconfig.json
Step 2: Create your Blog at Hyvor Blogs
Go to the Hyvor Blogs console and create your blog. Once you have created your blog in Hyvor Blogs, we need the subdomain of the blog. To get your blog subdomain, go to Console -> Settings -> Hosting -> Subdomain.
For this tutorial, I’m using headless-cms
as my subdomain.
To work on building this Next.js blog with Hyvor Blogs headless CMS, we use Hyvor Blogs Data API. Here is the Data API documentation of Hyvor Blogs for your reference.
The Hyvor Blogs Data API provides responses in JSON format, with all endpoints using the HTTP GET method. The base path for the API is https://blogs.hyvor.com/api/data/v0/{subdomain}
, for example, https://blogs.hyvor.com/api/data/v0/headless-cms
.
Now what is left there to do for us is fetching data from the Hyvor Blogs data API.
Let’s continue.
Step 3: Index page - Post loop page
This is the step in which we create the homepage/index page of the blog where you showcase all the posts of your blog including featured posts, pinned posts, etc.
Goto your Next.js project.
Get started by editing the
src/app/page.tsx
.
To fetch data from HB data API, we are using the built-in fetch
 function of Next.js which is the modern streamlined approach currently.
Initially, there is some dummy code in it, just remove it. After removing those in page.tsx
file, we are using it to call Hyvor Blogs data API and fetch the posts data. To do that, use the base path and call Data API’s posts
endpoint to fetch post data as shown below.
1export default async function HomePage() { 2 const subdomain = "headless-cms"; // Replace with your blog's subdomain 3 let posts = []; 4 5 try { 6 const res = await fetch( 7 `https://blogs.hyvor.com/api/data/v0/${subdomain}/posts` 8 ); 9 if (!res.ok) throw new Error(`API Error: ${res.statusText}`);10 const response = await res.json();11 posts = response.data;12 } catch (error) {13 console.error("Failed to fetch posts:", error);14 }15 return (16 <main>17 <h1>Blog Posts</h1>18 {posts.length > 0 ? (19 <ul>20 {posts.map((post: any) => (21 <li key={post.id}>22 <a href={`/${post.slug}`}>23 <h2>{post.title}</h2>24 </a>25 <p>{post.description}</p>26 <p>27 Published on: {new Date(post.published_at).toLocaleDateString()}28 </p>29 </li>30 ))}31 </ul>32 ) : (33 <p>No posts found or an error occurred.</p>34 )}35 </main>36 );37}
Now this is what it looks like when you successfully fetch post data into your index page of this Next.js blog.
I have added some CSS to style this blog using Tailwind to make it more visually appealing. I mean, who loves to stare at an ugly blog, right?
Step 4: Post page
In this step, we are going to show post content when clicking on a particular post. Therefore when a post loads its content with a URL that looks like /[slug]
.
Create a new route folder named
[slug]
in theapp
folder:/app/[slug]
.Then create a
page.tsx
in it:/app/[slug]/page.tsx
.Fetch post content data in that
page.tsx
as follows.
1export default async function PostPage({ 2 params, 3}: { 4 params: Promise<{ slug: string }>; 5}) { 6 const { slug } = await params; 7 const subdomain = "headless-cms"; // Replace with your subdomain 8 9 const res = await fetch(10 `https://blogs.hyvor.com/api/data/v0/${subdomain}/post?slug=${slug}`11 );12 13 if (!res.ok) {14 throw new Error(`API Error: ${res.statusText}`);15 }16 17 const post = await res.json();18 19 return (20 <main>21 <h1>{post.title}</h1>22 <p>{post.description}</p>23 <p>Published on: {new Date(post.published_at).toLocaleDateString()}</p>24 <div dangerouslySetInnerHTML={{ __html: post.content }} />25 </main>26 );27}
Once completed, it will look like this.
You can style the content of posts using CSS. Refer to our documentation. Refer to our content-style post which has all the content blocks.
Step 5: Pagination
Now we are going to set up pagination on the homepage of our Next.js blog.
To add pagination, we use the Pagination Object of Hyvor Blogs data API. To limit the number of posts we show on each page, we will be using limit
and page
params. The URLS of the pagination pages will be like /page/1
.
Create a reusable Pagination component.
You can add pagination buttons without using a component but that will bring lots of repetition of code in your project. Therefore to create a Pagination component, create a folder named
components
in yoursrc
folder:/src/components
In the
components
folder create a file namedPagination.tsx
. This is where we going to design our pagination component.
1interface PaginationProps { 2 pagePrev: number | null; 3 pageNext: number | null; 4 basePath?: string; // Optional base path for URLs 5} 6 7export default function Pagination({ 8 pagePrev, 9 pageNext,10 basePath = "/page",11}: PaginationProps) {12 return (13 <nav>14 {/* Previous Button */}15 {pagePrev && (16 <a17 href={pagePrev === 1 ? "/" : `${basePath}/${pagePrev}`}18 >19 Previous20 </a>21 )}22 23 {/* Next Button */}24 {pageNext && (25 <a26 href={`${basePath}/${pageNext}`}27 >28 Next29 </a>30 )}31 </nav>32 );33}
Go back to
/app/page.tsx
file. We have to make some changes to limit the number of posts shown on each page. Changes are to make the root page/
should correspond to/page/1
and limit the number of posts on a page.
1import Pagination from "@/components/Pagination"; 2 3export default async function HomePage() { 4 const subdomain = "headless-cms"; // Replace with your blog's subdomain 5 const currentPage = 1; // Root URL corresponds to the first page 6 const postsPerPage = 3; // Number of posts per page 7 8 let posts = []; 9 let pagination = { page_prev: null, page_next: null };10 11 try {12 const res = await fetch(13 `https://blogs.hyvor.com/api/data/v0/${subdomain}/posts?page=${currentPage}&limit=${postsPerPage}`14 );15 if (!res.ok) throw new Error(`API Error: ${res.statusText}`);16 const response = await res.json();17 posts = response.data;18 pagination = response.pagination;19 } catch (error) {20 console.error("Failed to fetch posts:", error);21 }22 23 return (24 <div>25 <main>26 <div>27 <ul>28 {posts.map((post: any) => (29 <li key={post.id}>30 <a31 href={`/${post.slug}`}32 >33 {post.title}34 </a>35 <p>{post.description}</p>36 <p>37 Published on:{" "}38 {new Date(post.published_at).toLocaleDateString()}39 </p>40 </li>41 ))}42 </ul>43 </div>44 <Pagination45 pagePrev={pagination.page_prev}46 pageNext={pagination.page_next}47 />48 </main>49 </div>50 );51}52
Then create a folder in your
app
folder namedpage
. Inside thepage
folder, create another folder named[pageNumber]
. Inside the[pageNumber]
folder, create apage.tsx
file:/app/page/[pageNumber]/page.tsx
.
1import Pagination from "../../../components/Pagination"; 2 3export default async function PaginatedPage({ 4 params, 5}: { 6 params: Promise<{ pageNumber: string }>; 7}) { 8 const subdomain = "headless-cms"; // Replace with your subdomain 9 const postsPerPage = 3;10 11 // Await the params to access `pageNumber`12 const { pageNumber } = await params;13 const currentPage = parseInt(pageNumber);14 15 let posts = [];16 let pagination = { page_prev: null, page_next: null };17 18 try {19 const res = await fetch(20 `https://blogs.hyvor.com/api/data/v0/${subdomain}/posts?page=${currentPage}&limit=${postsPerPage}`21 );22 23 if (!res.ok) throw new Error(`API Error: ${res.statusText}`);24 const response = await res.json();25 posts = response.data;26 pagination = response.pagination;27 } catch (error) {28 console.error("Failed to fetch posts:", error);29 }30 31 return (32 <div>33 <main>34 <div>35 <ul>36 {posts.map((post: any) => (37 <li key={post.id}>38 <a39 href={`/${post.slug}`}40 >41 {post.title}42 </a>43 <p>{post.description}</p>44 <p>45 Published on:{" "}46 {new Date(post.published_at).toLocaleDateString()}47 </p>48 </li>49 ))}50 </ul>51 </div>52 <Pagination53 pagePrev={pagination.page_prev}54 pageNext={pagination.page_next}55 />56 </main>57 </div>58 );59}
Et Viola! We have just completed the pagination for our Next.js blog.
Step 6: Deploying
You can deploy your blog with Vercel or self-host it on a Node.js server, Docker image, or static HTML files. Here are the guides provided by Next.js for self-hosting. Follow the steps in those guides to deploy your Next.js blog.
Here is the link to our Next.js blog which we built in this tutorial deployed using Vercel.
And, here is the project’s GitHub repository for your reference.
Tips
To add an author’s page, you can use the
authors
endpoint which returns the authors object.To add a tags page, you can use the
tags
endpoint which returns the tags object.To do multi-language blogging, you can use
language
param and language object.
Yay! You've built a Next.js blog using a Headless CMS Hyvor Blogs.
Feel free to expand this project using our Data API endpoints and objects.
Let me know by commenting down below if you hit a dead-end and need assistance.
You can also check our headless CMS tutorials written for other front-end frameworks and libraries.
Comments