How To Make A Next JS Blog With Markdown And TypeScript

cleggacus

Liam Clegg

Posted on August 21, 2022

How To Make A Next JS Blog With Markdown And TypeScript

This tutorial will show you how to make a Next js blog with markdown and typescript. Next js is a React framework that will allow you to SSR (server side render), boosting its SEO performance. SEO optimization will allow you to increase your social presence on Google search. Whether you’re a student, a freelancer or professional this is an essential skill to have when becoming a professional web developer.

Setup

The easiest way to start the project is with the create next app typescript boilerplate.

# with yarn
yarn create next-app blog --typescript

# with npm
npx create-next-app blog --ts
Enter fullscreen mode Exit fullscreen mode

After this, you will need to install all the relevant dependencies.

gray-matter is used to read the metadata such as the thumbnail, description, and title. react-markdown is used to render out the markdown into HTML. react-syntax-highlighter is used to add syntax highlighting to the code blocks within the rendered markdown.

# with yarn
yarn add gray-matter react-markdown react-syntax-highlighter
yarn add @types/react-syntax-highlighter --dev

# with npm
npm install gray-matter react-markdown react-syntax-highlighter
npm install  @types/react-syntax-highlighter --save-dev
Enter fullscreen mode Exit fullscreen mode

remove pages/api directory since it is not needed

Create Articles

Create a directory named uploads with some template markdown files. Metadata is surrounded by 3 dashes and has a title, description, and thumbnail. An example of an article is below. The name of the file will be the URL slug.

