Build a Nuxt Module to Sync Algolia with Nuxt Content

Daniel Kelly | Jan 02, 2021
  • Vue
  • Javascript
  • Nuxt
Build a Nuxt Module to Sync Algolia with Nuxt Content

TLDR

I published the results of this tutorial to npm as a package. If you're looking to just grab and go, you can install the module via npm. If you want to learn more about Nuxt modules and the Algolia javascript SDK, read on! Note this package only handles syncing the data to Algolia, it does nothing about rendering a search UI for your Nuxt website. You can read more about implementing the UI in Algolia's official docs for Vue Instant Search or stay tuned for my next post.

Why Sync Content to Algolia?

Using Nuxt as a static site generator is a great developer experience and makes for a fast site. Storing the content in static files alone however doesn't make search simple. Algolia not only makes search simple (can start out with simple text search) but scales tremendously with the ability to provide all kind of filtering and weights to your search data. You can read more about the features of Algolia here.

Creating the Module

In order to sync our content to Algolia, we're going to be building a Nuxt module. Modules make it simple to reuse logic across different projects and run custom code at specified points during the Nuxt lifecycle. We're going to hook into the generation process and run custom code at the end of it to read our content and insert it into Algolia via the Algolia Javascript SDK.

Module "interface"

We will interact with the module by configuring it in nuxt.config.js

// nuxt.config.js
export default{
    
  // this registers the module to be used
  // it's only needed during the build so it can be registered under buildModules (not modules)
  buildModules: [
    '~/modules/nuxt-content-algolia',
    // ...
  ],
  
  // configuration options here
  nuxtContentAlgolia: {
    appId: process.env.ALGOLIA_APP_ID,
    // !IMPORTANT secret key should always be an environment variable
    // this is not your search only key but the key that grants access to modify the index
    apiKey: process.env.ALGOLIA_API_KEY,

    // relative to content directory
    // each path get's its own index
    paths: [
      {
        name: 'blog',
        // optional (will use name if index not specified)
        index: process.env.ALGOLIA_INDEX || 'blog',
        fields: ['title', 'description', 'bodyPlainText', 'tags'],
      },
    ],
  },
}

Accepting module options

Our actual module can live in a file at modules/nuxt-content-algolia (notice this is where we told nuxt config to look for the "build module"). First thing we should do in our file is to allow our module to accept the configuration options from the nuxt.config.js and merge those options with the some defaults.

// modules/nuxt-content-algolia

export default function algoliaModule(moduleOptions = {}) {
    this.options.nuxtAlgolia = this.options.nuxtContentAlgolia || {}
    const config = {
        hook: 'generate:done',
        ...this.options.nuxtAlgolia,
        ...moduleOptions,
    }
}

Require Algolia app id and api key

The Algolia SDK requires us to provide an app id and a secret api key to interact with it. If those aren't provided we might as well not even attempt to sync things to algolia, so let's check for those up front. If they don't exist we'll report an error to the console and return from the module to let the build continue on its merry way.

const consola = require('consola')
export default function algoliaModule(moduleOptions = {}) {
    // config...
    if (!config.appId || !config.apiKey) {
      consola.error(
        new Error('appId and apiKey are required for nuxt-algolia module')
      )
      return
    }
}

Hook into Nuxt after generate

Nuxt provides several hooks to execute code at different times during the generate process. See all of the generate hooks on Nuxt's website. I've made the hook configurable from the options with generate:done as the default. This works great for SSG but wanted to leave it customizable as I'm not as familiar with actually running Nuxt on a server.

export default function algoliaModule(moduleOptions = {}) {
    // config...
    // requirements...
  
    // config.hook references the default "generate:done"
    this.nuxt.hook(config.hook, async (nuxt) => {
      // ... where the magic happens
    })
}

Read content and sync to Algolia

this.nuxt.hook(config.hook, async (nuxt) => {
  // require the nuxt content function
  const { $content } = require(`${nuxt.options.srcDir}/node_modules/@nuxt/content`)
  
  // loop through paths provided at config
  for (let i = 0; i < config.paths.length; i++) {
    const path = config.paths[i]
    const indexName = path.index || path.name
    
    // get the content from the file system
    let docs = await $content(path.name).fetch()

    // transform each "document" to only include the fields specified in the config 
    docs = docs.map((doc) => {
      const newDoc = {}
      path.fields.forEach((field) => (newDoc[field] = doc[field]))
      newDoc.objectID = doc.slug
      return newDoc
    })

    // initialize the algolia search package with our app info and index name
    const client = algoliasearch(config.appId, config.apiKey)
    const index = client.initIndex(indexName)

    // clear the index in case any documents were removed
    index.clearObjects()
    
    // Finally save the new documents and output a success message
    const { objectIDs } = await index.saveObjects(docs)
    consola.success(
      `Indexed ${objectIDs.length} records in Algolia for: ${indexName}`
    )
  }
})

Conclusion

Nuxt modules are super powerful tools that allow you to hook into different points of the Nuxt lifecycle and make standard ways of dealing with the same problem across projects. Nuxt modules provide the perfect avenue for taking content stored with Nuxt content in your file system and mirroring that content in a powerful search index like Algolia.

If you're interested in taking the next step with Nuxt + Algolia and actually implementing the search UI, sign up for the newsletter so you don't miss the next post!

    Join the Newsletter

    Subscribe to get the latest content by email.
    We won't send you spam. Unsubscribe at any time.