Adding a subscription form to the web views of Mailcoach original

by Freek Van der Herten – 4 minute read

After sending a new edition of my newsletter, I usually tweet out a webview URL together with a URL where people can subscribe to the newsletter. A webview is a hard to guess URL that people who didn't subscribe can visit to read the content of the newsletter.

Until last week, the webview URL just displayed the content of the sent campaign. Recently, I added a subscription form to that webpage, so people don't need to go to a separate page to subscribe. Here's how that looks like:

screenshot

In this blog post, I'd like to share how that subscription form was added.

Customizing the webview

It probably comes as no surprise that I use our homegrown Mailcoach package to sent out my newsletter. Whenever a campaign (that's Mailcoach speak for newsletter) is sent, Mailcoach makes the campaign available as webview.

You can get to the URL of the webview of a campaign:

$campaign->webViewUrl();

Up until last week, I just used the webViewUrl in the Blade view that displays all past campaigns

@foreach($pastCampaigns as $campaign)
    <div>
        <a href="{{ $campaign->webViewUrl() }}">{{ $campaign->subject }}</a>
        <div class="text-gray-700 text-xs">sent on {{ $campaign->sent_at->format('jS F Y') }}</div>
    </div>
@endforeach

The first thing I did was create a route newsletter.show of my own to take control of which view is rendered when people see an archived newsletter.

@foreach($pastCampaigns as $campaign)
    <div>
        <a href="{{ route('newsletter.show', $campaign->id) }}">{{ $campaign->subject }}</a>
        <div class="text-gray-700 text-xs">sent on {{ $campaign->sent_at->format('jS F Y') }}</div>
    </div>
@endforeach

The controller that gets called to serve the newsletter.show route, returns the newsletter.blade.php view.

From using an iframe...

This was my first naive approach to display a form above the webview. The HTML page just started with a subscription form, followed by an iframe that displays that webview URL. I used an iframe to prevent the CSS from the main site to bleed through in the campaign HTML.

<html>
    <head>
        <style>{!! file_get_contents(public_path('css/app.css')) !!}</style>
        <link rel="stylesheet" href="https://cloud.typography.com/6194432/6581412/css/fonts.css"/>
        <title>{{ $campaign->subject }}</title>
    </head>
    <body>
        <header class="w-full mb-4 p-4 sm:p-6 md:px-8 md:py-7 bg-orange-100 border-b-2 border-orange-200 text-xs text-gray-700">
            <div class="max-w-lg mx-auto space-y-2">
                <p>
                    Every two weeks I send out a newsletter like this one, containing lots of interesting stuff for the modern
                    PHP
                    developer.
                </p>
                <p>
                    Subscribe to get the next edition in your mailbox.
                </p>
                @include('front.newsletter.partials.form')
            </div>
        </header>

        <iframe href="{{ $campaign->webViewUrl() }}" />
    </body>
</html>

This would get me 95% of the way there. A challenging problem that I couldn't solve entirely on my own was the janky scroll behavior caused by having an iframe on the page.

Here an example of that "janky" feel:

... to using a web component

Luckily my colleague Seb had an excellent solution: web components. You can think of a web component as a custom HTML tag in which a separate DOM, called the shadow DOM, is rendered. This shadow DOM does not get the CSS of the main page applied to it.

Here's that view that display a subscription form + newsletter again, but this time a web component is used. A bit of JavaScript is needed at the top to configure the web component. The $webview contains the full page content returned by $campaign->webViewUrl().

<html>
    <head>
        <style>{!! file_get_contents(public_path('css/app.css')) !!}</style>
        <link rel="stylesheet" href="https://cloud.typography.com/6194432/6581412/css/fonts.css"/>
        <title>{{ $campaign->subject }}</title>
        <script>
            window.customElements.define('campaign-webview', class NewsletterEmbed extends HTMLElement {
                connectedCallback() {
                    const shadow = this.attachShadow({ mode: 'closed' });
                    shadow.innerHTML = this.getAttribute('contents');
                }
            })
        </script>
    </head>
    <body>
        <header class="w-full mb-4 p-4 sm:p-6 md:px-8 md:py-7 bg-orange-100 border-b-2 border-orange-200 text-xs text-gray-700">
            <div class="max-w-lg mx-auto space-y-2">
                <p>
                    Every two weeks I send out a newsletter like this one, containing lots of interesting stuff for the modern
                    PHP
                    developer.
                </p>
                <p>
                    Subscribe to get the next edition in your mailbox.
                </p>
                @include('front.newsletter.partials.form')
            </div>
        </header>

        <campaign-webview contents="{{ $webview }}"></campaign-webview>
    </body>
</html>

This is pretty neat! It does exactly what we want: the main page's CSS doesn't bleed through, and there's no janky scroll behavior.

To know more about web components and the shadow DOM, read this excellent blog post by Seb.

Join 9,500+ smart developers

Get my monthly newsletter with what I learn from running Spatie, building Oh Dear, and maintaining 300+ open source packages. Practical takes on Laravel, PHP, and AI that you can actually use.

"Freek publishes a super resourceful and practical newsletter. A must for anyone in the Laravel space"

Joey Kudish — Shipping with AI as a teammate

No spam. Unsubscribe anytime. You can also follow me on X.

Found something interesting to share? Submit a link to the community section.