coding

Adding a copy button to code snippets on Gatsby: The lazy way

Don't feel like creating a Gatsby plugin? Then this blog post is for you.

Written in May 5, 2022 - 🕒 2 min. read

I constantly write about code and share code snippets in my blog posts and copy-pasting code is an essential skill for software engineers, and since selecting a text then pressing CTRL + C then CTRL + V is a lot of work, I decided to add a copy button to all my code snippets to facilitate the life of all my fellow software engineers.

Possible solutions

The proper way to do this is by creating a plugin to tap into the Gatsby HTML generation process and access the code snippets object in the AST data structure. There is a very nice tutorial by @thundermiracle on how to do this.

Of course, I decided to do it the shitty way, which is by altering the HTML string on the createPages function in the gatsby-node.js file.

The code

Inside the loop that creates the post pages, I added the following code on gatsby-node.js:

exports.createPages = async function ({ actions, graphql }) {
    const { data } = await graphql(` /* some graphQL query */ `);
    data.allMarkdownRemark.edges.forEach((edge) => {
        const alternativeHtml = generateAlternativeHtml(edge.node.html);
        actions.createPage({
            component: require.resolve(`./src/templates/PostTemplate.jsx`),
            context: { alternativeHtml },
        })
    })
}

For the generateAlternativeHtml function I will use the jsdom package to parse the HTML string and convert it to a DOM object and use the querySelectorAll method to find all the code snippets and add the copy button to them.

function generateAlternativeHtml(html) {
    const dom = new JSDOM(html);
    const { document } = dom.window;
    const codeSnippets = document.querySelectorAll('.gatsby-highlight');

    codeSnippets.forEach((codeSnippet) => {
        const wrapper = document.createElement('div');
        wrapper.classList.add('copy-code-block');

        // add your own css styles here, css classes, etc.
        const copyButton = document.createElement('button');
        copyButton.innerHTML = 'Copy';
        codeSnippet.insertAdjacentHTML(`afterend`, copyButton);
    });

    return document.body.innerHTML;
}

Now all the code snippets will have a copy button, but the button doesn’t do anything. I will add the onclick event handler to the copy button to copy the code snippet to the clipboard. That needs to be done in the React part of the code with an useEffect in the PostTemplate.jsx file.

To copy the code snippet to the clipboard I will use the clipboard-copy package.

import copy from 'clipboard-copy';

const BlogPostTemplate = ({ data, pageContext }) => {
    const { alternativeHtml } = pageContext;

    useEffect(() => {
        const codeSnippets = document.querySelectorAll('.copy-code-block button');
        codeSnippets.forEach((elementWrapper) => {
            codeSnippet.addEventListener('click', () => {
                const codeSnippet = elementWrapper.parentElement.querySelector('.gatsby-highlight');
                const code = codeSnippet.innerHTML;
                copy(code);
            });
        });
    }, []);

    return (
        <section>
            <h1>{data.markdownRemark.frontmatter.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: alternativeHtml }} />
        </section>
    );
};

Done! Hacky and gross, but also beautiful in many ways.

Conclusion

This solution adds a tiny bit of overhead to the Static Site Generator process and some DOM manipulation in the client, but it’s a good solution for the time being. If you’re looking for a quick and easy solution, this is the way to go.

That’s it for this post. I hope you enjoyed it.

Tags:


Post a comment

Comments

No comments yet.