In this blog post, I detail how you can begin creating a blog similar to mine using Nuxt 3 and Nuxt's Content module that renders markdown pages. I've also created a starter template that you can fork from here if you want to jump in and get started yourself.

I had recently become very excited when I learned about the new release of Nuxt3 (I know, I'm late). I've used Vue since 2018, and I was eager to complete a project in 2020 using Nuxt2. Since then, I have spent some time working with React for another project that I am apart of. I've come to the conclusion that I prefer Vue over React, and I'm interested in speaking more about getting people on board with using Vue/Nuxt. We'll speak about why I pick Vue over React at another time.

Backstory

Once I found out about Nuxt3, I instantly started brainstorming projects I could start so that I could begin learning the new changes of the framework. I came up with some pretty good ideas, but they were all too elaborate, and I knew I didn't want to just do another To-Do List app because that was just too boring. Then, I remembered how a coworker of mine had his own blog that's just a small static site. The site uses Jekyll, Github Pages and Markdown to create a static site with blog articles. This intrigued me, so I looked through the documentation of Nuxt3, and, sure enough, Nuxt also lets you render Markdown files. Nuxt has a module called Content that allows you to use Markdown files to store your content. This is awesome for two reasons:

  1. You can create blog articles without the need for a database, and in turn, a backend service.
  2. Markdown comes out of the box with text formatting features.

At this point, I was convinced that I could get this up and running pretty easily, and I had a mostly finished site in about 10 days. There were a couple of gotchas that I encountered, but overall, it was a pretty solid project from beginning to end, and I hope it will last me for quite a while.

Gotchas That I Encountered

The first thing that I encountered was super easy, and hardly even needs that much of an explanation. Not only am I knew to Nuxt3, but I found out that I'm new to Vue3 as well. All of the work I had ever done in Vue was in Vue2, so I was lost for a little bit when I discovered I had to use .value to get and set some values.

Also, because of my particular implementation where I have blogs separated explicitly by categories, I have to pull the raw data from the folder that contains the blogs for that category, which means that I have to use the ._rawValue field. This is because the content module automatically pulls the .md files that are only at the root of the directory. Since I have all my content in subdirectories, I have to do a little bit more work. If you don't plan on having blogs separated, you shouldn't have to worry about this.

The second, and waaay less trivial issue was with Github Pages. As I was using static site generaiton, and I thought that I could use something like Github Pages for incredibly low-effort deployment and hosting for this site. I mean, that's what my coworker did with his Jekyll site, this should be just as easy.

Spoiler alert. It was not.

I'm not going to go into all of the issues that I discovered with routing and inconsistent builds and so on while trying to get this blog up and running on Github Pages. Turns out, Github emphasizes their support of Jekyll sites, and using those on there is very straightforward and easy. Everything else, good luck. This forced me try out Vercel for CI/CD and hosting instead. I ended up being very impressed by Vercel, and would be interested in experimenting more with the platform. For this to be such a non-trivial issue, it actually had a very trivial solution:

Don't use Github Pages.

The Implementation

Now, I'm going to show you how you can have a blog site similar to mine using just Nuxt3 and Markdown pages! There's a few prerequisites that you should have first to make this as smooth as possible:

  1. Experience with Javascript
  2. Experience with a Javacsript framework, such as Angular, React or Vue
  3. Experience with Markdown
  4. Node >= v16.10.0
  5. npx

If you do not have npx installed, you can do so now by running

npm install -g npx

to install npx globally on your system.

Markdown is a super easy thing to use, and if you've ever had to update the README.md in a git repo, you should be familiar with it enough to keep up. Either way, you can check out this Markdown guide.

To get started, open up your terminal and navigate to the folder that you wish to store your projects. Run

npx nuxi init your_project_name

and when that's finished, navigate to the project folder and run npm install or yarn install. Once that's completed, start your new Nuxt app by running

npm run dev

You should have a window pop up with your new application at http://localhost:3000.

New Nuxt Site

There have been a few times where the step to run npm install has the installation running for an incredibly long time for me. If this happens to you, you should be safe to cancel it an run yarn install instead. Just remember that you'll need to use yarn from this point on to install dependencies (yarn add ___). You can still start the application with npm run dev.

Now, if you look in your project folder, you should see a bunch of new files and folders. In this folder, create a new folder called content. This is where we keep out markdown files. In that folder, create a new file called test.md. In our new Markdown file, add the following lines of code.

---
title: 'This is a Test'
date: '01/01/2020'
description: 'This is a test description.'
---

This will create some metadata for our Markdown content. You can create what ever metadata fields you want, such as tags, written_by, etc. These fields won't be rendered as part of the markdown content when it is rendered, but we can still access this data when accessing the files. Let's also add in a couple of lines of "Lorem Ipsum" in the markdown file so that we have something to render.

# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Elementum nibh tellus molestie nunc non blandit massa. Pellentesque elit ullamcorper dignissim cras. Ultricies mi eget mauris pharetra.

This will create a header in the markdown with a couple of lines that we can see when we render the content. Save the file, and go back to the terminal.

We now need to install the Nuxt Content package in order to get this markdown file rendered on the page:

npm install --save-dev @nuxt/content

or

yarn add --dev @nuxt/content

Now, go into the nuxt.config.ts file, and replace everything with this:

export default defineNuxtConfig({
    modules: [
        '@nuxt/content'
    ],
})

The current structure of our site is very basic. It's almost like a Single-Page-Application at this point. But, we know that we want to have a whole website. Our website will need a home page, and a page to render the content. Also, we'll have multiple blog posts, so we will need to render the content dynamically.

Open the app.vue file, and you should see something that looks like this:

<template>
  <div>
    <NuxtWelcome />
  </div>
</template>

Something awesome about Nuxt3 is that it has auto importing of components and composables, as well as other things. The <NuxtWelcome /> component is a Nuxt created component that holds their welcome page. There's another Nuxt component called <NuxtPage /> that we will need to replace the <NuxtWelcome /> component in the divs. This component will render any page that is routed to in the application. Now, we can add our pages.

Create a new folder called pages. In the pages folder, add a new file called index.vue. This page will be our home page, and Nuxt will automatically know to navigate to this page when navigating to the / route. Create another folder called post. In the post folder, add another file called [post].vue When a url for the site has post in the route, Nuxt will automatically know to route to this folder. Since the [post] file is in brackets, Nuxt will also know that the name of this page can be dynamic. This is exactly what we need to render the posts dynamically.

Starting with the home page, let's get all of our posts and list them on the page so that we can navigate to them. In the index.vue file, add

<template>
    <div>
        <div v-for="blog in data" :key="blog">
            <div @click="goToPost(blog._path)">
                <div>
                    <p>{{ blog.date }} 
                        <span>
                            &nbsp;&nbsp;&nbsp;{{ blog.title }}
                        </span>
                    </p>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
const router = useRouter();
const { data } = useAsyncData('posts', 
    () => queryContent()
            .only([
                'title', 
                '_path', 
                'description', 
                'date'
            ])
            .sort({ date: -1 })
            .find());
const goToPost = (path) => {
    router.push(`post${path}`);
};
</script>

This will create a page that gathers all of the blogs in our content folder, gets the title and the date, and renders them all out onto the page. They will also be clickable, and using the useRouter() composable that is auto imported in to the component, clicking the item in the list will automatically send you to that page. Give it a try!

Index Page

I hope you gave it a try, because you'll see a blank page! You may say "Hey, I thought were were supposed to see the content?" Not quite yet. We still have to tell the [post].vue file how to find the content and how to render it. So let's do that now.

Open up the [post].vue file, and add

<template>
    <div v-if="currentBlog" >
        <h2>{{ currentBlog.title }}</h2>
        <p>{{ currentBlog.date }}</p>
        <p>{{ currentBlog.description }}</p>
        <ContentRenderer>
            <ContentRendererMarkdown :value="currentBlog" />
        </ContentRenderer>
    </div>
</template>

<script setup>
const { post } = useRoute().params;
const { data } = useAsyncData('post', 
    () => queryContent()
        .where({_path: `/${post}`})
        .findOne());
var currentBlog = data;
</script>

This will get the path of the post from the page route, search for that path in the content folder, and return it to be rendered on the page. You can also see how we're pulling that metadata from markdown and rendering it as well. Now, when you navigate to that page again, you should see this:

Post Page

And you're done! If you add more markdown pages to the content folder, you should see them populate on the home page, and you should be able to navigate to each of them. It should also work if you navigate directly to the URLs as well, since it's a static site that doesn't need to populate data from a server.

Obviously, this is just a start, but the hard part is out of the way. There's plenty more you can do from here:

  1. Add CSS or SASS styling to give it a personal look.
  2. Add SEO elements to the home and post pages.
  3. Refactor the logic to support different blog categories like I did (difficulty: hard).

If you made it this far, I appreciate you reading how I set this blog up. You can fork the starter repository here if you want to go ahead and get started!