Creating the Basic Job Listing
Tags: nextjs, tailwindcss, markdown, react
Published: 2021-11-20 01:19:06

# ExamPro Markdown Lab Part 1

This is part of the [ExamPro](https://www.exampro.co/) Next.js course. [Preview of complete lab deployed on Vercel](https://exampro-markdown.vercel.app/)

In this lab, we will be creating the Job Listings portion of ExamPro using the following stack:

## Technology Stack

- Node.js (12.22.0 or later)
- Next.js (12.0.4)
- React (17.0.2)
- TailwindCSS (3.0.0)
- gray-matter (4.0.3)
- marked (4.0.3)

## Application Screenshots

`localhost:3000/jobs/` display a list of all jobs
![](https://exampro-support.s3.amazonaws.com/Nextjs/list_of_all_jobs.png)

`localhost:3000/jobs/[slug]` displays individual jobs
![](https://exampro-support.s3.amazonaws.com/Nextjs/individual_job_listing.png)

## Getting Started

You may choose to start a new repository or continue with the current `exampro-nextjs` project

If you're starting from scratch, proceed to Step 1.

### Setting Up Next.js

1. Create a new Next.js app called `exampro-markdown`

```bash
npx create-next-app@latest exampro-markdown
```

2. Change to the `exampro-markdown` directory

```bash
cd exampro-markdown
```

### Setting Up TailwindCSS

2. Install TailwindCSS, its peer-dependencies, plugins, and other Tailwind Labs tools

```bash
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```

```bash
npm install @headlessui/react @heroicons/react
```

3. Initialize your Tailwind configuration files

```bash
npx tailwindcss init -p
```

4. Include Tailwind in your CSS by replacing the original content with the following lines in your `./styles/globals.css` file

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

5. In `tailwind.config.js`, add `orange` to your colors by adding the following line at the top of the file

```javascript
const colors = require('tailwindcss/colors');
```

and extending the color palette to including `orange`

```javascript
module.exports = {
  content: ['./components/**/*.js', './pages/**/*.js'],
  theme: {
    extend: {
      colors: {
        orange: colors.orange,
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
};
```

### Setting Up [Prettier and Husky](https://prettier.io/docs/en/install.html) Hooks (optional)

1. Install Prettier, Husky, and lint-staged

```bash
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"
```

2. Create `.prettierrc.json` in the root directory

```json
{
  "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
}
```

3. Create `.prettierignore` in the root directory

```
package.json
package-lock.json
node_modules/
.cache
.next
```

4. In the `package.json`, add the following scripts and lint-staged:

```json
  "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"
  }
```

### Install gray-matter and marked

```bash
npm install --save gray-matter
npm install marked
```

### Removing unnecessary file and code

1. Delete the `styles/Home.module.css` file
2. Remove everything inside the parent `<div>` element in `./pages/index.js` and the `import` lines

```javascript
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
```

### Setting up [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig)

This specifies path mapping to be computed relative to baseUrl option.

1. Create `jsconfig.json` file

```json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["components/*"],
      "@/config/*": ["config/*"],
      "@/styles/*": ["styles/*"]
    }
  }
}
```

### Using provided components and stylesheets

1. Copy the following components and stylesheet into your project. These are React components that have been styled using TailwindCSS. `Markdown.module.css` is used to style the Markdown content

- Footer from `./components/Footer.js`
- Header from `./components/Header.js`
- Layout from `./components/Layout.js`
- Main from `./components/Main.js`
- Job from `./components/jobs/Job.js`
- JobsHeader from `./components/jobs/JobsHeader.js`
- TypeLabel from `./components/jobs/TypeLabel.js`
- TypeList from `./components/jobs/TypeList.js`
- `./styles/Markdown.module.css`

2. Update the `./pages/index.js` file to include the Layout and Main components

```javascript
import Main from '@/components/Main';
import Layout from '@/components/Layout';

export default function Home() {
  return (
    <Layout>
      <Main />
    </Layout>
  );
}
```

3. Run `npm run dev` to start the server, you should see
   ![](https://exampro-support.s3.amazonaws.com/Nextjs/starting_app.png)

## Markdown Implementation

### Job Postings

1. Create `/jobs` directory and fill it with job postings in markdown (`.md` files).
2. You can copy the `.md` files in the `/jobs` of the repository or create your own using [Lorem Markdownum](https://jaspervdj.be/lorem-markdownum/). Make sure to include frontmatter above your markdown. Frontmatter looks like:

```markdown
---
title: 'Cloud Support Engineer'
type: 'Part-Time'
location: 'Remote'
category: 'Operations, IT and Support Engineering'
---
```

#### JobPostings Component (Page component that shows list of all jobs)

2. Create `pages/jobs/index.js` file

- Import the `fs` and `path` modules
- Import `matter` from `gray-matter`
- Import the Job Component
- Import the Layout component

```javascript
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';
```

3. Create the getStaticProps() function

```javascript
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),
    },
  };
}
```

4. Your `JobPostings()` function will take the `jobs` prop from the `getStaticProps()` function and map through each of the job markdown files in `/jobs`

```javascript
// 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>
  );
}
```

#### Markdown Component (For parsing markdown)

This component handles the parsing of the markdown content to html so we can style it using Markdown.module.css

1. Create `./components/Markdown.js` file

```javascript
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>
  );
}
```

#### JobPage Component (Individual job posting)

1. Create `./pages/jobs/[slug].js` file
2. Import the following

```javascript
import { 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';
```

3. Create a getStaticPaths() function

```javascript
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,
  };
}
```

4. Create a getStaticProps() function

```javascript
// 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,
    },
  };
}
```

5. Your JobPage() function will take `content` and `data` as props from getStaticProps() and will display them as React Components

```javascript
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>
  );
}
```