How to remove portal.min.js but keep email subscription functionality?

First of all, thank you all for support and answering questions on this forum. Ghost community is awesome!

I want to completely remove portal.js, since I don’t have paid members and don’t need them to login. The content is free and and public so there is no need to sign in to view it. However, I do need the email subscription form on the homepage (and I already have it) to collect email so I could send newsletters.

I need only this:

  1. User lands on the homepage and enters his email into the subscription form.
  2. Email confirmation request is sent to the user.
  3. User confirms an email. Email is written to the database of email subscribers. No cookies, or files like “OPTIONS tiers?key=…”, “OPTIONS newsletters?key=…”, “newsletters?key=…”, “tiers?key=…”, “settings?key=…” need to be loaded. I want to get rid of all that to improve performance and privacy.
  4. User is redirected to the homepage after he confirms an email. There is a toast notification to inform about the successful subscription.
  5. I’m able to send email newsletters to the subscribers. But I don’t need them to login or store their sessions. Loaded resources has to be reduced as much as possible to improve performance.

I inspected portal.min.js and found code that seems to be relevant to the form:

function vu(e) {
            var n = e.siteUrl,
                t =,
                r = e.member;
            n && (n = n.replace(/\/$/, ""),"form[data-members-form]"), (function(e) {
                var t = e.querySelector("[data-members-error]");
                e.addEventListener("submit", (function r(i) {
                    !function(e) {
                        var n,
                            t = e.event,
                            r = e.form,
                            i = e.errorEl,
                            o = e.siteUrl,
                            a = e.submitHandler;
                        r.removeEventListener("submit", a),
                        i && (i.innerText = ""),
                        r.classList.remove("success", "invalid", "error");
                        for (var l ="input[data-members-email]"), s ="input[data-members-name]"), c = (null === r || void 0 === r || null === (n = r.dataset) || void 0 === n ? void 0 : n.membersAutoredirect) || "true", u = null === l || void 0 === l ? void 0 : l.value, p = s && s.value || void 0, d = void 0, f = [], h ="input[data-members-label]") || [], m = 0; m < h.length; ++m)
                        r.dataset.membersForm && (d = r.dataset.membersForm),
                        var g = po(),
                            v = {
                                email: u,
                                emailType: d,
                                labels: f,
                                name: p,
                                autoRedirect: "true" === c
                        g && (v.urlHistory = g),
                        fetch("".concat(o, "/members/api/send-magic-link/"), {
                            method: "POST",
                            headers: {
                                "Content-Type": "application/json"
                            body: JSON.stringify(v)
                        }).then((function(e) {
                            if (r.addEventListener("submit", a), r.classList.remove("loading"), !e.ok)
                                return ss.fromApiResponse(e).then((function(e) {
                                    throw e
                        })).catch((function(e) {
                            i && (i.innerText = ss.getMessageFromError(e, "There was an error sending the email, please try again")),
                        event: I,
                        errorEl: t,
                        form: e,
                        siteUrl: n,
                        submitHandler: r

I want to store it locally. I understand that this bit is a part of a larger function with paid members, Stripe, etc.

Is this the only piece of code I need to make “data-members-form” working?
Removing portal.min.js will not affect /ghost/#/signin page, right?
How can I achieve described above goals?

What’s missing from your proposed flow is that subscribers confirm their emails. I know I’m not answering the question you asked, but humor me for a moment while I tell you a story:

A friend scored '` as his email address. Someone he doesn’t know (whose gmail address is probably very similar) keeps entering his address into websites. He gets unwanted bulk mail, but he has also gotten concert tickets, gift cards, password reset requests, insurance information, online bills, etc. For a while, he tried contacting the legit-looking businesses to tell them they had a mistake. Now he just reports them as spam.

A decade ago, someone put my email into a “sell your house fast” type website that was clearly selling its data. Typo or a prank, I don’t know. To this day, I’m still getting spammy emails asking whether I still want to sell the house. (I suspect typo, because it looks like a legit house address four hours away.) I tried hitting the unsubscribe button for a while, but now I’m just reporting them as spam.

So really, before you send that newsletter, you’ve got to make sure it’s a legit email address and they want your stuff.

Happily, the only part of email opt-in that the portal code handles is the happy Success ‘toast’/badge that briefly appears when a user clicks the link in their email.

So, back to your question, how can you get rid of portal but keep the subscription button working? Basically, you just need a POST request to the /send-magic-link endpoint that includes the user’s email address (and optionally labels and name). This code looks like it does that, but minified code in a scrolling window is pretty hard to read… I think you’re maybe missing some function definitions.

You might instead try looking at the new embeddable sign-up widget (available under settings > membership > portal) - that’s much simpler. Mine (minimal) looks like:

<div style="min-height: 58px;max-width: 440px;margin: 0 auto;width: 100%">
<script src="" data-button-color="#66298e" data-button-text-color="#FFFFFF" data-site="" async>

… but then you’re loading this other code that isn’t super small either. Hrm. Let me look, I think I’ve got working code that does what you want without loading React!

Yeah, here’s some code that does basically what you want:

  <div class='form-group'>
    <label for='subscribe-email' >{{t 'Your email address' }}</label>
    <input type='email' name='email' id='subscribe-email' placeholder='{{t 'Your email address' }}'>
    <button value='{{t 'Subscribe' }}' class='subscribe-button'>{{t 'Subscribe' }}</button>

  <div class='alert--success'>{{t 'Please check your inbox and click the link to confirm your subscription.' }}</div>
  <div class='alert--invalid'>{{t 'Please enter a valid email address' }}</div>
  <div class='alert--error'>{{t 'An error occurred, please try again later.' }}</div>
      async function sendMagicLink(name, email) {
        let body = await JSON.stringify({
            "email": email,
            "emailType": "signup",
            "urlHistory": [],
            "name": name
        let response = await fetch('/members/api/send-magic-link/', {"headers": {"content-type": "application/json"}, "body": body, "method": "POST"})
        // there should really be some error handling here, but instead I'm just sending the reader to another page. If you're not doing that, you'd read the response and decide which message above to reveal. // 
        window.location.href='/signup-2/?email=' + encodeURIComponent(email)


  document.querySelector('.subscribe-button').addEventListener('click', (e) => {
1 Like

You are right, confirmation is necessary and I missed it. I have added it to the flow.

Thank you for the code, I will test it!

1 Like