Building a Job Board with Nuxt.js and BCMS as a Headless CMS

By Nyore Chadere
Read time 9 min
Posted on November 30, 2023
Share it on:
Job Board with Nuxt.js

Did you know you do not need to pay extra money for job board software? If you're looking to build a job board or a career page for an organization, all you need is a headless CMS that provides you with all the features you need for a job board system.

One such Headless CMS is BCMS, an intuitive headless CMS that empowers developers to manage and model content with modern tech stacks. To simplify your job board development experience, BCMS offers the following features:

Decoupling Architecture: Break free from limitations. Manage and structure job content independently from the user interface, opening up boundless possibilities for customization.

Effortless content creation: Create and modify job listings, company profiles, and application forms effortlessly without impacting the seamless experience for job seekers.

Multi-platform publishing: BCMS excels at seamlessly publishing content across various platforms and devices, ensuring your job board reaches a global audience without a hitch.

Get to the world: Expand your job board's reach with BCMS's multilingual capabilities, enabling content translation and localization to attract talent worldwide.

HR Analytics: Stay in control with BCMS's powerful analytics and seamless job submission management, empowering your HR team to make data-driven decisions.

In this article, we will build and deploy a Job board application that uses Nuxt on the front end and BCMS as the backend CMS to track applications and manage content.

This application is not a standalone application like a LinkedIn Jobs but will be like a career page on a website where they list job openings and visitors can apply.

But you can take the same approach and expand it further to build a standalone job board like LinkedIn or Indeed with relative ease.

Set Up BCMS

You have two options when it comes to integrating BCMS into your Nuxt project:

Use the following steps to get started using BCMS.

1. Create A BCMS Instance

Log in to your dashboard and create a BCMS Instance; choose which version you want, either the cloud version or the self-hosted version. For this project, I will be using the cloud version.

BCMS Instance

2. Modelling Data in BCMS

Modeling data in a headless CMS means defining the structure and organization of your content, such as articles, products, or job listings, by specifying the types of information each content item can have (e.g., title, description, image).

A flexible data model makes this process effortless, allowing developers to fetch and display this content across various platforms and devices through API calls without being tied to a specific frontend design.

Now, for BCMS, two features make all this possible

  1. Templates - Templates in BCMS define our content structure; they specify the information each content can have. Based on that structure, you can create multiple or single entries.

  2. Entries - Each entry in BCMS represents a single record of a Template.

Now, let's plan out how we will model our data or, in other words, create our job posting template.

Job board cover

From the image above, we can see the first things we'll need

  • Title - String - Required

  • Location - String - Required

  • Date Posted - Date - Required

  • Salary Range - String - Optional

Now, let's delve deeper into the full detail page and uncover what additional elements we need:

job detail page
  • Job Category - String - Required

  • Job Description - Rich Text - Required

  • Candidate Profile - Rich Text - Required

  • Job Perks - Rich Text - Required

  • Application Form - This form resides only on the front end, but the user-submitted content will be reflected in our BCMS. It will require a unique template. The magic happens when a user submits the form. It automatically creates a new entry. Sweet :)😊

With all of this completed, it's time to start modeling our data on BCMS. Here's how to do this:

Now go to your BCMS and create a new template named "Opening" (or any name you choose) with an optional description.

job template creation

