Published May 31, 2024

Last Updated May 31, 2024

Using Laravel as an email HTML generator

Categories: Laravel , Less-conventional Laravel

Tags: #Laravel , #Conventions , #Nuxt , #PHP

Feel free to skip to the "Hello Laravel, my old friend" heading for the meat of the post. (Sorry, no anchor link; same-page links broken at the moment...)


Background

Where I work, we had been using existing email marketing platforms with their built-in design tools to build our marketing emails. It's a solved problem, so a no-brainer, right?

But recently, we've decided to use fully custom HTML emails again, at least for some things. We lean heavily on aesthetic in nearly everything, so it makes sense that we'd ultimately want more control over this.

Before using Laravel, I tried a couple of other tools: Nuxt, then vanilla PHP.

Nuxt for email HTML?

I know, this probably sounds weird.

I tried Nuxt at first because I like Vue for component composition and I remembered Nuxt being good for generating static sites. How different could the experience be for generating HTML for email?

I quickly found out. Even though it was relatively easy to generate single HTML files, there were a couple of issues.

Issue #1: JS assets and script tags

Every time you generate a static site with Nuxt, it also generates JS chunks for you and links them in the appropriate pages—however it thinks appropriate for how you've written your components.

Since this meant having to open the final output and strip out all script tags, it might've become a bigger nuisance over time, and I didn't find a way at the time to avoid this—either directly in Nuxt or with Nitro's config options (the tool Nuxt uses for SSR).

Issue #2: Regenerating the whole project

Unlike with an actual website's files, I didn't want to regenerate every output file. Assuming we're saving the final version of each HTML file that we're going to send (spoiler: I am), I don't want to accidentally overwrite the final revision of each past email just because a component changed—not to mention how annoying it would be to find and commit the 1 we needed while discarding all other changes every time.

After dealing with these annoyances, I remembered: duh, I'm a PHP developer!

The vanilla PHP solution

So I quickly started replacing the Nuxt project with PHP templates, a few custom functions and a render script.

It was fine; it worked well and it was lightweight, but it also had some issues.

Issue #1

Rendering was clumsy because, in my case, I was requiring both the email name and layout name for render, and building an output buffer (ob_start(), <div><?= "some output"; ?></div>, $rendered = ob_get_clean() basically) manually (in more than 1 place).

Issue #2

The way I built it (quickly, on a short timeframe again), I couldn't just compose my templates the way I could with Vue. It was all basicaly <?php include COMPONENTS . '/path/to/component.php' ?>.

Templates-only also made locking a particular email into a selected layout a bit annoying. My render script needed 2 arguments: the email, and (if I wanted) the layout template name (if different from the default I had set).

I could've gotten around this with classes for components and layouts with enough time, but this was something I needed to avoid spending much time on.

Issue #3

Viewing the in-progress email in my browser meant having to re-run my render script every time manually. This wasn't a huge deal for the first 1 or 2 custom emails, but it'll definitely become an unnecessary annoyance later.

Hello Laravel, my old friend

In hindsight, I could've saved a ton of time if I had started here.

I created a Laravel project, migrated my existing templates to Blade, and rewrote my render script as a custom artisan command.

Along the way, I discovered a few helpful things, like view() having extra methods I needed:

  • view()->exists('path.to.view.file') to check that a view file exists
  • view('path.to.view.file')->render() to get the rendered output

So with artisan app:email:render <name>, I was able to simply do something like this to save the final output:

Storage::put("$name.html", view('path.to.email')->render());

And then log to the console where it was saved:

$this->info(Storage::path("$name.html"));

Final thoughts

Laravel really is fun and nice to work with, even for smaller less-conventional use cases like this.

I initially avoided it because I thought I'd have to dig into how Laravel does rendering under the hood to get the final output. But it ended up being stupid easy, like plenty of other things in Laravel.

I also didn't want to spin up a whole Laravel project for what seemed like a small task, but it ended up still being worth it. I just cleaned up the default DB stuff like migrations, seeders, factories etc. and only used the view layer and a custom command.

Since this project won't be deployed on a server (especially public-facing), it'll do it's job well in its simplest state.

Please confirm whether you would like to allow tracking cookies on this website, in accordance with its privacy policy.