ExamPro Markdown Lab Part 1
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:
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

localhost:3000/jobs/[slug] displays individual jobs

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
- Create a new Next.js app called
exampro-markdown
npx create-next-app@latest exampro-markdown
- Change to the
exampro-markdowndirectory
cd exampro-markdown
Setting Up TailwindCSS
- Install TailwindCSS, its peer-dependencies, plugins, and other Tailwind Labs tools
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npm install @headlessui/react @heroicons/react
- Initialize your Tailwind configuration files
npx tailwindcss init -p
- Include Tailwind in your CSS by replacing the original content with the following lines in your
./styles/globals.cssfile
@tailwind base;
@tailwind components;
@tailwind utilities;
- In
tailwind.config.js, addorangeto 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)
- Install Prettier, Husky, and lint-staged
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"
- Create
.prettierrc.jsonin 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
}
- Create
.prettierignorein the root directory
package.json
package-lock.json
node_modules/
.cache
.next
- In the
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"
}
Install gray-matter and marked
npm install --save gray-matter
npm install marked
Removing unnecessary file and code
- Delete the
styles/Home.module.cssfile - Remove everything inside the parent
<div>element in./pages/index.jsand theimportlines
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.
- Create
jsconfig.jsonfile
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/config/*": ["config/*"],
"@/styles/*": ["styles/*"]
}
}
}
Using provided components and stylesheets
- Copy the following components and stylesheet into your project. These are React components that have been styled using TailwindCSS.
Markdown.module.cssis 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
- Update the
./pages/index.jsfile 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>
);
}
- Run
npm run devto start the server, you should see
Markdown Implementation
Job Postings
- Create
/jobsdirectory and fill it with job postings in markdown (.mdfiles). - You can copy the
.mdfiles in the/jobsof 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'
---
JobPostings Component (Page component that shows list of all jobs)
- Create
pages/jobs/index.jsfile
- Import the
fsandpathmodules - Import
matterfromgray-matter - Import the Job Component
- Import the Layout component
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';
- Create the getStaticProps() function
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),
},
};
}
- Your
JobPostings()function will take thejobsprop from thegetStaticProps()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>
);
}
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
- Create
./components/Markdown.jsfile
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)
- Create
./pages/jobs/[slug].jsfile - Import the following
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';
- Create a getStaticPaths() function
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,
};
}
- Create a getStaticProps() function
// 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,
},
};
}
- Your JobPage() function will take
contentanddataas 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>
);
}
Top comments (0)