How to Build a Nuxt Blog with Headless CMS

Build your Nuxt blog with headless CMS - Hyvor Blogs. In this article, we have explained everything in step by step.

In this tutorial, we will build a Nuxt blog using Hyvor Blogs as the headless CMS (Content Management System). In other words, I will walk you through all the necessary steps to build a Nuxt headless blog.

Nuxt is a free and open-source framework that you can use for building websites with Vue.js.

Before starting this Nuxt blog tutorial, here is a brief explanation about Headless CMS for beginners. However, if you aren’t a novice, skip to the tutorial.

What is a Headless CMS and Why Use It?

Simply, a headless CMS is a type of CMS that exists among us. Mainly there are two types of CMS: traditional and headless. You might know what a traditional CMS is. If not, a traditional CMS is a platform that provides tools to create, manage, and deliver digital content, typically for blogs and websites. It combines both theΒ backend (content management)Β andΒ frontend (presentation)Β in a single system, making it an all-in-one solution for building and maintaining websites.

But, when it comes to a headless CMS, things are contradictory. Headless CMS is a content management system that focuses solely on managing and delivering content through an API, without being tied to any specific frontend (presentation layer).

why headless cms build nuxt blog
But….. why headless?

The term β€œheadless” comes with the meaning of having no literal head: you can add any kind of head you want. In other words, with a headless CMS, developers have the total freedom and the flexibility to build and customize the front end independently using any framework or technology.

That is exactly what we are doing in this tutorial. We are using Hyvor Blogs as our headless CMS just to manage content and fetch and retrieve data via a Data API. On the other hand, we are using Nuxt to build β€œthe head”-the frontβ€”of the blog as we prefer. In case you are wondering, you can useΒ SvelteKit,Β Next.js,Β Astro, and so much more instead of Nuxt if you want.

For further clarification, here is an infographic to help you understand the inner workings of a headless CMS.

how headless cms works - Nuxt blog headless
Inner workings of Headless CMS

The Headless CMS market size is valued at around USD 605 Million in 2022 and is expected to grow to USD 3.8 Billion in 2032.

Storyblok

Why Hyvor Blogs?

Hyvor Blogs is an all-in-one headless CMS tailored especially for blogging. It provides you the perfect space to write, and manage your blog posts, including features like comments, memberships, and newsletters with Hyvor Talk, and offers an easy Data API approach to integrate your blog with any front-end framework or platform like Nuxt.

As a Hyvor Blogs user, you get to experience the following features, especially from Hyvor Blogs as a headless CMS. For example,

  • 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 write, edit, collaborate with your team, and manage all your blog content.

Hyvor Blogs console

Together, Nuxt and Hyvor Blogs enable you to create a high-performing, static/dynamic astro blog where content management and front-end customization work seamlessly. This duo will be a perfect integration for your headless CMS.

Now, let’s get started with the tutorial. First, we are going to set up a Nuxt project on our computer.

Step 1: Setup a Nuxt Blog Project


Prerequisites

  1. Create your new Nuxt blog project. Run the following command in your terminal. Replace your preferred blog project name with <project-name> .

1npx nuxi@latest init <project-name>
  1. Start the development server: Open your project from your IDE, and run the following command to start the development server.

1npm run dev

This will start the development server and you can see your new project using http://localhost:3000/ .

It’ll look something like this.

Nuxt headless blog - Hyvor blog nuxt headless cms
Nuxt Blog

This is what our project hierarchy looks like initially.

1my-nuxt-blog/
2β”œβ”€β”€ assets/
3β”œβ”€β”€ composables/
4β”‚ └── useHyvorBlogs.ts
5β”œβ”€β”€ public/
6β”‚ └── favicon.ico
7β”‚
8β”œβ”€β”€ services/
9β”‚ └── hyvorBlogs.ts
10β”‚
11β”œβ”€β”€ styles/
12β”‚ β”œβ”€β”€ main.css
13β”‚ └── tailwind.css
14β”‚
15β”œβ”€β”€ tests/
16β”‚ β”œβ”€β”€ unit/
17β”‚ β”œβ”€β”€ integration/
18β”‚ └── e2e/
19β”‚
20β”œβ”€β”€ .env
21β”œβ”€β”€ .gitignore
22β”œβ”€β”€ nuxt.config.ts
23β”œβ”€β”€ package.json
24β”œβ”€β”€ README.md
25└── tsconfig.json

