Emailing HTML on NextJS

A picture of wall made out of lego, taken from front face.

Since NextJS handles both backend and frontend seamlessly, emailing things now become another quest to finish, especially when you need to use template. This blog will be filled with my rants and lessons about this quest of creating my contact page. You can also check my logs for this in issue chez14/christianto.net#13.

Previously, in a world where backend handles everything (data flow, frontend codes, etc), emailing something in html format is just another common task to do. All we need to do is just make a template, make sure everything works and compatible with intended receiver, call the template engine to render the template to a variable, and then just send the captured html content, with your library of choice.

In NextJS however, it’s not that easy. React handles things for browser and not email clients, and we can’t just export email-compatible static html element from it, because all styling are not included in one giant string! In addition to that, React might include JS dependencies too, in which, its certainly won’t run in email clients. So the solution is to add our choice of html templating engine.

That being said, adding them isn’t just adding dependencies for the engine and done. The engine need to be able to read the template file, and render things properly too. Since the template file isn’t related with the main site, this is the main challenge in my NextJS project to solve.

The Quest

Let’s start with simple template. This is my email template for Contact Page:

The template contains simple stylings in the head, and a body content in pug. In normal backend stuff, we can just refer this by following code:

Notice how the template is loaded? I’m just using the old-plain path.join, because I thought it can just be loaded like that, and the file will be transported alongside everything, but apparently, not! Both NextJS and Vercel only cares what Webpack says.

Because of this, there are several options we can try:

So let’s start the journey with one that are the most effortless first: loading them with copy-webpack-plugin.

copy-webpack-plugin

Originally, I thought this plugin will also add some kind of entries on the NFTs since it actually copy files directly from the normal path to build path, but apparently it is not. The copied file do get copied to the build folder (in this case, .next folder), but it does not get collected to .vercel/output folder, because the plugin doesn’t add something to the NFTs. This means loading on prod is not possible.

NextJS Build
Vercel Build

file-loader

Then continue with file-loader. We want the pug themselves decide how to load template file. If the template file quite big, they might have mechanism to load them chunk by chunk, though, it might be rare occasion for the template to be more than few MBs big. So this is my reason why I want to try this option.

After configuring (commit e7d23403), immediately when we test it on local, it can’t even find the location mentioned by the file-loader. The file is renamed, and the folder isn’t original path anymore, as expected. The unexpected part of this are, NextJS dev server’s current working directory is still in the project root, not the .next folder. Webpack’s loader’s root directory are set to the .next folder, so upon loaded, the file’s path is actually set to that path, not project path.

If I’m not lazy, I might need to create a new library to handle cases like this, but since one of my requirement of my own website is “be as effortless as you can be”, making another library isn’t one of them. In addition to that, I actually kind of afraid that adding extra loaders as dependencies will actually be a not maintainable solution. When making this blog, I just realized that file-loader is actually have been abandoned, and users informed to use it’s predecessor instead: asset-loader, in which, I actually haven’t tried it yet.

To summarize, with file-loader it can be imported via Webpack, but the path informed by the loader is sort of unusable. So, because of this bummer, I don’t want to continue to explore this option any further.

raw-loader

raw-loader will actually load the file to memory—well, in this case, to variable— every time we import the thing in the script. As mentioned earlier, this is not an ideal solution, because if the file is really big, it will just eats a lot of memory! But well, since it’s a rare occasion for this case, and are I don’t have any other way, this is the last resort I should try.

This is how the code looked like after we configure the module declaration for Typescript (commit 98b99e3b):

So that’s how I did it for now. It works, at least for now, and I don’t have to maintain another library for Webpack/NextJS.

Another Potential Solution

Putting the file to public path of NextJS project will actually bring the file to .vercel/output folder. This… might works, but it violates my personal standards:

  • Never serve any files that doesn’t meant to be accessible from public.
    Learning from earlier experience with managing one of my client website, you never know what files in the node_modules or vendor folder. There might be vuln or backdoor from dependencies poisoning that you’re unaware of.
  • If possible, never put said file into folder that are served by the web server, even if you can block the access to those files/folders.
    For example, you put node_modules or vendors folder in public_html, then you block them in .htaccess. This might work for now, but when your infra team decide to try experimenting to use Nginx, the folder is accessible again because Nginx ignore your .htaccess file. Your Infra team now have to add another config for those.

Oh anyway, there’s another problem with this too: getting the correct path! As mentioned in file-loader section earlier, loading file from fixed folder like this is very tricky! I have to dig deep to NextJS code to see which variable or config classes that provides the necessary information to the path I’m interested with.

So with those potential problems, I decided that this wont be a good fit for this exploration.

Conclusion

So, after testing all those options we can choose, I settled with using raw-loader due to properly obtaining the path to the file is tricky. Especially because NextJS Dev Server current working directory expectedly runs on project root instead of build folder. There are another solution: put them in public path, but putting the file on this folder is actually makes me uncomfy because it violates my personal standards. That being said, the feature works pretty well, for now.

An animation of small version of Yuma (from World Trigger) on top of supersized Repurika, flying up and down.