If you run mail for clients on the Enhance control panel, you have probably hit this: a client stops receiving email, nobody knows why, and it turns out their mailbox quietly filled up days ago. Enhance does not send any warning as a mailbox approaches its quota, and it does not tell the user when they have gone over it. The first anyone hears about it is a complaint that email has "stopped working".

This post walks through a small, self-contained script that fixes that. It watches every mailbox on the server, warns users before they hit the wall, tells them plainly when they are full, and sends you an admin digest so nothing slips past. It runs on stock Enhance with no plugins and no risky config edits.

The problem in a bit more detail

Dovecot, which Enhance uses under the bonnet for IMAP and POP3, already tracks each mailbox's quota. You can see it any time with doveadm quota get -A. The information is right there. What is missing is anything that acts on it. There is no built-in job that says "this mailbox is at 90%, send the owner a heads-up".

Dovecot does actually have a native feature for exactly this, called quota_warning, which fires a script the moment a delivery pushes a mailbox across a threshold. It is the textbook answer, and on a hand-built mail server it is what you would reach for. The trouble on Enhance is that Enhance owns the Dovecot configuration and regenerates it whenever mailboxes are added or changed. Any quota_warning block you add by hand is liable to be wiped the next time the panel rewrites the config. You would be building on sand.

The approach

Rather than fight the panel, the script sits entirely outside it. It is a plain cron job that:

  1. Asks Dovecot for every mailbox's current usage, using the same doveadm quota get -A you can run by hand.
  2. Works out which usage band each mailbox is in (80%, 90%, 95%, 100% by default).
  3. Sends an alert only when a mailbox rises into a new band, so a mailbox parked at 87% for a fortnight generates one warning, not one every fifteen minutes.
  4. Optionally delivers a plain-English warning straight into the user's own mailbox, with a different message once they are actually full.
  5. Emails you, the admin, a single digest of everything that moved.

Because it only ever reads quota state and sends mail, it touches none of Enhance's managed configuration. An Enhance config regeneration cannot break it. That is the whole point of doing it this way.

One environment note worth knowing. As of Enhance v12, the mail stack runs as native systemd services rather than Docker containers, so Dovecot is a normal dovecot.service unit and both doveadm and dovecot-lda are callable directly on the host. There is no docker exec and no namespace wrapping to worry about. If you are on an older Enhance that still uses Docker, the calls to doveadm and dovecot-lda would need wrapping in docker exec, but on v12 and later it is all direct.

Before you start

You will need root on the mail server, and you should confirm three binary paths, since distributions vary a little:

command -v doveadm
ls -l /usr/lib/dovecot/dovecot-lda
command -v sendmail

On a current Ubuntu-based Enhance box these are typically /usr/bin/doveadm, /usr/lib/dovecot/dovecot-lda and /usr/sbin/sendmail. If yours differ, note them, as they go into the config block of the script.

It is also worth running doveadm quota get -A once by hand just to see the output. Each mailbox produces a STORAGE row and a MESSAGE row, and the script reads the percentage from the last column of the STORAGE rows.

The script

Save this as mailquota-notify.sh. Every setting you are likely to change lives in the clearly marked CONFIG block near the top, and the rest is commented so you can follow what it does.

#!/usr/bin/env bash
#
# mailquota-notify.sh
# ==================================================================
# Sends mailbox quota notifications for a Dovecot mail server, the
# feature the Enhance control panel does not provide out of the box.
#
# What it does:
#   * Polls every mailbox's quota with `doveadm quota get -A`.
#   * When a mailbox rises into a new usage band (80/90/95/100% by
#     default) it sends ONE alert. It will not nag on every run while
#     the mailbox sits in the same band.
#   * Optionally delivers a plain-English warning straight into the
#     user's own mailbox, with a distinct "you are now full" message
#     once they hit 100%.
#   * Emails the admin a single digest of everything that crossed a
#     band this run.
#   * A separate weekly mode emails a capacity-planning summary of
#     every mailbox above a chosen percentage.
#
# Why a cron poller and not Dovecot's own quota_warning feature:
# Enhance regenerates the Dovecot configuration when mailboxes change,
# so hand-edited quota_warning blocks get wiped. This script touches
# NONE of Enhance's managed config. It only reads quota state and
# sends mail, so a config regeneration cannot break it.
#
# Environment note (Enhance v12): Dovecot runs as a native systemd
# unit, so `doveadm` and `dovecot-lda` are callable directly on the
# host. No Docker, no `docker exec`, no namespace juggling.
#
# ------------------------------------------------------------------
# INSTALL
#   sudo install -o root -g root -m 750 \
#        mailquota-notify.sh /usr/local/sbin/mailquota-notify.sh
#   sudo mkdir -p /var/lib/mailquota-notify
#
# SCHEDULE (add to /etc/cron.d/mailquota-notify, see the blog post):
#   */15 * * * * root /usr/local/sbin/mailquota-notify.sh >/dev/null 2>&1
#   0 8 * * 1   root /usr/local/sbin/mailquota-notify.sh weekly >/dev/null 2>&1
#
# MODES
#   (no argument)   poll quotas and alert on band crossings
#   weekly          email the planning summary, then exit
# ------------------------------------------------------------------

set -euo pipefail

######################### CONFIG ##########################
# Everything you are likely to change lives in this block.

