Self-hosting Podcasts with GatsbyJS
Gatsby 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 Gatsby website.
One popular way to create articles in Gatsby 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 Gatsby, 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 Gatsby starter.
Conclusion
Hopefully, I could show in this article that Gatsby can easily be extended to include additional functionality, like a self-hosting podcast page.
Further Reading
- Gatsby: https://www.gatsbyjs.org/
- React: https://reactjs.org/
Published
6 Jun 2019