A Crash Course in Vue.js
Posted on April 3, 2023
Creating a website these days seems to be a lot more complicated than what it used to be, with a vast plethora of Javascript frameworks that seemingly keep on multiplying endlessly. Even after picking one, these frameworks are being developed at a fast pace, resulting in many new versions that arenβt backwards compatible, leaving most StackOverflow answers outdated.
Nevertheless, after having had my trusty static Beautiful Jekyll website, I decided to take the plunge and code up a website from scratch. This post covers the basics of the Vue.js framework, which I ended up using, along with several lessons learnt along the way. My website is completely open source, so my hope is that the code along with this blog post could kickstart your own personal website, in case you desire one such.
Also, a disclaimer: Iβm still a Vue.js beginner, with only a few months of experience at this point, so take everything I say with many grains of salt.
This post is part of a series on Vue.js:
- A Crash Course in Vue.js
- Dark Mode in Vue.js
# Hello World with a Foot
There are several web development frameworks out there these days. Should you go with React, Angular, Vue, Svelte or something completely different?
It seems that Angular is the most complicated one out of those four, which seemed a bit too overkill for a simple personal website. Svelte is very new compared to the others, meaning that there is currently a lack of learning resources, making it a bit harder for beginners like me to get started. So my choice was either React or Vue, and I somewhat arbitrarily opted for Vue. Maybe it was simply a nice balance between being the underdog and having a reasonably sized community π€·
Anyway, whatβs Vue all about? Essentially, itβs all about components (which seems like is the case for all of the above frameworks as well). These are small building blocks of your website, such as a menu, a footer, the content of a blog post, and so on. As with classical websites, these consist of three parts: HTML for the actual content of the component, CSS for the styling, and Javascript for any code that belongs to that component. In Vue these components are bundled together in separate files, with a special .vue extension.
A simple βHello Worldβ main component (usually called App.vue) could simply look like:
<script setup></script>
<template>
<p>Hello, world!</p>
</template>
<style scoped></style>
In this example we see the three parts. All the Javascript is put in the <script setup> tag, HTML belongs to the <template> tag, and all CSS styling lies within the <style scoped> tag. We of course only have HTML in this example, so letβs try adding a basic footer as a separate component. We thus create a separate Footer.vue component, like so:
<script setup>
const year = new Date().getFullYear();
const name = "Your name here";
</script>
<template>
<div class="footer">
<p class="copyright">Β© {{ year }} {{ name }}</p>
</div>
</template>
<style scoped>
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: #f2f2f2;
}
.copyright {
font-size: 13px;
text-align: center;
}
</style>
This simply adds a footer containing a copyright statement with some basic styling. Note that we can use the year and name Javascript variables directly in the HTML part as { { year } } and { { name } } - handy!
Adding this component to our App.vue file then looks like the following:
<script setup>
import Footer from "./Footer.vue";
</script>
<template>
<div class="main-view">
<p>Hello, world!</p>
</div>
<footer />
</template>
<style scoped>
.main-view {
margin-top: 80px;
margin-bottom: 150px;
}
</style>
Note that I added some top and bottom margins around the main view. The reason for this is to ensure that thereβs enough room for both the footer and a header (weβll build the header below).
Thatβs all well and good, but how do you make these .vue files into an actual website? This requires a bit of boiler plate code, but the easiest way to automate this initialisation of a project is to use the create-vue package. Simply write npm init vue@3 in your terminal, name your project and simply say βNoβ to everything. This creates a folder with the following basic structure
.
βββ README.md
βββ index.html
βββ package.json
βββ public
β βββ favicon.ico
βββ src
β βββ App.vue
β βββ assets/img
β β βββ base.css
β β βββ logo.svg
β β βββ main.css
β βββ components
β β βββ HelloWorld.vue
β β βββ TheWelcome.vue
β β βββ WelcomeItem.vue
β β βββ icons
β β βββ IconCommunity.vue
β β βββ IconDocumentation.vue
β β βββ IconEcosystem.vue
β β βββ IconSupport.vue
β β βββ IconTooling.vue
β βββ main.js
βββ vite.config.js
For our basic example, letβs remove all the files in the components folders and add in our Footer.vue component, and replace App.vue with our file defined above. To keep things simple, letβs remove the CSS styling as well, by deleting the three files in the assets/img folder and adding an empty main.css file in there. We end up with the following structure:
.
βββ README.md
βββ index.html
βββ package-lock.json
βββ package.json
βββ public
β βββ favicon.ico
βββ src
β βββ App.vue
β βββ assets/img
β β βββ main.css <-- empty
β βββ components
β β βββ Footer.vue
β βββ main.js
βββ vite.config.js
If we now run npm install && npm run dev in the terminal, weβll see our fancy new website, with a working footer component!
# Adding navigation
Itβs not really a website if we canβt navigate to other pages, so letβs add that. Vue is really good at so-called Single Page Applications (SPAs), which technically speaking consists of a single page, but still allows for navigation. Instead of redirecting the page to a new page, the βnavigationβ stays on the same page but updates all the content on the page! We call these βvirtual pagesβ views. The result is way faster than using normal multi-page applications.
# vue-router
Adding navigation on an SPA with Vue is done using a module called vue-router. We simply install it with npm install vue-router. With it installed, letβs set up some views, HelloWorld.vue and HelloAnotherWorld.vue, both of which we put in a folder views:
<template>
<p>Hello, world!</p>
</template>
<template>
<p>Hello, another world!</p>
</template>
Note that I left out the Javascript and CSS part of the components - these are not really necessary if theyβre empty anyway. We also need to change our main component App.vue, which is the main view. We add a new router-view tag, which is updated with the component belonging to the current URL:
<script setup>
import Footer from "./components/Footer.vue";
</script>
<template>
<div class="main-view">
<router-view :key="$route.fullPath" />
</div>
<footer />
</template>
<style scoped>
.main-view {
margin-top: 80px;
margin-bottom: 150px;
}
</style>
Top tip: Including :key="$route.fullPath" in the router-view ensures that the views are updated properly when the URLs change.
Next, we change the main.js file to the following, which sets up the routes, coupling URLs to views:
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import HelloWorld from "./views/HelloWorld.vue";
import "./assets/img/main.css";
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/",
name: "Hello world",
component: HelloWorld,
},
{
path: "/another-world",
name: "Hello another world",
component: () => import("./views/HelloAnotherWorld.vue"),
},
],
});
createApp(App).use(router).mount("#app");
Top tip: By specifying () => import('./views/HelloAnotherWorld.vue') instead of importing HelloAnotherWorld and using that directly ensures that HelloAnotherWorld is loaded lazily, so we donβt have to spend resources loading it when we enter the site.
Our repo structure now looks like the following:
.
βββ README.md
βββ index.html
βββ package-lock.json
βββ package.json
βββ public
β βββ favicon.ico
βββ src
β βββ App.vue
β βββ assets/img
β β βββ main.css <-- empty
β βββ components
β β βββ Footer.vue
β βββ main.js
β βββ views
β βββ HelloAnotherWorld.vue
β βββ HelloWorld.vue
βββ vite.config.js
If we now run npm run dev again, we will now see the HelloWorld view, and if we go to http://localhost:517x/another-world then weβll see the HelloAnotherWorld view. Hooray! π
# Adding a Top Menu
So far the navigation is quite opaque, requiring the user to know the URLs of the other views. To make things a bit easier, letβs add a top menu with some navigation, with a Header component. The key bit here is to use router-link tags instead of normal anchor tags, as they allow navigation to other views without actually loading a new page:
<template>
<div class="header">
<nav class="navbar">
<router-link class="nav-item" to="/"> Hello world </router-link>
<router-link class="nav-item" to="/another-world">
Hello another world
</router-link>
</nav>
</div>
</template>
<style scoped>
.header {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 9999;
background-color: #f2f2f2;
}
.navbar {
display: flex;
justify-content: end;
padding: 1.3rem 1.3rem;
}
.nav-item {
margin-left: 25px;
}
</style>
We insert this Header component the same way we inserted the Footer component, inside the App.vue main file:
<script setup>
import Header from "./components/Header.vue";
import Footer from "./components/Footer.vue";
</script>
<template>
<header />
<div class="main-view">
<router-view :key="$route.fullPath" />
</div>
<footer />
</template>
<style scoped>
.main-view {
margin-top: 80px;
margin-bottom: 150px;
}
</style>
And voilΓ , a top menu!
# Blog posts!
So far we have a website that allows for multiple views, with associated navigation, and a header and footer. That alone could be sufficient for your needs, in which case I would skip to the βDeploying the Websiteβ section below.
Iβll here be covering how I managed to set up a blog on my website. A blog is a bit more complex compared to normal static views, as we need to load in all the blog posts, present them, and we need to dynamically update a Blog view with the content of the blog post. Iβll keep it relatively simple and high level, and not get too bogged down in the details here.
My overall idea was to have a Blog view, which displays all the blog post titles, and clicking on these takes you to a Post view, whose content is then updated according to an associated Markdown file.
# Working with Markdown Files
My first problem was how to deal with Markdown files in Vue at all. Luckily there is a Vue plugin called vite-plugin-md which essentially converts Markdown files into Vue components, so that we can use them like we did with our Header and Footer components above. As always, we install it simply as npm install vite-plugin-md. We also need to change the vite.config.js file to the following, which allows our app to use the Markdown files:
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import Vue from "@vitejs/plugin-vue";
import Markdown from "vite-plugin-md";
export default defineConfig({
plugins: [Vue({ include: [/\.vue$/, /\.md$/] }), Markdown()],
resolve: {
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
},
});
The only new lines here is the Markdown import from our new plugin, including it in the plugins list, and including Markdown files in the Vue build process, to make sure that they are included when we deploy our website. With this setup, we can now simply import Markdown files in our views as import Post from 'my-awesome-post.md and use them as <Post/>, just like normal components.
Thereβs a problem though: we donβt know the names of all the posts in advance, and we donβt want to manually add more imports everytime we finish a new blog post. Instead, we should have a folder containing the blog posts, and it should simply import all of these and set them up properly. This is what weβll be doing next, in our Blog view.
# Adding a Blog View
Our Blog view is the first time we really need a bit of Javascript to handle the dynamic loading of the Markdown posts. We start by creating a new folder, posts, inside the src folder (I tried moving it out to the root of the repositori, but that doesnβt seem to work). Letβs add a couple of posts to the folder, my-awesome-post.md and my-awesome-second-post.md:
# What a Nice Post!
Here's some content.
# What a Nice Second Post!
Here's some more content.
Our repo structure now looks like the following:
.
βββ README.md
βββ index.html
βββ package-lock.json
βββ package.json
βββ public
β βββ favicon.ico
βββ src
β βββ App.vue
β βββ assets/img
β β βββ main.css
β βββ components
β β βββ Footer.vue
β β βββ Header.vue
β βββ main.js
β βββ posts
β β βββ my-awesome-post.md
β β βββ my-awesome-second-post.md
β βββ views
β βββ HelloAnotherWorld.vue
β βββ HelloWorld.vue
βββ vite.config.js
We next create our Blog view. Within it we would firstly like to extract an array of all the names of our posts within the posts folder. In Javascript this can be done with Object.keys(import.meta.globEager('@/posts/*.md')), where @ indicates the src folder here. import.meta.globEager returns a dictionary with filenames as keys and modules as values, and weβre only interested in the keys for now here.
This will return the full path to the posts, however, so we trim them and remove the .md suffix as well, to get an array postNames containing the two names my-awesome-post and my-awesome-second-post. Our Javascript part of the Blog view thus looks like this:
<script setup>
const postNames = Object.keys(import.meta.globEager('@/posts/*.md')).map(
(file) => file.split('/').slice(-1)[0].slice(0, -3)
)
</script>
We next need to define the HTML part of the view. This is where weβll be using our first Vue directive, which are special keywords we can use within the HTML part of our components. In this case weβll be using the v-for directive, which allows us to iterate over an array defined in the Javascript part of the component. The HTML part of the Blog view then ends up looking like this:
<template>
<h1>Blog</h1>
<div v-for="postName in postNames">
<router-link :to="`/posts/${postName}`">{{ postName }}</router-link>
</div>
</template>
Note here that weβre suddenly using :to instead of to inside the router-link tag. This is yet another Vue directive and is really a short-hand for v-bind:to. This allows us to not simply put in a fixed value inside the to argument, but instead add Javascript that connects the value of to to the different postName values.
Also, weβre linking to /posts/${postName}, but we havenβt actually defined that route yet. Thatβs the next step, along with adding a Post view.
# Adding a Post View
The last part of the blog is adding a view for the individual blog posts. This view will depend on the ID of the blog post, so we need to feed in an ID somehow. In Vue, this is done using something called props. These are simply variables that we can feed into a component, which it can then use internally. In this case, we want to feed in the ID, and we specify that the view expects an ID using the defineProps function.
We also need to import the Markdown post. Unfortunately Vue does not support importing dynamic components as import PostContent from '../posts/${id}.md'; that only works for static paths. Instead, we can achieve it using defineAsyncComponent:
<script setup>
import { defineAsyncComponent } from "vue";
const { id } = defineProps({ id: { type: String, required: true } });
const PostContent = defineAsyncComponent(() => import(`../posts/${id}.md`));
</script>
<template>
<PostContent />
</template>
In a nutshell, this view takes in an ID, loads in the associated blog post, and displays it. Next up, we need to add a new route, and also add a new link to our top menu. For the route, we simply add the following to the list of routes inside main.js:
{
path: '/posts/:id',
name: 'Post',
component: () => import('./views/Post.vue'),
props: true,
},
Note the :id keyword here, which states that whatever weβll put after /posts/ will be used as the id and be sent to the view. We need props: true here as well, to ensure that this id is actually being sent to the Post view.
Lastly, we add a link to the blog in the top menu by simply adding the following line to the Header component:
<router-link class="nav-item" to="/posts">Blog</router-link>
And thatβs it!
# Deploying the Website
We now have a working website which we can run locally, so letβs try to get it live. After a lot of research I ended up with vercel, mostly because it supports both web frameworks like Vue.js as well as Python applications.
Simply install it with npm install --global vercel, and then run the vercel command in your repo. This will deploy the website to a test environment where you can check if everything is as it should be. If thatβs the case then you can run vercel --prod to deploy it properly, all free of charge.
On the vercel website you can then attach the website to a custom domain if you own one, and also link it up to your GitHub/GitLab/Bitbucket repo, to automatically deploy when new changes are pushed to the repo.
# Wrapping up
We did lots in this post! We started from absolutely nothing, and managed to build a working website with navigation as well a blog using content from Markdown files, and deployed the website using vercel.
Now, the website is not the prettiest, as Iβve focused mostly on the functionality and not spent a long time on styling. However, as my own website (the one youβre currently on) was built using almost exactly the same structure, you can get inspired from the styling and other various extra features, as the code is all open source.
Have fun coding π