Repost Markdown Posts on Medium with NodeJS & API Automation

January 5, 2023

4 min read

Repost Markdown Posts on Medium with NodeJS & API Automation
Watch on YouTube

Let's take a blog post stored as a markdown file, convert it into a Medium format with NodeJS, and publish it through Medium API.

Here's an example of a markdown file from my blog. It starts with a metadata section in YAML format that holds basic info, such as title or date, together with a YouTube video I want to promote at the beginning of the post. I store all the images and the markdown for a blog post in the same folder.

---
date: "2022-12-31"
title: "Simple Money Making Model"
description: "How to grow net worth with different types of income"
category: "personal-finance"
youTubeVideo: https://youtu.be/OKbZX0vWEZA
featuredImage: ./main.jpeg
---

Let me share a simple model for increasing net worth that will provide you with more clarity on ways to make and grow your money.

![](./model.png)

Let's start with four fundamental methods of generating wealth. The first and most obvious way is to trade time for money by working at a job or being a solo freelancer...

Once I have a finished blog post, I call the postOnMedium command and pass the slug or folder name as an argument.

npx tsx commands/postOnMedium.tsx money

The function gets the markdown file, converts it to the Medium format, then queries the user to get its id and publishes the story.

const postOnMedium = async (slug: string) => {
  const mediumPost = await prepareContentForMedium(slug)

  const user = await getMediumUser()

  const { url } = await postMediumStory(user.id, {
    ...mediumPost,
    contentFormat: "markdown",
    canonicalUrl: `https://radzion.com/blog/${slug}`,
    publishStatus: "public",
  })

  console.log(url)
}

const args = process.argv.slice(2)

postOnMedium(args[0])

After reading the file, we use the front-matter library to parse the markdown and separate metadata attributes from the content.

const prepareContentForMedium = async (slug: string): Promise<MediumPost> => {
  const markdownFilePath = getPostFilePath(slug, "index.md")
  const markdown = fs.readFileSync(markdownFilePath, "utf8")
  let { body, attributes } = fm(markdown) as ParsedMarkdown
  const { featuredImage, youTubeVideo, demo, github, title, keywords } =
    attributes

  const insertions: string[] = []

  const images = body.match(/\!\[.*\]\(.*\)/g)
  await Promise.all(
    (images || []).map(async (imageToken) => {
      const imageUrl = imageToken.match(/[\(].*[^\)]/)[0].split("(")[1]
      if (imageUrl.startsWith("http")) return

      const imagePath = getPostFilePath(slug, imageUrl)

      const mediumImageUrl = await uploadImageToMedium(imagePath)
      const newImageToken = imageToken.replace(imageUrl, mediumImageUrl)
      body = body.replace(imageToken, newImageToken)
    })
  )

  if (featuredImage) {
    const mediumImageUrl = await uploadImageToMedium(
      getPostFilePath(slug, featuredImage)
    )
    insertions.push(`![](${mediumImageUrl})`)
  }

  if (youTubeVideo) {
    insertions.push(`[👋 **Watch on YouTube**](${youTubeVideo})`)
  }

  const resources: string[] = []
  if (github) {
    resources.push(`[🐙 GitHub](${github})`)
  }
  if (demo) {
    resources.push(`[🎮 Demo](${demo})`)
  }
  if (resources.length) {
    insertions.push(resources.join("  |  "))
  }

  return {
    content: [...insertions, body].join("\n\n"),
    title,
    keywords: keywords,
  }
}

Markdown refers to all our images as local files, so we need to upload them to Medium and update the source to URLs pointing to Medium's storage.

const uploadImageToMedium = async (imagePath: string) => {
  const formData = new FormData()
  const fileStream = fs.createReadStream(imagePath)
  fileStream.pipe(sharp().jpeg())
  const blob = await streamToBlob(fileStream, "image/jpeg")
  formData.append("image", blob)

  const uploadFileResponse = await fetch("https://api.medium.com/v1/images", {
    method: "POST",
    body: formData,
    headers: {
      Authorization: mediumAuthorizationHeader,
    },
  })

  const { data }: FetchResponse<UploadImageResponse> =
    await uploadFileResponse.json()
  return data.url
}

We send the image to Medium by reading the file into a stream, converting it to a blob, and sending it as form data. Since some of my images are in the .webp format, I run them through the sharp library to turn them inoto .jpeg, otherwise Medium will reject them.

Besides changing the images, we want to include the featured image as the first element of markdown and add links to YouTube, Github, and demo if they are present.

To interact with Medium API, we need an integration key that you can get from the settings and store it as an environment variable.

const mediumAuthorizationHeader = `Bearer ${process.env.MEDIUM_INTEGRATION_TOKEN}`

After preparing the content, we can query the user id and publish the story as a duplicate of existing content by specifying a canonical URL.

const getMediumUser = async () => {
  const userResponse = await fetch("https://api.medium.com/v1/me", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: mediumAuthorizationHeader,
    },
  })

  const { data }: FetchResponse<User> = await userResponse.json()

  return data
}