Add the properties we listed above (except the application form) into our newly minted template. Some properties are already generated for us, including the Title, Slug, and the Created At /Updated at dates. (Not visible on the BCMS, but we'll get it when we fetch the data.)

Your template should look something like this at this point:

job opening template

Next Step: Create the template for the application form. These are what we need for the application form:

application form
  • First Name - String - Required

  • Last Name - String - Required

  • Email Address - String - Required

  • Phone Number - Number - Optional

  • Portfolio/Github - String - Required

  • Resume/CV - Media - Required

  • Cover Letter - Rich Text - Required

Your template should look something like this at this point:

application form finished

The Next step is to add an entry to your opening template and add one or two entries so we can fetch them in the next step.

After you've done that, navigate to the key manager and create an API key, which you'll use to fetch data, create the API key, and toggle all permissions on for the key.

Setup Nuxt and BCMS

Nuxt is an intuitive web framework based on Vue.js that makes building web applications simple and powerful. With Nuxt, you can build any kind of website or web application with optimized performance in mind.

We will now create a BCMS project that uses Nuxt using the BCMS CLI. To use the CLI.

First, Install the CLI npm install @becomes/cms-cli -g

Second, create a BCMS project bcms --website --create and follow the prompts.

This will generate a BCMS Nuxt project for you with your env and other configurations properly hooked up to your BCMS. Now go into the folder in your terminal and run npm run dev and go to http://localhost:3000

Create your Job Board Template

Alright, coding enthusiasts, it's time to create our very own template! This simple yet powerful template will serve as a guide for this tutorial. To create your template, head over to our code editor and follow these steps:

  • Head over to the pages folder and delete everything there

  • Make a new app.vue on the root page and add these lines of code to it

      
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
      
  • Restart your dev server. Then, repopulate the pages folder with the template for the career page. (Take a look at the finished design for the Jobboard page here)

  • Download the pages folder along with the assets folder. You'll see two files named index.vue and [slug].vue respectively.

index.vue - This is the welcoming mat of our Nuxt application, the home page where all the job openings will be displayed. We'll use a special tool called 'NuxtLink component', provided by Nuxt, to dynamically navigate to the details of each job post.

[slug].vue - This is a dynamic path that can change based on the content. The brackets let Nuxt know this page isn't static. So, for example, you could have blog.com/hello-world or blog.com/hello-nuxt. The part after the slash ('hello-world' or 'hello-nuxt') is what we call a 'slug'. These slugs allow us to fetch data dynamically, meaning it changes based on what the slug is.

For BCMS, this slug is generated for us when modeling our data. So, we fetch data for an opening using its slug. This is why it makes sense to name it [slug], though we can name it anything. More on data fetching later on.

Fetch and display data from BCMS with Nuxt

Now, it's time to fetch data from our CMS and display it on our Nuxt frontend. Empowered by the robust capabilities of Typescript, we'll seamlessly extract information from BCMS. This will transform how our application interacts with data, bringing a new level of dynamism and interactivity.

How to fetch data for index.vue

To get started, create a script tag in your pages/index.vue file in between the template tag and style tag, like this.

      
</template>

<script setup lang='ts'>
</script>

<style>
      

With that created, we'll now use a method: useAsyncData provided to us by Nuxt to fetch data from an API or a module. In this case, we'll use the BCMS module to query the data for us automatically.

      
import { OpeningEntry } from '~~/bcms/types';

const { data, error } = useAsyncData(async (ctx) => {

  // Get all Opening entries
  const openings = (await ctx?.$bcms.entry.getAll({
    // Template name or ID
    template: 'opening',
  })) as OpeningEntry[];
  return {
    entries: openings
  };
});

const entries = data.value?.entries;
      

In the code above, you see an imported module named OpeningEntry, This is a typescript Interface that defines the type of a variable. It Might be named differently depending on what you named your Template when modeling data in BCMS. As for me, I named mine Opening so it is now OpeningEntry.

To check your name, go to 'bcms' in the root folder, open 'types', and find 'entry'. You'll see a list of your created templates. Look for the one handling job openings.

Opening interface

Looks like magic, right? 😊

The BCMS CLI automatically adds these interfaces during app creation. This helps TypeScript fetch your data in the defined format, offering benefits like:

  • Autocompletion: No need to remember if it's 'salaryRange' or 'salary_range.' Your editor will suggest available options. It's a time-saver when dealing with numerous templates.

  • Error Identification: If you change a template name in BCMS, like removing 'location input,' every usage of 'location' in your code will be highlighted in red, making it easier to spot and fix issues.

Now, let's populate our template with what we have fetched.

      
<NuxtLink class="links" :to="opening.meta.en?.slug" v-for="opening in entries" :key="opening._id">
        <h3 class="job-title">{{ opening.meta.en?.title }}</h3>
        <div class="job-details">
          <section class="job-location">
            <img src="~/assets/location.svg" alt="">
            <h5>{{ opening.meta.en?.location }}</h5>
          </section>
          <h5 class="job-price-range">{{ opening.meta.en?.salary_range }}</h5>
          <h5 class="job-date">
            {{ opening.createdAt }}
          </h5>
        </div>
</NuxtLink>

      

We loop through all the possible job posts we might have using v-for="opening in entries" and attach a unique identifier to them using their ID. :key="opening._id"

With that done, we now have access to the values of our fetched content, and we can now use those values in their respective fields, as shown in the code.

Remember, if your variable names differ, your editor will suggest the correct ones, thanks to BCMS's TypeScript feature.

The returned date for the createdAt date is not in a readable format; rather, it is in milliseconds. So, we'll convert it to readable format using JavaScript. So, in our script tag, we can create a function that expects the milliseconds and format it using the options.

      
const formattedDate = (milliseconds:number) => {
  const date = new Date(milliseconds);

  const locales: string | string[] | undefined = 'en-US';
  const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' };

  const formattedDate = date.toLocaleDateString(locales, options);

  return formattedDate;

}
      

So we can now pass milliseconds to formattedDate in our template.

      
<h5 class="job-date">{{ formattedDate(opening.createdAt) }}</h5>
      

How To Fetch Data For [slug].vue

For [slug].vue, instead of fetching all openings, we'll retrieve just one using the slug. This dynamic path activates when we select an opening from the home page. We'll import this slug from Nuxt to query data from BCMS.

      
<script setup lang='ts'>
import { OpeningEntry } from '~~/bcms/types';

const route = useRoute();
const { data, error } = useAsyncData(async (ctx) => {

  // Get Single Opening entries
  const opening = (await ctx?.$bcms.entry.get({
    // Template name or ID
    template: 'opening',
    entry: route.params.slug
  })) as OpeningEntry;
  return {
    opening: opening
  };
});
const opening = data.value?.opening;
</script>
      

Take note that this time around OpeningEntry is without brackets OpeningEntry[].

First, we’ll start with the sidebar div.

      
<div class="sidebar-item">
        <p class="sidebar-item-title">Location</p>
        <p class="sidebar-item-data">{{ opening?.meta.en?.location }}</p>
      </div>
      <div class="sidebar-item">
        <p class="sidebar-item-title">Salary</p>
        <p class="sidebar-item-data">{{ opening?.meta.en?.salary_range }} /year</p>
      </div>
      <div class="sidebar-item">
        <p class="sidebar-item-title">Job Type</p>
        <p class="sidebar-item-data">Full Time</p>
        <!-- Forgot to make a value for this in BCMS so I'll leave it as it is. -->
      </div>
      <div class="sidebar-item">
        <p class="sidebar-item-title">Date Posted</p>
        <p class="sidebar-item-data">{{ formattedDate(opening?.createdAt) }}</p>
				<!-- Using the same formatter for the date. Copy the code to your script tag !-->
</div>

      

One More thing. On our HTML, add a v-if="opening tag to your job description div
 

The v-if is a vue directive that will only display that div if the opening value is true. So we can now use the opening value comfortably in our template knowing that by the time we access it, it is already fetched and ready to use, minimizing typescript errors in vscode.

With that done, we can now go ahead and populate our front end with the data we just got.

For the title, it will be pretty straightforward like we did it before.

      
<h1 class="job-explainer-title">
        {{ opening.meta.en?.title }}
</h1>

      

But for the other parts of the job details that use rich text field on the bcms that allows us to make lists and other different tags on our BCMS, how is data returned? If you check the data in your browser console, you should see that it returns the elements as arrays containing objects that hold the value.

And those values contain the different HTML Tags.

job detail results in console

So, to get the HTML tags and their values, we’ll have to loop through the array and use a vue directive to render the data.

      
<div class="job-explainer-description">
    <pre v-for="(item, index) in opening?.meta.en?.job_description" v-html="opening?.meta.en?.job_description[index].value"></pre>
</div>
<div class="job-explainer-profile">
    <pre v-for="(item, index) in opening?.meta.en?.candidate_profile" v-html="opening?.meta.en?.candidate_profile[index].value"></pre>
 </div>
 <div class="job-explainer-perks">
    <pre v-for="(item, index) in opening?.meta.en?.job_perks" v-html="opening?.meta.en?.job_perks[index].value"></pre>
</div>

      

I already gave them some styles in our CSS so everything should be pretty well aligned and good to go.

How To Send Application Data to BCMS

Posting data to a headless CMS for some Headless CMSs is a big ask they simply can't provide.

But BCMS decided to simplify our lives and developer experience; they provided us a feature that allows us to post data, amongst other things called Serverless Functions.

What are serverless Functions?

BCMS Functions are JavaScript functions that you can execute by sending an HTTP request to the back-end API.

Once you create a function, it will be available at POST:https://{CMS_DOMAIN}/api/function/{FUNCTION_NAME

We can look at them as requests you’ll send from a backend language like Node.js, that talks to a server, be it your database, and it executes your requests.

The easiest way to add additional functionality to the BCMS backend is by creating BCMS Functions.

How can we use Functions to Send Job application Data

This tutorial, although theoretical and not hands-on, will provide a comprehensive guide on using functions. It will cover how to create these functions and demonstrate their application in our Nuxt Job Board.

Here's a step-by-step:

  • We create a function in our app, either cloud or local.

  • We send the job application data to the function

  • The function receives and validates the data based on the validation parameters we set.

  • The function updates the application entry with the data we sent.

Take the example of this pseudo code below to see what it would look like.

      
const function = importFunction;

const applicationData = {
  Name: "Nyore Chadere",
  Cv: cv-File,
  Github: https://github.com/chaderenyore,
  ...otherData
};

function.send(data);

      

This is a high-level overview of how functions make it easy for us to add features to our backend. You can learn more about how to create and use functions in this tutorial.

How To Deploy Your Code Into A Live Website

To wrap up, we've equipped you with the knowledge needed to create dynamic websites with Nuxt.js and a headless CMS. Now, it's time to turn your development efforts into a live website. We'll cover hosting and deployment steps. We’ll be deploying our BCMS instance before deploying the Nuxt front end.

How To Deploy A BCMS Local Instance

Using the cloud version of BCMS simplifies your tasks. It handles server setup and maintenance and ensures 24/7 uptime, allowing you to focus on front-end development. The API URL remains consistent from development to production.

However, for the self-hosted version, you'll need to handle setup and configuration yourself. A helpful guide on the BCMS documentation shows how to host your local instance on a Digital Ocean Server. We'll update the other servers shortly.

Take note now that your URL has changed, which means you can no longer make requests to [http://localhost:8080/nuxt-jobboard.yourbcms.com] (https://nyores-org-nuxt-jobboard.yourbcms.com). This means that you’ll have to update all the places this URL appeared in your code to the new URL your server provides.

Luckily for us, the only place where BCMS CLI puts that information when building our app is the .env file, so we can update the new URL and ensure everything will work as before.

Deploying your Nuxt Frontend

Now it’s time to deploy our Nuxt frontend. We will deploy to Static hosting providers like GoDaddy and hosting providers like Vercel.

How To Deploy To Static Hosting Providers

There are two ways to deploy a Nuxt application to any static hosting service:

  • Static site generation (SSG) with ssr: true pre-renders routes of your application at build time. (This is the default behavior when running nuxi generate.) It will also generate /200.html and /404.html single-page app fallback pages, which can render dynamic routes or 404 errors on the client (though you may need to configure this on your static host).

  • Alternatively, you can prerender your site with ssr: false (static single-page app). This will produce HTML pages with an empty 

     where your Vue app would normally be rendered. You will lose many of the benefits of prerendering your site, so it is suggested instead to use  to wrap the portions of your site that cannot be server-rendered (if any).

Use the [nuxi generate command](https://nuxt.com/docs/api/commands/generate) to build and pre-render your application npx nuxi generate

That's it! You can now deploy the .output/public directory to any static hosting service or preview it locally with npx serve .output/public

Deploying to Vercel

Take note that these same steps can be applied to Firebase and Netlify with little to no changes at all. Go here to see a full list of where you can deploy to and how to deploy.

How To Deploy to Vercel using Git

  1. Push your code to your Git repository (GitHub, GitLab, Bitbucket).

  2. Import your project into Vercel.

  3. Vercel will detect that you are using Nuxt and will enable the correct settings for your deployment.

  4. Your application is deployed!

Wrapping It Up: You built your Job Board

And that’s it for this tutorial. We’ve covered a lot of ground when it comes to starting a new project with Nuxt and BCMS. This has shown you how BCMS headless CMS, makes your life easier with its features. You can now build out your ideas while BCMS does the heavy lifting for you with the knowledge you just gained.