DEV Community

Donna Brown
Donna Brown

Posted on

How to Build a Developer Blog using Next JS 13 and Contentlayer - Part two

Next JS 13 structure

The blog directory structure will follow a similar approach to the link listed below but I named the directory posts and placed it under the app directory.
https://nextjs.org/docs/getting-started/project-structure

Create the file app/posts/page.tsx. Add this code to the file.

app/posts/page.tsx

import Link from "next/link";
import { allPosts, Post } from "contentlayer/generated";
import { compareDesc } from "date-fns";

function PostCard(post: Post) {
  return (
    <div>
      <h2>
        <Link href={post.url}>{post.title}</Link>
      </h2>
      <p>{post.excerpt}</p>
    </div>
  );
}

function page() {
  const posts = allPosts.sort((a, b) =>
    compareDesc(new Date(a.date), new Date(b.date))
  );

  return (
    <div>
      <div>
        {posts.map((post, idx) => (
          <PostCard key={idx} {...post} />
        ))}
      </div>
    </div>
  );
}

export default page;

Enter fullscreen mode Exit fullscreen mode

This code sorts the generated posts by date. It then displays the link to the post, the title, and the excerpt.

Create the directory app/posts/[slug]. Then create the file app/posts/[slug]/page.tsx. The square brackets mean this is a dynamic segment. When you don’t know the exact name of the file ahead of time and want to create routes from dynamic data. Note the function generateMetaData. Dynamic information, such as the current route parameters, can be set by exporting a generateMeta function that returns a Metadata object.

app/posts/[slug]/page.tsx

import { DetailedHTMLProps, HTMLAttributes } from "react";
import { allPosts } from "contentlayer/generated";
import { useMDXComponent } from "next-contentlayer/hooks";
import type { MDXComponents } from "mdx/types";
import { format, parseISO } from "date-fns";
import { notFound } from "next/navigation";
import { CopyButton } from "@/components/CopyButton";
import "../../globals.css";

export const generateStaticParams = async () =>
  allPosts.map((post: any) => ({ slug: post._raw.flattenedPath }));

export const generateMetadata = ({ params }: any) => {
  const post = allPosts.find(
    (post: any) => post._raw.flattenedPath === params.slug
  );
  return { title: post?.title, excerpt: post?.excerpt };
};

const PostLayout = ({ params }: { params: { slug: string } }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);

  // 404 if the post does not exist.
  if (!post) notFound();

  const MDXContent = useMDXComponent(post!.body.code);

  const mdxComponents: MDXComponents = {
    // Override the default <pre> element
    pre: function ({
      children,
      ...props
    }: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>) {
      const propsObj = { ...props };
      const propsValues = Object.values(propsObj);
      const [, , dataLanguage, dataTheme, code] = propsValues;
      const lang = dataLanguage || "shell";

      return (
        <pre data-language={lang} data-theme={dataTheme} className={"py-4"}>
          <div className='bg-gray-50 rounded-md overflow-x-auto'>
            <div
              className={
                "bg-gray-200 dark:text-black flex items-center relative px-4 py-2 text-sm font-sans justify-between rounded-t-md"
              }
            >
              {lang}
              <CopyButton text={code} />
            </div>

            <div className={"p-2"}>{children}</div>
          </div>
        </pre>
      );
    },
  };

  return (
    <div className='mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0'>
      <h1 className='text-xl'>{post.title}</h1>
      <p>
        <span className='text-gray-500 dark:text-gray-400'>
          {format(parseISO(post.date), "LLLL d, yyyy")}
        </span>
      </p>
      <article>
        <MDXContent components={mdxComponents} />
      </article>
    </div>
  );
};

export default PostLayout;


Enter fullscreen mode Exit fullscreen mode

Create the file app/components/CopyButton.tsx. CopyButton implements copy to clipboard functionality.

app/components/CopyButton.tsx

"use client"; // The "use client" directive is a convention to declare a boundary
// between a Server and Client Component module graph.

