DEV Community

Cover image for Want to crosspost to dev.to? There's a GitHub action for that.
Jeremy Ward πŸ˜ŽπŸ€“
Jeremy Ward πŸ˜ŽπŸ€“

Posted on • Updated on

Want to crosspost to dev.to? There's a GitHub action for that.

If a tree falls in the woods, does it make a sound?

The internet is much like a forest with content and sites being like trees. There are thousands of trees growing in the forest off the beaten path that no one knows about. If you want your site or content to be discovered, the easiest way is to plant your tree close to the main trail.

Over the last 6 years of Software Development, I've learned a lot, but I've never shared it outside of my full-time job. If there's not an easy way for people to find the blogs I put out, then I'm still not sharing my thoughts with others. This is where sites like dev.to and hashnode come into play. I can write the markdown for my blog posts in vim then crosspost to other blog distribution platforms. I definitely didn't want to copypasta all my content every time, so I've decided to automate this process and open-source it.

  1. Let's go over the code.
  2. Let's take a look at how to use the GitHub Action.

DaCode

The first order of business is to find out if your last commit contains articles.

In order to do so, we first require two inputs from the GitHub workflow: github-token & content-dir. These two arguments will be retrieved using the @actions/core package. The github-token argument will be used to initiate an Octokit Rest client using the @actions/github package. We can use this instance to request the commit data. Once we have the response, all we have to do is iterate over the list of files and see if the filename match a given content directory (from the content-dir argument).

import * as core from '@actions/core';
import * as github from '@actions/github';

export const getFiles = async (): Promise<string[]> => {
  const octokit = github.getOctokit(core.getInput('github-token'));

  const commit = await octokit.repos.getCommit({
    ...github.context.repo,
    ref: github.context.sha,
  });

  return (commit?.data?.files || [])
    .map((file: any) => file.filename)
    .filter((filename: string) => filename.includes(core.getInput('content-dir')));
};
Enter fullscreen mode Exit fullscreen mode

If this code finds markdown files, we will cycle through each file, sending it into a publish function.
We will use Node's file system readFileSync function to read the file into memory. Then, use the @github-docs/frontmatter package to parse the markdown so we can checkout the frontmatter which is just the "data" at the top of markdown files.

If the frontmatter indicates the post is published, we can go ahead and start crossposting.
Let's have a look at the current version of the publish function.

import * as core from '@actions/core';
import * as fs from 'fs';
import devTo from './dev-to';

const frontmatter = require('@github-docs/frontmatter');

const logResponse = async (title: string, destination: string, publishCallback: Promise<any>): Promise<void> => {
  const {status} = await publishCallback;
  console.table({title, destination, status});
};

const publish = async (path: string): Promise<void> => {
  try {
    const markdown = fs.readFileSync(`./${path}`, 'utf8');
    const {data} = frontmatter(markdown);

    if (data.published) {
      if (core.getInput('dev-to-token').length > 0) {
        logResponse(data.title, 'Dev.to', devTo.publish(markdown));
      }
    } else {
      console.log(`Article ${data.title} NOT published. Skipping.`);
    }
  } catch (err) {
    core.setFailed(err.message);
  }
};

export default publish;
Enter fullscreen mode Exit fullscreen mode

Currently, I am only crossposting to dev.to, but soon I want to update the GitHub Action to crosspost to hashnode.com as well. πŸ€™

To crosspost to dev.to, the publish function passes the markdown into the publish method on an authenticated instance of DevTo.
The devTo.publish method uses the auth token and node-fetch to POST your markdown to dev.to's api and boom it's done.

And here is the dead simple code for our DevTo class:

import fetch from 'node-fetch';
import * as core from '@actions/core';

class DevTo {
  token: string;
  constructor() {
    this.token = core.getInput('dev-to-token');
  }

  async publish(body_markdown: string): Promise<any> {
    const body = {
      article: {
        body_markdown,
      },
    };

    return fetch('https://dev.to/api/articles', {
      method: 'post',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json',
        'api-key': this.token,
      },
    }).then((response: any) => response.json());
  }
}

export default new DevTo();
Enter fullscreen mode Exit fullscreen mode

That's really it. That's all the code needed to keep you from having to copy and paste your blog markdown to multiple distributors!

DaAction

As I was writing this code, I was like why don't I make it a GitHub Action and share it with anyone that would like to use it. So I did 😎.

Let's take a look at how you can use this action in your GitHub blog repo.

Inside your GitHub repo, add a workflow file. Mine is .github/workflows/crosspost.yml.

I'm using nuxt's content module to build my blog, and have it configured to look for blog posts in the ./content/articles/ directory. So, let's tell our action to only run when a file is changed in that directory:

name: CrossPost

on:
  push:
    paths:
    - './content/articles/*'
Enter fullscreen mode Exit fullscreen mode

Next, we need to start writing the yaml for the job itself. First, we will check out the code:

jobs:
  crosspost:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Code
      uses: actions/checkout@v2
Enter fullscreen mode Exit fullscreen mode

Super simple.

Next step will be to run the crosspost-markdown action and pass in the necessary arguments (content-dir and dev-to-token). These can be set in the secrets section of your repo's settings.

jobs:
  crosspost:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Code
      uses: actions/checkout@v2

    - uses: basicBrogrammer/crosspost-markdown@v0.1.0 # remember to see what the latest version is 8)
      with:
        content-dir: './content/articles/'
        dev-to-token: ${{ secrets.DEV_TO }}
        github-token: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

All done. Now when you push to your repo the GitHub Action will run and if you have any new blog posts it will publish them to dev.to.

Cache me on Tweeter, HowBowDat? Click Me. I dare you.

Top comments (8)

Collapse
 
yoursunny profile image
Junxiao Shi

I'm cross-posting manually as I don't have that many articles.
One issue is that, I need to adjust the embedded images from relative path to absolute path including the domain.
How did you solve the image path problem?

Collapse
 
basicbrogrammer profile image
Jeremy Ward πŸ˜ŽπŸ€“

I'm guessing to solve this problem it may be specialized depending on if you are using gatsby, nuxt, hugo, jekyll etc but if we could figure it out that would be awesome. I'll start trying to figure out how to make it work for nuxt and put it in the readme of crosspost-markdown

Collapse
 
basicbrogrammer profile image
Jeremy Ward πŸ˜ŽπŸ€“

I haven't yet. Currently I'm using Google images that are copy right free

Collapse
 
stereobooster profile image
stereobooster

Oh nice idea. How I haven't thought about it before. I wonder if there are action to crosspost to other platforms

Collapse
 
basicbrogrammer profile image
Jeremy Ward πŸ˜ŽπŸ€“

I'm planning on adding hashnode and medium since they allow posting markdown. If you want to take a wack at one of those or have ideas to make it better send me a PR github.com/basicBrogrammer/crosspo...

Warning I haven't added tests yet :( lol but its a WIP
Bug I noticed today It reposted one of my previous posts bc I updated it. I need to figure out a way to decide to do a POST or a PUT for the articles πŸ€”

Collapse
 
stereobooster profile image
stereobooster

In my blog when I crosspost to another platform I add id to the front matter (see example here). My blog doesn't have comments instead I have links in the bottom, which says "discuss on dev.to" and it links to my crossposted article on dev.to.

So one way to handle it is to update frontmatter and commit again

Thread Thread
 
basicbrogrammer profile image
Jeremy Ward πŸ˜ŽπŸ€“

nice update the frontmatter to include the dev.to (other platform) link and queue off of that πŸ‘ I dig it.
Also, I'm gonna steal your "discuss" idea bc my site doesn't have comments either 🀣

Thread Thread
 
basicbrogrammer profile image
Jeremy Ward πŸ˜ŽπŸ€“

Did some quick refactoring to handle the POST or PUT. The Action will print out the dev.to slug and id and those can be added to the front matter (devToSlug and devToId respectively) I'll cut a release tomorrow morning πŸ‘
github.com/basicBrogrammer/crosspo...