How to Build a Job Board with Next.js, Tailwind CSS, and Strapi

How to Build a Job Board with Next.js, Tailwind CSS, and Strapi

In this article, you will learn how to build a job board with Next.js and Tailwind CSS with Strapi as a backend.

Author: Trecia Kat

Do you ever ask yourself, “What can I build today?"" You don’t have to worry about that anymore, especially if this is your first Next.js project. In this tutorial, we will learn how to build a job board with Next.js and Tailwind CSS. Below is a list of what this tutorial covers.

  • A brief introduction to Next.js and Strapi
  • Setting up Strapi
  • Build the architecture of your content
  • Creating your content with Strapi’s content manager
  • Setting up your front end
  • Connecting the backend to the frontend
  • Dynamic API routes
  • Conclusion

An Introduction to Next.js and Strapi

Next.js is a React framework that allows you to build supercharged single-page Javascript apps, SEO-friendly and extremely user-facing static websites and web applications using the React framework. Its advantages include hybrid static and server rendering, TypeScript support, smart bundling, route prefetching, and more, with no extra configuration needed.

Strapi is a JavaScript-built headless CMS that helps you build and manage the backend of your project with ease while allowing you to use any frontend framework of your choice. Let’s see how we can use these powerful tools together to create our job board application.

Setting up Strapi

Make sure you have Node.js and NPM installed on your machine before continuing.

  1. In your code editor, create your project folder and inside it, create a folder called backend. I used VSCode, and my project folder is named job board.
    mkdir backend && cd backend
  1. Proceed to install Strapi on your machine by running this command.

     npx create-strapi-app@latest <app-name> --quickstart
    

    Strapi will then install all the necessary dependencies in the backend folder.

  2. After the installation, run the command below:

      npm run develop
    

    This starts the Strapi server at localhost:1337 and automatically opens up the sign-up form in your browser.

  3. Fill in your details and sign in. Welcome to your admin dashboard.

Strapi Admin Dashboard

Build Your Content Architecture

Go to the 'Content-Type Builder'. This is where you’ll create your data structure. Click on the 'create new collection'. This modal below should appear. The display name will be "job”. This name will be displayed for your URL link (http://localhost:1337/api/job).

content type builder

Click 'continue’. This will take you to the next modal. In this modal, you can select the fields for our collection types. In our case, we will select:

  1. Text: The name will be “vacancy”.
  2. Text: The name will be “recruiter”.
  3. Rich Text: The name will be “description.”
  4. Email: The name will be “email”.
  5. Number: The name will be “salary” and we will choose the integer number format.
  6. Media: The name will be "image." Set the type to “single media”. Proceed to click on advanced settings and select “allowed types of media” and check only “images”.

Save everything. After that, go back and click “create new collection” again. This time, the display name should be "Category.” Click “continue” and select “relation”. This is to enable us relate this collection type to our job type. You will see how as we proceed. Add another field and select the name field. For the name, we will name it "name.” Then, save.

Edit Jobs Modal

Once you’ve completed the actions above, your collection type should look like the screenshot below. Let’s add some content.

Collection Type

Creating Your Content

We are now ready to add some content. Go to category and click on the “create new entry” button. There, we will enter the type of category the job belongs to. For our first entry, we will add “entry”, then proceed to add intermediate, junior, and senior.

Don’t forget to save each entry and publish them. You should have something similar to the image below:

"Create New Entry" page

Go to job, click “create new entry,” and fill in your data. When completed, it should look like the screenshot below. Don’t forget to relate your categories to your job type. For this one, I selected “entry”. Create multiple entries, save and publish.

Collection Summary

Make sure you’ve saved and published your content. Next, we will go to Settings > Users and Permissions Plug-In > Roles > Public. What we are doing here is to make sure that the data is accessible from the frontend. If you don’t do that, you will get this error:

Not Public Error

