Create a blog with Vue 3 + Tailwindcss + Supabase
Tags: vue, javascript, codenewbie
Published: 2022-09-07 16:08:29

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.

{% embed https://www.youtube.com/watch?v=0NB9lgFcOT4 %}

## 1. Install Vue.js

**Prerequisites**: install [Node.js](https://nodejs.org/en/) 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](https://dev.to/guillaumeduhan/vue-tailwindcss-config-f8l).

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