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.
5 days ago
JSn1nj4 opened Issue #216 at JSn1nj4/ElliotDerhay.com
5 days ago
JSn1nj4 opened Issue #215 at JSn1nj4/ElliotDerhay.com
5 days ago
JSn1nj4 opened Issue #214 at JSn1nj4/ElliotDerhay.com
1 week ago
JSn1nj4 opened Issue #1 at JSn1nj4/laundry-list
1 week ago
JSn1nj4 created main at JSn1nj4/laundry-list