Create a Fully Functional Next.js Digital Garden - Part 3 - Syntax Highlighting and Basic Styling
Bannon Tanner - Tue May 09 2023
Introduction
After the previous post, several issues were identified with the blog app, such as rendering errors in the console, displaying all blog posts on every individual blog page, lacking custom styling, and an overall unpolished appearance due to the absence of applied default styling.
Transitioning to Remark
To resolve the rendering errors in the console, a switch from the react-markdown
package to the remark
library was made. This change required some refactoring of the application and the introduction of new dependencies.
npm install remark remark-html
Rather than transforming the markdown string within the posts dynamic file, the application will now convert it directly within the posts utility function, returning a string of properly formatted HTML.
The three remark packages ()remark
, remark-html
, and remark-gfm
) should be imported into the lib/posts.ts
file. Subsequently, a new function, convertContentToHtml
, should be added to the bottom of the file:
// lib/posts.ts
import { remark } from "remark";
import remarkHtml from "remark-html";
import remarkGfm from "remark-gfm";
// ...
function convertContentToHtml(content: string): string {
return remark()
.use(remarkGfm)
.use(remarkHtml, { sanitize: false })
.processSync(content)
.toString();
}
This new function can be employed to convert the markdown content prior to returning it. The returned postData
should be updated to include the converted content.
// lib/posts.ts
// ...
// parse the post front-matter
const { data, content } = matter(fileContents);
const convertedContent: string = convertContentToHtml(content);
const postData: PostData = {
id,
date: data.date,
...data,
content: convertedContent,
};
return postData;
// ...
As the utility function now returns a formatted HTML string instead of a markdown string, an adjustment to the dynamic posts page is required.
The imports for react-markdown
and remark-gfm
should be removed from the dynamic page. Additionally, the <ReactMarkdown ...
line of the JSX should be replaced with the following:
// app/blog/[id]/page.tsx
<div dangerouslySetInnerHTML={{ __html: content }}></div>
With these changes, the application will no longer display console rendering errors, allowing for the continued development of additional features.
Adding Styling
To enhance the aesthetic appeal of the blog application, a simple dark theme available on GitHub will be incorporated. This will involve replacing the default global styles and refining the styling throughout the project. The complete contents of the CSS file will not be replicated here, but the following additions should be made:
.main {
max-width: 768px;
margin: 0 auto;
padding 20px 0;
}
pre {
overflow-x: scroll;
padding: 10px;
}
These modifications will apply basic styling at a global level. However, the dynamic blog page requires the application of additional styles from the CSS.
Alter the return statement of the Post
component to incorporate a few classNames from the CSS and remove the placeholder classNames added previously:
// app/blog/[id]/page.tsx
return (
<div key={id} className="main">
<h1>{title}</h1>
<h3>
{author} - {date}
</h3>
<div
className="simple-container"
dangerouslySetInnerHTML={{ __html: content }}
></div>
</div>
);
As the react-markdown
package is no longer used, it can be safely removed.
npm remove react-markdown
Implementing Syntax Highlighting
To improve the reading experience and make the code examples more visually appealing, syntax highlighting will be added using remark-shiki
and shiki
. Install and import the dependencies, then add the plugin to the remark function chain in the convertContentToHtml
function within the posts utility file.
npm install remark-shiki shiki
// lib/posts.ts
// ...other imports
import * as shiki from "shiki";
import withShiki from "@stefanprobst/remark-shiki";
// ...
function convertContentToHtml(content: string): string {
const highlighter = await shiki.getHighlighter({ theme: "dracula-soft" });
return remark()
.use(remarkGfm)
.use(remarkHtml, { sanitize: false })
.use(withShiki, { highlighter })
.processSync(content)
.toString();
}
The text editor should immediately start complaining because the getHighlighter
function is asynchronous and the convert function is not. This forces the conversion of functions in this file to asynchronous ones, returning Promises. This will involve refactoring the getSortedPostsData
function and convertContentToHtml
function.
// lib/posts.ts
// ...imports and interface
export async function getSortedPostsData(): Promise<PostData[]> {
// read files from /app/posts
const fileNames = fs.readdirSync(postsDirectory);
const allPostsDataPromises: Promise<PostData>[] = fileNames.map(
async (fileName: string) => {
const id: string = fileName.replace(/\.md$/, "");
// read markdown file as string
const fullPath: string = path.join(postsDirectory, fileName);
const fileContents: string = fs.readFileSync(fullPath, "utf8");
// parse the post front-matter
const { data, content } = matter(fileContents);
const convertedContent: string = await convertContentToHtml(content);
const postData: PostData = {
id,
date: data.date,
...data,
content: convertedContent,
};
return postData;
}
);
// wait for all promises to resolve
const allPostsData = await Promise.all(allPostsDataPromises);
// sort posts by date
return allPostsData.sort((a: PostData, b: PostData) =>
a.date < b.date ? 1 : -1
);
}
async function convertContentToHtml(content: string): Promise<string> {
const highlighter = await shiki.getHighlighter({ theme: "dracula-soft" });
return remark()
.use(remarkGfm)
.use(remarkHtml, { sanitize: false })
.use(withShiki, { highlighter })
.processSync(content)
.toString();
}
This modification also necessitates converting all calls to the getSortedPostsData
function to asynchronous ones. Transform any components that are not already asynchronous and use await
when calling the function. In the dynamic Post
component, the generateStaticParams
function, and the Blog
component, alter the call as follows:
- const posts: PostData[] = getSortedPostsData();
+ const posts: PostData[] = await getSortedPostsData();
Now any code rendered in a blog post will be highlighted with an eye-catching "dracula-soft" syntax highlighting.
Fixing Dynamic Page
Previously, the dynamic page was mapping through all posts and displaying them in reverse-date order simultaneously. The code has been updated to find the desired post based on the id
returned from generateStaticParams
.
Additionally, it is helpful to destructure the desired properties in the dynamic posts page. As such, the PostData
type has been refined, with more properties added for improved typing in both the dynamic page and the posts utility function.
Following these changes, the completed components and utility function should appear as follows:
// lib/posts.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { remark } from "remark";
import remarkHtml from "remark-html";
import remarkGfm from "remark-gfm";
import * as shiki from "shiki";
import withShiki from "@stefanprobst/remark-shiki";
const postsDirectory: string = path.join(process.cwd(), "public/posts");
export interface PostData {
id: string;
title: string;
author: string;
date: string;
[key: string]: any;
content: string;
}
export async function getSortedPostsData(): Promise<PostData[]> {
// read files from /app/posts
const fileNames = fs.readdirSync(postsDirectory);
const allPostsDataPromises: Promise<PostData>[] = fileNames.map(
async (fileName: string) => {
const id: string = fileName.replace(/\.md$/, "");
// read markdown file as string
const fullPath: string = path.join(postsDirectory, fileName);
const fileContents: string = fs.readFileSync(fullPath, "utf8");
// parse the post front-matter
const { data, content } = matter(fileContents);
const convertedContent: string = await convertContentToHtml(content);
const postData: PostData = {
id,
title: data.title,
author: data.author,
date: data.date,
...data,
content: convertedContent,
};
return postData;
}
);
// wait for all promises to resolve
const allPostsData = await Promise.all(allPostsDataPromises);
// sort posts by date
return allPostsData.sort((a: PostData, b: PostData) =>
a.date < b.date ? 1 : -1
);
}
async function convertContentToHtml(content: string): Promise<string> {
const highlighter = await shiki.getHighlighter({ theme: "dracula-soft" });
return remark()
.use(remarkGfm)
.use(remarkHtml, { sanitize: false })
.use(withShiki, { highlighter })
.processSync(content)
.toString();
}
// app/blog/[id]/page.tsx
import { getSortedPostsData, PostData } from "@/lib/posts";
export default async function Post({ params }: { params: { id: string } }) {
const { id } = params;
const posts: PostData[] = await getSortedPostsData();
const { title, author, date, content } = posts.find(
(post) => post.id === id
) as PostData;
if (!posts) return <div>Loading...</div>;
return (
<div key={id} className="main">
<h1>{title}</h1>
<h3>
{author} - {date}
</h3>
<div
className="simple-container"
dangerouslySetInnerHTML={{ __html: content }}
></div>
</div>
);
}
// generate route segments
export async function generateStaticParams() {
const posts: PostData[] = await getSortedPostsData();
return posts.map((post) => ({
id: post.id,
}));
}
// app/blog/page.tsx
import Link from "next/link";
import { getSortedPostsData, PostData } from "@/lib/posts";
export default async function Blog() {
const posts: PostData[] = await getSortedPostsData();
return (
<main>
<h1>All Blog Posts</h1>
<ul>
{posts.map(({ id, title, author, date }) => (
<li key={id}>
<Link href={`/blog/${id}`}>
<h2>{title}</h2>
<p>
{author} - {date}
</p>
</Link>
</li>
))}
</ul>
</main>
);
}
Conclusion
In summary, a series of enhancements were made to the blog application. First, the react-markdown package was replaced with the remark library, resolving console rendering errors and streamlining the markdown conversion process. Next, a simple dark theme was applied to improve the overall aesthetics, and the project underwent a CSS refactoring process. Subsequently, syntax highlighting was introduced via remark-shiki and shiki to make code examples more visually appealing. Lastly, the dynamic page was adjusted to display the desired post based on the returned ID from generateStaticParams, and the PostData type was refined for better typing.
These improvements have not only elevated the visual appearance and functionality of the blog application but have also laid a solid foundation for future development and feature additions.
Check out the current state of the project and the repository.