Repost Markdown Posts on Medium with NodeJS & API Automation

radzion

Radzion Chachura

Posted on January 9, 2023

Repost Markdown Posts on Medium with NodeJS & API Automation

Watch on YouTube | 🐙 GitHub

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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,
    tags: keywords,
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}`
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
radzion
Radzion Chachura

Posted on January 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related