I am using the RapidAPI extension on VSCode to make a request to the API. Check the boxes for “find” for each of your content types: job and category. You can go back again and check if the API call returns JSON data.

You will notice that the JSON data returned does not contain everything, including the image. To enable the API to return all the JSON data, make sure your API endpoint includes the query string with the populate parameter like this: http://localhost:1337/api/jobs?populate=* . The API is ready for use in the frontend.

Setting up the Frontend

Before we install Tailwind CSS and Next.js, create a folder named frontend. You will find the full documentation on how to install Tailwind CSS with Next.js, as styling is not the main priority of this tutorial.

    mkdir frontend && cd frontend

Install Next.js in the frontend folder and then run the command to launch the Next.js localhost:3000 server in the browser.

    npx create-next-app <app-name>
    npm run dev

Your job board will contain a basic card component that will display the title, description, image, email, and recruiter’s name that will lead to the full detailed page of the job post. You will then create a page that displays each of the job posts using dynamic routing.

In the root folder of the pages folder “frontend/pages”, create a new folder called “jobs” and inside it, create a file called index.js (frontend/pages/jobs/index.js). Write up an async function to fetch the jobs from the backend.

    // to fetch data
    export const getStaticProps = async () => {
         const res = await fetch("<http://localhost:1337/api/jobs?populate=*>")
         const data = await res.json()

         return {
              props: {
                   getJobs: data.data
              }
         }
    }

Connecting the Backend to the Frontend

In the same file (frontend/pages/jobs/index.js), beneath the function we wrote above, write up the JobPage component. This component will take the “getJob” object as an argument and display whatever we’ve specified.

    import Link from 'next/link'

    const JobPage = ({getJobs}) => {
        // Minimising the paragrahps on the card component
        const MAX_LENGTH = 250

        return ( 
         <>
              <section className="w-full px-4 py-24 mx-auto max-w-7xl md:w-4/5">
                   <div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
                        {getJobs.map( job => (
                            <Link key={job.key} href={'/jobs/' + job.id}>
                              <div>
                                 <h2 className="mb-2 text-xl font-bold leading-snug text-gray-900">
                                     <a href="#" className="text-gray-900 hover:text-purple-700">{job.attributes.vacancy}</a>
                                 </h2>
                                     <p className="mb-4 text-sm font-normal text-gray-600">
                                                   {job.attributes.description.substring(0, MAX_LENGTH) + " ..."}
                                      </p>
                                 <a className="flex items-center text-gray-700" href="#">
                                   <div className="avatar">
                                      <img
                                        className="flex-shrink-0 object-cover object-center w-12 h-12 rounded-full"
                                        src={`http://localhost:1337${job.attributes.image.data.attributes.url}`}
                                        alt={"Photo of " + job.attributes.recruiter} 
                                       />
                                    </div>
                                   <div className="ml-2">
                                       <p className="text-sm font-semibold text-gray-900">{job.attributes.recruiter}</p>
                                       <p className="text-sm text-gray-600">{job.attributes.email}</p>
                                   </div>
                                 </a>
                               </div>
                            </Link>
                        ))}   
                   </div>

                 {/* btns */}
                 <div className="flex flex-col items-center justify-center mt-20 space-x-0 space-y-2 md:space-x-2 md:space-y-0 md:flex-row">
                     <Link href="/"><a href="#" className="px-3 py-2 text-indigo-500 border border-indigo-500 border-solid hover:text-black md:w-auto">Home</a>
                     </Link>
                 </div>
             </section>     
         </>
         )
    }

    export default JobPage;

Then go to your browser and type in: http://localhost:3000/jobs. Your application should look like this:

Sample Application Screenshot

Dynamic API Routes

We want to see the full details of each job post on our board. How can we do that? With Next.js, we will use dynamic routes to create a single page based on its Id.

