Inline audio player in Sanity.io rich text
How to use the Portable Text Editor's flexibility to insert dynamic content in the middle of paragraphs
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"} ] }
The `of` array defines which custom types should be rendered inline Accept only audio files Notice the difference with custom types at the same level as the `type: block`: these, like the image here, will be standalone blocks which are placed between paragraphs, not inside them.
This will allow us to add the "Inline audio player" element to our paragraph from the insert menu:
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 ;)
PS: This is also available in video on YouTube: https://www.youtube.com/watch?v=syFw-_XibFE