Crossposting articles from Gatsby to Medium, Dev.to, and Hashnode
Tags: crosspost, gatsby, medium, devto
Published: 2021-08-22 18:17:19

---
title: Crossposting articles from Gatsby to Medium, Dev.to, and Hashnode
published: true
date: 2021-08-21 00:00:00 UTC
tags: crosspost,GatsbyJS,Medium,DevTo
canonical_url: https://MihaiBojin.com/personal-site/crossposting-articles-from-gatsby?utm_source=DevTo&utm_medium=organic&utm_campaign=top-promo
cover_image: https://MihaiBojin.com/static/4eeceea1953bd722f7aa57f1efbb37bf/073a4/markus-spiske-SOVuvSqyHNY-unsplash.jpg
series: Building a developer blog with GatsbyJS
---

As sole authors, especially when starting up, we don't have established audiences. Without a critical mass of people to reach, the content we put so much love and care into ends up being ignored and not helping anyone. This is tough to deal with from a psychological level, but also what many people call ' **the grind**.'

With all that, it's essential to take any opportunity to reach a broader audience. One solution is to crosspost our content on various distribution platforms.

Since I write for software developers, my go-to places are [Medium.com](https://medium.com), [Dev.To](https://dev.to), and [Hashnode.com](https://hashnode.com).

* * *

Generally, this is a two-step process:

1. create an [RSS feed](https://www.gatsbyjs.com/docs/how-to/adding-common-features/adding-an-rss-feed/) containing your most recent articles
2. (a) have the destination monitor the feed and pull new articles or (b) use automation tools to push new content using an API

Let's dig in!

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#publishing-an-rss-feed-from-gatsbyjs)Publishing an RSS feed from GatsbyJS

Start by installing one of Gatsby's plugins:

`npm install gatsby-plugin-feed`

Once installed, add it to your `gatsby-config.js` file:

```javascript
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        query: ``,
        setup(options) {
          return {};
        },
        feeds: [
          {
            title: "RSS feed",
            output: '/rss.xml',
            query: ``,
            serialize: ({ query: {} }) => {},
          },
        ],
      },
    },
  ],
};
```

The configuration above is not enough to generate a usable feed.

Let me break it down, option-by-option.

> Note: when I first implemented the RSS feed on my site, I struggled to find examples that explained how I could achieve everything I wanted. I hope this article will help others avoid my experience.

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#retrieve-site-metadata)Retrieve site metadata

First, each feed will need to access the site's metadata. The format of your site's metadata can be different, but generally, the GraphQL query to select it will look as follows. If it doesn't work, you can always start your GatsbyJS server in development mode (`gatsby develop`) and debug the query using the [GraphiQL explorer](http://localhost:8000/___graphql).

```javascript
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        query: `
          {
            site {
              siteMetadata {
                title
                description
                author {
                  name
                }
                siteUrl
              }
            }
          }        
        `,
      },
    },
  ],
};
```

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#setup)Setup

The `gatsby-plugin-feed` relies on a few pre-defined (but not well-documented) field names to build the resulting RSS.

Below, you can see a snippet with line-by-line explanations:

```javascript
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        setup(options) {
          return Object.assign({}, options.query.site.siteMetadata, {
            // make the markdown available to each feed
            allMarkdownRemark: options.query.allMarkdownRemark,
            // note the <generator> field (optional)
            generator: process.env.SITE_NAME,
            // publish the site author's name (optional)
            author: options.query.site.siteMetadata.author.name,
            // publish the site's base URL in the RSS feed (optional)
            site_url: options.query.site.siteMetadata.siteUrl,
            custom_namespaces: {
              // support additional RSS/XML namespaces (see the feed generation section below)
              cc:
                'http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html',
              dc: 'http://purl.org/dc/elements/1.1/',
              media: 'http://search.yahoo.com/mrss/',
            },
          });
        },
      },
    },
  ],
};
```

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#select-articles-to-include-in-the-rss-feed)Select articles to include in the RSS feed

This and the next sections are highly specific to your site and are based on how you set up your posts' frontmatter. You will likely need to customize these, but they should hopefully serve as an example of how to generate a feed will all the necessary article data.

The provided GraphQL query filters articles with `includeInRSS: true` in their frontmatter and sorts the results by publishing date (`frontmatter___date`), most recent first.

Since articles on my site support a featured (cover) image (`featuredImage {...}`), we want to select and include it in the feed, along with the `alt` and `title` fields.

We also select several other fields from frontmatter and some made available by `allMarkdownRemark`.

```javascript
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        feeds: [
          {
            query: `
              {
                allMarkdownRemark(
                  filter: { frontmatter: { includeInRSS: { eq: true } } }
                  sort: { order: DESC, fields: [frontmatter___date] },
                ) {
                  nodes {
                    excerpt
                    html
                    rawMarkdownBody
                    fields {
                      slug
                    }
                    frontmatter {
                      title
                      description
                      date
                      featuredImage {
                        image {
                          childImageSharp {
                            gatsbyImageData(layout: CONSTRAINED)
                          }
                        }
                        imageAlt
                        imageTitleHtml
                      }
                      category {
                        title
                      }
                      tags
                    }
                  }
                }
              }            
            `,
          },
        ],
      },
    },
  ],
};
```

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#serializing-the-necessary-data)Serializing the necessary data

The last step in the generation process is to merge all the available data and generate complete feed entries. This is achieved during serialization:

```javascript
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        feeds: [
          {
            serialize: ({ query: { site, allMarkdownRemark } }) => {
              // iterate and process all nodes (articles)
              return allMarkdownRemark.nodes.map((node) => {
                // store a few shorthands that we'll need multiple times
                const siteUrl = site.siteMetadata.siteUrl;
                const authorName = site.siteMetadata.author.name;
                
                // populate the canonical URL
                const articleUrl = `${siteUrl}${node.fields.slug}`;
                
                // retrieve the URL (src=...) of the article's cover image
                const featuredImage =
                  siteUrl +
                  node.frontmatter.featuredImage?.image
                    .childImageSharp.gatsbyImageData.images.fallback
                    .src;

                // augment each node's frontmatter with extra information
                return Object.assign({}, node.frontmatter, {
                  // if a description isn't provided,
                  // use the auto-generated excerpt
                  description:
                    node.frontmatter.description || node.excerpt,
                  // article link, used to populate canonical URLs
                  link: articleUrl,
                  // trick: you also need to specify the 'url' attribute so that the feed's
                  // guid is labeled as a permanent link, e.g.: <guid isPermaLink="true">
                  url: articleUrl,
                  // specify the cover image
                  enclosure: {
                    url: featuredImage,
                  },
                  // process local tags and make them usable on Twitter
                  // note: we're publishing tags as categories, as per the RSS2 spec
                  // see: https://validator.w3.org/feed/docs/rss2.html#ltcategorygtSubelementOfLtitemgt
                  categories: node.frontmatter.tags
                    .map((tag) => makeTwitterTag(tag))
                    // only include the 5 top-most tags (most platforms support 5 or less)
                    .slice(0, 5),
                  custom_elements: [
                    // advertise the article author's name
                    { author: site.siteMetadata.author.name },
                    // supply an image to be used as a thumbnail in your RSS (optional)
                    {
                      'media:thumbnail': {
                        _attr: { url: featuredImage },
                      },
                    },
                    // specify your content's license
                    {
                      'cc:license':
                        'https://creativecommons.org/licenses/by-nc-sa/4.0/',
                    },
                    // advertise the site's primary author
                    {
                      'dc:creator': renderHtmlLink({
                        href: siteUrl,
                        title: process.env.SITE_NAME,
                        text: authorName,
                      }),
                    },
                    // the main article body
                    {
                      'content:encoded':
                        // prepend the feature image as HTML
                        generateFeaturedImageHtml({
                          src: featuredImage,
                          imageAlt:
                            node.frontmatter.featuredImage?.imageAlt,
                          imageTitleHtml:
                            node.frontmatter.featuredImage?.imageTitleHtml
                        }) +
                        // append the content, fixing any relative links
                        fixRelativeLinks({
                          html: node.html,
                          siteUrl: site.siteMetadata.siteUrl,
                        }),
                    },
                  ],
                });
              });
            },
          },
        ],
      },
    },
  ],
};
```

There are a few caveats here:

I'm using a few custom functions I wrote, here is the source code:

```javascript
// Generates HTML for the featured image, to prepend it to the node's HTML
// so that sites like Medium/Dev.to can include the image by default
function generateFeaturedImageHtml({
  src,
  imageAlt,
  imageTitleHtml,
}) {
  const caption = imageTitleHtml
    ? `<figcaption>"${imageTitleHtml}"</figcaption>`
    : '';
  return `<figure><img src="${src}" alt="${imageAlt}" />${caption}</figure>`;
}

// Takes a tag that may contain multiple words and
// returns a concatenated tag, with every first letter capitalized
function makeTwitterTag(tag) {
  const slug = tag
    .replaceAll(/[^\w]+/g, ' ')
    .split(/[]+/)
    .map((word) => upperFirst(word))
    .join('');
  if (slug.length === 0) {
    throw new Error(
      `Invalid tag, cannot create empty slug from: ${tag}`,
    );
  }
  return slug;
}

// Prepends the siteURL on any relative links
function fixRelativeLinks({ html, siteUrl }) {
  // fix static links
  html = html.replace(
    /(?<=\"|\s)\/static\//g,
    `${siteUrl}\/static\/`,
  );

  // fix relative links
  html = html.replace(/(?<=href=\")\//g, `${siteUrl}\/`);

  return html;
}
```

Most of the extra tags are optional but can be helpful if your RSS gets redistributed further, in which case you'd want users to have as much metadata about you and your site as possible!

We prepend the cover image using `generateFeaturedImageHtml`, because [Medium's API](https://github.com/Medium/medium-api-docs) does not support the cover image as a field. However, it parses the first image of the submitted raw content and stores it as a cover. This qualifies as a 'trick' 😊.

Since GatsbyJS uses React routes, all internal links will end up as relative links in the RSS. We fix this by prepending the site URL on any such links (this includes images), using the `fixRelativeLinks` function. (Shoutout to [Mark Shust](https://markshust.com/2020/06/25/fixing-images-in-gatsby-rss-feeds/), who documented how to fix this, first!)

When specifying tags in your frontmatter, define the most relevant ones up-top. For example, Medium only supports five tags, and DevTo supports four, which is why we truncate the number of tags(categories) included in the RSS.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#uploading-articles-to-various-distribution-platforms)Uploading articles to various distribution platforms

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#devto)Dev.to

Dev.to supports [publishing to DEV Community from RSS](https://dev.to/settings/extensions). This is very easy to set up once you've done all the above. They will parse your RSS feed and create draft articles from new content. You can then review and publish these in the DEV community!

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#mediumcom)Medium.com

Medium does not support reading RSS feeds, so in this case, [we have to rely on Zapier to crosspost](https://www.mariokandut.com/how-to-automatically-post-from-gatsby-to-medium-and-dev-to/).

I won't repeat the same content here; please see the tutorial linked above for details on how to configure [Zapier](https://zapier.com), and have it crosspost your content, for free!

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#hashnodecom)Hashnode.com

The more recent platform I'm crossposting to is [Hashnode.com](https://Hashnode.com).

Since I've been using Zapier for Medium for a while, I thought it would be fun to build a [Zapier integration](https://platform.zapier.com/quickstart/create-integration). I started with their UI and later exported and completed the code, so that [everyone can use it!](https://github.com/MihaiBojin/zapier-integration-create-hashnode-story)

If you're a developer, feel free to clone or fork [my Zapier integration](https://github.com/MihaiBojin/zapier-integration-create-hashnode-story), deploy it in your account, and use it to crosspost on Zapier.com.

Unfortunately, Zapier requires any officially published integrations to be supported by the destination platform (in this case, [Hashnode](https://hashnode.com)). I've reached out to ask them to take over this code and make it available for their community, but so far, I haven't heard back.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#conclusion)Conclusion

I hope this helps GatsbyJS users properly define RSS feeds and successfully crosspost their articles to reach a wider audience. If you follow this tutorial and struggle, please reach out via [DM](https://twitter.com/mihaibojin)!

Thanks!

* * *

If you liked this article and want to read more like it, [please subscribe to my newsletter](https://motivated-founder-807.ck.page/db1cf284bf "newsletter link"); I send one out every few weeks!