April 27, 2021

Inline audio player in Sanity.io rich text

By Henrique Doro

How to use the Portable Text Editor's flexibility to insert dynamic content in the middle of paragraphs

Ξ» npm install -g @sanity/cli
Ξ» sanity init
Get started for free

We can host audio files in Sanity and plug them into paragraphs by adding custom types to the block.of array. You can see this in action in my Learn GROQ guide. Without further ado, here's the process:

Starting with the schema, we need to add the inlineAudio schema type to our block content's of property (documentation on this property):

export default {
  name: "contentBody",
  title: 'Body of content',
  type: 'array',
  of: [
    {
      type: 'block',
      of: [
        {
          name: 'inlineAudio',
          type: 'file',
          title: 'Inline audio player',
          options: {
            accept: 'audio/*',
          },
        },
      ],
    },
    {type: "image"}
  ]
}

This will allow us to add the "Inline audio player" element to our paragraph from the insert menu:

Screenshot of the rich text editor of this guide with the insert menu open

Now editors have the ability to add inline audio players to their content! πŸŽ‰πŸŽ‰ Let's cover how to render this in the front-end. Here's a portion of the component that renders the block content of my articles (I'm using React and the @sanity/block-content-to-react package):

import * as React from 'react'
import BlockContent from '@sanity/block-content-to-react'

import AudioPlayer from '../AudioPlayer'

const serializers = {
  types: {
    // Handler for the "inlineAudio" _type
    inlineAudio: ({ node }) => {
      // The component we use to render the actual player
      return <AudioPlayer {...node} />
    },
  },
}

const ArticleBlockContent = (props) => {
  return (
    <BlockContent
      blocks={props.blocks}
      serializers={serializers}
    />
  )
}

export default ArticleBlockContent

And here's the AudioPlayer component that actually renders the data into an actionable button for users:

import * as React from 'react'

const AudioPlayer = (props) => {
  // Used to store the audio element once instanciated
  const [audioEl, setAudioEl] = React.useState()

  if (!props.asset?._ref) {
    return null
  }
  
  const { _ref: ref } = props.asset
  // Example:
  // From: file-ff7d1c2d7bd5ac367359d57f0319f5f458bc3c3d-m4a
  // To: https://cdn.sanity.io/files/q2j8cwsg/production/ff7sfgc2d7bd5ac367359d57f0319f5f458bc3c3d.m4a?dl

  const assetRefParts = ref.split('-') // ["file", "ff7...", "m4a"]
  const id = assetRefParts[1] // "ff7..."
  const format = assetRefParts[2] // "m4a"
  const assetUrl = `https://cdn.sanity.io/files/${process.env.NEXT_PUBLIC_SANITY_PROJECT_ID}/${process.env.NEXT_PUBLIC_SANITY_DATASET}/${id}.${format}`

  function playAudio() {
    try {
      if (!audioEl) {
        const audio = new Audio(assetUrl)
        setAudioEl(audio)
        audio.play()
      } else {
        audioEl.play()
      }
    } catch (error) {}
  }
  return (
    <button onClick={playAudio} aria-label="Play audio">
      πŸ”Š
    </button>
  )
}

export default AudioPlayer

And that's it! There are many more interesting use cases of inline blocks, some of which I hope to cover in the future.

Reach me at meet@hdoro.dev or @hdorodev if you have any questions ;)

Ξ» npm install -g @sanity/cli
Ξ» sanity init
Get started for free