36
Creating the Basic Job Listing
This is part of the ExamPro Next.js course. Preview of complete lab deployed on Vercel
In this lab, we will be creating the Job Listings portion of ExamPro using the following stack:
You may choose to start a new repository or continue with the current
exampro-nextjs
projectIf you're starting from scratch, proceed to Step 1.
exampro-markdown
npx create-next-app@latest exampro-markdown
exampro-markdown
directory
cd exampro-markdown
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npm install @headlessui/react @heroicons/react
npx tailwindcss init -p
./styles/globals.css
file
@tailwind base;
@tailwind components;
@tailwind utilities;
tailwind.config.js
, add orange
to your colors by adding the following line at the top of the file
const colors = require('tailwindcss/colors');
and extending the color palette to including
orange
module.exports = {
content: ['./components/**/*.js', './pages/**/*.js'],
theme: {
extend: {
colors: {
orange: colors.orange,
},
},
},
variants: {
extend: {},
},
plugins: [],
};
Setting Up Prettier and Husky Hooks (optional)
npm install --save-dev --save-exact prettier
npm install --save-dev husky lint-staged
npx husky install
npm set-script prepare "husky install"
npx husky add .husky/pre-commit "npx lint-staged"
.prettierrc.json
in the root directory
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"bracketSameLine": false,
"jsxSingleQuote": false,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"printWidth": 100
}
.prettierignore
in the root directory
package.json
package-lock.json
node_modules/
.cache
.next
package.json
, add the following scripts and lint-staged:
"scripts": {
...
"prettier": "prettier --write \"./**/*.{md,json,html,css,js,yml}\"",
"prettier-check": "prettier --check \"./**/*.{md,json,html,css,js,yml}\"",
...
},
...
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
npm install --save gray-matter
npm install marked
styles/Home.module.css
file<div>
element in ./pages/index.js
and the import
lines
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
Setting up
jsconfig.json
This specifies path mapping to be computed relative to baseUrl option.
jsconfig.json
file
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/config/*": ["config/*"],
"@/styles/*": ["styles/*"]
}
}
}
Markdown.module.css
is used to style the Markdown content./components/Footer.js
./components/Header.js
./components/Layout.js
./components/Main.js
./components/jobs/Job.js
./components/jobs/JobsHeader.js
./components/jobs/TypeLabel.js
./components/jobs/TypeList.js
./styles/Markdown.module.css
./pages/index.js
file to include the Layout and Main components
import Main from '@/components/Main';
import Layout from '@/components/Layout';
export default function Home() {
return (
<Layout>
<Main />
</Layout>
);
}
npm run dev
to start the server, you should see

/jobs
directory and fill it with job postings in markdown (.md
files)..md
files in the /jobs
of the repository or create your own using Lorem Markdownum. Make sure to include frontmatter above your markdown. Frontmatter looks like:
---
title: 'Cloud Support Engineer'
type: 'Part-Time'
location: 'Remote'
category: 'Operations, IT and Support Engineering'
---
pages/jobs/index.js
filefs
and path
modulesmatter
from gray-matter
import { promises as fs } from 'fs';
import path from 'path';
import matter from 'gray-matter';
import Job from '@/components/jobs/Job';
import Layout from '@/components/Layout';
export async function getStaticProps() {
// Read from /jobs directory
const files = await fs.readdir(path.join('jobs'));
// Map through jobs directory
const jobs = files.map(async (filename) => {
// Set 'slug' to name of md file
const slug = filename.replace('.md', '');
// Read all markdown from file
const markdown = await fs.readFile(path.join('jobs', filename), 'utf-8');
// Extract data from markdown
const { data } = matter(markdown);
// return slug and data in an array
return {
slug,
data,
};
});
return {
props: {
jobs: await Promise.all(jobs),
},
};
}
JobPostings()
function will take the jobs
prop from the getStaticProps()
function and map through each of the job markdown files in /jobs
// Takes the `jobs` prop from the getStaticProps() function
export default function JobPostings({ jobs }) {
return (
<Layout title="Jobs | ExamPro">
<div className="px-4 py-4 sm:px-6 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
Job Postings
</h2>
</div>
</div>
<div className="bg-white my-4 shadow overflow-hidden divide-y divide-gray-200 sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200">
{/* Maps through each job */}
{jobs.map((job, index) => (
<Job key={index} job={job} />
))}
</ul>
</div>
</Layout>
);
}
This component handles the parsing of the markdown content to html so we can style it using Markdown.module.css
./components/Markdown.js
file
import { marked } from 'marked';
import styles from '@/styles/Markdown.module.css';
// Takes content (for example from ./pages/jobs/[slug].js)
export default function Markdown({ content }) {
return (
// Uses marked to parse markdown to html
<div className={styles.markdown} dangerouslySetInnerHTML={{ __html: marked(content) }}></div>
);
}
./pages/jobs/[slug].js
fileimport { promises as fs } from 'fs';
import path from 'path';
import Link from 'next/link';
import matter from 'gray-matter';
import { BriefcaseIcon, LocationMarkerIcon, UsersIcon } from '@heroicons/react/solid';
import Markdown from '@/components/Markdown';
import Layout from '@/components/Layout';
export async function getStaticPaths() {
// Read from the /jobs directory
const files = await fs.readdir(path.join('jobs'));
// Map through the files
const paths = await Promise.all(
files.map(async (filename) => ({
params: {
// Create a slug using the name of the file without the .md extension at the end
slug: filename.replace('.md', ''),
},
}))
);
return {
paths,
fallback: false,
};
}
// This function takes the slug from getStaticPaths()
export async function getStaticProps({ params: { slug } }) {
// Read file with name of slug + .md extension in the /jobs directory
const markdown = await fs.readFile(path.join('jobs', slug + '.md'), 'utf-8');
// Use `matter` to extract the content and data from each file
// content is the body of the markdown file
// data is the frontmatter of the markdown file
const { content, data } = matter(markdown);
// Return content, data, and slug as props
return {
props: {
content,
data,
slug,
},
};
}
content
and data
as props from getStaticProps() and will display them as React Components
export default function JobPage({ content, data }) {
return (
<Layout title={`${data.title} | ExamPro`}>
<div className="px-4 py-4 sm:px-6 md:flex md:items-center md:justify-between lg:flex lg:items-center lg:justify-between">
<div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
{data.title}
</h2>
<div className="mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6">
<div className="mt-2 flex items-center text-sm text-gray-500">
<UsersIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
{data.category}
</div>
<div className="mt-2 flex items-center text-sm text-gray-500">
<LocationMarkerIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
{data.location}
</div>
<div className="mt-2 flex items-center text-sm text-gray-500">
<BriefcaseIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
{data.type}
</div>
</div>
</div>
<div className="mt-5 flex lg:mt-0 lg:ml-4">
<span className="sm:ml-3">
<Link href="/jobs" passHref>
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-400"
>
Back to Jobs
</button>
</Link>
</span>
</div>
</div>
<div>
<Markdown content={content} />
</div>
</Layout>
);
}
36