We just wrapped up the biggest update to Server Side Up since we launched, migrating our old WordPress install to Nuxt Content! For us, this dramatically simplified the steps to produce content and add new features. The tools are powerful, the writing process is smooth, the SEO optimizations are all there, and you have full control over the look and feel of your final product with minimal development effort.
Interested in learning more? Let's take a dive into why we chose Nuxt Content, our approach, and how we implemented some of the features.
There's a ton of reasons why we chose Nuxt Content over the competitors for our new Server Side Up blog.
First of all, you get access to all the Nuxt Modules. There are so many first-party supported modules that you can install along with your Nuxt Content module. These include Nuxt Image, Nuxt Fonts, and even Nuxt UI.
If there's not a first party module, there's hundreds of third-party modules you can pick from. But most importantly, you can even customize Nuxt Content by adding features of your own through Vue components and regular JavaScript code.
For years we hosted Server Side Up on WordPress. It did what it needed, but extending it was a pain. There were so many moving parts, areas to validate, things changing that we felt it got in the way of what we wanted to do, which was write great content and share updates. With Nuxt Content, a sprinkle of JavaScript here, or a Vue component there and we got a whole new feature.
This is huge for modern websites. Google gives a massive SEO boost to those whose pages load quickly. With Nuxt Content, you get a pretty large headstart. Some of the bigger issues for page speed usually come with image compressions and size optimizations. These were solved using the Nuxt Image module which automatically optimizes your images upon build. This dramatically speeds up the maintenance of your site.
Also, since you can server side render (SSR) your pages (highly recommended whether it's for a static site, or just SSR in general), they can be cached and loaded pretty much instantly. Speaking of SSR, it's no longer a MASSIVE PAIN to configure! It comes out of the box with Nuxt and Nuxt Content!
While we have a few areas to optimize on our Pagespeed insights, they can be easily addressed.
This was probably the biggest key to us choosing Nuxt Content, the ability to write content in Markdown/MDC. Other CMS editors we found to be cumbersome and get in the way, making a lot of friction to write and produce more content. With Nuxt Content, we can easily throw together content in Markdown, it's formatted nicely, and we can launch it.
Markdown Components (MDC) are also insanely useful. They allow you to extend markdown with Vue components to add functionality in the middle of your content. Want to collect an email address? Simply make an MDC sign up form and add it to your markdown.
While a lot of our choices came down to familiarity with the Nuxt ecosystem, I believe that Nuxt Content is a competitive product that's worth looking into. Even if you have familiarity with some of the other products.
We chose Nuxt content because we are familiar with Vue and the Nuxt ecosystem. While our Server Side Up blog is the biggest site we manage with Nuxt Content, we've been working with Nuxt Content for a long time and have deployed multiple sites using it, including:
Because we have familiarity with the structure, plugins, ecoystem and the experience in working with Nuxt Content, the choice was much easier. You get a handy CLI to kick off development with ability to add first party plugins. Then you get a light weight install where you can get started immediately and grow along with what you need. I'll explain more about the plugins, configuration, and writing content a little later in the post.
This was the bigger choice and we had a lot to consider. WordPress has been around for ever, runs a huge majority of the internet, and has an extensive ecosystem of plugins and publicly available support. We also have 20+ years developing for WordPress, speaking at WordCamps and using WordPress to write content. So why leave?
Well, even with a ton of experience, WordPress requires a lot of steps, configuration, and reliance on 3rd party plugins to make things work the way you want to. While we can easily make that happen, those steps are a lot quicker with Nuxt Content.
For example, let's add a field to a post in WordPress. Just one field, like "Related Product". You have to either extend the post meta through a custom plugin or theme. Or you could install the Advanced Custom Fields (ACF) plugin, which is massive. In all honesty, ACF is a beautifully written plugin, and does the job, but now it's one more plugin to maintain and update.
Quick tangent, I have nothing against plugins or packages, we use Nuxt packages in the new Server Side Up site. It's just that we use way leaner, way less, hyper-focused packages and quickly build the tools we need. In the end it's much easier to maintain.
Now if we were to add that custom field to a post, we'd simply add a tick in the front-matter of the markdown file in Nuxt Content:
- related_product: Bugflow
That's it. Now you have that data associated with your post! Of course, this is just an example, but this small example that only gets bigger as you add more functionality like series, custom post types, or post formats. With Nuxt content, these extensions are minimal. With WordPress, they require a little more configuration.
I touched on this already on why we chose Nuxt Content, but it also played a big role on why we chose to leave WordPress. The ease of writing content in Nuxt Content is light years better than WordPress, but that comes with a big caveat, we know Markdown. WordPress is known to be a simple writing and blogging platform for people of all disciplines. You don't have to be a developer to get a WordPress instance going and start producing content. It's famous for the simple editor.
However, knowing Markdown and setting a structure for our content, we have full control at low overhead. We can write in a simple text editor and just focus on writing, query what we want where we want it, and group content with ease.
What about shortcodes, and inline functionality that blocks provide? Simple. Nuxt Content handles Markdown Components (MDC). For a Vue developer, you literally can make whatever you want and embed that component in your markdown file. The code snippets you will see later on are an example of this. No need to create and register a plugin + shortcode, just drop your component into your Nuxt install under the /components/content
directory and you can embed it in your text.
To sum up: We chose Nuxt content because there is a huge ecosystem of plugins, we are familiar with Vue, and the ease of generating features and writing content was far superior to anything else we tried.
Alright, let's talk a little about our approach.
There were two major things we needed to figure out. First, how were we going to get our content out of WordPress. Second, how were we going to deploy our new site. This is where Jay and I had to work together.
Luckily, there's a super handy WordPress to Markdown CLI export tool. You simply give it an export from WordPress and it converts it to Markdown. Not only that, all the extra fields and images are structured in a way where you could drag and drop into your static site generator. You can even do your custom post types. For us, we had a "series" custom post type that it handled perfectly.
While we still had to structure what we needed with our new install, this tool took care of the heavy lifting and literally saved us hours.
The next thing to figure out was getting our deployment process down. This is where Jay shines. If you want to read more about how we host the new blog, check out Jay's post Goodbye WordPress, Hello New CMS. I'll spoil the fun part, since Nuxt Content is so lightweight, we actually host it in Jay's basement. And still get a super high lightspeed score!
Once we had our content from WordPress exported, we needed a way to group and organize our content. Luckily, in Nuxt Content version 3 this is extremely convenient and straight forward. They've added Content Collections, which, if you couldn't tell by the name, is exactly what we are looking for.
Content collections allow you to define content and define what data is associated with the content in the form of a schema. It also allows you to query content with ease.
For Server Side up, we have 3 different types of content:
Pages are individual pages that live top level in our blog such as Open Source, Hire Us, and Subscribe.
As we drill down and organize our more dynamic content, we have blog and series content. Series are just groupings of blog posts. If you have a larger site you can organize these content collections as complex as you want.
When defining your content collections, you define the source directory, what type of content it is and a schema. Below is how we defined the content collections within Server Side Up:
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'
export default defineContentConfig({
collections: {
blog: defineCollection({
source: 'blog/*.md',
type: 'page',
schema: z.object({
title: z.string(),
categories: z.array(z.string()),
date: z.date(),
author: z.string(),
published: z.boolean().default(true),
header_image: z.boolean().default(false),
video_url: z.string().optional(),
series: z.string().optional()
})
}),
content: defineCollection(
asSeoCollection({
type: 'page',
source: '**/*.md',
}),
),
series: defineCollection({
source: 'series/*.md',
type: 'page',
schema: z.object({
title: z.string(),
categories: z.array(z.string()),
description: z.string(),
published: z.boolean().default(true),
header_image: z.boolean().default(false),
posts: z.array(z.string()),
video_url: z.string().optional()
})
})
},
})
Under the hood, the schema is defined using Zod. These schemas ensure that when you create content, the front matter of your markdown file contains the required parameters. Super slick!
Blog pots are pretty straight forward, but it's our series
collection which is interesting. By default all content lives in the /content
directory within your Nuxt install. Anything in the top of that directory will be the top level page. For example, our Hire Us resides in /content/hire-us.md
.
Now with series, we place them in the /content/series
directory. The goal with a series is not only to have content describing the series, but also group posts in logical order for someone to read and get a larger in-depth understanding on a subject.
If you look at the series
collection within our content collections, under schema you will see posts
defined as z.array(z.string())
which is an array of strings. Next, check out the blog
collection and find the series
key of the schema which is defined as z.string().optional()
which means it's an optional string.
What this does, is in our series piece of content, we can define an array of URLs (strings) in order that make up the series. So when we query from the perspective of a series, we can get all associated posts in order.
From the flip side, in an individual post, we can query the series defined by the URL in the series
key. Now we can show the previous and next links if and only if the post is in a series! This level of functionality is not only super flexible, it's really quick to implement! This, I believe, shows off some of the places where Nuxt content really shines.
We have our content structured, how do we query it? Querying content within Nuxt Content is also a breeze. You just make use of the provided composables.
For example, on our Blog page, we have a component where we load all of our blog posts in a list. To query these posts, we use the following code:
const { data: posts } = await useAsyncData('blog', () => queryCollection('blog')
.select('path', 'title', 'categories', 'date')
.all())
The useAsyncData()
composable makes a cached query under the key of blog
. Within that composable we call queryCollection('blog')
and pass in the collection we defined. We can extend the query to select only the fields we need, similar to an SQL command, which allows us to save on memory. Now we have a posts
variable we can work with within our component.
How about a series? Same thing. Let's look at an individual series page query, where we are loading the content for a single page:
const slug = useRoute().params.slug
const { data: series } = await useAsyncData(`series-${slug}`, () => {
return queryCollection('series').path(`/series/${slug}/`).first()
});
This would be the same for loading an individual blog post as well. We use the useRoute()
composable provided by Nuxt, then query the collection where the path matches the URL we are on. This allows us to get the individual post.
How about tying the series and posts together? Remember, in our front matter on a series collection, defined in our content collection, we have a posts
array key. This is an array of URLs that correlate to blog posts:
const route = useRoute()
const { data:posts } = await useAsyncData(route.path, () => {
return queryCollection('blog')
.where('path', 'in', series.posts)
.all()
});
After we query the series, we have an array key called posts
that contain the array of posts. Now, on the series page, we can query all posts where the path is in the array key. You'd just do the opposite approach on the individual post page to get the series it's connected to. Super easy to structure your data in Nuxt Content and query what you want!
Within WordPress, tags and categories ship by default. This is one thing you have to add within Nuxt, but honestly, it's so easy and flexible I prefer it.
When working with Nuxt, you can configure your app with the app.config.ts
. This file is essentially all the static data that makes up your site. We store links, author information, categories, etc. in this file. Let's focus on the categories.
export default defineAppConfig({
categories: {
ansible: {
name: 'Ansible',
slug: 'ansible',
color: '#717680',
logo: 'CategoriesAnsibleLogo',
outline: 'CategoriesAnsibleOutline',
link: '/categories/ansible/'
},
browserextensions: {
name: 'Browser Extensions',
slug: 'browserextensions',
color: '#EF6820',
logo: 'CategoriesBrowserExtensionsLogo',
outline: 'CategoriesBrowserExtensionsOutline',
link: '/categories/browserextensions/'
},
devops: {
name: 'DevOps',
slug: 'devops',
color: '#F04438',
logo: 'CategoriesDevOpsLogo',
outline: 'CategoriesDevOpsOutline',
link: '/categories/devops/'
},
//....
}
});
To add a category, or add a field to an existing category, you just simply add to the array. This array is included anywhere you want within your Nuxt Content install. To add a category to a post, simply use the categories
key in the front-matter of your Markdown file and pass it the slug
of one of the categories.
This is how we migrated our category structure from WordPress to Nuxt Content. What you see is a snippet that runs the code of Server Side Up right now.
For the most part, Server Side Up is much less module/plugin heavy than our previous WordPress website. With that being said we do make use of the incredible modules provided by Nuxt. Most of them are first party, Nuxt team maintained modules, but we do utilize a 3rd party module to handle all the SEO. Obviously a very important part of our Server Side Up site which we will cover next.
Nuxt Image is an insanely powerful, first party, image optimization module. Want your images optimized for the best quality for web without doing it manually? This module takes care of it. Best part? You can run the optimizations locally on build if you want. That means all images can be compressed and formatted as .webp
images WITHOUT the use of a 3rd party platform (even though it supports a ton of 3rd party platforms).
Nuxt Fonts is another first party module that we use on Server Side Up. This module handles the downloading and caching of all the fonts used within our website, ensuring they are loaded as efficiently as possible.
While not a first party plugin, very close. Vueuse is a collection of utilities that are commonly used in Vue development. The Nuxt module just autoloads them. They come in handy for date formatting, event passing, and so many other small things that you commonly use that doesn't require you to re-invent the wheel.
One of the two 3rd party modules we use is Nuxt Social Share. It's a lightweight, simple module that adds components for you to integrate social sharing on your site. Super important for a blog like Server Side Up! This module provides a simple component that allows for easy social shares and making sure the link is in the right format for each network.
Now there's one major module we left out, and that's because it deserves it's own section. That's Nuxt SEO.
One of the biggest challenges with static sites is SEO. That's because it requires server side rendering (SSR) to have the pages pre-rendered before returning a response. Coming from a language like PHP which is server side, it's not really thought about. You have to run PHP on the server, you can't run it in the client.
With a javascript framework like Nuxt, you can actually run the whole thing client side. Which is awesome for a lightweight app, but terrible for SEO. That means you need to ensure your Nuxt content site is server side rendered. This used to be a HUGE pain, but Nuxt Content comes with everything configured right out of the box.
Now that we have SSR, we need to implement proper SEO features. That's where the Nuxt SEO comes in. And holy smokes, it's awesome!
Nuxt SEO is the equivalent to a plugin like Yoast in WordPress. It provides all the tools you need for meta data, social share descriptions, Open Graph tags, structured data, sitemaps, you name it. While there has been some criticism online about challenges on setting it up, we didn't have much of a problem with Server Side Up.
We were able to set all the structured data, open graph information, and descriptions with ease. What's even crazier? Dynamic OG image generation. We created a simple template using a Vue component. From there, we can populate and generate the title, display categories, and even logos of the technology used by the post. All of it is handled automatically!
My gut feeling is that a lot of the fear of running a site or blog with Nuxt Content is the feeling like you are going to miss out on SEO tools. With Nuxt SEO and SSR out of the box combined with the blazing fast page speed of a pre-compiled site, I honestly think you will get better SEO results. We did and we host our site in Jay's basement! Our page speed is faster, and we have more flexible tooling. When set up correctly, we were able to take advantage of all these features on Server Side Up.
Nuxt content ships with a ton of other super helpful features we implemented that you can too, like full text search, syntax highlighting for code, layouts and templates for pages, etc.
One thing we didn't do on Server Side Up, but have on other Nuxt Content installs, was add server routes. If needed to integrate with secure 3rd party platforms, you can add server routes with ease, keeping your API tokens secure.
Overall, I'm beyond thrilled with our Nuxt Content migration. I feel like this is the next generation of Server Side Up that will stick around for a long time. It's ultra flexible, super lightweight, and extremely fast. It's a breeze to generate content and we can keep linking everything together through custom collections as our site grows.
Server Side Up is our biggest Nuxt Content site we've run, but we have installed Nuxt Content on all of our open source landing pages and have done websites for our clients. If you have any questions, feel free to reach out on X (formerly Twitter), or on Discord.
If you want us to do the entire migration for you, we are available for hire! We can migrate your site off of WordPress, start fresh with a new Nuxt Content install, and make sure everything is set up, structured, and optimized. Don't know markdown? No worries, you can use Nuxt Studio. We will make sure your Nuxt Content install is easily managed through a GUI.
Professional developers choose Server Side Up to ship quality applications without surrendering control. Explore our tools and resources or work directly with us.
We're a community of 3,000+ members help each other level up our development skills.
Active Discord Members
We help each other through the challenges and share our knowledge when we learn something cool.
Stars on GitHub
Our community is active and growing.
Newsletter Subscribers
We send periodic updates what we're learning and what new tools are available. No spam. No BS.
Be the first to know about our latest releases and product updates.