Create a blog with Vue 3 + Tailwindcss + Supabase

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

Then:

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

Install supabase:

npm i @supabase/supabase-js

Finally:

npm i && npm run dev

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:

src/views/Home.vue
src/views/Post.vue
src/components/Header.vue
src/store.js
src/supabase.js

You can immediately change your Header.vue component to:

<template>
  <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>
  </header>
</template>

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'
</script>

<template>
  <Header />
  <RouterView />
</template>

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

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

You should now create an .env file:

touch .env

VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=

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

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
  .from('posts')
  .select()

  if (error) throw new Error(error)

  store.posts = posts
} 

fetchPosts()
</script>

<template>
  <div class="Home">
    <main class="container mx-auto">
      <div v-if="store.posts.length < 1">
        There is no posts.
      </div>
      <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/${item.id}`)">
          <h1 class="text-slate-900 text-3xl font-bold">
            {{ item.title }}
          </h1>
          <p>{{ getWordsNumber(item.description) }} words.</p>
        </div>
      </div>
    </main>
  </div>
</template>

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 => x.id === parseInt(route.params.id))
  if (found) {
    Object.assign(post, found)
    return
  }
  let { data, error } = await supabase
  .from('posts')
  .select()
  .eq('id', id)
  .single()

  if (error) throw new Error(error)

  Object.assign(post, data)
} 

fetchPost(route.params.id)
</script>

<template>
  <div class="Post text-center container mx-auto">
    <div v-if="!post">
      No post found.
    </div>
    <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>
    </div>
  </div>
</template>

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

Best !

Guillaume