# Usage bands, in percent. An alert fires only when a mailbox RISES
# into a higher band, so a box parked at 87% will not re-alert every
# run. 100 is included so crossing into "full" always sends a fresh
# notice, even if the box was already in the 95 band beforehand.
BANDS=(80 90 95 100)

# Restrict to specific domains (space-separated). Empty string = every
# mailbox on the server. Handy for a first test: set this to a single
# domain you own, prove it works, then set it back to "".
#   Example test value: DOMAIN_FILTER="example.com"
DOMAIN_FILTER=""

# Where the admin digest and weekly summary are sent.
# IMPORTANT: use an address that is NOT hosted on this server. If this
# server's mail breaks, an on-server address means you never get the
# very alert that would tell you.
ADMIN_EMAIL="[email protected]"

# From: header AND envelope sender for all outgoing mail this script
# sends. The envelope sender matters: if you relay through a shared
# smarthost such as MailChannels, this address must be one the
# smarthost is authorised to send for, or the mail is rejected. See
# the "MailChannels gotcha" section of the blog post.
MAIL_FROM="[email protected]"

# Deliver an individual warning INTO the user's own mailbox?
#   0 = admin digest only
#   1 = also notify the end user. Delivery uses dovecot-lda with a
#       noenforcing quota override, so the warning still lands even
#       when the mailbox is already at or over 100%.
NOTIFY_USER=1

# Weekly summary threshold: list every mailbox at or above this
# percentage. Independent of the band-crossing alerts above.
SUMMARY_MIN_PCT=50

# Local-parts never sent a user notification (they still appear in the
# admin digest and weekly summary). Matched case-insensitively against
# the part before the @. Keeps role and no-reply boxes quiet.
SKIP_LOCALPARTS_REGEX='^(test|donotreply|do-not-reply|noreply|no-reply|admin|postmaster|abuse)$'

# Binary paths. Confirm on your box with:
#   command -v doveadm ; ls -l /usr/lib/dovecot/dovecot-lda ; command -v sendmail
DOVEADM="/usr/bin/doveadm"
LDA="/usr/lib/dovecot/dovecot-lda"
SENDMAIL="/usr/sbin/sendmail"

# Where per-mailbox alert state is remembered (one small file each).
STATE_DIR="/var/lib/mailquota-notify"

# syslog tag. Read the script's own log with:  journalctl -t mailquota-notify
LOG_TAG="mailquota-notify"

####################### END CONFIG ########################

mkdir -p "$STATE_DIR"

log() { logger -t "$LOG_TAG" -- "$*"; }

# Return the highest band whose threshold is <= pct, else 0.
band_for() {
  local pct="$1" b out=0
  for b in "${BANDS[@]}"; do
    (( pct >= b )) && out="$b"
  done
  echo "$out"
}

# Turn an email address into a filesystem-safe state key.
state_file_for() {
  local user="$1"
  echo "$STATE_DIR/${user//[^A-Za-z0-9._-]/_}"
}

# True if the address is inside DOMAIN_FILTER (or the filter is empty).
in_scope() {
  local domain="${1##*@}" d
  [[ -z "$DOMAIN_FILTER" ]] && return 0
  for d in $DOMAIN_FILTER; do [[ "$domain" == "$d" ]] && return 0; done
  return 1
}

