DEV Community

Cover image for Building a website using Markdown content with Next.js App Router and Fusionable
Roberto B.
Roberto B.

Posted on

Building a website using Markdown content with Next.js App Router and Fusionable

Learn how to build a static website using Next.js App Router and Fusionable. This guide covers Markdown content, dynamic filtering, and rendering with Showdown for a flexible, fast static site.

In this guide, we’ll use Next.js App Router (introduced in Next.js 13) and Fusionable to create a static website with Markdown-based content. The App Router provides a modern approach to building React applications with layouts, server components, and streamlined data fetching.

What we'll cover:

  • Setting up Next.js and Fusionable
  • Organizing Markdown content
  • Loading and filtering content with Fusionable
  • Rendering Markdown with Showdown
  • Using the App Router for dynamic routes

If you want to provide feedback, give a star, ask a feature request, or know more, here is the open-source repository of Fusionable https://github.com/Hi-Folks/fusionable

The tools/features we are going to use

To build our sample (and simple) website, we are going to use these tools:

  • Next.js is a powerful React framework for building fast and scalable web applications. It provides features like server-side rendering (SSR), static site generation (SSG), and API routes, making it ideal for modern web development.
  • The App Router in Next.js (introduced in version 13) is a modern way to organize your application using React Server Components (RSC). It simplifies layouts, data fetching, and routing while improving performance and reducing client-side JavaScript.
  • Fusionable is a JavaScript library for managing and querying content. It supports structured data querying (filtering, sorting, and limiting) and is perfect for static websites with Markdown-based content or other lightweight data sources. More info here: https://github.com/Hi-Folks/fusionable
  • Showdown is a versatile library that converts Markdown into HTML. It’s useful for rendering rich content on static websites by transforming plain text Markdown files into fully formatted HTML.

Set Up Your NEXT.js Project

Start by creating a new Next.js project and installing Fusionable and Showdown.

# Create a new Next.js app with TypeScript
bunx create-next-app@14 my-nextjs-site --typescript

# Install dependencies
cd my-nextjs-site
bun add fusionable showdown
Enter fullscreen mode Exit fullscreen mode

If you prefer to use npm:

# Create a new Next.js app with TypeScript
npx create-next-app@14 my-nextjs-site --typescript
# Install dependencies
cd my-nextjs-site
bun install fusionable showdown
Enter fullscreen mode Exit fullscreen mode

For the purpose of this tutorial, we want to keep the app minimal so I used this options:
✔ Would you like to use ESLint? No
✔ Would you like to use Tailwind CSS? No
✔ Would you like to use src/ directory? Yes
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/*)? No

Installing Next.js project

Organize Your Markdown Content

Create a directory for your Markdown files. For example:

mkdir -p content/posts
Enter fullscreen mode Exit fullscreen mode

Add a sample Markdown post in content/posts:

# content/posts/my-first-post.md

---
title: "My First Post"
date: "2023-10-01"
slug: "my-first-post"
highlight: true
---

This is an example **Markdown** post.

Enter fullscreen mode Exit fullscreen mode

Each post contains frontmatter metadata (title, date, slug, highlight) and the Markdown content.

Create a Homepage to List Posts

In the App Router, define a homepage component in src/app/page.tsx.
We will implement the getPosts function to fetch and render all posts using Fusionable.

Fusionable simplifies loading and querying Markdown files. https://github.com/Hi-Folks/fusionable

// app/page.tsx
import Link from 'next/link';
import FusionCollection from 'fusionable/FusionCollection';

function getPosts() {
  const collection = new FusionCollection()
    .loadFromDir('content/posts')
    .orderBy('date', 'desc');
  return collection.getItemsArray();
}

export default function HomePage() {
  const posts = getPosts(); // Static generation by default

  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.fields.slug}>
            <Link href={`/posts/${post.fields.slug}`}>
              {post.fields.title}
            </Link>
            <p>{post.fields.date}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Enter fullscreen mode Exit fullscreen mode

Create Dynamic Post Pages

In the App Router, dynamic routes are handled using [slug] folders. Create the following structure:

src/
  app/
    posts/
      [slug]/
        page.tsx

Enter fullscreen mode Exit fullscreen mode

In src/app/posts/[slug]/page.tsx, we are going to write the code for loading and rendering a single post.

// app/posts/[slug]/page.tsx
import FusionCollection from "fusionable/FusionCollection";
import { FusionFieldsType, FusionItemType } from "fusionable/FusionItem";
import Showdown from 'showdown';

function getPostBySlug(slug: string):FusionItemType {
  const collection = new FusionCollection().loadFromDir('content/posts');
  const post = collection.getOneBySlug(slug);
  if (!post) {
    throw new Error('Post not found');
  }
  return post.getItem();
}

export default function PostPage({ params }: { params: { slug: string } }) {
  const post = getPostBySlug(params.slug); // Static generation by default
  const fields: FusionFieldsType = post.fields;
  const converter = new Showdown.Converter();
  const contentHTML = converter.makeHtml(post.content);

  return (
    <>
      <h1>{fields.title}</h1>
      <p>{fields.date}</p>
      <div dangerouslySetInnerHTML={{ __html: contentHTML }} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Convert Markdown to HTML with Showdown

In the src/app/posts/[slug]/page.tsx file, we use Showdown to convert Markdown content to HTML dynamically:

import Showdown from 'showdown';

// Initialize Showdown and convert Markdown content
const converter = new Showdown.Converter();
const contentHTML = converter.makeHtml(post.content);
Enter fullscreen mode Exit fullscreen mode

The resulting contentHTML is rendered with dangerouslySetInnerHTML for proper HTML display.

Final Touch: Full Folder Structure

Here’s your project’s folder structure after implementing the above steps:

my-nextjs-site/
├── src/app/
│   ├── page.tsx
│   ├── posts/
│   │   ├── [slug]/
│   │   │   ├── page.tsx
├── content/
│   ├── posts/
│   │   ├── my-first-post.md
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

With Next.js App Router and Fusionable, you’ve built a fully functional static site with:

  • Markdown-based content: Easy to write and manage
  • Fusionable queries: For filtering and sorting content
  • Showdown integration: For converting Markdown to HTML
  • Static generation: Fast and SEO-friendly

You can now expand this project with categories, tags, or additional filters to make it even more dynamic. Have fun building!

Top comments (5)

Collapse
 
robertobutti profile image
Roberto B.

If you are more interested in Svelte and SvelteKit, here is the article: dev.to/robertobutti/how-to-build-a...

Collapse
 
pengeszikra profile image
Peter Vivo • Edited

Are you which markdown editor prefer to use on markdown content creation side for next.js? Which is also have some syntax highlighter for codeblocks?

Collapse
 
robertobutti profile image
Roberto B.

Hi @pengeszikra In my process of creating Markdown, I'm using a "desktop" application for editing Markdown content. For example, I use Typora and/or Obsidian.

Collapse
 
chandragie profile image
chandragie

Haven't found any better way, but I don't like the idea of rebuilding the app for content changes in SSR strategy, including NextJS ISR which is awesome.

Collapse
 
robertobutti profile image
Roberto B.

Please, @chandragie, elaborate more on your use case if you want. In this article, I covered the easiest way/approach. There are different valid approaches for handling the rendering. I'm happy to discuss more. Eventually, I can extend with another scenario.