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
Sign up at https://app.thebcms.com/
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.
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
Now, let's plan out how we will model our data or, in other words, create our job posting template.
From the image above, you can see the first things you'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 you need:
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 your 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.
Add the properties 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:
Next Step: Create the template for the application form. These are what we need for the 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:
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)
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.
export interface OpeningEntryMeta { title: string; slug: string; location: string; salary_range?: string; job_description: BCMSPropRichTextDataParsed; job_category: string; candidate_profile: BCMSPropRichTextDataParsed; job_perks: BCMSPropRichTextDataParsed; } export interface OpeningEntry { _id: string; createdAt: number; updatedAt: number; templateId: string; templateName: string; userId: string; status?: string; meta: { en?: OpeningEntryMeta; } content: { en?: BCMSEntryContentParsedItem[]; } }
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 the template with what you 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>
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, you now have access to the values of fetched content, and you 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 you can now pass milliseconds to formattedDate in your 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, you’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 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. 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. I already gave them some styles in our CSS so everything should be pretty well aligned and good to go. Posting data to a headless CMS for some Headless CMSs is a big ask they simply can't provide. BCMS Functions are JavaScript functions that you can execute by sending an HTTP request to the back-end API. 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. You create a function in our app, either cloud or local. You send the job application data to the function The function receives and validates the data based on the validation parameters you set. The function updates the application entry with the data you sent. Take the example of this pseudo code below to see what it would look like. This is a high-level overview of how functions make it easy for you to add features to your backend. To wrap up, I'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. I'll cover hosting and deployment steps. You’ll be deploying your BCMS instance before deploying the Nuxt front end. 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. Now it’s time to deploy our Nuxt frontend. We will deploy to Static hosting providers like GoDaddy and hosting providers like Vercel. There are two ways to deploy a Nuxt application to any static hosting service: Static site generation (SSG) with Alternatively, you can prerender your site with Use the [ 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. Push your code to your Git repository (GitHub, GitLab, Bitbucket). Import your project into Vercel. Vercel will detect that you are using Nuxt and will enable the correct settings for your deployment. Your application is deployed! 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.v-if="opening
tag to your job description div
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, you can now go ahead and populate your front end with the data you 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>
<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>
How To Send Application Data to BCMS
But BCMS decided to simplify your lives and developer experience; they provided a feature that allows you to post data, amongst other things called Serverless Functions.What are serverless Functions?
Once you create a function, it will be available at POST:https://{CMS_DOMAIN}/api/function/{FUNCTION_NAME
You 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 you use Functions to Send Job application Data
Here's a step-by-step:
const function = importFunction;
const applicationData = {
Name: "Nyore Chadere",
Cv: cv-File,
Github: https://github.com/chaderenyore,
...otherData
};
function.send(data);
How To Deploy Your Code Into A Live Website
How To Deploy A BCMS Local Instance
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
How To Deploy To Static Hosting Providers
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).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).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
How To Deploy to Vercel using Git
Wrapping It Up: You built your Job Board
Other starters you may want to learn about
There are many example apps and starter projects to get you started.
Join our Newsletter
Get all the latest BCMS updates, news and events.
By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.