---
title: "Eget Duis Sem Tincidunt Ac Ullamcorper Et Turpis Magna Viverra"
description: "risus eu lectus a consectetur aliquam nullam enim tellus urna nunc sagittis aenean aliquam ullamcorper consectetur dictumst sit, placerat eget lobortis eget elit nibh blandit scelerisque consectetur condimentum diam tempor. nisl erat semper gravida tempor aliquam suscipit a viverra molestie sit porta cras ultricies, fermentum habitasse sit semper cum eu eget lacus purus viverra cursus porttitor nisi nisl."
thumbnail: https://blogthing-strapi.cleggacus.com/uploads/0_d65573c0b9.jpg
---
# In Eu Sapien Tellus Id
## Ullamcorper Elit Semper Ultricies Morbi
sit at blandit cras id eu congue et platea massa lectus netus vulputate suspendisse sed, risus habitasse at purus nibh viverra elementum viverra arcu id vulputate vel. ipsum tincidunt lorem habitant dis nulla consectetur tincidunt iaculis adipiscing erat enim, ultrices etiam mollis volutpat est vestibulum aliquam lorem elit natoque metus dui est elit. mollis sit tincidunt mauris porttitor pellentesque at nisl pulvinar tortor egestas habitant hac, metus blandit scelerisque in aliquet tellus enim viverra sed eu neque placerat lobortis a. laoreet tempus posuere magna amet nec eget vitae pretium enim magnis, cras sem eget amet id risus pellentesque auctor quis nunc tincidunt tortor massa nisl velit tortor. a volutpat malesuada nisi habitasse id volutpat nibh volutpat suspendisse nunc justo elementum ac nec, elementum pulvinar enim sociis nunc eleifend malesuada platea nunc posuere aliquet ipsum.
\`\`\`ts
function someFunc(thing: string){
    const thing2 = thing[0];
    return thing2;
}
\`\`\`
Enter fullscreen mode Exit fullscreen mode

Interfaces

Before adding code it is best to create an interfaces directory and add some interfaces so we know the structure of the data that is fetched. These interfaces will make use that the metadata and info of an article post follow a set structure.

interface ArticleMeta {
    title: string;
    slug: string;
    description: string;
    thumbnail: string;
}

interface ArticleInfo {
    meta: ArticleMeta;
    content: string;
}

export type {
    ArticleMeta,
    ArticleInfo
}
Enter fullscreen mode Exit fullscreen mode

Components

We can now create a components directory that will store all components used in the project. This will include a card component and a markdown component which will hold our code for rendering our markdown with syntax highlighting.

Card Component

The card component will take in the property article which will be of type ArticleMeta. This is declared in the interface IProps.

components/card.tsx

import Link from "next/link";
import { FunctionComponent } from "react";
import { ArticleMeta } from "../interfaces/article";
import styles from "../styles/card.module.css";

interface IProps {
    article: ArticleMeta;
}

const Card: FunctionComponent<IProps> = ({ article }) => {
    return <Link href={`/article/${article.slug}`}>
        <div className={styles.card}>
            <img src={article.thumbnail} />

            <div className={styles.info}>
                <h1>{article.title}</h1>
                <p>{article.description}</p>
            </div>
        </div>
    </Link>
}

export default Card;
Enter fullscreen mode Exit fullscreen mode

The card is styled so that it can be in a grid made with CSS flex.

styles/card.module.css

.card{
    cursor: pointer;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    width: 300px;
    height: 400px;
    margin: 20px;
    background-color: #fff;
    box-shadow: 0 4px 8px 0 #0001, 0 6px 20px 0 #0001; 
    border-radius: 10px;
    transition: all 0.3s;
}

.card:hover{
    width: 320px;
    height: 420px;
    margin: 10px;
}

.card:hover .info {
    padding: 20px 30px;
}

.card img{
    width: 100%;
    flex: 1;
}

.card .info{
    width: 100%;
    height: 200px;
    padding: 20px;
    transition: all 0.3s;
}

.card .info h1,
.card .info p {
    color: #555;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.card .info h1{
    margin: 0;
    font-size: 1.3em;
    -webkit-line-clamp: 2;
}

.card .info p{
    margin: 10px 0 0 0;
    -webkit-line-clamp: 4;
}
Enter fullscreen mode Exit fullscreen mode

Markdown Component

The markdown component will take the prop content. Content is a string that holds the markdown code to be rendered.

import ReactMarkdown from 'react-markdown';
import { NormalComponents, SpecialComponents } from 'react-markdown/src/ast-to-react';
import { materialLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter';
import { FunctionComponent } from 'react';

interface IProps {
    content: string;
}

const Markdown: FunctionComponent<IProps> = ({content}) => {
    const components: Partial<NormalComponents & SpecialComponents> = {
        code({node, inline, className, children, ...props}) {
            const match = /language-(\w+)/.exec(className || '');

            return (!inline && match) ? (
                <SyntaxHighlighter style={materialLight} PreTag="div" language={match[1]} children={String(children).replace(/\n$/, '')} {...props} />
            ) : (
                <code className={className ? className : ""} {...props}>
                    {children}
                </code>
            )
        }
    }

    return <div className="markdown-body">
        <ReactMarkdown components={components} children={content} />
    </div>
}

export default Markdown;
Enter fullscreen mode Exit fullscreen mode

To style the markdown it is surrounded by a div tag with the class name markdown-body. Copy the CSS file from https://github.com/cleggacus/next-blog-medium-tutorial/blob/master/styles/markdown.css and save it as styles/markdown.css

Add the line below to your _app.tsx file to import the CSS file.

import '../styles/markdown.css'
Enter fullscreen mode Exit fullscreen mode

Pages

There are 2 pages that are needed: an index page and an article page. The index page will show all the articles in a grid and the article page will show all the contents of the article.

Index Page

There are 2 pages that are needed: an index page and an article page.

The index page will show all the articles in a grid and the article page will show all the contents of the article.

import styles from '../styles/Home.module.css'
import Card from '../component/card'
import fs from 'fs'
import matter from 'gray-matter'
import { ArticleMeta } from '../interfaces/article'
import { FunctionComponent } from 'react'

interface IProps {
    articles: ArticleMeta[];
}

const Home: FunctionComponent<IProps> = ({ articles }) => {
    return (
        <div className={styles.container}>
        {
            articles.map((article, i) => (
                <Card key={i} article={article} />
            ))
        }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Then we can fetch the articles with getStaticProps. Get static props is an async function that will statically generate the page with the fetched data returned from the function.

fs.readdirSync(“uploads”) is used to get an array of all the files in the uploads directory.

const files = fs.readdirSync("uploads");
Enter fullscreen mode Exit fullscreen mode

The files are then read and mapped to an array of ArticleMeta. The files are read using readFileSync and casting it to a string.

const data = fs.readFileSync(`uploads/${file}`).toString();
Enter fullscreen mode Exit fullscreen mode

matter(string).data will return the metadata of the markdown. The slug is then generated by splitting at the ‘.’ char and getting the string at index 0. This will remove the ‘.md’ extension of the filename

return {
    ...matter(data).data,
    slug: file.split('.')[0]
}
Enter fullscreen mode Exit fullscreen mode

The full code of getStaticProps is below.

export async function getStaticProps() {
    const files = fs.readdirSync("uploads");

    let articles = files.map(file => {
        const data = fs
            .readFileSync(`uploads/${file}`)
            .toString();

        return {
            ...matter(data).data,
            slug: file.split('.')[0]
        };
    });

    return {
        props: {
            articles: articles
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

The final index.tsx file is shown in the code below

import styles from '../styles/Home.module.css'
import Card from '../component/card'
import fs from 'fs'
import matter from 'gray-matter'
import { ArticleMeta } from '../interfaces/article'
import { FunctionComponent } from 'react'

interface IProps {
    articles: ArticleMeta[];
}

const Home: FunctionComponent<IProps> = ({ articles }) => {
    return (
        <div className={styles.container}>
        {
            articles.map((article, i) => (
                <Card key={i} article={article} />
            ))
        }
        </div>
    )
}

export async function getStaticProps() {
    const files = fs.readdirSync("uploads");

    let articles = files.map(file => {
        const data = fs
            .readFileSync(`uploads/${file}`)
            .toString();

        return {
            ...matter(data).data,
            slug: file.split('.')[0]
        };
    });

    return {
        props: {
            articles: articles
        }
    };
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

styles/Home.module.css

.container{
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    min-height: 100vh;
    width: 100%;
    padding: 20px;
}
Enter fullscreen mode Exit fullscreen mode

Image of index page

Article Page

The article file is at the location ‘pages/article/[slug].tsx’

The article component takes an article prop of type ArticleInfo to create the article page.

import { FunctionComponent } from "react";
import fs from 'fs';
import matter from "gray-matter";
import styles from '../../styles/article.module.css';
import { ArticleInfo } from "../../interfaces/article";
import Markdown from "../../component/markdown";

interface IProps {
    article: ArticleInfo;
}

const Article: FunctionComponent<IProps> = ({ article }) => {
    return <div className={styles.article}>
        <div className={styles.thumbnail}>
            <img src={article.meta.thumbnail} />

            <div className={styles.title}>
                <h1>{article.meta.title}</h1>
            </div>
        </div>

        <div className={styles.content}>
            <Markdown content={article.content} />
        </div>
    </div>
}
Enter fullscreen mode Exit fullscreen mode

The square brackets in the file name are used for a dynamic route. To statically generate the article pages the getStaticPaths function is used. getStaticProps will return an array of all the routes that have a page.

Each file in the uploads directory is mapped to an array of routes. The routes are the slugs of the articles. The slug is generated the same way it was on the home page.

export async function getStaticPaths() {
    const files = fs.readdirSync("uploads");
    const paths = files.map(file => ({
        params: {
            slug: file.split('.')[0]
        }
    }))

    return {
        paths,
        fallback: false,
    }
}

export default Article;
Enter fullscreen mode Exit fullscreen mode

After the paths are generated each page is rendered. The slug is taken in through the ctx parameter.

const {slug} = ctx.params;
Enter fullscreen mode Exit fullscreen mode

The filename is found with the slug by adding the ‘.md’ extension back on the end of the slug. The info in the file is then parsed by using gray matter.

matter(string).data will return the metadata of the Markdown.

matter(string).content will return the body of the Markdown.

The data and content are added to an object called article which is of type ArticleInfo.

export async function getStaticProps({ ...ctx }) {
    const { slug } = ctx.params;

    const content = fs
        .readFileSync(`uploads/${slug}.md`)
        .toString();

    const info = matter(content);

    const article = {
        meta: {
            ...info.data,
            slug
        },
        content: info.content
    }

    return {
        props: {
            article: article
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The complete code for pages/article/[slug].tsx is below.

import { FunctionComponent } from "react";
import fs from 'fs';
import matter from "gray-matter";
import styles from '../../styles/article.module.css';
import { ArticleInfo } from "../../interfaces/article";
import Markdown from "../../component/markdown";

interface IProps {
    article: ArticleInfo;
}

const Article: FunctionComponent<IProps> = ({ article }) => {
    return <div className={styles.article}>
        <div className={styles.thumbnail}>
            <img src={article.meta.thumbnail} />

            <div className={styles.title}>
                <h1>{article.meta.title}</h1>
            </div>
        </div>

        <div className={styles.content}>
            <Markdown content={article.content} />
        </div>
    </div>
}

export async function getStaticProps({ ...ctx }) {
    const { slug } = ctx.params;

    const content = fs
        .readFileSync(`uploads/${slug}.md`)
        .toString();

    const info = matter(content);

    const article = {
        meta: {
            ...info.data,
            slug
        },
        content: info.content
    }

    return {
        props: {
            article: article
        }
    }
}

export async function getStaticPaths() {
    const files = fs.readdirSync("uploads");
    const paths = files.map(file => ({
        params: {
            slug: file.split('.')[0]
        }
    }))

    return {
        paths,
        fallback: false,
    }
}

export default Article;
Enter fullscreen mode Exit fullscreen mode

The CSS for the article page is at styles/aricle.css

.article{
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    min-height: 100vh;
    padding-bottom: 100px;
}

.thumbnail{
    position: relative;
    width: 100%;
    height: 700px;
}

.thumbnail .title{
    position: absolute;
    padding-bottom: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.thumbnail .title h1{
    text-align: center;
    width: 70%;
    color: #fff;
    font-size: 3em;
}

.thumbnail img{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: brightness(0.5);
}

.content{
    z-index: 1;
    margin-top: -100px;
    padding: 50px;
    border-radius: 10px;
    width: 70%;
    background-color: #fff;
    box-shadow: 0 4px 8px 0 #0001, 0 6px 20px 0 #0001; 
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, next js can easily be used as a way to server-side render react code. We have used both getStaticProps and getStaticPaths to general static pages with the static and dynamic routes.

Article page

Get the full source code for this project at https://github.com/cleggacus/next-blog-medium-tutorial

💖 💪 🙅 🚩
cleggacus
Liam Clegg

Posted on August 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related