Blog.

Rendering Markdown Lists in Next.js - A Step-by-Step Guide

Cover Image for Rendering Markdown Lists in Next.js - A Step-by-Step Guide
Josh Peterson
Josh Peterson

Picture this: You’ve poured your heart into drafting the perfect blog post in Obsidian, complete with crisp Markdown lists and handy task checkboxes. You hit publish on your Next.js blog, only to find your beautifully structured lists look like a jumbled mess. Sound familiar? In today’s web development hustle, where every pixel counts, nailing that polished presentation isn’t just nice—it’s a must. Lucky for you, there’s a way to make those Markdown lists pop on your site, and I’m about to spill the secrets. Whether it’s nested bullets or checked-off tasks, this guide will transform your content into a reader’s dream, effortlessly bridging Obsidian and Next.js. Ready to level up your blog game? Let’s dive in!

The Rendering Process

Here’s how I transform Markdown lists from Obsidian into HTML for my Next.js blog:

  1. Processing Markdown Content:
    I write blog posts in Obsidian using Markdown. These drafts are processed by a TypeScript file, markdownToHtml.ts, which converts the Markdown into HTML. This ensures that list structures—like bullets, numbers, and checkboxes—are accurately translated for the web.

  2. Using the remarkGfm Plugin:
    To support advanced Markdown features, I use the remarkGfm plugin with the remark library. This plugin enables GitHub Flavored Markdown (GFM) features, such as task lists with checkboxes, which I often use in Obsidian.

  3. Styling with CSS:
    After conversion, I style the HTML lists using a CSS module, markdown-styles.module.css. This file defines:

    • ul: Unordered lists with disc bullets.
    • ol: Ordered lists with decimal numbering.
    • Task lists with functional checkboxes.
    • Spacing and indentation for nested lists.
  4. Testing with a Markdown File:
    I test the process using markdown-test.md, a file with various list types—unordered, ordered, mixed, and task lists—to ensure everything renders correctly.

Here are examples of how these lists look after rendering:

Unordered Lists

- Item 1
- Item 2
- Item 3
  - Nested item 3.1
  - Nested item 3.2
- Item 4

Renders as:

  • Item 1
  • Item 2
  • Item 3
    • Nested item 3.1
    • Nested item 3.2
  • Item 4

Ordered Lists

1. First item
2. Second item
3. Third item
4. 1. Nested item 3.1
5. 2. Nested item 3.2
6. Fourth item

Renders as:

  1. First item
  2. Second item
  3. Third item
    1. Nested item 3.1
    1. Nested item 3.2
  4. Fourth item

Mixed Lists

- Unordered item
  1. Ordered sub-item 1
  2. Ordered sub-item 2
- Another unordered item
  - Unordered sub-item
  - Another unordered sub-item

Renders as:

  • Unordered item
    1. Ordered sub-item 1
    2. Ordered sub-item 2
  • Another unordered item
    • Unordered sub-item
    • Another unordered sub-item

Task Lists

- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
  - [x] Completed subtask
  - [ ] Incomplete subtask

Renders as:

  • Completed task
  • Incomplete task
  • Another task
    • Completed subtask
    • Incomplete subtask

The markdownToHtml.ts File: How It Works

The core of this process is the markdownToHtml.ts file. Here’s the code, followed by a breakdown:

import { remark } from "remark";
import html from "remark-html";
import remarkGfm from "remark-gfm";

export default async function markdownToHtml(markdown: string) {
  const result = await remark()
    .use(remarkGfm)
    .use(html, { sanitize: false })
    .process(markdown);
  
  // Convert the markdown to HTML first
  let content = result.toString();
  
  // Adjust code block classes for Prism.js highlighting
  content = content.replace(
    /<pre><code class=\"language-(\w+)\">/g,
    '<pre class="language-$1"><code class="language-$1">'
  );
  
  return content;
}

Code Breakdown

  • Markdown Processing:

    • remark() initializes the processor.
    • .use(remarkGfm) adds GFM support for features like task lists and complex list structures.
    • .use(html, { sanitize: false }) converts the processed Markdown to HTML, preserving raw HTML (with caution for security).
    • .process(markdown) applies these steps to the input Markdown string.
  • HTML Output:

    • result.toString() generates the HTML string, including list tags (<ul>, <ol>, <li>) and checkbox inputs for task lists.
  • Syntax Highlighting Setup:

    • The replace method tweaks <pre><code> tags (e.g., language-typescript) to match Prism.js conventions. This ensures code blocks are highlighted client-side when the page loads.

Example Transformation

For a task list like:

- [x] Write blog post
- [ ] Publish blog post

The function outputs:

<ul>
  <li><input type="checkbox" checked disabled> Write blog post</li>
  <li><input type="checkbox" disabled> Publish blog post</li>
</ul>

This HTML is then styled with CSS and enhanced with JavaScript (e.g., Prism.js for code blocks).

Tying It All Together

The markdownToHtml.ts output integrates with:

  • Styling: markdown-styles.module.css applies visual rules to lists and checkboxes.
  • Testing: markdown-test.md confirms all list types render as expected.

This workflow—using markdownToHtml.ts, remark-gfm, and CSS—ensures that my Obsidian Markdown lists become polished HTML on my Next.js blog. It handles everything from simple bullet points to nested task lists with ease.