Build a static website with Markdown content, using Nuxt and Fusionable (server API approach)

robertobutti

Roberto B.

Posted on November 18, 2024

Build a static website with Markdown content, using Nuxt and Fusionable (server API approach)

Creating a content-driven static website with Nuxt and Fusionable is simple and efficient.
Nuxt's powerful features for static site generation and Fusionable's robust content querying capabilities make managing Markdown content effortless.

In this guide, we will explore how to:

  • Set up Nuxt framework and Fusionable library
  • Create Markdown-based content
  • Query and display content dynamically using Fusionable
  • Using Nuxt server API
  • Convert Markdown to HTML using Showdown

If you like, you can give a star ⭐ to the Fusionable open-source project: https://github.com/Hi-Folks/fusionable

Creating the project

Start by creating a Nuxt project with TypeScript and installing the required dependencies.

# Create a new Nuxt.js project
bunx nuxi@latest init my-nuxt-site
cd my-nuxt-site

# Install dependencies for Fusionable and Showdown
bun add fusionable showdown
Enter fullscreen mode Exit fullscreen mode

Organizing the Markdown content

Create a folder to store your Markdown files (posts). For example:

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

Add a Markdown file for your first post for example content/posts/post-1.md:

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

Welcome to my first blog post! This is written in **Markdown**.
Enter fullscreen mode Exit fullscreen mode

Each Markdown file contains frontmatter metadata (e.g., title, date, slug) and the Markdown content.

Creating Server-Side APIs with Fusionable

Nuxt's server API capabilities allow you to create server-side endpoints (in the /server/api/ directory exposing /api/* HTTP endpoints) to centralize and efficiently handle data logic. These APIs bridge your application with the underlying content or data sources (e.g., Markdown files, databases, or external APIs).

How can you use the API approach?

  • During static site generation (SSG), Nuxt calls these APIs at build time, caching the results into the generated HTML.
  • For server-side rendering (SSR) or client-side navigation, the APIs dynamically serve the latest data.

Why and when should you use the API approach?

By leveraging these server APIs, you decouple your content querying logic from the components, ensure cleaner architecture, and open the door for future flexibility, such as integrating with a CMS or a database.
In our example, we are parsing the markdown directly from the file system.

For parsing, filtering, and sorting Markdown files, we are going to use the Fusionable library

API for retrieving all published posts

Use the defineEventHandler function to create endpoints under the /server/api/ directory. These endpoints are automatically available as server-side routes. For example, /server/api/index.ts queries all published Markdown posts:

import FusionCollection from 'fusionable/FusionCollection';

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

export default defineEventHandler((event) => {
  const posts = getPosts();
  return posts;
})
Enter fullscreen mode Exit fullscreen mode

This API (the endpoint is /api/) returns a list of all the published posts ordered by date.
The API processes the Markdown files on the server, performs any required filtering or sorting, and returns a JSON response.

API for retrieving a single post by slug

Server APIs also support dynamic parameters. For example, /server/api/posts/[slug].ts fetches a specific Markdown post by its slug:

import FusionCollection from 'fusionable/FusionCollection';

function getPostBySlug(slug: string) {
  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 defineEventHandler((event) => {
  const slug: string = getRouterParam(event, 'slug') ?? ""
  const post = getPostBySlug(slug);
  return {
    fields: post.fields,
    content: post.content
  };
})
Enter fullscreen mode Exit fullscreen mode

In the defineEventHandler function, we use the getRouterParam to retrieve the slug parameter.
Then, after loading the markdown files from the content/posts directory via the loadFromDir() method, we can use getOneBySlug(slug) to retrieve a specific post.

Loading data in Nuxt pages

Now we understand how to create and expose API for running server-side code, and we can focus on the rendering/displaying part.

Homepage: displaying the list of posts

In the pages/index.vue file (if you started the Nuxt from scratch, probably you should create it), we are going to call the "all posts" API via await useFetch("/api/"):

<template>
  <main>
    <h1>Blog Posts</h1>
    <ul>
      <li v-for="post in data" :key="post.fields.slug">
        <NuxtLink :to="`/posts/${post.fields.slug}`">{{ post.fields.title }}</NuxtLink>
        <p>{{ post.fields.date }}</p>
      </li>
    </ul>
  </main>
</template>

<script setup lang="ts">
const { data }  = await useFetch(`/api/`)
</script>
Enter fullscreen mode Exit fullscreen mode

Post page: rendering a single post

The /api/posts/[slug] API fetches a single post. To render it, create a dynamic route file: pages/posts/[slug].vue:

<template>
  <article>
    <h1>{{ data.fields.title }}</h1>
    <p>{{ data.fields.date }}</p>
    <div v-html="converter.makeHtml(data.content)"></div>
  </article>
</template>

<script setup lang="ts">
import Showdown from 'showdown';
const { slug } = useRoute().params
const { data } = await useFetch(`/api/posts/${slug}`)
const converter = new Showdown.Converter();

</script>
Enter fullscreen mode Exit fullscreen mode

The Showdown library transforms Markdown content into HTML. The [slug].vue page converts the data.content field via makeHtml() method and v-html attribute for safe display in your templates.

Full folder structure

Here's the structure of the project:

my-nuxt-site/
├── content/
│   ├── posts/
│   │   ├── my-first-post.md
├── pages/
│   ├── index.vue
│   ├── posts/
│   │   ├── [slug].vue
├── server/
│   ├── api/
│   │   ├── index.ts
│   │   ├── posts/
│   │   │   ├── [slug].ts
Enter fullscreen mode Exit fullscreen mode

Building your static website

Once your Nuxt application is ready, you can generate a static site version for optimal performance and SEO. Nuxt's static site generation creates pre-rendered HTML files for your pages, making them fast to load and easy to deploy on any static hosting platform.

To generate your static site, use the following command:

bunx nuxi generate
Enter fullscreen mode Exit fullscreen mode

This command performs the following steps:

  • Builds the application: compiles your Nuxt app with all required assets.
  • Pre-renders pages: generates static HTML for all routes, including dynamic ones like /posts/[slug].
  • Outputs to .output/public directory: the final static site is stored in the .output/public folder, which is ready for deployment.

As you can see, even if we used the API approach, you don't need a server with a JavaScript runtime to execute the API at runtime. The "server-side" code of the API is executed at the building time (when you run the nuxi generate command).

Wrapping up

With Nuxt and Fusionable, we've built a powerful static site using server APIs to handle content loading. Here's what we achieved:

  • Server APIs: centralized content logic with Fusionable.
  • Dynamic routes: use Nuxt's dynamic routing for individual post pages.
  • Rendering Markdown: render Markdown content as HTML using Showdown.
  • Static generation: benefit from Nuxt's fast, SEO-friendly static site generation.

This architecture is scalable and keeps your content management flexible. Happy coding!

💖 💪 🙅 🚩
robertobutti
Roberto B.

Posted on November 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related