Skip to content

Next.js NoSSR-component & obfuscating email-addresses

During the last few weeks I rebuild my personal website (in fact THIS very website) with Next.js. One of the many questions I had to ask myself was how to embed my email as safely as possible and giving those nasty web scraping bots a hard time getting it. As often in development, there are a multiple ways to go about this as there is more than one way how to fetch email-addresses from any website.

On the least recent version of my portfolio website, which was a simple HTML & CSS site without any CMS in the background, I used a method where I would only include empty spans with an id in the initial markup and add the email-links into these spans with Vanilla JS only after the page has been fully loaded.

For this new version of my website however, I'm not only using Next.js but also the Headless CMS DatoCMS to manage all of my pages' content + my blog posts. Everything you see on the site can basically be managed from inside of the CMS including the header- & footer content.

As I didn't want to put any HTML-markup into my CMS content and to avoid the usage of dangerouslySetHtml in my React code I decided to tackle this problem in two steps.

Step 1: Exclude email-address from server-side rendering

As you might be aware of, Next.js allows you to server render your pages such that the browser does not just receive an empty HTML-file like it does with any SPA-framework, but the first render actually happens on either request time (for server-rendered pages) or at build time (for static pages). But this makes the job of a bot who tries to scrape an email-address from this plain initial markup very easy.

To tackle this issue, I decided that I want to do this by opting out from SSR for the JSX that renders any email-addresses to the page. Many UI frameworks like Material UI come with their own version of a NoSSR-component for React. However, as I don't want to include a whole UI framework just for this one usecase, I decided to implement it myself and Next.js makes this very easy thanks to their build-in function for dynamic imports. Actually, all that is needed to make this happen is this short code snippet.

import dynamic from "next/dynamic";

const NoSsr: React.FC = ({ children }) => <>{children}</>;

export default dynamic(() => Promise.resolve(NoSsr), {
  ssr: false,
});

Just include this in your project as a separate component file. This can then be used to exclude a specific part of a subtree or a whole component from SSR by wrapping the JSX you want to exclude with this component:

<ul>
  {socialMediaIcons.map(({ id, name, url, reactIconIdentifier }) => {
    const icon = <SocialMediaIconLink /* props left out */ />

    if (reactIconIdentifier === "email") return <NoSSR key={id}>{icon}</NoSSR>;

    return icon;
  })}
</ul>

Step 2: Save fake email in CMS, replace it at runtime

Here I was thinking that step 1 would already suffice. However, I quickly became aware of the fact that Next.js still includes the email-address as part of the JSON-data that gets send out to to the client for initial rehydration. So while the markup itself is clean, the JSON still gives it away.

To tackle this second problem, I came up with the following solution: I would actually save a fake email-address that still looks like a valid one together with my other CMS content and only on the client side after the initial page load I would replace the fake one with my real address.

Finally, I packaged the logic into a resusable custom hook with the end result looking like this.

import { useEffect } from "react";
import Router from "next/router";

const useReplaceHrefInAllMatchedLinks = (urlToMatch: string, replaceUrl: string) => {
  useEffect(() => {
    const updateLinks = () => {
      setTimeout(() => {
        const allLinks = document.querySelectorAll("a");

        allLinks.forEach((link) => {
          if (link.href === urlToMatch) {
            link.href = replaceUrl;
          }
        });
      }, 1000);
    };

    updateLinks();

    Router.events.on("routeChangeComplete", updateLinks);

    return () => {
      Router.events.off("routeChangeComplete", updateLinks);
    };
  }, [replaceUrl, urlToMatch]);
};

export default useReplaceHrefInAllMatchedLinks;

This hook runs everytime a route change happens, but only after one second to ensure that all of the other code has already been run.

Both steps together are at least some kind of safety roadblock, however if you don't want to worry about this at all you might also just want to use a contact form with a honeypot field or a recaptcha to avoid spam and to keep your email safe.

But, If you want to include an email to set the barrier low for any potential client or any other people who want to get in touch with you, I think this two-step approach poses one possible solution on how to tackle this issue in Next.js.

Patrick Obermeier

Patrick Obermeier