Now we have successfully set up our Nuxt blog project.

Step 2: Create your Blog at Hyvor Blogs

We have to create a blog space to start content writing and content management for our blog. To do that, go to the Hyvor Blogs console and create your blog.

get your blo subdomain  - Astro blog build with Hyvor Blogs headless cms-
Hyvor Blogs Console - Get your subdomain

Once completed Go to Console -> Settings -> Hosting -> Subdomain. Copy your blog’s subdomain.

For this tutorial, my blog subdomain is headless-cms .

For our next steps, we are going to use Hyvor Blogs Data API to fetch and render blog data. 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}, in this case,Β https://blogs.hyvor.com/api/data/v0/headless-cms.

Let’s continue.

Step 3: Index page/ Home page

This is the step where we fetch post data and display it all on the blog's index page. Originally, Nuxt did not have any folders or files for layouts or components (but they have predefined components serving). So, first of all, we have to create several folder structures in our project to store our components, service files, and routes.

  1. Create new folders named as /pages, /components, /services , /layouts. After these folders are created, this will be our project hierarchy.

1β”œβ”€β”€ README.md
2β”œβ”€β”€ app.vue
3β”œβ”€β”€ components --------------------------- components dir (store component files)
4β”œβ”€β”€ layouts --------------------------- layouts dir (layout files)
5β”œβ”€β”€ node_modules
6β”œβ”€β”€ nuxt.config.ts
7β”œβ”€β”€ package-lock.json
8β”œβ”€β”€ package.json
9β”œβ”€β”€ pages --------------------------- pages dir (pages/routes)
10β”œβ”€β”€ public
11β”œβ”€β”€ server
12β”œβ”€β”€ services --------------------------- services dir (API calls services)
13└── tsconfig.json
  1. Go to layouts folder and create a file named default.vue . This is where we create the whole layout of our blog: page layout.

    I’m adding a header and a footer for our layout. Note that I have added the header and footer as components in the components folder. Thus we will be simply importing those in detault.vue as follows.

1<template>
2 <div>
3 <!-- Common Header -->
4 <Header />
5
6 <!-- Dynamic Page Content -->
7 <main>
8 <slot />
9 </main>
10
11 <!-- Common Footer -->
12 <Footer />
13 </div>
14</template>
15
16<script setup lang="ts">
17import Header from "@/components/Header.vue";
18import Footer from "@/components/Footer.vue";
19</script>
  1. In /pages folder, create a new file named index.vue . This is where we write codes for our index page. In the index.vue, just add some dummy text to check if everything works perfectly.

1<template>
2some dummy texts here
3</template>
  1. Go to app.vue, remove the existing code and edit it as shown below to make the layouts and page routings work.

1<template>
2 <div>
3 <NuxtRouteAnnouncer />
4 <NuxtLayout>
5 <NuxtPage></NuxtPage>
6 </NuxtLayout>
7 </div>
8</template>

Now see whether the added dummy text and the layout you created display correctly.

Nuxt blog with hyvor blogs headless cms
The simple layout of the blog after organizing the files
  1. Create a file named fetchPosts.ts in the services folder. This is where we call the Hyvor Blogs data API to fetch the list of all posts of our blog using the /posts endpoint.

1export async function fetchPosts() {
2 try {
3 const response = await fetch(
4 "https://blogs.hyvor.com/api/data/v0/headless-cms/posts"
5 );
6 if (!response.ok) {
7 throw new Error("Failed to fetch posts");
8 }
9 const res = await response.json();
10 return res.data;
11 } catch (error) {
12 console.error(error);
13 return [];
14 }
15}

Now, let’s create a component to show all those posts on our index page.

  1. Create a file named BlogPostCard.vue in the components folder.

