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 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:

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:

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!