Thomas Derflinger BlogFR

Self-hosting Podcasts with GatsbyJS

Podcast image with microphone

GatsbyJS is a fast static Content Management System (CMS). It can be individually extended by anybody knowing a bit of JavaScript and React. Since podcasts are undergoing a revival, this article shows how to self-host podcast audio files on your GatsbyJS website.

One popular way to create articles in GatsbyJS is with markdown (.md) files. These are essentially text files with a special syntax for things like images. These markdown files have also special metadata that can later be queried.

For the purpose of adding audio files to your system, I add a metadata field to the markdown. It is called audio and has a boolean flag. Also, the audio file of your podcast needs to be added to the same directory as the .md file and added as an attachment. Adding the audio file as an attachment ensures that it is copied to the public folder where the generated site resides.

index.md

...
audio: true
attachments:
  - "./podcast.mp3"
...

In this example, the podcast file podcast.mp3 is copied to the public/static folder in the generated site.

Next, we need a page to display the podcasts next to the article preview. This can be achieved by querying all articles for the audio field set to true. This is a GraphQL query and common in GatsbyJS, ensuring that only articles with podcasts are selected.

podcasts.jsx

import React from 'react'
import Helmet from 'react-helmet'
import { graphql } from 'gatsby'
import Layout from '../../components/Layout'
import Post from '../../components/Post'
import Sidebar from '../../components/Sidebar'
import icon32 from '../favicon.ico'

class PodcastsRoute extends React.Component {
  render() {
    const items = []
    const { title, subtitle } = this.props.data.site.siteMetadata
    const posts = this.props.data.allMarkdownRemark.edges
    posts.forEach(post => {
      items.push(<Post data={post} key={post.node.fields.slug} audio="true" />)
    })

    return (
      <Layout>
        <div>
          <Helmet>
            <title>{title}</title>
            <meta name="description" content={subtitle} />
            <link rel="shortcut icon" type="image/ico" href={icon32} />
          </Helmet>
          <Sidebar {...this.props} />
          <div className="content">
            <div className="content__inner">
              <div className="page">
                <h1 className="page__title">Podcasts</h1>
                <div className="page__body">{items}</div>
              </div>
            </div>
          </div>
        </div>
      </Layout>
    )
  }
}

export default PodcastsRoute

export const pageQuery = graphql`
  query IndexQueryPod {
    site {
      siteMetadata {
        title
        subtitle
        copyright
        menu {
          label
          path
          icon
        }
        author {
          name
          email
          telegram
          twitter
          github
          rss
          vk
        }
      }
    }
    allMarkdownRemark(
      limit: 1000
      filter: { frontmatter: { layout: { eq: "post" }, draft: { ne: true }, audio: { eq: true } } }
      sort: { order: DESC, fields: [frontmatter___date] }
    ) {
      edges {
        node {
          fields {
            slug
            categorySlug
          }
          frontmatter {
            title
            lang
            license
            date
            category
            description
            audio
            attachments {
              publicURL
              childImageSharp {
                fixed(width: 150) {
                  ...GatsbyImageSharpFixed
                }
              }
            }
          }
        }
      }
    }
  }
`

The podcasts page uses a modified Post component that also displays an audio widget for all posts that contain a podcast.

components/Post/index.jsx

import React from 'react'
import { Link } from 'gatsby'
import Img from 'gatsby-image'
import moment from 'moment'
import AudioWidget from '../AudioWidget'
import './style.scss'

class Post extends React.Component {
  render() {
    const {
      title, date, category, description, attachments,
    } = this.props.data.node.frontmatter
    const { slug, categorySlug } = this.props.data.node.fields

    return (
      <div>
        <div className="post">
          <div className="post__meta">
            <time className="post__meta-time" dateTime={moment(date).format('MMMM D, YYYY')}>
              {moment(date).format('MMMM YYYY')}
            </time>
            <span className="post__meta-divider" />
            <span className="post__meta-category" key={categorySlug}>
              <Link to={categorySlug} className="post__meta-category-link">
                {category}
              </Link>
            </span>
          </div>
          <div className="post__hover">
            <h2 className="post__title">
              <Link className="post__title-link" to={slug}>
                {title}
              </Link>
            </h2>
          </div>
        </div>
        <div className="breaker" />
        <AudioWidget
          audio={this.props.audio}
          audioFile={attachments[0] ? attachments[0].publicURL : ''}
        />
      </div>
    )
  }
}

export default Post

The podcast audio file needs to be the first attachment. The full file URL is given in the property audioFile of the AudioWidget component.

Now the AudioWidget component is needed.

AudioWidget/index.jsx

import React from 'react'

class AudioWidget extends React.Component {
  render() {
    let audio = null

    if (this.props.audio) {
      audio = (
        <div>
          <audio controls>
            <source src={this.props.audioFile} type="audio/mpeg" />
          </audio>
          <br className="post-breaker" />
        </div>
      )
    }

    return <div>{audio}</div>
  }
}

export default AudioWidget

The audio widget is a simple HTML5 audio control supported by all browsers. No widget is displayed when the audio flag is false.

The code in this example is based on the gatsby-v2-starter-lumen starter.

How this solution looks like can be seen on the Podcasts page on my blog:

https://www.tderflinger.com/en/podcasts

Should you have any questions, please let me know below. If there is sufficient demand I can package this solution as a GatsbyJS starter.

Conclusion

Hopefully, I could show in this article that GatsbyJS can easily be extended to include additional functionality, like a self-hosting podcast page.

Further Reading

Published 7 Jun 2019

Thomas Derflinger

Thomas Derflinger

I am an independent entrepreneur and software developer.

Web is a topic I really enjoy. Let's get in touch!