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

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

Install supabase:

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

Finally:

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:

src/views/Home.vue
src/views/Post.vue
src/components/Header.vue
src/store.js
src/supabase.js
Enter fullscreen mode Exit fullscreen mode

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

<template>
  <Header />
  <RouterView />
</template>
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

VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
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
  .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>
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 => 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>
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 !

Guillaume

Oldest comments (0)

Stop sifting through your feed.

Find the content you want to see.

Change your feed algorithm by adjusting your experience level and give weights to the tags you follow.