Create a new file inside the page folder, name it [id].js (frontend/pages/jobs/[id].js). Proceed with this code below.

    export const getStaticPaths = async () => {
        const res = await fetch("<http://localhost:1337/api/jobs?populate=*>")
        const { data: jobs } = await res.json()

                    const paths = jobs.map( (job) => {
            return  {
                params: { 
                    id: job.id.toString(),
                }
         }})

        return {
            paths,
            fallback: false
        }
    }

    export const getStaticProps = async ({params}) => {
        const {id} = params
        const res = await fetch(`http://localhost:1337/api/jobs/${id}?populate=*`)
        const {data: job } = await res.json()

              return {
            props: {job},
            revalidate: 1,
        }

    }

The getStaticPaths function gets called during build time and pre-renders paths for you, which are based on the job Id from each individual object returned from the API. Make sure that the Id is converted from integer to string for it to properly work.

The getStaticProps function now only fetches a single job post based on its Id in the URL. The data fetched will now look like this: http://localhost:3000/jobs/1 and return the contents for Id 1.

To style your full detailed job post page, you can copy and paste this code for the DetailedJobs component.

    const DetailedJobs = ({job}) => {
        const Max_DATE_LENGHT = 10
        const Max_TIME_LENGHT = 11

        return ( 
            <>
            {/* {JSON.stringify(job, null, 2)} */}
            <article className="px-4 py-5 mx-auto max-w-7xl">
                <div className="w-full mx-auto mb-12 text-left md:w-3/4 lg:w-1/2">
                    <p className="mt-6 mb-2 text-xs font-semibold tracking-wider uppercase text-primary">{job.attributes.categories.data[0].attributes.type + " LEVEL"}</p>
                    <h1 className="mb-3 text-3xl font-bold leading-tight text-gray-900 md:text-4xl">
                    {job.attributes.vacancy}
                    </h1>
                    <div className="flex mb-6 space-x-2 text-sm">
                    <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">CSS</a>
                    <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">Tailwind</a>
                    <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">AlpineJS</a>
                    </div>
                    <a className="flex items-center text-gray-700" href="#">
                    <div className="avatar">
                        <img 
                        className="flex-shrink-0 object-cover object-center w-24 h-24 rounded-full"
                        src={`http://localhost:1337${job.attributes.image.data.attributes.url}`}
                        alt={"Photo of " + job.attributes.recruiter} />
                    </div>
                    <div className="ml-4">
                        <p className="font-semibold text-gray-800 text-md">{job.attributes.recruiter}</p>
                        <p className="text-sm text-gray-500">{job.attributes.publishedAt.substring(0, Max_DATE_LENGHT)}</p>
                        <p className="text-sm text-gray-500">{job.attributes.createdAt.substring(16, Max_TIME_LENGHT)}</p>
                    </div>
                    </a>
                </div>

                <div className="w-full mx-auto prose md:w-3/4 lg:w-1/2">
                    <p>
                    {job.attributes.description}
                    </p>
                </div>

                <div className="flex flex-col items-center justify-center mt-10 space-x-0 space-y-2 md:space-x-2 md:space-y-0 md:flex-row">
                     <Link href="/jobs"><a href="" className="px-3 py-2 text-indigo-500 border border-indigo-500 border-solid hover:text-black md:w-auto">Back</a>
                     </Link>
                 </div>
            </article>
            </>
         )
    }

    export default DetailedJobs;

Your final application should look like this:

The Job Application

Conclusion

I hope you’ve enjoyed this tutorial and are looking forward to building additional projects with Strapi and Next.js.

When deploying your application on Vercel, make sure you change Strapi’s URL link to the one you’ve hosted your Strapi app to avoid deployment issues. In my case, I hosted my Strapi application on Heroku.

Code Sample

This project in the tutorial is absolutely open source and if you want to add a feature or edit something, feel free clone it and make it your own or to fork and make your pull requests.

Looking for more things to build with Strapi? Here’s how you can build a podcast application with Strapi and Next.js.