Build a static website with Markdown content, using Nuxt and Fusionable (server API approach)
Roberto B.
Posted on November 18, 2024
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
Organizing the Markdown content
Create a folder to store your Markdown files (posts). For example:
mkdir -p content/posts
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**.
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;
})
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
};
})
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>
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>
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
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
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!
Posted on November 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.