Crossposting articles from Gatsby to Medium, Dev.to, and Hashnode

Photo: Distribute news
“Photo by Markus Spiske on Unsplash
  1. create 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

Publishing an RSS feed from GatsbyJS

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

Retrieve site metadata

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

Setup

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/',
},
});
},
},
},
],
};

Select articles to include in the RSS feed

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
}
}
}
}
`,
},
],
},
},
],
};

Serializing the necessary data

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,
}),
},
],
});
});
},
},
],
},
},
],
};
// 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;
}

Uploading articles to various distribution platforms

Dev.to

Medium.com

Hashnode.com

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mihai Bojin

Mihai Bojin

Software Engineer at heart, Manager by day, Indie Hacker at night. Writing about DevOps, Software engineering, and Cloud computing. Opinions my own.