# Weekly capacity-planning summary. Admin only, never mails users.
weekly_summary() {
  local rows=() user pct value limit
  while read -r user pct value limit; do
    [[ "$pct" =~ ^[0-9]+$ ]] || continue
    (( pct >= SUMMARY_MIN_PCT )) || continue
    in_scope "$user" || continue
    rows+=("$pct|$user|$value|$limit")
  done < <("$DOVEADM" quota get -A | awk '$4=="STORAGE" { print $1, $NF, $5, $6 }')

  if (( ${#rows[@]} == 0 )); then
    log "weekly summary: no mailboxes at or above ${SUMMARY_MIN_PCT}%"
    return
  fi

  {
    echo "From: $MAIL_FROM"
    echo "To: $ADMIN_EMAIL"
    echo "Subject: [mail quota] Weekly summary - ${#rows[@]} mailbox(es) at/above ${SUMMARY_MIN_PCT}%"
    echo
    echo "Mailboxes on $(hostname -f) at or above ${SUMMARY_MIN_PCT}% usage, highest first:"
    echo
    printf '%-45s %6s  %9s  %9s\n' "MAILBOX" "USED" "SIZE" "LIMIT"
    printf '%-45s %6s  %9s  %9s\n' "-------" "----" "----" "-----"
    printf '%s\n' "${rows[@]}" | sort -t'|' -k1,1 -rn | while IFS='|' read -r pct user value limit; do
      hsize="$(numfmt --to=iec --suffix=B $(( value * 1024 )) 2>/dev/null || echo "${value}K")"
      if [[ "$limit" == "-" ]]; then
        hlimit="none"
      else
        hlimit="$(numfmt --to=iec --suffix=B $(( limit * 1024 )) 2>/dev/null || echo "${limit}K")"
      fi
      printf '%-45s %5s%%  %9s  %9s\n' "$user" "$pct" "$hsize" "$hlimit"
    done
    echo
    echo "Any box at or over 100% is no longer receiving mail."
  } | "$SENDMAIL" -t -f "$MAIL_FROM"
  log "weekly summary sent to $ADMIN_EMAIL (${#rows[@]} mailbox(es))"
}

# Mode dispatch: "weekly" runs the summary and exits, anything else polls.
MODE="${1:-poll}"
if [[ "$MODE" == "weekly" ]]; then
  weekly_summary
  exit 0
fi

declare -a NEW_ALERTS=()

# Read one STORAGE row per mailbox. doveadm's columns are:
#   $1=address  $2=User  $3=quota  $4=STORAGE|MESSAGE  $5=Value  $6=Limit  $NF=percent
while read -r user pct limit; do
  [[ "$pct" =~ ^[0-9]+$ ]] || continue     # skip rows with no numeric percent (unlimited boxes)
  in_scope "$user" || continue

  cur_band="$(band_for "$pct")"
  sf="$(state_file_for "$user")"
  last_band="$(cat "$sf" 2>/dev/null || echo 0)"

  if (( cur_band > last_band )); then
    # Risen into a new, higher band: alert.
    NEW_ALERTS+=("$user|$pct|$limit")
    echo "$cur_band" > "$sf"
    log "ALERT $user at ${pct}% (band ${cur_band}, was ${last_band})"

    if (( NOTIFY_USER == 1 )); then
      localpart="${user%@*}"
      if ! [[ "${localpart,,}" =~ $SKIP_LOCALPARTS_REGEX ]]; then
        if (( pct >= 100 )); then
          # Over quota: mail is being rejected, say so plainly.
          subject="Your mailbox is full (${pct}%) - action needed"
          body="Your mailbox ($user) is now ${pct}% full, which is at or over its limit.

Because it is full, you are NOT receiving new email. Messages sent to you
will be delayed and may be returned to the sender. New mail will only
start arriving again once the mailbox is back below its limit.

Please delete older or larger messages, and empty your Trash and Junk
folders, to bring it below the limit as soon as possible.

To arrange more space, please contact your administrator."
        else
          # Approaching quota: a friendly heads-up.
          subject="Your mailbox is ${pct}% full"
          body="Your mailbox ($user) is currently ${pct}% full.

Once it reaches 100% you will stop receiving new email. Please delete
older or larger messages, and empty your Trash and Junk folders.

To arrange more space, please contact your administrator."
        fi
        # noenforcing lets the warning be delivered even past 100%.
        "$LDA" -d "$user" -o "plugin/quota=maildir:User quota:noenforcing" <<EOF || log "lda delivery failed for $user"
From: $MAIL_FROM
To: $user
Subject: $subject

$body
EOF
      fi
    fi

  elif (( cur_band < last_band )); then
    # Usage fell back below a band. Lower the watermark so a later rise
    # alerts again rather than being silently suppressed.
    echo "$cur_band" > "$sf"
  fi
done < <("$DOVEADM" quota get -A | awk '$4=="STORAGE" { print $1, $NF, $6 }')

# Send one admin digest, only if something newly crossed a band.
if (( ${#NEW_ALERTS[@]} > 0 )); then
  {
    echo "From: $MAIL_FROM"
    echo "To: $ADMIN_EMAIL"
    echo "Subject: [mail quota] ${#NEW_ALERTS[@]} mailbox(es) crossed a usage threshold"
    echo
    echo "These mailboxes on $(hostname -f) have risen into a new usage band:"
    echo
    printf '%-45s %6s\n' "MAILBOX" "USED"
    printf '%-45s %6s\n' "-------" "----"
    for row in "${NEW_ALERTS[@]}"; do
      IFS='|' read -r u p l <<< "$row"
      printf '%-45s %5s%%\n' "$u" "$p"
    done
    echo
    echo "Any box at or over 100% is no longer receiving mail."
  } | "$SENDMAIL" -t -f "$MAIL_FROM"
  log "digest sent to $ADMIN_EMAIL for ${#NEW_ALERTS[@]} mailbox(es)"
fi

Installing it

Drop the script into a sensible location, make it root-owned and executable, and create its state directory:

sudo install -o root -g root -m 750 mailquota-notify.sh /usr/local/sbin/mailquota-notify.sh
sudo mkdir -p /var/lib/mailquota-notify

The state directory is where the script remembers which band each mailbox was last in. That memory is what stops it re-sending the same warning on every run.

Configuring it

Open the CONFIG block and set these for your server.

ADMIN_EMAIL is where your digest and weekly summary go. Use an address hosted somewhere other than this server. The one time you most need the alert is when this server's own mail is unwell, and an on-server address is exactly the one that will not reach you at that moment. A mailbox on a different provider is ideal.

MAIL_FROM is the address the script sends as, used both in the From header and, importantly, as the envelope sender. More on why that matters below.

NOTIFY_USER decides whether end users get warned in their own inbox. Set it to 1 to notify users, which is the behaviour most people want, or 0 to keep it admin-only while you get comfortable with it.

BANDS are the thresholds. The defaults of 80, 90, 95 and 100 work well. Keeping 100 as its own band matters, because it guarantees that crossing into "full" always produces a fresh notification even for a mailbox that was already sitting in the 95% band.

SUMMARY_MIN_PCT sets the floor for the weekly planning summary. At 50 you get a weekly list of everything half full or more, which is handy for spotting mailboxes that will need attention soon.

SKIP_LOCALPARTS_REGEX lists local-parts that should never receive a user-facing warning, such as noreply and donotreply boxes. They still appear in your admin digest, they just do not get mailed directly, since nobody reads them.

DOVEADM, LDA and SENDMAIL are the binary paths you confirmed earlier. Adjust if yours differ.

Scheduling it with cron

The tidiest way is a drop-in file in /etc/cron.d, which is idempotent and easy to inspect later:

sudo tee /etc/cron.d/mailquota-notify >/dev/null <<'EOF'
# Mailbox quota notifications
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# Every 15 minutes: poll quotas, alert on band crossings
*/15 * * * * root /usr/local/sbin/mailquota-notify.sh >/dev/null 2>&1

# Monday 08:00: weekly planning summary
0 8 * * 1 root /usr/local/sbin/mailquota-notify.sh weekly >/dev/null 2>&1
EOF
sudo chmod 644 /etc/cron.d/mailquota-notify

Two details specific to /etc/cron.d files: each line needs the user (root) after the time fields, which a normal user crontab does not, and MAILTO="" stops cron from emailing you its own output, since the script handles all of its own mail. Cron picks the file up on its own within a minute, no restart needed. Confirm it is in place with:

cat /etc/cron.d/mailquota-notify

Testing it safely

Two habits make testing painless.

First, scope it to a single domain you control. Set DOMAIN_FILTER="yourdomain.com" in the config, and the script ignores every other domain on the server while you experiment. Clear it back to "" when you are happy.

Second, force a crossing. If nothing in your test domain happens to be near a band, temporarily add a low band so a real mailbox crosses it, for example BANDS=(50 80 90 95 100), run the script by hand, watch what happens, then put the bands back. To repeat a test against the same mailbox, clear its remembered state first. State files live in /var/lib/mailquota-notify/ with the @ replaced by an underscore, so [email protected] becomes a file called user_yourdomain.com.

Run either mode by hand while testing:

sudo /usr/local/sbin/mailquota-notify.sh          # poll now
sudo /usr/local/sbin/mailquota-notify.sh weekly    # send the weekly summary now

Reading the logs

The script logs to syslog under its own tag, so you can see exactly what it has been doing:

journalctl -t mailquota-notify --since today

You will see an ALERT line each time a mailbox crosses a band, plus lines confirming the digest and weekly summary were sent. A poll run that finds nothing to report writes nothing, which is normal and not a sign of trouble.

Wrapping up

That is the whole thing: a single self-contained script, a couple of cron lines, and no changes to anything Enhance manages. Users get a warning before their mailbox fills, a clear message when it does, and you get a quiet digest instead of a surprise support ticket.

If you adapt it, the obvious extensions are a daily re-nudge for mailboxes that stay over quota, and per-domain routing so each client's alerts go to their own address rather than all landing on you. Both are small additions to the same structure.

Disclaimer

Provided as-is with no warranty. Use at your own risk. The script runs as root and sends email, so test it on a spare domain first. I accept no liability for any loss or disruption resulting from its use. Always review the code and confirm it suits your own setup before running it in production.

If you're using Breakdance builder and want to improve your site's semantic HTML structure, you might have noticed that wrapping your main content in a <main> tag isn't straightforward. Here's a simple JavaScript solution that automatically wraps your designated sections in a proper <main> element.

The Problem

With Breakdance builder you can only designate one section with the <main> tag. But what if you want multiple sections within <main>? Breakdance builder doesn't provide a native way to wrap multiple sections in a <main> tag. This is important for accessibility and SEO, as the <main> element helps screen readers and search engines identify the primary content of your page.

The Solution

This lightweight JavaScript snippet allows you to mark your first and last sections with custom attributes, and it automatically wraps everything in between with a <main> tag.

How It Works

The code is simple and efficient:

  1. It waits for the DOM to fully load
  2. Finds sections marked with [firstsection] and [lastsection] attributes
  3. Creates a new <main> element
  4. Moves all sections between (and including) the marked sections into the <main> tag

The Code

document.addEventListener('DOMContentLoaded', function() {
  const first = document.querySelector('[firstsection]');
  const last = document.querySelector('[lastsection]');

  if (first && last) {
    // Create the <main> element
    const main = document.createElement('main');

    // Insert <main> before the first section
    first.parentNode.insertBefore(main, first);

    // Move all elements between first and last (inclusive) into <main>
    let el = first;
    while (el) {
      const next = el.nextElementSibling;
      main.appendChild(el);
      if (el === last) break;
      el = next;
    }
  }
});

Implementation Steps

image
image

Benefits

Conclusion

This simple solution bridges the gap in Breakdance builder's semantic HTML capabilities. By adding just a few lines of JavaScript and two custom attributes, you can ensure your site follows best practices for accessibility and SEO.

Solving the EmailIt hotmail delivery problem

If you manage multiple email delivery services, you might want WordPress to use one SMTP provider for certain recipients (e.g. Microsoft addresses like @hotmail.com) and another provider for everyone else.

This can improve deliverability, reputation management, and success rates when sending to tricky domains. In particular, this method has been put together to address delivery issues with Microsoft's free email services, such as hotmail.com and live.com, that EmailIt continues to suffer from.

Fortunately, this can be done neatly in WordPress using FluentSMTP and a few lines of PHP.

Why You Might Need This

Some email hosts handle certain destinations better than others. For example:

FluentSMTP already supports multiple SMTP connections — it simply chooses one based on the “From” email address. With a short code snippet, you can automatically switch that “From” address depending on the recipient domain.

Step 1 – Set Up Two FluentSMTP Connections

Go to Settings → FluentSMTP → Settings and create two connections:

Primary connection (default)

Secondary connection (for Microsoft addresses)

For my use case, my primary connection is with MailGun and the secondary with EmailIt.

FluentSMTP automatically chooses the connection based on the “From” address.
The code below will change that address dynamically depending on the recipient.

Step 2 – Add the Routing Snippet

Add this snippet to your site using a code snippets plugin, a custom plugin, or your theme’s functions.php.

/**
 * Route emails through different FluentSMTP connections depending on the recipient domain.
 *
 * Requires:
 * - Primary FluentSMTP connection (default): e.g. [email protected]
 * - Secondary FluentSMTP connection: e.g. [email protected]
 *
 * Adjust the domain lists and email addresses as needed.
 */
add_action( 'phpmailer_init', function ( $phpmailer ) {
    if ( ! $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer ) {
        return;
    }

    // Combine all recipient types (To, CC, BCC)
    $recipients = array_merge(
        (array) $phpmailer->getToAddresses(),
        (array) $phpmailer->getCcAddresses(),
        (array) $phpmailer->getBccAddresses()
    );

    if ( empty( $recipients ) ) {
        return;
    }

    // Define which recipient domains should trigger the secondary route
    $special_domains = [
        'hotmail.com',
        'hotmail.co.uk',
        'outlook.com',
        'outlook.co.uk',
        'live.com',
        'live.co.uk',
        // Add more domains here if needed
    ];

    $use_secondary = false;

    foreach ( $recipients as $recipient ) {
        $email = isset( $recipient[0] ) ? strtolower( trim( $recipient[0] ) ) : '';
        if ( ! $email || strpos( $email, '@' ) === false ) {
            continue;
        }

        $domain = substr( strrchr( $email, '@' ), 1 );

        if ( in_array( $domain, $special_domains, true ) ) {
            $use_secondary = true;
            break;
        }
    }

    if ( ! $use_secondary ) {
        return; // send normally
    }

    // Switch the "From" to match the secondary FluentSMTP connection
    $from_email = '[email protected]'; // secondary connection
    $from_name  = get_bloginfo( 'name' );

    $phpmailer->setFrom( $from_email, $from_name, false );
    $phpmailer->Sender = $from_email; // optional but recommended
}, 5 );

How It Works

  1. WordPress uses PHPMailer to send emails.
  2. FluentSMTP hooks into PHPMailer and chooses which SMTP connection to use based on the From address.
  3. The snippet above runs before FluentSMTP and updates the From address if any recipient matches the domains in your list.
  4. FluentSMTP sees the new From address and automatically routes the email through the corresponding SMTP connection.

Step 3 – Test the Routing

  1. Send a test email from WooCommerce or a form plugin to a non-Microsoft address (e.g. Gmail).
    → It should send via your primary connection.
  2. Send a test to a Hotmail or Outlook address.
    → It should now show as sent via your secondary connection in the FluentSMTP Email Logs.

If you use both providers’ dashboards, you’ll also see the messages arriving through the expected route.

Notes & Tips

In Summary

By combining FluentSMTP’s multi-connection feature with a simple PHPMailer hook, you can intelligently route WordPress emails based on the recipient domain — no extra plugin or service required.

It’s a lightweight, flexible way to:

MailPoet is one of the most popular email marketing solutions for WordPress. It has a very good integration with WooCommerce and many sites will ask shoppers to sign up to an email newsletter at checkout. However, there is a MailPoet and WooCommerce unsubscribe issue. This blog will detail what the issue is and how you can overcome it.

Table of Contents

What is MailPoet?

MailPoet is a plugin for WordPress that allows users to create and send email newsletters and automated emails from within their WordPress site. It can be used to create and manage mailing lists, design and send newsletters, and track the success of email campaigns. The plugin also includes features such as a built-in WYSIWYG editor for designing newsletters, integration with popular email service providers, and support for sending automated emails, such as welcome messages or abandoned cart reminders. It also can be integrated with Woocommerce, to create, send and track email marketing campaigns from the woocommerce store.

It has a free plan which allows 1,000 subscribers to be signed up before needing to purchase a subscription. This is one of the most generous free plans out of all email marketing solutions for WordPress.

What is the MailPoet WooCommerce unsibscribe problem?

MailPoet has very strong integration with WooCommerce, including an option to sign up shoppers to a newsletter on checkout:

image 160

This works really well, however there is a problem. Let's walk through this scenario:

  1. A new user purchases a product and checks the box to sign up to the newsletter
  2. The user is added to the newsletter once they've confirmed their email address (if enabled)
  3. A little while later, the same user places another order with the vendor. This time, they do not check the newsletter sign up box as they know they are already subscribed
  4. MailPoet will now unsubscribe the user from the newsletter - PROBLEM!

The MailPoet Woocommerce unsubscribe problem can be described as:

If a returning shopper fails to check the newsletter sign up box on WooCommerce checkout, MailPoet will automatically unsubscribe them from the newsletter. Mailpoet treats the failure to check the sign up box as an implicit unsubcribe action.

You've worked hard to build up your subscriber list and now MailPoet, for most of us, is making an incorrect determination on how to handle an unchecked sign up box. Luckily there is a solution.

How to fix the MailPoet Woocommerce unsubscribe problem?

The third party plugin Add-On WooCommerce - MailPoet 3 can be used to overcome this problem. The first thing to do it is install this plugin to you WordPress site.

Next, go to your MailPoet --> WooCommerce settings and uncheck the 'Opt-in on checkout' setting:

image 161

You now need to configure the Add-On WooCommerce - MailPoet 3 plugin. To do this, you can find the settings in WooCommerce --> Settings --> MailPoet. Check 'Enable Subscription' and the relevant options below it:

image 162

In the free version of this plugin, multi-subscription is not enabled. So go to the 'Available Lists' tab and ensure the newsletter you want shoppers to subscribe to is selected:

image 163

That's it. Now any returning shopper who does not select the newsletter opt-in will not be unsubscribed from your newsletter.

Summary

MailPoet is a great email marketing tool with very good integration with WooCommerce. It has an annoying problem when returning shoppers will be unsusbscribed from your mailing lists if they do not check the newsletter signup box every time they place an order. Using the Add-On WooCommerce - MailPoet 3 plugin from Tikweb allows you to overcome this problem and keep users subscribed to your mailing lists, even if they do not check the 'subscribe' box on subsequent orders.

Do you need help with your email marketing or your WordPress / WooCommerce site? Reach out to Web X Design Studio for a free chat about your needs. Why not join are growing list of happy customers?

I subscribe to Freepik for premium images for use on my and on my clients' website builds. Freepik provides a wide variety of high-quality vector images as well. My design tool is Affinity Designer, but if you try to open up the freepik .eps vector file directly, this is what it will look like:

Broken Freepik .eps vector in Affinity Designer

The vector is broken into a checkerboard and is impossible to edit. Freepik is adding some data to the vector file which doesn't make it directly compatible with Affinity, but with an intermediary step, you can edit these correctly with Affinity.

The trick is to import them into Photopea and then export them as. Open the file in Photopea:

Open the Freepik .eps file in Photopea
In Photopea, 'Save as PSD'

Now go to 'File' and 'Save as PSD'. That's all you need to do in Photopea. The .PSD file can now be opened in Affinity and edited as you would any other vector:

Freepik vector now editable in Affinity Designer

Introduction

A Content Delivery Network (CDN) can speed up your website by:

There are many CDN vendors for you to choose from including the likes of Cloudfare and Bunny. This guide will show you how to configure your WordPress site to use the CloudFront CDN from Amazon Web Services (AWS).

Why Choose AWS CLoudFront CDN?

CloudFront is a great choice as it has over 300 edge server locations, which means that no matter where in the world your visitors are coming from, there will be an edge server close to them.

CloudFront also makes it economical to get going, and for low usage sites, it could be totally free for you. Each month the first 1TB of outbound traffic is free. Traffic over 1TB is then charged per TB.

Being built within the overall Amazon Web Services network, you can also be sure that this is going to be a reliable, available and well-supported service.

AWS CloudFront Configuration

This guide will document what you need to configure in CloudFront to use with your WordPress site that is not hosted at AWS itself. If you are hosting your WordPress site in AWS, you will likely need a slightly different configuration.

Once you have created or logged in to your AWS account, search for CloudFront and go to the CloudFront service:

Creating a CloudFront Distribution

You'll need to create a CloudFront Distribution. Go to 'Distributions' and click on 'Create distribution':

The following settings work well for me, depending on your configuration, you may need to change these. Give these a go first of all:

If you do not make these selections you may end up with errors stating that content has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

Click on 'Create Distribution and the distribution will be created. It can take a few minutes. Copy the 'Distribution domain name' as that's what you'll need to enter in to LiteSpeed or other WordPress caching plugins:

Configuring LiteSpeed Cache with CloudFront CDN

If you have not done so already, install the LiteSpeed Cache WordPress plugin. There are many optimisation settings within LiteSpeed, but this guide will just focus on the CDN. Goto the LiteSpeed CDN page:

Hosting Google Fonts in CloudFront

If you are using Google Fonts, it makes sense to host them in CloudFront along with your other files. I use Perfmatters alongside LiteSpeed:

On the Fonts tab:

Checking CloudFront Is In Use

The first thing to do is to purge the LiteSpeed cache. This is particularly important if are reconfiguring LiteSpeed. Once purged, go to your website, and open up your browsers developer tools:

Select 'sources' and if necessary refresh the page. Looking in the sources you should see the CloudFront distribution domain name and if you expand it out, you will see the content being served through CloudFront.

You want your email address on your website for customers to easily contact you, but you don't want a ton of spam coming through. If you don't take steps to protect the email address, spambots will inundate you with unwanted emails. Luckily for us, WordPress has an anti-spambot function that we can easily take advantage of within Oxygen Builder.

Add in a text link element to your page. For the URL enter:

mailto:[oxygen data='phpfunction' function='antispambot' arguments='[email protected]']

If you want to pre-set a subject for the email to be sent to you, modify this statement as:

mailto:[oxygen data='phpfunction' function='antispambot' arguments='[email protected]']?subject=email%20subject

In the text field, enter the following shortcode:

email: [oxygen data='phpfunction' function='antispambot' arguments='[email protected]']

Your text link should now look like this:

On your frontend. you will see:

Clicking on the email address will open your favourite email client with the subject automatically set.

If you look at the source code for the page, you will see that the email address is obfuscated with hex codes:

The spam bots look at the source code, not the frontend, so this now means you can safely add your email address to your website and be safe in the knowledge the bots will not spam you.

Thanks to How to hide email address from spam bots in Oxygen? | OxyWP for guiding me in the right direction.

Oxygen has recently released a Floating Icon Menu as part of their composite elements. It works great! It doesn't automatically close when a child menu icon is clicked through. This guide will show you how to do that.

On my main site I have the Floating Icon Menu configured as such:

Closed
Open

When you add a Floating Icon Menu, you will see something like this in the structure panel:

It's the javascript of the 'Floating Icon Menu Code' element you will need to edit. By default it will look like this:

var floatingMenuIcon = document.querySelector('.oxel_floating_icon_menu__main_icon');

floatingMenuIcon.addEventListener('click', (e) => {
  if( !floatingMenuIcon.classList.contains('oxel_floating_icon_menu__main_icon--active') ) {
  floatingMenuIcon.classList.add('oxel_floating_icon_menu__main_icon--active');
  } else {
  floatingMenuIcon.classList.remove('oxel_floating_icon_menu__main_icon--active');                                
  }
});

To automatically close the floating icon menu on a click of any of the child menu elements, you will need to add and modify the following code below the existing javascript:

function Close_Floating_Menu(){
  floatingMenuIcon.classList.remove('oxel_floating_icon_menu__main_icon--active')
}

document.getElementById("link-180-51").onclick=Close_Floating_Menu;
document.getElementById("link-177-51").onclick=Close_Floating_Menu;

The link id's are those of the link wrappers of the child menu elements. In my case, those labelled 'WhatsApp' and 'message' in the structure panel detailed above. If you have more than 2 child menu elements, then ensure you add a 'Close_Floating_Menu' line for each child menu you have. My final code looks like this:

This is my first time writing any javascript, so it may not be optimal. If you find a better way to do this, then please comment below. Perhaps Oxygen will add this capability in a future release??

This article will show you how to configure the popular WordPress backup plugin - UpdraftPlus - to use Amazon S3 storage. Hosting provider backups should never be relied upon. They are stored in the same location as your website. If anything happens to that location, you lose your website and your backups i.e. you're fu**ed!

There are many reasons to use Amazon Web Services (AWS) S3 Storage, not least, cost -which I will talk about in another blog post. To get started with AWS, you will need to create an AWS account if you do not already have one.

Now your bucket has been created, you need to create a user, think service account, that UpdraftPlus will use to communicate with AWS

To configure UpdraftPlus to write to AWS S3, you need the 'Access Key', the 'Secret Access key' and the bucket name:

Now, all future backups will be written to your AWS S3 bucket. Ensure you do not store too many copies (I'm storing 7 days' worth) otherwise your costs will increase. And remember, it is no good creating a backup if you do not know how to restore that backup, or if the backup copy is not good for any reason. Periodic restores for all your sites (to a temporary WordPress installation) should be performed. You don't want to end up in a disaster scenario not knowing how to restore, or not having any working backups. I'll add a restore process in another blog post.

Google has released their Material Icons which contains more than 1,400 icons. This guide will show you how to add them in to Oxygen Builder.

You need to create an SVG design set to import into Oxygen. The easiest way to do this is to use IcoMoon or download the design set I've created using this method from here. Scroll down and find the Material icons:

Click on 'Add' and on the next screen, select 'select all' from the hamburger menu in the top right. Of course, if you do not want all the icons, you can just select the ones you are interested in:

To generate the SVG design set, select 'Generate SCG & More' from the bottom of the screen (this may take a little time):

Click on the little gear icon, give the design set a name and ensure 'Add <title> to definitions in symbols-defs.svg' is selected:

Close the window and 'download' the design set. Extract the files from the zip that you have downloaded. You have all the icons in SVG and PNG format, but what you are looking for is the symbol-dev.svg file in the root folder. If you do not want to go through the above process, you can download the design set from here.

The design set needs to be added into Oxygen now. Go to Oxygen --> Settings --> SVG Sets. Enter a name for the set, and select the design set SVG file you have either generated or downloaded:

Now, when adding an icon to your page, you can select the Google Material Icons:

I want to apply some consistent styling to images within blog posts i.e. a shadow. Now I expect this could be done with a class through the Gutenberg editor, but a simple piece of CSS styling within the blog page template does what I need.

This is the current structure of my blog post template:

To add in the styling for the images (well it can be used for any elements), add in a code block at the top of the inner content, with code such as:

.ct-inner-content img {
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
  align: center;
}

Now all images on my posts will have a shadow and be center aligned. Targeting just the inner content will prevent the shadow from being applied to images like the logo in the header or footer sections.

You want to scroll to a particular place on a page? Then you need an anchor link. How do you add anchor links within Oxygen Builder?

First of all, each element has an 'id' and you could link directly to that id. For the element below, I could link directly to #code_block-79-51

However, that is not very meaningful. If you want to have a meaningful name for the anchor link, add in a 'code block' to the div you which to link to. Add in the following HTML code to create a hidden anchor link:

<a id="name-of-anchor-link"></a>

e.g.

If you want a visible anchor link, modify the code to be:

<a id="name-of-anchor-link">Visible Anchor Link Text</a>

To link through to the anchor link, just use #name-of-anchor-link e.g.

Bit Form is a new flexible form builder for WordPress. It's currently available for an extremely attractive lifetime deal (i.e. $30 for 50 sites or $50 for 1,000 sites). It supports Google reCAPTCHA to prevent spam messages, but there are no details on how to configure it. If you do not set it up properly, you will see 'ERROR for site owner: Invalid site key':

Follow these steps to configure Bit Form and Bit Form Pro to work with Google reCAPTCHA:

Logon to reCaptch admin and in the top right corner, click '+' to add a new site. Fill in the form. Note Bit Form currently just support reCAPTCHA v2 but v3 is on their roadmap:

Once you click 'submit' your reCAPTCHA site and secret key will be displayed:

Go back to WordPress, and click on 'Bit Form' in the left-hand menu. Under reCAPTCHA copy and paste in the site and secret keys from above:

Now when you publish your contact form, reCAPTCHA will be working as expected:

Do you want to showcase your portfolio in laptop, tablet and mobile phone device mockups? This video tutorial from Permaslug will walk you through. Here's step by step instructions, along with a couple of points I found out along the way:

[device_mockup device="iphone-x" colour="black"][/device_mockup]
.device-frame div::-webkit-scrollbar {
    display: none;
}

Many thanks to the following authors and resources:

Device Mockups in Oxygen Builder | Permaslug

Devices.css - Modern devices in pure CSS (picturepan2.github.io)

Oxygen Builder allows you to do some hover effects on buttons and other elements. I came across this site:

Hover.css - A collection of CSS3 powered hover effects (ianlunn.github.io)

and liked the border transition effects. I want to apply the 'underline from center' effect on my buttons. Here's how I did it with Oxygen Builder:

In my blog listing page, I wanted the blog image as a div background, with the heading aligned to the top of the div and the date and 'read more' button aligned to the bottom of the page.

This proved much easier to achieve than what I first thought. In Oxygen builder, set the Vertical Item Alignment of the containing div to be 'Space Between':

Ensure you have more than 2 child elements in the containing div, wrap individual elements in divs as necessary, add some margin and this is the end result:

A very simple way to ensure the top and bottom child elements are all aligned.

How do you take a "screenshot" of an entire web page? The visible section and the content below the fold? Maybe you want to highlight your web design portfolio or share the full page with your clients.

There's an easy way to do this via Chrome or Edge extensions. I use the free extension GoFullPage. Once installed you will have this little camera icon on your toolbar:

Clicking on it immediately takes a full-page screenshot of your site. Here are the results of one of my sites:

To take a full screenshot of a mobile screen, you do not have to capture on a mobile device. Reduce the width of your desktop browser and you will have the simulation of the mobile display which can be captured using the same method.

With this blog, many times I will want to paste code snippets. Building the blog content and adding a code snippet is easy within the Gutenburg editor. When displaying in my Oxygen Builder Post template, the default code block styling was looking like:

The code in the middle is hard to differentiate from the paragraph text - top and bottom line in the image.

Oxygen does not provide a means to customise the code block styling from within the UI, but this can easily be changed within a stylesheet. I always create a 'CustomCode' stylesheet from the Oxygen 'Manage' menu:

I want to change the styling of the code block to make it stand out more and to change the code display font to Courier. Add this to your CustomCode sytlesheet:

.wp-block-code {
  border: 2px solid darkgrey;
  background-color: #E6E6E3;
  padding: 10px
}

code {
  font-family: "courier new";
  font-weight: 600;
  font-size: 16px;
}

and this is the result:

Much clearer I think you'll agree. Of course, you can vary the styling to get the right effect for yourselves.

I've used the free SEO plugin, SmartCrawl by WPMU Dev previously when using the Divi them builder. I liked it and preferred it to Yoast. To me, the free version had more capabilities than the free version of Yoast. I also preferred the user experience of SmartCrawl.

On switching to Oxygen Builder, I wanted to carry on using SmartCrawl. Out of the box, I found it not to be 100% compatible. The SEO and Readability scoring on pages, posts etc. was not showing. I found this article, but it was not totally clear what needed to be done.

Here I'll detail the steps I took to get SmartCrawl working smoothly with Oxygen Builder:

<?php
function smartcrawl_analysis_content( $content, $id ) {

   // HTML of Oxygen's content.
   $cf = do_shortcode( get_post_meta( $id, 'ct_builder_shortcodes', true ) );

   return $content . $cf;

}
add_filter( 'wds-analysis-content', 'smartcrawl_analysis_content', 10, 2 );

That's it. If you look at you installed plugins, under 'Must Use', you will see the filter you've just created:

If you look at any of your pages, or posts, you will now see the Readability Score:

Smartcrawl readability score with Oxygen Builder

Since creating my first site where I encountered this issue, I have seen SmartCrawl and Oxygen Builder "play nice" out of the box. So only apply this filter if you are having problems displaying the Readability score.

crossmenu
linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram