coding

How to import Gatsby posts into Medium with Gist code snippets

Written in October 24, 2021 - 🕒 5 min. read

Medium is a great platform to share your blog posts with a wider audience, but it is a bit annoying that to have code snippets you need to create a GitHub Gist (or others) and embed it into the post.

As I write a lot of tutorials in my blog, adding all my code snippets one by one to GitHub Gist takes a lot of time, and I’m very lazy, so let’s automate that.

code snippet to gist

Code snippets on Gatsby

To add code snippets in Gatsby, you only need to use the markdown triple backtick and the language that you’re using, like the example below.

```javascript
    console.log('hello world!');
```

With that, Gatsby will generate an HTML like the one below. This will be important for later.

<div class="gatsby-highlight" data-language="javascript">
    <pre class="language-javascript">
        <code class="language-javascript">
            <!-- the span element is not exactly like this, this is just an example -->
            <span>console.log('hello world!');</span>
        </code>
    </pre>
</div>

The problem

Medium has a super useful tool to import any post from anywhere into a Medium story, but sadly every code snippet will be ignored by it.

Ideally, I could have a special URL for my blog posts where all my code snippets would be replaced by a GitHub Gist URL with that code, and this is exactly what I’m going to do next 😎.

Creating a special URL for Medium

I want to be able to add /medium-import to the end of my blog posts’ URL and load a special post page with all code snippets replaced by GitHub Gists.

On the gatsby-node.js file, in the createPages function, I will create an extra page with the /medium-import path in the end.

const posts = postsResult.data.allMarkdownRemark.edges;
for (const post of posts) {
    createPage({
        path: `${post.node.fields.path}/medium-import`,
        component: path.resolve('./src/templates/MediumPost.jsx'),
        context: {
            mediumHTML: await generateMediumHTML(post.node.html, post.node.frontmatter.title),
        },
    });
}

All my blog posts can be accessed via /blog-post-url and also /blog-post-url/medium-import now.

Generating a different HTML for Medium

For the generateMediumHTML function, I will use the querySelectorAll to find all the HTML nodes with code snippets and replace them with GitHub Gists URLs.

Since all of this code will be executed on Node, I will need jsdom to be able to manipulate the HTML DOM.

const jsdom = require('jsdom');
const generateMediumHTML = async (htmlString, postTitle) => {
    const gistUrls = await generateGistUrlsForPost(htmlString, postTitle);

    const dom = new jsdom.JSDOM(htmlString);
    const result = dom.window.document.querySelectorAll('.gatsby-highlight');
    result.forEach((element, index) => {
        element.textContent = gistUrls[index];
    });

    return dom.window.document.body.innerHTML;
};

All code snippets will be replaced by <div class="gatsby-highlight" data-language="javascript">https://gist.github.com/some-gist-id</div>.

Using the GitHub Gist API

I will use the GitHub Gist API for two things, to get all my existing Gists to avoid creating the same Gist twice with a GET request, and to create a new Gist with a POST request.

Since the code will be executed in Node, I will use node-fetch for the API requests.

const gistAccessToken = process.env.GITHUB_ACCESS_TOKEN;

// Get all existing gists under my github username
const response = await nodeFetch('https://api.github.com/gists', {
    method: 'GET',
    headers: {
        Authorization: `token ${gistAccessToken}`,
        'Content-type': 'application/json',
    },
});
const gistAccessToken = process.env.GITHUB_ACCESS_TOKEN;

// create a new gist
const response = await nodeFetch('https://api.github.com/gists', {
    method: 'POST',
    headers: {
        Authorization: `token ${gistAccessToken}`,
        'Content-type': 'application/json',
    },
    body: JSON.stringify({
        description: 'Code for blog post',
        public: true,
        files: {
            ['file-name.js']: {
                content: 'console.log("hello world!");',
            },
        },
    }),
});

For the generateGistUrlsForPost function, I will again use the querySelectorAll function to get the code via the textContent property and then send it to GitHub via the Gist API, for that I will need a GitHub Personal Access Token.

const generateGistUrlsForPost = async (htmlString, postTitle) => {
    const gistAccessToken = process.env.GITHUB_ACCESS_TOKEN;

    const dom = new jsdom.JSDOM(htmlString);
    const result = dom.window.document.querySelectorAll('.gatsby-highlight > pre > code');
    const slug = convertToKebabCase(postTitle);

    // Get all existing gists under my github username
    const response = await nodeFetch('https://api.github.com/gists', {
        method: 'GET',
        headers: {
            Authorization: `token ${gistAccessToken}`,
            'Content-type': 'application/json',
        },
    });
    const responseData = await response.json();
    const files = responseData.map((data) => Object.keys(data.files));
    const fileNames = files.flat();

    const gistUrls = [];
    let index = 1;
    for (const element of result) {
        const code = element.textContent;
        const extension = element.getAttribute('data-language');
        const fileName = `${slug}-script-${index}.${extension}`;

        // if the gist for the file already exists, then don't create a new one
        if (fileNames.includes(fileName)) {
            const existingGist = responseData.find(
                (data) => Object.keys(data.files).includes(fileName)
            );

            gistUrls.push(existingGist.html_url);
        } else {
            const res = await nodeFetch('https://api.github.com/gists', {
                method: 'POST',
                headers: {
                    Authorization: `token ${gistAccessToken}`,
                    'Content-type': 'application/json',
                },
                body: JSON.stringify({
                    description: `Code for post "${postTitle}"`,
                    public: true,
                    files: {
                        [fileName]: {
                            content: code,
                        },
                    },
                }),
            });

            const data = await res.json();
            gistUrls.push(data.html_url);
        }

        index += 1;
    }

    return gistUrls;
};

Rendering the new HTML

In the React component template, I have access to a new attribute called mediumHTML inside the pageContext, and that is the new HTML with all code snippets replaced with GitHub Gists.

import React from 'react';
import { graphql } from 'gatsby';

const MediumPostTemplate = ({ data, pageContext }) => {
    const { markdownRemark } = data;
    const { title } = markdownRemark.frontmatter;
    const { mediumHTML } = pageContext;

    return (
        <article>
            <header>{title}</header>
            <section
                dangerouslySetInnerHTML={{ __html: mediumHTML }}
            />
        </article>
    );
};

export default MediumPostTemplate;

export const pageQuery = graphql`
    query MediumPostBySlug($slug: String!, $categoryImage: String) {
        markdownRemark(fields: { slug: { eq: $slug } }) {
            frontmatter {
                title
            }
        }
    }
`;

Now I can go into the Medium import tool and import any post via the /blog-post-url/medium-import.

With all this automation in place, expect to see a lot more Medium posts from me 😊.

Tags:


Post a comment

Comments

No comments yet.