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;
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;
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>
);
};
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;
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",
},
};
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;
}
}
}
});
},
],
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;
}
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:
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.
Top comments (0)