Como importar posts com sintaxe de código colorida no Medium usando o Gatsby

Escrito em 24 de outubro de 2021 - 🕒 5 min. de leitura

Medium é uma ótima plataforma para compartilhar posts com um público mais amplo, mas é um pouco irritante que, para ter códigos, você precisa criar um Gist do GitHub (ou outros) e embuti-los ao post.

Como eu escrevo muitos tutoriais de programação no meu blog, adicionar todos os meus códigos um por um ao Gist do GitHub leva muito tempo, e como eu sou muito preguiçoso, vou automatizar isso.

code snippet to gist

Code snippets no Gatsby

Para adicionar códigos no Gatsby, basta usar o backtick triplo e a linguagem que você está usando, como o exemplo abaixo.

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

Com isso, Gatsby irá gerar um HTML como o abaixo. Isso será importante para mais tarde.

<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>

O problema

O Medium tem uma ferramenta super útil para importar qualquer post de qualquer lugar para um post no Medium, mas infelizmente todos os códigos serão ignorados por esse importe.

Seria perfeito ter uma URL especial para os meus post onde todos os meus códigos fossem substituídos por URLs de Gists do GitHub com aquele código, e isso é exatamente o que farei a seguir 😎.

Criando uma URL especial para o Medium

Eu quero ser capaz de adicionar /medium-import no final da URL dos meus posts e carregar uma página especial com todos os códigos substituídos por Gists do GitHub.

No arquivo gatsby-node.js, na função createPages, eu irei criar uma página adicional com /medium-import no final da URL.

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),
        },
    });
}

Todos os meus posts podem ser acessados via /blog-post-url e também /blog-post-url/medium-import agora.

Gerando um HTML diferente para o Medium

Para a função generateMediumHTML, eu vou usar o querySelectorAll para encontrar todos os Nodes do HTML com códigos e substituí-los por URLs de Gists do GitHub.

Como todo esse código será executado no Node, eu irei precisar do jsdom para poder manipular o DOM do HTML.

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;
};

Todos os códigos serão substituídos por <div class="gatsby-highlight" data-language="javascript">https://gist.github.com/some-gist-id</div>.

Usando a API do Gist

Eu vou usar a API do Gist para duas coisas, para obter todos os meus Gists existentes para evitar a criação do mesmo Gist duas vezes com um request de GET, e para criar um novo Gist com um request de POST.

Como o código será executado no Node, usarei o node-fetch para todas as solicitações da API.

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!");',
            },
        },
    }),
});

Para a função generateGistUrlsForPost, eu irei usar novamente a função querySelectorAll para obter o código por meio da propriedade textContent e, em seguida, enviá-lo ao GitHub por meio da API do Gist, para isso irei precisar de um 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;
};

Renderizando o novo HTML

No template do componente React, eu tenho acesso a um novo atributo chamado mediumHTML dentro do pageContext, que é o novo HTML com todos os códigos substituídos por URLs do Gist.

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
            }
        }
    }
`;

Agora posso usar a ferramenta de importação do Medium para importar qualquer post com a URL /blog-post-url/medium-import.

Com toda essa automação pronta, espere ver muito mais posts meus no Medium 😊.

Tags:


Publicar um comentário

Comentários

Nenhum comentário.