import { useState } from "react";
import ClipBoard from "./ClipBoard";

type Text = {
  text: string;
};
/**
 * CopyButton implements copy to clipboard functionality
 * @param text
 * @returns
 */
export const CopyButton = ({ text }: Text) => {
  const [isCopied, setIsCopied] = useState(false);

  const copy = async () => {
    await navigator.clipboard.writeText(text);
    setIsCopied(true);

    setTimeout(() => {
      setIsCopied(false);
    }, 10000);
  };

  return (
    <button
      className='dark:text-black flex ml-auto gap-2'
      disabled={isCopied}
      onClick={copy}
    >
      {/* clipboard icon */}
      <ClipBoard />
      {isCopied ? "Copied!" : "Copy code"}
    </button>
  );
};

Enter fullscreen mode Exit fullscreen mode

Create the file app/components/Clipboard.tsx for the clipboard icon.

import React from "react";

const ClipBoard = () => {
  return (
    <svg
      stroke='#000'
      fill='none'
      strokeWidth='2'
      viewBox='0 0 24 24'
      strokeLinecap='round'
      strokeLinejoin='round'
      className='h-4 w-4'
      height='1em'
      width='1em'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'></path>
      <rect x='8' y='2' width='8' height='4' rx='1' ry='1'></rect>
    </svg>
  );
};

export default ClipBoard;
Enter fullscreen mode Exit fullscreen mode

Modify contentlayer.config.ts:

Add these lines after the first line.

import rehypePrettyCode from "rehype-pretty-code";
import { visit } from "unist-util-visit";


/** @type {import('rehype-pretty-code').Options} */
const options: import("rehype-pretty-code").Options = {
  theme: {
   light: "light-plus",
  },
};
Enter fullscreen mode Exit fullscreen mode

Inside mdx{}, add:

rehypePlugins: [
      () => (tree) => {
        visit(tree, (node) => {
          if (node?.type === "element" && node?.tagName === "pre") {
            const [codeEl] = node.children;
            if (codeEl.tagName !== "code") return;
            node.raw = codeEl.children?.[0].value;
          }
        });
      },
      [rehypePrettyCode, options],
      () => (tree) => {
        visit(tree, (node) => {
          // Select all div elements that contain a data-rehype-pretty-code-fragment data attribute.
          if (node?.type === "element" && node?.tagName === "div") {
            if (!("data-rehype-pretty-code-fragment" in node.properties)) {
              return;
            }
            // Iterate over the pre children within each div (one for each theme) and
            // add the raw code content as a property to them.
            for (const child of node.children) {
              if (child.tagName === "pre") {
                child.properties["raw"] = node.raw;
              }
            }
          }
        });
      },
    ],

Enter fullscreen mode Exit fullscreen mode

This function traverses the node tree of the content and extracts the unmodified ( raw text ) from all code elements nested inside the pre tag. It then stores the text content on the pre node. This will give us a way to keep the unmodified code content from the node’s raw property.

Delete all the lines after the first three lines in globals.css. Add these lines after the tailwind setup:


pre > code {
  display: grid;
}

code {
  counter-reset: line;
}

/* Apply line numbers only when showLineNumbers is specified: */
code[data-line-numbers] > [data-line]::before {
  counter-increment: line;
  content: counter(line);

  /* Other styling */
  display: inline-block;
  width: 1rem;
  margin-right: 2rem;
  text-align: right;
  color: gray;
}

Enter fullscreen mode Exit fullscreen mode

When you select a post containing a code block you should see syntax highlighting. Before the code block you should see the name of the programming language and a copy button. If you click the copy button, the text will be pasted in an editor. You should see something like this:

Image description

In part three, I will show how to add a navbar, how to switch between dark and light mode, how to display stats on your github projects, and add information for seo.

Part Three
https://dev.to/dbrownsoftware/how-to-build-a-developer-blog-using-next-js-13-and-contentlayer-part-three-1155

Top comments (0)