GitHub action to publish your blog post to dev.to

I started writing when I joined dev.to in 2017, joining the community motivated me.

After a few articles I decided to create my own personal blog. However, I've always wanted to continue contributing to dev.to. That's why I post articles on my personal blog and then share them on dev.to with the canonical. I suppose it's a standard practice and more than one of you are doing it.

In order to make my life a little easier, I've recently made a GitHub action that posts directly to dev.to when it detects a new article on my blog.

How I detect a new post

To know if the article is new and needs to be published, you can use the markdown metadata to find out. In my case, I keep the date of publication as metadata (in case I want to publish it another day even if it's merged to master).

Then, once it's posted to dev.to with the GitHub action I create another metadata so it gets tagged as published.

Why? Because the GitHub action will run:

  • Whenever something is pushed to master.
  • Every day at 17:00 UTC.

This way, marking the post as already published, we avoid publishing it twice if we push an article to master at 16:00.

GH action diagram to publish to dev.to
GH action diagram to publish to dev.to

GitHub action in action

name: Publishing post

on:
  push:
    branches: [master]
  schedule:
    - cron: '0 17 */1 * *'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2

      - name: Publishing post
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: yarn install --pure-lockfile
      - run: DEV_TO=${{secrets.DEV_TO}} yarn run publish:post
      - run: |
          git config user.name aralroca
          git config user.email aral-rg@hotmail.com
          git add -A
          git diff --quiet && git diff --staged --quiet || git commit -m "[bot] Published to dev.to"
          git push origin master

What it does?

  • Programs the action on push to master and every day at 17:00 UTC using a cron.
  • Installs dependencies with yarn install --pure-lockfile
  • Sets environment variable DEV_TO using GitHub secrets. This is required for our script.
  • Runs our script to publish to dev.to
  • Commits and pushes to master only when there are changes.

Script to publish to dev.to

In our package.json file we have to indicate that the script runs our node file:

{
  "scripts": {
    "publish:post": "node ./publish/index.js"
  }
}

This is the content of our script that publishes articles to dev.to:

async function deploy() {
  const post = getNewPost()

  if (!post) {
    console.log('No new post detected to publish.')
    process.exit()
  }

  await deployToDevTo(post)
}

console.log('Start publishing')
deploy()
  .then(() => {
    console.log('Published!')
    process.exit()
  })
  .catch((e) => {
    console.log('ERROR publishing:', e)
    process.exit()
  })

The getNewPost function returns the post already formatted in the way dev.to needs, null in case that there aren't new posts:

const fs = require('fs')
const path = require('path')
const matter = require('gray-matter')

const deployToDevTo = require('./dev-to')

function getNewPost() {
  const today = new Date()

  return (
    fs
      .readdirSync('posts')
      .map((slug) => {
        const post = matter(fs.readFileSync(path.join('posts', slug)))
        return { ...post, slug }
      })
      .filter((p) => {
        const created = new Date(p.data.created)

        return (
          !p.data.published_devto &&
          created.getDate() === today.getDate() &&
          created.getMonth() === today.getMonth() &&
          created.getFullYear() === today.getFullYear()
        )
      })
      .map(({ slug, data, content }) => {
        const id = slug.replace('.md', '')
        const canonical = `https://aralroca.com/blog/${id}`
        const body = `***Original article: ${canonical}***\n${content}`

        return {
          body_markdown: body,
          canonical_url: canonical,
          created: data.created,
          description: data.description,
          main_image: data.cover_image,
          published: true,
          series: data.series,
          slug,
          tags: data.tags,
          title: data.title,
        }
      })[0] || null
  )
}

I use the gray-matter library to retrieve the markdown metadata and its content.

Here's the deployToDevTo function used in our script:

const fetch = require('isomorphic-unfetch')
const path = require('path')
const fs = require('fs')

function createPost(article) {
  return fetch('https://dev.to/api/articles', {
    method: 'POST',
    headers: {
      'api-key': process.env.DEV_TO,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ article }),
  })
    .then((r) => r.json())
    .then((res) => {
      console.log('dev.to -> OK', `https://dev.to/aralroca/${res.slug}`)
      return res.slug
    })
    .catch((e) => {
      console.log('dev.to -> KO', e)
    })
}

async function deployToDevTo(article) {
  const devToId = await createPost(article)

  if (!devToId) return

  const postPath = path.join('posts', article.slug)
  const post = fs.readFileSync(postPath).toString()
  let occurrences = 0

  // Write 'published_devto' metadata before the second occourrence of ---
  fs.writeFileSync(
    postPath,
    post.replace(/---/g, (m) => {
      occurrences += 1
      if (occurrences === 2) return `published_devto: true\n${m}`
      return m
    })
  )
}

We request to the dev.to API to upload the article and then modify our markdown file to add the published_devto: true metadata. This way, our GitHub action will detect that there are changes to upload to master.

Conclusions

In this short article we've seen how to create a GitHub action to post automatically our personal blog new articles to dev.to. I hope you find it useful.

Discuss on Dev.toDiscuss on TwitterEdit on GitHub
More...