DEV Community 👩‍💻👨‍💻

Cover image for Create a blog with Vue 3 + Tailwindcss + Supabase
Guillaume Duhan
Guillaume Duhan

Posted on

Create a blog with Vue 3 + Tailwindcss + Supabase

Hello guys,

I have been developing web applications with Vue since 7 years now and I am loving it!

Vue 3 just came out and I found interesting to write a quick post on how to create your first Vue 3 application with Tailwindcss.

1. Install Vue.js

Prerequisites: install Node.js version 15.0 or higher.

Use the CLI to build your app:

npm init vue@latest
Enter fullscreen mode Exit fullscreen mode


✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ./<your-project-name>...
Enter fullscreen mode Exit fullscreen mode

Install supabase:

npm i @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode


npm i && npm run dev
Enter fullscreen mode Exit fullscreen mode

Your project should be running now.

2. Install Tailwindcss

In a previous article, I explained every steps to follow. Please check this article to install Tailwindcss.

3. Clean your views & components

By default, you have components and views. Remove them and instead create theses files:

Enter fullscreen mode Exit fullscreen mode

You can immediately change your Header.vue component to:

  <header class=" text-center my-8">
    <h1 class="text-blue-300 text-6xl font-bold mb-4" @click="$router.push('/')">Guillaume's blog</h1>
    <p class="text-xl text-slate-500">A blog with posts on what I like.</p>
Enter fullscreen mode Exit fullscreen mode

4. Vue-Router from App.vue

Here, we want a root-file (App.vue) that displays our routes.

<script setup>
import { RouterView } from 'vue-router'
import Header from './components/Header.vue'

  <Header />
  <RouterView />
Enter fullscreen mode Exit fullscreen mode

Also, we need our router to display these pages:

// src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
      path: '/',
      name: 'Home',
      component: () => import('../views/Home.vue')
    }, {
      path: '/post/:id',
      name: 'Post',
      component: () => import('../views/Post.vue')

export default router

Enter fullscreen mode Exit fullscreen mode

Now, you should have 2 routes available.

5. Configure Supabase

Please check my video in order to create your table "Posts" and configure your Supabase.

Go to supabase.js and configure your client:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

const supabase = createClient(supabaseUrl, supabaseAnonKey);

export default supabase
Enter fullscreen mode Exit fullscreen mode

You should now create an .env file:

touch .env

Enter fullscreen mode Exit fullscreen mode

Please restart your server.

6. Let's create a store.

In store.js, let's create a reactive constant. Reactive is important here to make your variable dynamic and display elements. Otherwise, in your app, data will stay blank.

import { reactive } from 'vue'

const store = reactive({
  posts: []

export {store};
Enter fullscreen mode Exit fullscreen mode

7. Home is home

Our Home.vue view display a list of posts fetched from Supabase.

In order to do so, we create a fetchPosts function triggered immediately on mount.

If there are no posts, a message appear and says: 'there is no posts'.

<script setup>
import { store } from '../store'
import supabase from '../supabase'

const getWordsNumber = (str) => (str.split(' ').length)

const fetchPosts = async () => {
  let { data: posts, error } = await supabase

  if (error) throw new Error(error)

  store.posts = posts


  <div class="Home">
    <main class="container mx-auto">
      <div v-if="store.posts.length < 1">
        There is no posts.
      <div v-else>
        <div class="PostItem border border-slate-200 mb-4 p-4 rounded-lg cursor-pointer" v-for="item, itemIndex in store.posts" :key="itemIndex" @click="$router.push(`/post/${}`)">
          <h1 class="text-slate-900 text-3xl font-bold">
            {{ item.title }}
          <p>{{ getWordsNumber(item.description) }} words.</p>
Enter fullscreen mode Exit fullscreen mode

You might see that on click, we go to post route previously created.

8. Post view

Our post view is a bit different.

We also have to get our post from Supabase otherwise our data will be empty.

Please take a look at fetchPost function. There is a guard: if our post is already in our store, we won't fetch but apply our found item to post variable.

<script setup>
import { reactive } from 'vue'
import { useRoute } from 'vue-router'
import {store} from '../store'
import supabase from '../supabase'

const route = useRoute()

let post = reactive({})

const fetchPost = async (id) => {
  const found = store.posts.find(x => === parseInt(
  if (found) {
    Object.assign(post, found)
  let { data, error } = await supabase
  .eq('id', id)

  if (error) throw new Error(error)

  Object.assign(post, data)


  <div class="Post text-center container mx-auto">
    <div v-if="!post">
      No post found.
    <div v-else>
      <h1 class="text-slate-900 text-3xl font-bold mb-4">{{ post.title }}</h1>
      <p class="text-md text-slate-300">{{ post.created_at }}</p>
      <p class="text-xl text-slate-500">{{ post.description}}</p>
Enter fullscreen mode Exit fullscreen mode

I hope you enjoyed this quick article, if you have any question or best practices, please be my guest.

Best !


Latest comments (0)

Let's team up together 🤝

We're Hiring

We're hiring for a Senior Full Stack Engineer to join the DEV team. Want the deets? Head here to learn more about who we're looking for.