Skip to content
Sanity
    • Platform

      Sanity Studio

      Flexible editing environment

      APIs

      Connect to anything

      Content Lake

      Real-time database

      Try product demo

      Features

      Real-time collaboration

      Fearlessly work with content

      Precise content querying

      Treat content as data with GROQ

      Localization

      Coherent messaging across territories

    • Use cases

      E-commerce

      Richer shopping experiences

      Marketing sites

      Control your story

      Products & services

      Innovate and automate

      Mobile apps

      Content backend for every OS

      View all

      Integrations

      Shopify
      Mux
      Vercel
      Netlify
      Algolia
      Cloudinary
      BigCommerce
      Commerce Layer
      Smartling
      Transifex
      View all
    • Learn

      Documentation
      Studio API Reference
      API reference
      Guides
      GROQ cheat sheet
      Sanity UI
      Get started

      Build and share

      Templates
      Tools and plugins
      Schemas and snippets
      Project showcase
      Share your work
      Browse Exchange

      Frameworks

      React
      Vue
      Next.js
      Nuxt.js
      Svelte
      Remix
      Gatsby
      Astro
      Angular
      Eleventy
      View all
    • Discover

      Blog
      Resource library
      Agency partners
      Become a partner
      Technical support
      Talk to sales

      Case studies

      Puma

      Source of truth for global markets

      Aether

      Unique digital shopping experience

      Morning Brew

      Omnichannel media distribution

      InVision

      Delivering exceptional customer experiences

      View all

      Popular guides

      Headless CMS
      Structured content
      Content modeling
      Headless SEO
      Static websites
      View all
    • Enterprise
    • Pricing
    • Log in
    • Contact sales
    • Get started
Contact salesGet started
Published May 19th 2022

Migrating the Netlify Blog from Hugo to Eleventy using Sanity

Migrating more than 700 markdown files and importing them to our internal Sanity was easy, thanks to some handy tools.

Sam Tancharoensuksavai

Sam is a Frontend Engineer with a love for crafting beautiful and delightful experiences on the web

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 a draft 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 the pagination object is referencing the blogPosts 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 the post 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!

Page content

  • Using markdown-to-sanity
    • Referencing Existing Sanity Data
  • Importing your NDJSON into Sanity
  • Tying Sanity and Eleventy together
    • Eleventy Global Data
    • Using the Data
    • Rendering Markdown
    • Syntax Highlighter
  • Conclusion

Product

Sanity StudioAPIsContent LakeSecurity & Compliance
  • Sanity vs Contentful
  • Sanity vs Strapi
  • Sanity vs Wordpress
  • Sanity vs Adobe Experience Manager
  • Sanity vs Hygraph
  • Sanity vs Sitecore
  • Sanity vs Storyblok
  • Sanity vs Contentstack
  • Sanity vs Prismic
  • Sanity vs Drupal
  • Sanity vs ButterCMS

Resources

DocumentationBlogResource libraryCase Studies
  • React Blog
  • Gatsby Blog
  • Next.js Landing Pages
  • Making a PWA
  • Single Page Application
  • Svelte & Typescript App
  • Vue & Tailwind Blog
  • Developer Portfolio Templates
  • Form validation with Yup
  • Live Preview with Next.js and Sanity.io
  • Next.js blog
  • Next.js personal website
  • Clean Next.js + Sanity app
  • Clean Remix + Sanity app
  • Clean SvelteKit + Sanity app
  • All Templates
  • Agency partners
  • Technology partners
  • Headless CMS 101
  • Static Sites 101
  • Headless Commerce 101
  • CMS for enterprise
  • Headless SEO
  • Localization
  • Content as a Service
  • What is a DXP?
  • Typescript 101
  • Ecommerce SEO
  • What is a Composable DXP?
  • What is an API?
  • GraphQL vs REST
  • React CMS
  • Next.JS CMS
  • CMS for Shopify
  • API-first CMS
  • Content platform
  • Multilingual CMS
  • Static Site CMS
  • Gatsby CMS
  • Node CMS
  • E-commerce CMS
  • Vue CMS
  • Angular CMS
  • GraphQL CMS
  • Newspaper CMS
  • Magazine CMS
  • CMS for apps
  • Remix CMS
  • Nuxt CMS
  • SvelteKit CMS
  • Agile CMS
  • Eleventy CMS

Company

Contact SalesEnterpriseCareersTerms of ServicePrivacy PolicyAccessibility Statement

Stay connected

  • GitHub
  • Slack
  • Twitter
  • YouTube
  • Stack Overflow
  • Blog RSS
  • Newsletter
©Sanity 2023