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.
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.
Make sure you have Node.js and NPM installed on your machine before continuing.
job board
. mkdir backend && cd backend
npx create-strapi-app@latest <app-name> --quickstart
Strapi will then install all the necessary dependencies in the backend folder.
npm run develop
This starts the Strapi server at localhost:1337 and automatically opens up the sign-up form in your browser.
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).
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:
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.
Once you’ve completed the actions above, your collection type should look like the screenshot below. Let’s add some 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:
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.
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:
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.
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.
1 // to fetch data
2 export const getStaticProps = async () => {
3 const res = await fetch("<http://localhost:1337/api/jobs?populate=*>")
4 const data = await res.json()
5
6 return {
7 props: {
8 getJobs: data.data
9 }
10 }
11 }
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.
1 import Link from 'next/link'
2
3 const JobPage = ({getJobs}) => {
4 // Minimising the paragrahps on the card component
5 const MAX_LENGTH = 250
6
7 return (
8 <>
9 <section className="w-full px-4 py-24 mx-auto max-w-7xl md:w-4/5">
10 <div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
11 {getJobs.map( job => (
12 <Link key={job.key} href={'/jobs/' + job.id}>
13 <div>
14 <h2 className="mb-2 text-xl font-bold leading-snug text-gray-900">
15 <a href="#" className="text-gray-900 hover:text-purple-700">{job.attributes.vacancy}</a>
16 </h2>
17 <p className="mb-4 text-sm font-normal text-gray-600">
18 {job.attributes.description.substring(0, MAX_LENGTH) + " ..."}
19 </p>
20 <a className="flex items-center text-gray-700" href="#">
21 <div className="avatar">
22 <img
23 className="flex-shrink-0 object-cover object-center w-12 h-12 rounded-full"
24 src={`http://localhost:1337${job.attributes.image.data.attributes.url}`}
25 alt={"Photo of " + job.attributes.recruiter}
26 />
27 </div>
28 <div className="ml-2">
29 <p className="text-sm font-semibold text-gray-900">{job.attributes.recruiter}</p>
30 <p className="text-sm text-gray-600">{job.attributes.email}</p>
31 </div>
32 </a>
33 </div>
34 </Link>
35 ))}
36 </div>
37
38 {/* btns */}
39 <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">
40 <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>
41 </Link>
42 </div>
43 </section>
44 </>
45 )
46 }
47
48 export default JobPage;
Then go to your browser and type in: http://localhost:3000/jobs. Your application should look like this:
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.
1 export const getStaticPaths = async () => {
2 const res = await fetch("<http://localhost:1337/api/jobs?populate=*>")
3 const { data: jobs } = await res.json()
4
5 const paths = jobs.map( (job) => {
6 return {
7 params: {
8 id: job.id.toString(),
9 }
10 }})
11
12 return {
13 paths,
14 fallback: false
15 }
16 }
17
18 export const getStaticProps = async ({params}) => {
19 const {id} = params
20 const res = await fetch(`http://localhost:1337/api/jobs/${id}?populate=*`)
21 const {data: job } = await res.json()
22
23 return {
24 props: {job},
25 revalidate: 1,
26 }
27
28 }
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.
1 const DetailedJobs = ({job}) => {
2 const Max_DATE_LENGHT = 10
3 const Max_TIME_LENGHT = 11
4
5 return (
6 <>
7 {/* {JSON.stringify(job, null, 2)} */}
8 <article className="px-4 py-5 mx-auto max-w-7xl">
9 <div className="w-full mx-auto mb-12 text-left md:w-3/4 lg:w-1/2">
10 <p className="mt-6 mb-2 text-xs font-semibold tracking-wider uppercase text-primary">{job.attributes.categories.data[0].attributes.type + " LEVEL"}</p>
11 <h1 className="mb-3 text-3xl font-bold leading-tight text-gray-900 md:text-4xl">
12 {job.attributes.vacancy}
13 </h1>
14 <div className="flex mb-6 space-x-2 text-sm">
15 <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">CSS</a>
16 <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">Tailwind</a>
17 <a className="p-1 bg-indigo-500 rounded-full text-gray-50 badge hover:bg-gray-200" href="#">AlpineJS</a>
18 </div>
19 <a className="flex items-center text-gray-700" href="#">
20 <div className="avatar">
21 <img
22 className="flex-shrink-0 object-cover object-center w-24 h-24 rounded-full"
23 src={`http://localhost:1337${job.attributes.image.data.attributes.url}`}
24 alt={"Photo of " + job.attributes.recruiter} />
25 </div>
26 <div className="ml-4">
27 <p className="font-semibold text-gray-800 text-md">{job.attributes.recruiter}</p>
28 <p className="text-sm text-gray-500">{job.attributes.publishedAt.substring(0, Max_DATE_LENGHT)}</p>
29 <p className="text-sm text-gray-500">{job.attributes.createdAt.substring(16, Max_TIME_LENGHT)}</p>
30 </div>
31 </a>
32 </div>
33
34 <div className="w-full mx-auto prose md:w-3/4 lg:w-1/2">
35 <p>
36 {job.attributes.description}
37 </p>
38 </div>
39
40 <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">
41 <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>
42 </Link>
43 </div>
44 </article>
45 </>
46 )
47 }
48
49 export default DetailedJobs;
Your final application should look like this:
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.
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.
Technical Writer, Tech Blogger, Developer Advocate Intern, Self-Taught Frontend Developer.