Building a Portfolio Site With Vue, Bulma, and Airtable
Tags: vue, webdev, beginners, tutorial
Published: 2019-08-02 18:57:56

---
title: Building a Portfolio Site With Vue, Bulma, and Airtable
published: true
description: A simple starting point for a for showing off your skills using Airtable as a CMS
tags: vue, webdev, beginners, tutorial
---

It's been about 3 years since I left full-stack bootcamp, and the first time that I've needed to put together a portfolio site. I wanted to make something that was relatively simple, that could be easily updated, and that would be easy to extend and improve as time goes on.

Is this tutorial perfect? Heck no! It's the first tutorial I've written, and I've been learning Vue self-guided, so some parts I'm sure could be better (let me know in the comments if you would have done something differently). That said, I know this could be helpful to someone out there!

You can see the code for my entire portfolio on github, created from this starting point, here: https://github.com/markjohnson303/portfolio

A finished example is at [hellomark.dev](https://hellomark.dev/), but it is a work in progress and you may see some things that are different from what is described here.

##The tools
**[Vue.js:](https://vuejs.org/)** I chose Vue for this project because it's the framework that I'm most familiar with. Some might say that it's overkill for a small project like this, and for you, it might be. It works well for me because it's comfortable and flexible enough for what I might do with it in the future. It's also what I'm hoping to use in my next role, so why not!

**[Bulma:](https://bulma.io/)** I haven't used Bulma before this project, but I wanted something that would allow me to get the site up quickly, then improve the styling easily over time. Bulma is simple to learn, but easy to build upon. It doesn't have the world's largest library of components, but what it does have is solidly built.

**[Airtable:](https://airtable.com/)** I've been wanting to use Airtable in a project for a while now. According to Airtable, it's "Part spreadsheet, part database", and was made to be flexible for all sorts of uses. I used it here as a CMS because it's really easy to use and has an awesome API with great documentation (that's customized to your database). Now that it's set up, I can use it across the site for all sorts of fun things. And it's free!

##Getting started
The first thing you need to do is set up your Vue project. We're going to use the Vue CLI to scaffold the project. Make sure you have vue and the Vue CLI installed:

`$ npm install -g vue`
`$ npm install -g @vue/cli`

Then create your project:
`$ vue create portfolio`

And fire it up:
`$ npm run serve`

Vue CLI gives you a very helpful starting point with a lot of the files and folders that we need. We're going to build off of this.

Let's also add our CSS framework, Bulma, now.
`$ npm install --s bulma`

And add the Sass stylesheet to our `App.vue` file
```javascript
<style lang="sass">
@import "~bulma/bulma.sass"
</style>
```

You can make any adjustments to the Bulma defaults here, above the import.

We'll install Axios (for working with our Airtable API)
`$ npm install --s axios`

We need [VueSimpleMarkdown](https://www.npmjs.com/package/vue-simple-markdown)so we can compose and style our posts with markdown.

`$ npm install -s vue-simple-markdown`

And in `main.js` we'll put:

```javascript
import VueSimpleMarkdown from 'vue-simple-markdown'
import 'vue-simple-markdown/dist/vue-simple-markdown.css'

Vue.use(VueSimpleMarkdown)
```

##Setting up our routes
We're going to have 5 main routes for this site: About, Contact, Home, Project, and Projects. Let's set those up in In `src/router.js`.

```javascript
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import About from "./views/About.vue";
import Contacts from "./views/Contact.vue";
import Projects from "./views/Projects.vue"
import Project from "./views/Project.vue"

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/about",
      name: "about",
      component: About
    },
    {
      path: "/contact",
      name: "contact",
      component: Contact
    },
    {
      path: "/projects",
      name: "projects",
      component: Projects
    },
    {
      path: "/project/:slug",
      name: "project",
      component: Project
    }
  ]
});
}
```

The odd one out is `path: "/project/:slug"`. We're going to use this route to display a single project from Airtable based on the slug later.

We're also going to make an empty component for each one in `src/views`, here's the empty `Contact.vue` for example. We'll fill these in later.

```javascript
<template>
  <div>

  </div>
</template>

<script>

export default {
  name: "contact",
};

</script>
```

##Adding header and footer
Let's add our header (with navigation) and footer, a little bit of styling, and a touch of Vue magic to make it work on mobile. We'll put this code in `App.vue` so that it will show up on every view.
```javascript
<template>
  <div id="app">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <nav class="navbar" role="navigation" aria-label="main navigation">
      <div class="navbar-brand">
        <router-link class="navbar-item" to="/">
          <img src="./assets/name-mark.jpg" width="112" height="28">
        </router-link>

        <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample" :class="{ 'is-active': showNav }" @click="showNav = !showNav">
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
        </a>
      </div>

      <div id="navbarBasicExample" class="navbar-menu" :class="{ 'is-active': showNav }">
        <div class="navbar-start">

        </div>

        <div class="navbar-end">
          <router-link to="/" class="navbar-item">
            Home
          </router-link>
          <router-link to="/about" class="navbar-item">
           About
          </router-link>
          <router-link to="/projects" class="navbar-item">
           Projects
          </router-link>
          <router-link to="/contact" class="navbar-item">
           Contact
          </router-link>
       </div>
     </div>
   </nav>
   <router-view />
   <footer class="footer">
    <div class="content has-text-centered">
      <p>
        Built by Mark Johnson with Vue.js, Bulma, and Airtable.
      </p>
    </div>
  </footer>
</div>
</template>

<script>
  export default {
    name: "App",
    data() {
      return{
        showNav: false
      }
    },
  };
</script>

<style type="text/css">
#app {
  min-height: 100vh;
  overflow: hidden;
  display: block;
  position: relative;
  padding-bottom: 168px; /* height of your footer */
}
footer {
 position: absolute;
 bottom: 0;
 width: 100%;
}
</style>

<style lang="sass">
@import "~bulma/bulma.sass"
</style>

```

##Static pages
The About, Home, and Contact pages don't have any dynamic content on them, so feel free to add whatever content you like. Here's what I did with the homepage, for example. I kept it very simple here, but you can embellish it however you like.
```javascript
<template>
  <div>
    <div class="hero is-cover is-relative is-fullheight-with-navbar is-primary">
    	<div class="hero-body">
    		<div class="container">
				<h1 class="title is-1">Hello, I'm Mark.</h1>
				<h2 class="subtitle is-3">A customer focused, entrepreneurially minded web developer.</h2>
			</div>
		</div>
    </div>
  </div>
</template>

<script>

export default {
  name: "home",
};
</script>
```

##Building the Projects page
The projects page is where things start to get interesting. We're going to be pulling our information in from Airtable and displaying a summary card for each project.

###Set up Airtable
Create a new base on Airtable called "Projects". Create the following fields: "Title" (single line text), "slug" (single line text), "Body"(long text), "Image"(attachment), "Date Published" (date), "Published" (checkbox), "Excerpt" (single line text).

Voila! You have a simple CMS! Fill it in with a few rows of dummy data so you can see what you're working with. Make sure the "slug" is unique! We'll use this to build our url in a later step.

###Build a card for displaying projects
We're going to create a component to display our project summary. It's also reusable so that you could create a blog with the same thing later!

In `src/components` create a new file called `PostCard.vue`. Fill it in as follows: 
```javascript
<template>
	<div class="post-card">
		<div class="card">
			<div class="card-image">
				<figure class="image is-square">
					<img :src="image" alt="Placeholder image">
				</figure>
			</div>
			<div class="card-content">
				<div class="media">
					<div class="media-content">
						<p class="title is-4">{{title}}</p>
						<p class="subtitle is-6">{{date}}</p>
					</div>
				</div>
				<div class="content">
					<p>{{snippet}}</p>
					<router-link :to="'/project/'+slug" class="button is-fullwidth">View Project</router-link>
				</div>
			</div>
		</div>
	</div>
</template>

<script>
	export default {
		name: "PostCard",
		props: {
			title: String,
			date: String,
			snippet: String,
			image: String,
			slug: String
		}
	};
</script>

```

We're going to bring in the props from the Projects page after we get the projects from Airtable's API. They'll fill in the template with content and a link to the full project view.

###Creating a service to bring in projects
Let's set up the connection to the Airtable API. Make a directory at `src/services`, and in it, put a file called `ProjectsService.js`. 

In the projects service, we're going to use Axios to call the Airtable API and get all of the projects. Your file should look like this:
```javascript
import axios from 'axios'

const Axios = axios.create({
  baseURL: "https://api.airtable.com/v0/[YOUR APP ID]/Projects"
});

Axios.defaults.headers.common = {'Authorization': `Bearer ` + process.env.VUE_APP_AIRTABLEKEY}

export default{
  getProjects() {
  	return Axios.get()
  }
}
```
You'll need to set up the `baseURL` with the ID of your Airtable base. Find it in the URL from Airtable.

You'll also need to add your API key from Airtable. I put mine in a file called `.env.local` in the root directory of the Vue project. This file is listed in .gitignore so you don't risk exposing it. All that's in the file is this: `VUE_APP_AIRTABLEKEY=[YOUR API KEY]`.

Finally, we're exporting a function that calls get on the API endpoint in the baseURL.

###Displaying the project cards
We're going to display the cards for our projects on the Projects view. In your `Projects.vue` template:
```javascript
<template>
	<div>
		<section class="hero is-medium is-primary is-bold">
			<div class="hero-body">
				<div class="container">
					<h1 class="title is-2">
						Projects that I have built
					</h1>
				</div>
			</div>
		</section>
		<section class="section">
			<div class="container is-fluid">
				<div class="columns is-multiline">
						<div class="column is-one-third" v-for="project in projects">
							<post-card v-bind="project"></post-card>
						</div>				
				</div>
			</div>
		</section>
	</div>
</template>
```

The thing to note here is `v-for="project in projects"` where we're creating an instance of `post-card` for every project and passing in the project details with `v-bind`.

The script section of the template looks like this:

```javascript
<script>
	import ProjectsService from '@/services/ProjectsService'
	import PostCard from '@/components/PostCard'
	export default {
		name: "projects",
		components: {
			PostCard
		},
		data() {
			return{
				airtableResponse: []
			}
		},
		mounted: function () {
			let self = this
			async function getProjects() {
				try{
			  		const response = await ProjectsService.getProjects()
			  		console.log(response)
			  		self.airtableResponse = response.data.records

			  	}catch(err){
			  		console.log(err)
			  	}
			  	}
			  getProjects()		  	
	 	},
	 	computed: {
	 		projects(){
	 			let self = this
	 			let projectList = []
	 			for (var i = 0; i < self.airtableResponse.length; i++) {
	 				if (self.airtableResponse[i].fields.Published){
		 				let project = {
		 					title: self.airtableResponse[i].fields.Title,
		 					date: self.airtableResponse[i].fields["Date Published"],
		 					snippet: self.airtableResponse[i].fields.Excerpt,
		 					image: self.airtableResponse[i].fields.Image[0].url,
		 					slug: self.airtableResponse[i].fields.slug
		 				}
		 				projectList.push(project)
		 			}
	 			}
	 			return projectList
	 		}
	 	}
	};
</script>
```
From the top, here's what happening:
* Import the ProjectsService and PostCard we created earlier
* Set up a variable to hold the response from Airtable
* When the component is mounted, call the endpoint we set up in the service. 
* Put the portion of the response we need in the airtableResponse variable.
* Create computed property with an array called "Projects" with one object for each published item in the Airtable response and the variables we need for each card.

If all went well, you should be able to load `/projects` and see each project you created in Airtable in a grid

##Crete the project view
Remember this bit of code from our router setup?
```javascript
{
 path: "/project/:slug",
 name: "project",
 component: Project
}
```

It's going to make it so we can access the slug in our Project component and pass it into our Projects Service so we can retrieve all of the information for the item with that slug Airtable.

Let's add a call for that in `ProjectsService.js`:
```javascript
getProject(slug) {
  return Axios.get("?filterByFormula={Slug}='" + slug + "'")
}
```

We're using the features of Airtable's API here to search for the post that contains the slug and return it.

Now let's create our Project view template:
```javascript
<template>
	<div>
		<section class="hero is-medium is-primary is-bold">
			<div class="hero-body">
				<div class="container">
					<h1 class="title is-2">
						{{project.title}}
					</h1>
					<h2 class="subtitle is-4">
						{{project.snippet}}
					</h2>
				</div>
			</div>
		</section>
		<section class="section">
			<div class="container is-fluid">
				<div class="columns">
					<div class="column is-two-thirds">
						<vue-simple-markdown :source="project.body"></vue-simple-markdown>
					</div>
					<div class="column is-one-third">
						<div class="columns is-multiline">
							<div class="column is-full" v-for="image in project.images">
								<img :src="image.url"/>
							</div>
						</div>
					</div>
				</div>
			</div>
		</section>
	</div>
</template>
```

This template is using the VueSimpleMarkdown plugin that we installed earlier. That means you can use markdown in the body field on Airtable to style your project. We're displaying the body in a column on the left, and all of the images from the item on the right.

Finally, the script section of the project component:
```javascript
<script>
	import ProjectsService from '@/services/ProjectsService'
	import PostCard from '@/components/PostCard'
	export default {
		name: "project",
		components: {
			PostCard
		},
		data() {
			return{
				airtableResponse: []
			}
		},
		mounted: function () {
			let self = this
			console.log("here 1")
			async function getProject() {
				try{
					const response = await ProjectsService.getProject(self.$route.params.slug)
					console.log(response)
					self.airtableResponse = response.data.records

				}catch(err){
					console.log(err)
				}
			}
			getProject()		  	
		},
		computed: {
			project(){
				let self = this
				if (self.airtableResponse[0]){
					let thisProject = {
						title: self.airtableResponse[0].fields.Title,
						snippet: self.airtableResponse[0].fields.Excerpt,
						images: self.airtableResponse[0].fields.Image,
						body: self.airtableResponse[0].fields.Body
					}
					return thisProject
				}
			}
		}
	};
</script>
```
Similar to the Projects view, but this time we're passing the slug into the `getProject` call. We should only get one response if the slug is unique.

Go to /projects/[your-slug] to see your project live!

##Conclusion
Whew. That was a lot! Now that we're done, we have a simple CMS displaying live data on a site built in Vue and styled with Bulma. Pretty cool!

I'm going to be playing with the styling, adding some new features, and cleaning things up, so keep an eye on [hellomark.dev](https://hellomark.dev/) to see what's next!

Let me know what you thought of this tutorial and any questions you have!