Crafting a Headless Blog with Vue.js and Dev.to API
Tags: tutorial, api, vue, webdev
Published: 2023-11-21 21:21:26

[![Substack Newsletter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qbg4qlzlytxbzbvj97ia.png)](https://benoitpetit.substack.com/subscribe)

Hello 👋 Step into the world of modern blogging, where crafting a feature-rich personal blog is a breeze without the complexities of a traditional backend. This guide unveils the seamless integration of Vue.js, Dev.to API (Forem), Vue Router, Vue Store (Vuex), and Markdown-it. Let's embark on the journey to create a dynamic and captivating blogging experience.

> 🎨 I kept the frontend simple, leaving it as a blank canvas for you to customize according to your preferences.

Blog Demo ✨
[https://dev-to-headless-vue.netlify.app
](https://dev-to-headless-vue.netlify.app)
Github project ✨
[https://github.com/benoitpetit/dev-to-headless-vue](https://github.com/benoitpetit/dev-to-headless-vue)
- Step 1: Setting Up Your Vue.js Project
- Step 2: Update Main.js
- Step 3: Update App.vue
- Step 4: Implementing Vue Router
- Step 5: Setting Up Vuex (Vue Store)
- Step 6: Create Views


## Integration example :

### Blog list
![sample blog headless dev.to](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iklbr3fqa5m7u3hc2vsd.png)

### Post details
![sample post headless dev.to](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqwx600ci7f9ae32n10e.png)

### Mardown & Code Style Integration ! 😎
![sample code headless dev.to](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ys7s6gx50jx80e8gkodp.png)

### **Blog Demo ✨**
[https://dev-to-headless-vue.netlify.app](https://dev-to-headless-vue.netlify.app/)


> 📢 Before delving into the code, here's a quick heads-up: make sure to check out the comments! They're like hidden gems, providing insights, tips, and maybe a joke or two. 😉 Don't miss the valuable information tucked away in the comments—they're your guide to understanding the code better. 😛
> 

### **Prerequisites**

Before we embark on the implementation, ensure that Node.js, NPM and Vue CLI are installed on your machine. If not, download and install them from [nodejs.org](https://nodejs.org/) and [cli.vuejs.org](https://cli.vuejs.org/guide/installation.html#installation)

## **Step 1: Setting Up Your Vue.js Project**

Begin by creating a new Vue.js project using the Vue CLI.

```bash
vue create my-personal-blog
cd my-personal-blog
```

Install prerequisite modules:

```bash
# Install vue-router
npm install vue-router

# Install vuex (vue-store)
npm install vuex

# Install node-fetch
npm install node-fetch

# Install markdown-it
npm install markdown-it

# Install highlight.js
npm install highlight.js
```

I don't think I've forgotten anything 🫣

## **Step 2: Update Main.js**

Update your **`main.js`** file to include router and store configurations.

```jsx

import { createApp } from "vue";
import App from "./App.vue";
// added router & store
import router from "./router";
import store from "./store";
// Import styles global & highlight.js for code style block
import "./assets/styles/app.css";
import "highlight.js/styles/atom-one-dark.css";

const app = createApp(App);
const head = createHead();

app.use(store);
app.use(router);
app.use(head);
app.mount("#app");
```
## **Step 3: Update App.vue**
in the **`App.vue`** file, add the link router and the view router
```html
<template>
  <div id="app">
    <header>
      <nav class="logo">
        <div>
          <img :src="devto" alt="Logo" />
        </div>
        <div>
          <h1>dev.to headless vue</h1>
        </div>
      </nav>
    </header>
    <div id="app">
      <div class="menu">
        <router-link to="/">Accueil</router-link>
        <router-link to="/blog">Blog</router-link>
      </div>
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
import { onBeforeMount } from "vue";
import { useStore } from "vuex";
import devto from "./assets/logo.png";

export default {
  name: "App",
  setup() {
    const store = useStore();

    onBeforeMount(() => {
      store.dispatch("getBlogContent");
    });

    return {
      devto,
    };
  },
};
</script>

<style>
.menu {
  display: flex;
  justify-content: center;
  margin-bottom: 20px;
}

.menu a {
  margin: 0 10px;
}
</style>

```


## **Step 4: Implementing Vue Router**

Vue Router empowers us to navigate through different views in our single-page application. Let's establish three routes: Home, Blog, and Post.

```jsx
import { createRouter, createWebHistory } from "vue-router";

import HomeView from "@/views/HomeView.vue";
import BlogView from "@/views/BlogView.vue";
import PostView from "@/views/PostView.vue";

const routes = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/blog",
    name: "blog",
    component: BlogView,
  },
  {
    // Here publication takes an id as parameter
    // it corresponds to the post id
    path: "/post/:id",
    name: "postView",
    component: PostView,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;
```

## **Step 5: Setting Up Vuex (Vue Store)**
Vuex manages the state of our application. In this case, we use it to store and retrieve blog posts.

```jsx
import { createStore } from "vuex";

export default createStore({
  state: {
    blogContent: [],
  },
  getters: {
    blogContent: (state) => {
      return state.blogContent;
    },
  },
  mutations: {
    SET_BLOG(state, blogContent) {
      state.blogContent = blogContent;
    },
  },
  actions: {
    async getBlogContent({ state, commit }) {
      // check if blogContent is already set
      // here we check that we have retrieved the list of articles from dev.to
      // to avoid executing the request each time the page is reloaded
      if (state.blogContent.length) return;

      // fetch blog content to dev.to api and set state
      const response = await fetch(
        // you can also retrieve the url with an .env file
        "https://dev.to/api/articles?username=benoitpetit"
      );
      const blogContent = await response.json();
      commit("SET_BLOG", blogContent);

      return blogContent;
    },
  },
});
```

## **Step 6: Create Views**
We are creating the various views that will host the content of our articles.

**`src/views/HomeView.vue`**
```html

<template>
  <div class="home-view">
    <h1>Welcome to the Home View!</h1>
    <p>This is the content of the home view.</p>
  </div>
</template>

<script>
export default {
  name: "HomeView",
};
</script>

<style scoped>
.home-view {
  text-align: center;
  margin-top: 50px;
}
</style>
```

**`src/views/BlogView.vue`**
```html
<template>
  <div class="blog-view">
    <h1>Welcome to the BlogView!</h1>
    <!-- check and loop blogcontent for show -->
    <div v-if="blogContent.length">
      <div v-for="(content, index) in blogContent" :key="index">
        <!-- here we create the link with the post id as parameter -->
        <router-link :to="`/post/${content.id}`">
          <h2>{{ content.title }}</h2>
        </router-link>
        <p>{{ content.description }}</p>
        <ul>
          tags:
          <span v-for="(tag, index) in content.tag_list" :key="index">
            {{ tag }},
          </span>
        </ul>
      </div>
    </div>
    <div v-else>Loading...</div>
  </div>
</template>

<script>
import { useStore } from "vuex";
import { computed } from "vue";

export default {
  name: "BlogView",
  setup() {
    const store = useStore();

    const blogContent = computed(() => store.state.blogContent);

    return {
      blogContent,
    };
  },
};
</script>
```

**`src/views/PostView.vue`**
```html
<template>
  <div class="post-view">
    <!-- Check if the post belongs to me! important though :)  -->
    <div v-if="postContent.user && postContent.user.username === 'benoitpetit'">
      <div class="title">
        <h1>{{ postContent.title }}</h1>
      </div>
      <a
        :href="
          postContent.user ? `https://dev.to/${postContent.user.username}` : ''
        "
        class="author"
      >
        <img
          class="rounded-circle"
          :src="postContent.user ? postContent.user.profile_image : ''"
          height="80"
          alt="profil pic"
        />
        <p>
          {{ postContent.user ? postContent.user.name : "" }}
        </p>
      </a>
      <div class="content-post-markdown">
        <Markdown
          class="content-post"
          :source="postContent.body_markdown"
        ></Markdown>
        <span class="post-id">The ID of the post is: {{ postId }}</span>
      </div>
    </div>
    <div v-else>
      <p>Sorry, you are not the author of this post</p>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, watch } from "vue";
import { useRoute } from "vue-router";
import Markdown from "vue3-markdown-it";

export default {
  name: "PostView",
  components: {
    Markdown,
  },
  setup() {
    const route = useRoute();
    const postId = ref(route.params.id);
    const postContent = ref([null]);

    // get post content we could put it in the blind too
    const fetchPost = async () => {
      try {
        const response = await fetch(
          // you can also retrieve the url with an .env file
          "https://dev.to/api/articles/" + postId.value
        );
        const data = await response.json();
        postContent.value = data;
      } catch (error) {
        console.error(error);
      }
    };

    // fetch data post on mounted
    onMounted(() => fetchPost());

    // watch for route change
    watch(
      () => route.params.id,
      (newId, oldId) => {
        if (newId !== oldId && newId !== undefined) {
          postId.value = newId;
          fetchPost();
        }
      }
    );

    return {
      postId,
      postContent,
    };
  },
};
</script>

```

For a more advanced integration, visit my blog : 👉️ [https://benoitpetit.dev/my-blog](https://benoitpetit.dev/my-blog)

> Hello developers! 👋 Sharing this Vue.js Headless Blog setup with the dev.to API. While the code serves its purpose, it's not a masterpiece—there's room for improvement! 😅 

> I haven't reinvented the wheel (or the code), and I'm confident it can be refined to adhere more closely to best practices. Feel free to fork it; it's my gift to you! Let's improve it together. Remember, this isn't a final product but a robust starting point for launching your blog. Dive in, experiment, and let's create something amazing!

> Happy coding! 💻✨

For more details on setting up a headless blog with the dev.to API, check out the Github project here: [https://github.com/benoitpetit/dev-to-headless-vue/](https://github.com/benoitpetit/dev-to-headless-vue/)

>🚀 Exciting updates ahead! In upcoming articles, I'll guide you through integrating @unhead/vue for enhanced SEO and improved link previews on social media for your personal headless blog on dev.to. Also, discover how to showcase your real-time follower count using a simple custom script. Stay tuned for these enhancements! 

See you later! Beπ ✨

---

> 🫶 If you appreciate my posts and would like to support my work, feel free to make a donation by clicking on the Stripe Sponsor link below or by scanning the QR code. [Stripe Link Sponsor](https://donate.stripe.com/00g6pJ6R19MJ15S7ss)

---

[![Stripe Sponsor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohkf7lrymedkort4w9iz.png)](https://donate.stripe.com/00g6pJ6R19MJ15S7ss)

--- 

👀 See me here : 

- [Substack](https://benoitpetit.substack.com/subscribe)
- [Mastodon](https://piaille.fr/@codingben) 
- [(X) Twitter](https://twitter.com/codingben_)
- [Github](https://github.com/benoitpetit)
- [NPM Packages](https://www.npmjs.com/~codingben)
- [benoitpetit.dev](https://benoitpetit.dev)
- [Dev.to](https://dev.to/benoitpetit)