1<template>
2 <article class="post-card">
3 <h2>{{ post.title }}</h2>
4 <p>{{ post.description }}</p>
5 <NuxtLink :to="`/${post.slug}`">Read more</NuxtLink>
6 </article>
7</template>
8
9<script setup lang="ts">
10defineProps({
11 post: {
12 type: Object,
13 required: true,
14 },
15});
16</script>
  1. Got to the index.vue and retrieve all the data we fetched using the posts endpoint using fetchPosts.ts . We are using our BlogPostCard component here.

1<template>
2 <div>
3 <BlogPostCard v-for="post in posts" :key="post.id" :post="post" />
4 </div>
5</template>
6
7<script setup lang="ts">
8import { ref, onMounted } from "vue";
9import { fetchPosts } from "../services/fetchPosts";
10
11interface Post {
12 id: number;
13 title: string;
14 description: string;
15 slug: string;
16 // Add other properties of the post object here for example, features_image, tags, created_at, and more as you want
17}
18
19const posts = ref<Post[]>([]);
20const loading = ref(true);
21
22onMounted(async () => {
23 posts.value = await fetchPosts();
24 loading.value = false;
25});
26</script>

Now here is what our index page looks like after displaying post data. PS: Don’t mind my CSS svp😜.

Nuxt blog build with hyvor blogs headless cms
Index page of the Nuxt blog

Note: Vue's reactivity system works best when state changes are made within lifecycle hooks likeΒ onMounted. If you place the fetch outside ofΒ onMounted, you could run into scenarios where Vue isn't aware of changes toΒ postsΒ andΒ loadingΒ until much later. When you callΒ fetchPosts()Β insideΒ onMounted, it guarantees Vue will properly track the state and update the UI when data is loaded.

Step 4: Post Page.

In this step, we are going to fetch and display the content of a particular post in the URL /post-slug .

  1. Create a typescript file fetchPost.ts in services folder.

We will be fetching post data by its slug.

1export async function fetchPost(slug: string) {
2 try {
3 const response = await fetch(
4 `https://blogs.hyvor.com/api/data/v0/headless-cms/post?slug=${slug}`
5 );
6 if (!response.ok) {
7 throw new Error("Failed to fetch post");
8 }
9 return await response.json();
10 } catch (error) {
11 console.error(error);
12 return null;
13 }
14}

And, now we are going to create a route for our post pages. Goto pages folder.

  1. Create a file named [slug].vue: /pages/[slug].vue .

And then using the fetchPosts function, we are retrieving and displaying our post content.

1<template>
2 <div>
3 <div v-if="loading">Loading post...</div>
4 <div v-else-if="error">{{ error }}</div>
5 <div v-else-if="post">
6 <h1>{{ post.title }}</h1>
7 <div v-html="post.content"></div>
8 </div>
9 </div>
10</template>
11
12<script setup lang="ts">
13import { ref, onMounted } from "vue";
14import { useRoute } from "vue-router";
15import { fetchPost } from "../services/fetchPost";
16
17const route = useRoute();
18const post = ref(null);
19const loading = ref(true);
20const error = ref("");
21
22onMounted(async () => {
23 const slug = route.params.slug; // Get the slug from the URL
24 try {
25 post.value = await fetchPost(slug);
26 if (!post.value) {
27 error.value = "Post not found.";
28 }
29 } catch (err) {
30 error.value = "Failed to load post.";
31 } finally {
32 loading.value = false;
33 }
34});
35</script>

That’s it! Here is our post page without CSS.

Post page of Nuxt blog with headless cms by hyvor blogs - Nuxt headless CMS
Post page

Step 5: Pagination

It’ll be nice to have a pagination for our Nuxt blog right? For pagination, we are using limit param, page param and pagination object for this step.

We’ll create a structure where paginated pages are accessible under URLs likeΒ /page/1,Β /page/2, etc. Here's how we can achieve this step by step:

  1. Create a dynamic page underΒ /pages/page/[page].vueΒ to handle paginated routes. This will also handle theΒ /Β route (/should behave likeΒ /page/1).

  2. Create a file named Pagination.vue in your components folder: We are creating a component for pagination.

