Migrating the Netlify Blog from Hugo to Eleventy using Sanity
Written by Sam Tancharoensuksavai
Over the last year, Netlify has been migrating our codebase from Hugo to Eleventy to streamline our development process and reduce client-side JavaScript. One of the last steps left was to migrate more than 700 markdown files, each representing a blog post, over to our internal Sanity instance. Luckily Sanity’s markdown-to-sanity tool made this process simple.
In this post, I’ll cover:
- using the markdown-to-sanity tool
- importing the
.ndjson
that's generated to our Sanity dataset - how we're using Eleventy to render our blog posts
- any small gotchas I came across
Using markdown-to-sanity
Sanity offers a concise CLI tool to help with the importing and exporting of datasets. The CLI tool requires that the file be a newline-delimited JSON (NDJSON) file. Basically, each line in a file is a valid JSON-object containing a document you want to import. Now, I needed a way to parse all 700 markdown files and convert them into a single .ndjson
file. Sanity to the rescue! Their markdown-to-sanity repo did the job beautifully.
After following the README.md
you should have the CLI tool installed on your machine. For organizational purposes, I created a new directory called /blog
at the root of the repository and dropped all of my markdown files in there. I also needed to bring over all of the static assets – such as images, gifs, and videos – that each blog post was referencing.
Referencing Existing Sanity Data
For each blog post, there is an author. Luckily, we already had these authors stored in our Sanity dataset. Essentially, we want to pass the author’s reference ID that Sanity has set when we create the JSON object of the blog post. Sanity provides a helpful client that you can use to fetch whatever data you need. All you need to do is pass a valid GROQ query into the fetch function.
Here’s a basic example of using the Sanity Client to fetch an author’s ID:
const sanityClient = require('@sanity/client')
const client = sanityClient({...})
async function getAuthorRef(name) {
const query = `*[_type == "author" && name == "${name}" && !(_id in path("drafts.*"))][0]{ "ref": _id }`
const data = await client.fetch(query).catch(err => console.error(err));
return {
"_ref": data && data.ref,
"_type":"reference"
}
}
Let’s break a couple of things down:
name
refers to the author’s name we are retrieving{ "ref": _id }
represents what we are returning back from the API call. We only want the_id
of the author to use as a reference ID!(_id in path("drafts.*"))
ensures that we do not retrieve any documents that are currently in adraft
state- We are returning an object with
"_type": "reference
to signify to Sanity that this is a reference and we set the_ref
to the ID of the author we just fetched
Then, I needed to modify the Sanity document, representing a blog post, before it got created and added to the .ndjson
file. Taking a look at the ./src/convertToSanityDocument.js
file, you'll notice that this is where we create the Sanity Document. I made a number of edits to the final object to ensure there was parity in the data between the markdown file and what was stored in Sanity.
Importing your NDJSON into Sanity
After successfully running the markdown-to-sanity tool, you should have a .ndjson
file where each new line is a single blog post. Now you want to import this data into your dataset stored in Sanity. You can reference the Importing Data article for more details.
With my newly generated .ndjson
file, I ran the Sanity CLI to import this file into our dataset. It should look something like this:
sanity dataset import my-data-dump.ndjson production
I also added the --replace
and --allow-failing-assets
flags to the end of my command, both of which are explained in the Importing Data document.
Tying Sanity and Eleventy together
If you’re looking for more granular detail into how we’re using Eleventy and Vue together, I’ll point you to Zach Leatherman’s post. In this section, I’ll talk about how we are calling Sanity, storing the response, and building templates around our data.
Eleventy Global Data
Just like in our example when we fetched the authors from our Sanity, we will be doing the same thing, but this time we will store the response within a global data file. Leveraging Eleventy’s global data gives us the ability to grab the data we need within a template file.
For instance, if we wanted to query for all the blog posts we might have code that looks something like this:
// _data/blogPosts.js
const sanityClient = require('@sanity/client')
const client = sanityClient({...})
module.exports = async function () {
const query = `*[_type == "blogPost"]`
return await client.fetch(query).catch(err => console.error(err));
};
Using the Data
Now that we have all our blog posts within Eleventy’s Global datastore, we can expect our posts to be available at data.blogPosts
. I’d like to create a static HTML file for each blog post. Eleventy’s Pagination feature makes this extremely easy, as the first line states, “pagination allows you to iterate over a data set and create multiple files from a single template.”
Let’s create a Vue template file that will serve as the template for our blog posts.
<!-- _includes/blog-post.vue -->
<template>
<h1>{{ post.title }}</h1>
</template>
<script>
export default {
data: function() {
return {
layout: "layout.njk",
tags: ["post"],
pagination: {
alias: "post",
data: "blogPosts",
size: 1,
},
permalink: {
build: (data) => `/blog/${data.post.slug.current}/`,
},
};
},
};
</script>
The following is happening in the code above:
data: "blogPosts"
inside thepagination
object is referencing theblogPosts
data object that we set in our global data store.size: 1
means that we want to chunk the array of all posts into single chunks where each chunk represents a single blog post.alias: "post"
is the variable you can use within this template file to refer to the chunk of data returned from paginating.- We are directly referencing the
post
object, set by the pagination alias, within the permalink, by grabbing the slug for the blog post and using it to create the blog’s permalink. - Within our
<template>
we are again referencing thepost
object to render the title of the blog post.
Rendering Markdown
First, we had Markdown files, then imported the Markdown content of those files into Sanity, and now we want to render out that Markdown into something browsers can understand: HTML. Traditionally, Eleventy leverages Liquid or Nunjucks to process Markdown into HTML, but remember we are using Vue.
Luckily, we knew someone who could help us patch that gap. Thus, Eleventy’s Render Plugin was born! Using this plugin we could feed the markdown string into the renderTemplate
function that this plugin makes available globally, and directly injects the output into our template file.
<!-- _includes/blog-post.vue -->
<template>
<h1>{{post.title}}</h1>
<span v-html="markdown" />
</template>
<script>
export default {
async serverPrefetch(data) {
const postMarkdown = post.markdown || ""
this.markdown = await this.renderTemplate(postMarkdown, "njk,md", data);
},
data: function() {
return {
layout: "layout.njk",
tags: ["post"],
pagination: {
alias: "post",
data: "blogPosts",
size: 1,
},
permalink: {
build: (data) => `/blog/${data.post.slug}/`,
},
};
},
};
</script>
Syntax Highlighter
What’s a developer-focused blog without pretty code blocks?
Eleventy offers another great plugin to take care of processing code blocks within a markdown file. Using the @11ty/eleventy-plugin-syntaxhighlight
plugin whenever we run our markdown through the renderTemplate()
function, the highlighter plugin will use PrismJS to process the block according to the language specified in the code fence.
Conclusion
What sounded like a daunting task, migrating over 700 markdown files into Sanity and pulling those blog posts into our Eleventy codebase, turned out to be a fairly straightforward process thanks to the great foresight and care the Sanity team brings to their product.
There are moments as a developer when you feel like you have a special set of superpowers, and being able to process so many files at one time and import that data into our internal Sanity just by executing a single command was a special moment for me. Thanks again to the great folks at Sanity, as well as Zach Leatherman for letting me nerd-snipe you for a couple of awesome features to the Eleventy ecosystem!