DEV Community

Cover image for Crafting a Headless Blog with Vue.js and Dev.to API
Beπ ✨
Beπ ✨

Posted on • Updated on • Originally published at benoitpetit.dev

Crafting a Headless Blog with Vue.js and Dev.to API

Substack Newsletter

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

Github project ✨
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

Post details

sample post headless dev.to

Mardown & Code Style Integration ! 😎

sample code headless dev.to

Blog Demo ✨

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 and cli.vuejs.org

Step 1: Setting Up Your Vue.js Project

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

vue create my-personal-blog
cd my-personal-blog
Enter fullscreen mode Exit fullscreen mode

Install prerequisite modules:

# 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
Enter fullscreen mode Exit fullscreen mode

I don't think I've forgotten anything 🫣

Step 2: Update Main.js

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


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");
Enter fullscreen mode Exit fullscreen mode

Step 3: Update App.vue

in the App.vue file, add the link router and the view router

<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>

Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Create Views

We are creating the various views that will host the content of our articles.

src/views/HomeView.vue


<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>
Enter fullscreen mode Exit fullscreen mode

src/views/BlogView.vue

<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>
Enter fullscreen mode Exit fullscreen mode

src/views/PostView.vue

<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>

Enter fullscreen mode Exit fullscreen mode

For a more advanced integration, visit 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/

🚀 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


Stripe Sponsor


👀 See me here :

Top comments (0)