1<template>
2 <div class="pagination">
3 <button
4 :disabled="!pagination.page_prev"
5 @click="goToPage(pagination.page_prev)"
6 >
7 Previous
8 </button>
9 <button
10 :disabled="!pagination.page_next"
11 @click="goToPage(pagination.page_next)"
12 >
13 Next
14 </button>
15 </div>
16</template>
17
18<script setup lang="ts">
19import { useRouter } from "vue-router";
20
21defineProps({
22 pagination: {
23 type: Object,
24 required: true,
25 },
26});
27
28const router = useRouter();
29
30function goToPage(page: number) {
31 if (page) {
32 router.push(`/page/${page}`);
33 }
34}
35</script>
  1. Create another service file to manage the pagination function.: /services/fetchPaginatedPost.ts .

1export async function fetchPaginatedPosts(page = 1, limit = 3) {
2 try {
3 const response = await fetch(
4 `https://blogs.hyvor.com/api/data/v0/headless-cms/posts?limit=${limit}&page=${page}`
5 );
6 if (!response.ok) {
7 throw new Error("Failed to fetch posts");
8 }
9 const res = await response.json();
10 return res; // Contains "data" and "pagination"
11 } catch (error) {
12 console.error(error);
13 return { data: [], pagination: null };
14 }
15}
  1. Go to your [page].vue and let’s handle how paginated pages work.

1<template>
2 <div>
3 <div v-if="loading">Loading posts...</div>
4 <div v-else-if="error">{{ error }}</div>
5 <div v-else>
6 <BlogPostCard v-for="post in posts" :key="post.id" :post="post" />
7 <Pagination :pagination="pagination" />
8 </div>
9 </div>
10</template>
11
12<script setup lang="ts">
13import { ref, onMounted } from "vue";
14import { useRoute, useRouter } from "vue-router";
15import { fetchPaginatedPosts } from "../../services/fetchPaginatedPosts";
16
17const route = useRoute();
18
19const posts = ref([]);
20const pagination = ref(null);
21const loading = ref(true);
22const error = ref("");
23
24onMounted(async () => {
25 const page = parseInt(route.params.page) || 1; // Default to page 1
26 try {
27 const res = await fetchPaginatedPosts(page);
28 posts.value = res.data;
29 pagination.value = res.pagination;
30 } catch (err) {
31 error.value = "Failed to load posts.";
32 } finally {
33 loading.value = false;
34 }
35});
36</script>

We have to make a slight change in index.vue file to manage the behavior of our index page to be as same as /page/1 .

  1. Go to index.vue file and make the following changes. I have added the redirect to /page/1.

1<template>
2 <div>
3 <BlogPostCard v-for="post in posts" :key="post.id" :post="post" />
4 </div>
5</template>
6
7<script setup lang="ts">
8import { ref, onMounted } from "vue";
9import { fetchPosts } from "../services/fetchPosts";
10
11interface Post {
12 id: number;
13 title: string;
14 description: string;
15 slug: string;
16 // Add other properties of the post object here for example, features_image, tags, created_at, and more as you want
17}
18
19const posts = ref<Post[]>([]);
20const loading = ref(true);
21
22onMounted(async () => {
23 posts.value = await fetchPosts();
24 loading.value = false;
25});
26
27const router = useRouter();
28router.push("/page/1");
29</script>

And you can do the redirect the other way around too if you want (/page/1 to /).

Et voila! The pagination is ready.

NUXT blog pagination - hyvor blogs headless cms
Pagination

Step 6: Build and Deploy

After you have created everything you need for your blog project, you must build the blog before deploying it.

  1. Build the blog, and run the following command.

1npm run build

You can preview the build using the following command.

1node .output/server/index.mjs

Then visit http://[::]:3000 to view the build preview.

For deploying, there are several methods that Nuxt provides you for hosting such as Static, Server Side Rendering (SSR), and more. Refer to their documentation for further steps.

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.

nuxt headless blog
Yay!

Yay! You've built a Nuxt blog using a Headless CMS: Hyvor Blogs. You can expand this project using our Data API endpoints and objects.

You can also check out ourΒ headless CMSΒ tutorials that areΒ written for other front-end frameworks and libraries.

If you have any questions, please comment below; I’d be happy to help.

Happy Blogging!

Comments

Published with Hyvor Blogs