This project is meant to teach Vue.js fundamentals by creating a blog, step by step. It uses the Material Design / Vuetify framework and stores its data in the Google Firebase cloud.
Follow these steps to continuously build a client-side Blog web app with Vue.js.
-
Create the application routes in
src/router/index.js:
const routes = [
{path: '/', component: Overview},
{path: '/posts', component: Overview},
{path: '/posts/:id', component: Read},
{path: '/posts/create', component: Create},
{path: '/posts/update', component: Overview},
{path: '/posts/save', component: Overview},
{path: '/posts/delete', component: Overview},
{path: '/categories', component: Categories},
{path: '/users', component: Users},
]- To add
propsto thePostCardcomponent, insert the following code:
const props = defineProps(['title', 'subtitle', 'avatar'])- In
Overview, create some test PostCards and pass the properprops.
const avatars = ref([
'/female_avatar.jpeg',
'/male_avatar.png',
'/user1-128x128.jpg',
])<v-col v-for="i in 9" :key="i" cols="auto">
<PostCard :title="'Post no ' + i"
:subtitle="'Freddie on ' + new Date().toDateString()"
:avatar="avatars[i % 3]">
Lorum ipsum...
</PostCard>
</v-col>- In
Create, add these form fields:
<v-text-field label="Title" v-model="title" :rules="titleRules" required/>
<v-select label="Categories" v-model="categories" multiple
:items="['Attractions', 'Beaches', 'Cities', 'Escape Rooms', 'Mountains', 'Museums']">
</v-select>
<v-textarea label="Content" v-model="body" :rules="bodyRules"></v-textarea>- Add some model states and validation rules:
const title = ref('')
const titleRules = [(value) => value ? true : 'Please enter a post title']
const categories = ref([])
const body = ref('')
const bodyRules = [(value) => value ? true : 'Please enter some post content']4. Load test postings from FireBase
- Follow these steps to connect your web app with
FireBase. - Create
db.jsa file, where you initialize your FireBase connecttion.
...
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
export default db-
Create a new
FireStoredatabase, a collection named 'posts' and some test documents.

-
In
Overview, load theFireStoredata in theonMountedhook and store the postings in thepostsstate:
const posts = ref([])
onMounted(async () => await loadPostings())
async function loadPostings() {
const postings = await getDocs(collection(db, "posts"))
...
posts.value.push(currentPost)
}- In
main.js, load the categories fromFireStore:
async function loadCategories() {
const catCollection = await getDocs(collection(db, "categories"))
...
}- Provide them to all Vue components:
app.provide('categories', await loadCategories())- In
App, inject and use them as list items:
const categories = inject('categories')
...
<v-list-item v-for="category in categories">
{{ category }}
</v-list-item>
- In
Overview, pass the wholepostobject instead oftitleandbody:
<PostCard :post="post" />- In
PostCard, replacetitleandbodywithprops.post.
<v-card :title="props.post.title" :subtitle="subtitle">- Inject the global
categoriesand create a computed property to display the post's categories:
const categories = inject('categories')
const subtitle = computed(() => {
const cats = props.post.categories?.map(cat => categories[cat])
return cats?.join(', ')
})- In the routes, enable dynamic route parameters:
{path: '/posts/:id', component: Read, props: true}- In
PostCard, add a dynamic link with thepost.id.
<v-card ... link :to="'/posts/' + props.post.id">- In
Read, implement the posting 'detail view':
<script setup>
const props = defineProps(['id'])
const post = ref({})
onMounted(async () => await loadPost(props.id))
async function loadPost(id) {
const postDoc = await getDoc(doc(db, "posts", id))
...
post.value = {...postDoc.data()}
}
</script><template>
...
<v-card :title="post.title" :subtitle="subtitle">
...
<v-card-text>
{{ post.body }}
</v-card-text>
</v-card>
</template>- In the routes, enable dynamic route parameters:
{path: '/posts/edit/:id', component: Edit, props: true}- In
Read, add a button with a link containing thepost.id.
<v-btn color="primary" variant="elevated" link :to="'/posts/edit/' + props.id">
Edit
</v-btn>- Rename
Create.vuetoEdit.vue. - For an existing post, load it's data using the ID:
onMounted(async () => {
// only execute for existing posting
if (props.id) await loadPost(props.id)
})
async function loadPost(id) {
const post = await getDoc(doc(db, "posts", id))
if (post.exists()) {
isNewDoc.value = false
title.value = post.data().title
body.value = post.data().body
categories.value = post.data().categories?.map(cat => cat.id)
}
}- Write a
savePost()function that stores the form data in FireStore:
async function savePost() {
// if there is a validation error, abort
if (!isValid.value) return false
// load the FireStore references for selected categories
const selectedCategories = categories.value?.map(catRef => doc(db, 'categories', catRef))
const newPost = {
title: title.value,
body: body.value,
categories: selectedCategories
}
const coll = collection(db, "posts")
if (isNewDoc.value) {
// create a new Posting in FireStore
await addDoc(coll, newPost)
} else {
// update the existing Posting in FireStore
await setDoc(doc(coll, props.id), newPost)
}
// forward to overview page
router.push('/posts')
}- In
Read, add a delete button:
<v-btn color="red-darken-4" variant="elevated" v-bind="props">
Delete
</v-btn><v-dialog width="auto">
<template v-slot:default="{ isActive }">
<v-card>
<v-toolbar color="red-darken-4" title="Delete posting"/>
<v-card-text>
<div class="text-h4 pa-8">
Are you sure that you<br/>
want to delete this post?
</div>
</v-card-text>
<v-card-actions class="justify-space-between">
<v-btn variant="elevated" color="grey-darken-1"
@click="isActive.value = false">
Cancel
</v-btn>
<v-btn variant="elevated" color="red-darken-4"
@click="deletePost(props.id)">
Yes, delete
</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>- Write a
deletePost()function that deletes the posting in FireStore:
async function deletePost(id) {
await deleteDoc(doc(db, "posts", id))
// forward to overview page
router.push('/posts')
}npm installnpm run devnpm run build








