DEV Community

loading...
Cover image for Build your own Transistor.fm site with Gatsby

Build your own Transistor.fm site with Gatsby

notrab profile image Jamie Barton ・5 min read

A few weeks ago I started to spec a website for an upcoming podcast series I'm recording. I'm using the popular Transistor.fm service and while they provide a hosted podcast website, it hit me there was no API if I wanted to go a little further and build my own brand around the podcast.

I'm a fan of Gatsby and while I could go about storing my podcast data separately in the repo or a headless CMS, it meant maintaining duplicate data about each podcast episode.

Gatsby has a great plugin ecosystem, and because it's GraphQL native, I can query for data across various APIs as I would just one, so I was beginning to explore other podcast services until I had the thought of scraping the Transistor.fm hosted website for data around my podcast and automatically syncing it with Gatsby.

I initially explored this idea by investigating libraries that would enable me to get the data I want, and in a way that build time wouldn't take a dive with Gatsby.

It then hit me that the way in which you submit podcasts to the likes of Spotify and iTunes is done by an RSS feed, and surely RSS is more predictable to parse than a webpage that could be updated at any time.

Creating a Gatsby plugin

I knew that I would end up wanting to get episode data for multiple podcasts over time, and I'm in favour of abstracting complexity where necessary, so I opted to create a Gatsby source plugin.

I won't go into detail about how to create a Gatsby plugin because Gatsby do a damn good job at explaining it better than I ever could, but for those interested, they have a dedicated section in their docs on how to do this.

I searched NPM for active RSS libraries that would help me read a remote RSS feed and parse the contents as JSON. I ended up discovering rss-parser.

The basic idea was simple, provide a custom Transistor.fm feed URL and parse it with rss-parser.

If I were to create a Gatsby source plugin, I'd need to hook into the sourceNodes API to build my local nodes based on the data returned from Transistor.

The code below is what I put together in less than an hour and successfully had nodes generated in Gatsby that represented my Transistor.fm hosted podcast.

const Parser = require('rss-parser');

exports.sourceNodes = async (
  { actions, cache, createNodeId, createContentDigest, reporter, store },
  { url }
) => {
  const { createNode } = actions;

  const parser = new Parser();

  const { items, ...show } = await parser.parseURL(url);

  items.forEach(item => {
    const nodeId = createNodeId(item.link);

    createNode({
      ...item,
      id: nodeId,
      internal: {
        contentDigest: createContentDigest(item),
        type: `TransistorEpisode`,
      },
    });
  });
};

That's it! 😍

Now I know there's a few things wrong with this;

  1. What happens if no url was provided?
  2. What about podcast artwork?
  3. What about JSON parse errors?

So I spent a few more minutes reminding myself on how createRemoteFileNode works, and how this could be used to download and create local file nodes from the podcast artwork hosted by Transistor.

I ended up modifying the code to look a little something like this:

const Parser = require('rss-parser');
const { createRemoteFileNode } = require('gatsby-source-filesystem');

exports.sourceNodes = async (
  { actions, cache, createNodeId, createContentDigest, reporter, store },
  { url }
) => {
  if (!url)
    return reporter.panicOnBuild(
      'gatsby-source-transistorfm: You must provide a url for your feed'
    );

  let feed;

  if (url.includes('http')) {
    feed = url;
  } else {
    feed = `https://feeds.transistor.fm/${url}`;
  }

  const { createNode } = actions;

  const parser = new Parser();

  const { items, image, ...show } = await parser.parseURL(feed);

  items.forEach(item => {
    const nodeId = createNodeId(item.link);

    createNode({
      ...item,
      id: nodeId,
      internal: {
        contentDigest: createContentDigest(item),
        type: `TransistorEpisode`,
      },
    });
  });

  if (image && image.url) {
    let imageNode;

    try {
      const { id } = await createRemoteFileNode({
        url: image.url,
        parentNodeId: show.id,
        store,
        cache,
        createNode,
        createNodeId,
      });

      imageNode = id;
    } catch (err) {
      reporter.error('gatsby-source-transistorfm', err);
    }

    await createNode({
      ...show,
      id: url,
      image___NODE: imageNode,
      internal: {
        type: `TransistorShow`,
        contentDigest: createContentDigest(show),
      },
    });
  }
};

Great! We've got images being fetched remotely and created as nodes on Gatsby build. I also made some naive attempts to show an error during build if no URL was provided, along with enabling the ability to provide the relative Transistor.fm feed name, instead of a full absolute URL.

Once I had this, and I fit the criteria of building a Gatsby plugin, I went ahead and published it to NPM.

Using the plugin

Now using the plugin is just like any other. Install the package via NPM or Yarn and add the plugin to gatsby-config.js and provide the url that the plugin expects.

yarn add gatsby-source-transistorfm
// In your gatsby-config.js
plugins: [
  {
    resolve: 'gatsby-source-transistorfm',
    options: {
      url: '...',
    },
  },
];

I'd also recommend installing gatsby-plugin-sharp, gatsby-transformer-sharp and gatsby-image to take full advantage of the local image node we created at build time.

It was then time to create a page to display our podcast and episode data.

// In your pages/*.js
import React from 'react';
import { graphql, useStaticQuery } from 'gatsby';
import Img from 'gatsby-image';
import ReactAudioPlayer from 'react-audio-player';

const pageQuery = graphql`
  {
    show: transistorShow {
      id
      title
      description
      image {
        childImageSharp {
          fluid(maxWidth: 560) {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }

    episodes: allTransistorEpisode {
      nodes {
        id
        title
        content
        enclosure {
          url
        }
      }
    }
  }
`;

const IndexPage = () => {
  const {
    show,
    episodes: { nodes: episodes },
  } = useStaticQuery(pageQuery);

  return (
    <React.Fragment>
      <h1>{show.title}</h1>
      <p>{show.description}</p>

      <Img
        fluid={show.image.childImageSharp.fluid}
        style={{ width: '260px' }}
      />

      <hr />

      {episodes.map(episode => (
        <article key={episode.id}>
          <h2>{episode.title}</h2>
          <p>{episode.content}</p>
          <ReactAudioPlayer
            src={episode.enclosure.url}
            controls
            preload="none"
          />
        </article>
      ))}
    </React.Fragment>
  );
};

export default IndexPage;

You can see in the above I also installed react-audio-player which is useful for showing an <audio> player that I can control with React.

Once that was done, it ended up looking something like this:

gatsby-source-transistorfm

And that's all! While there still a few bugs to fix and features to add, I have a functioning source plugin that sources my Transistor.fm data. I imagine the same plugin will work for any type of podcast but I've only tested it with Transistor so far!

Next up is to take what we have here and build out a Transistor.fm Gatsby theme. You can see that in progress here.

I hope this helps and inspires others to create Gatsby plugins. It was great fun! 😍

Discussion (0)

pic
Editor guide