Skip to content
Plugin PantryPlugins

Documentation / Pantry Subscribe

Pantry Subscribe

Contents: An email sign-up form with double opt-in.

An email sign-up form with double opt-in.

A sign-up form as a block or a shortcode, a confirmation email with a signed link, and the confirmed addresses kept in your own dashboard with a CSV export. It builds the list and stops there. Sending the newsletter is a different job, and this plugin does not pretend to do it.

Using it

  1. Activate the plugin. Plugin Pantry > Subscribe appears, with Subscribers directly under it.
  2. Open Subscribe and set the wording, the confirmation email and the limits.
  3. Put the form on a page, either way round:
    • the shortcode [pantry_subscribe_form]
    • the block Sign-up form (under Widgets in the inserter)

Both render the same form. There are no attributes and no second form: one plugin, one list.

What happens when somebody signs up

  1. The form posts to the page it sits on. The handler checks it, stores a pending subscriber and emails them a link.
  2. The page reloads with ?pantry-subscribe=sent and the form is replaced by your "After the form is sent" wording.
  3. Opening the link in the email flips the subscriber to confirmed. That link expires after seven days.
  4. Only confirmed subscribers are counted, and only they are exported.

The same success message is shown whether the address is new, already waiting or already confirmed. A form that said "you are already subscribed" would tell a stranger who is on your list.

An address that signed up and is still pending is sent at most one confirmation an hour, however many times the form is filled in. Somebody who unsubscribed and signs up again is put back to pending and sent a fresh link straight away.

Unsubscribe links

Every subscriber has a signed unsubscribe link that never expires, so any email you send later can carry one. The links are in the CSV export, one per row, and on each subscriber's own screen in the dashboard. In code:

$url = Pantry\Subscribe\Tokens::unsubscribe_url( $subscriber_id );

Opening it sets the subscriber to unsubscribed. Opening it twice is harmless. The address stays in the list, marked unsubscribed, so a later import cannot quietly add it back.

How the links are signed

A link carries the subscriber id, what it does and, for a confirmation, when it stops working:

?pantry-subscribe=confirm&id=42&t=<signature>&e=<expiry>

The signature is hash_hmac( 'sha256', $id . '|' . $action . '|' . $expires, wp_salt( 'auth' ) ) and it is compared with hash_equals(). Changing the id, the action or the expiry breaks the signature, and an expiry that has passed is refused even when the signature is right.

Spam handling

Four checks run in this order, before anything is stored:

  1. Nonce. A WordPress nonce tied to the form.
  2. Honeypot. A hidden pantry_subscribe_website field that must stay empty.
  3. Timing. A signed timestamp is put in the form when it is rendered. The signature is a wp_hash() of the timestamp, so it cannot be forged, and the sign-up is rejected if it comes back faster than the minimum time or more than a day later.
  4. Rate limit. A transient counts sign-ups per address per hour. The address is hashed with a salt before it is used as a key.

There is no CAPTCHA and there never will be.

The visitor's address is never stored in full. The last block is dropped (203.0.113.45 becomes 203.0.113.0) before it is saved with the subscriber.

Page caching

The form carries a nonce and a signed timestamp, and both stop being accepted about a day after the page was rendered. A page cache that keeps a copy of the page for longer than that hands every visitor an expired form, and they see the "try again" message. Keep the cache lifetime of pages carrying the form under a day, or exclude those pages from the cache.

Subscribers

Every address is one private pantry_subscriber post, titled with the address. The list screen shows email, name, status, when they signed up and the page they signed up on, with a search box and a status filter. Deleting a row deletes the subscriber: an address you have deleted can sign up again from scratch.

Export confirmed subscribers streams a CSV of the confirmed addresses only, with the name, the confirmation time, the sign-up time, the source page and the unsubscribe link for each one.

Settings

All on Plugin Pantry > Subscribe, stored in one option, pantry_subscribe_settings.

Sign-up form

Setting Default What it does
Ask for a name On Shows a name field next to the address.
Name required Off The name must be filled in. Ignored when the name field is not shown.
Email label Email Label for the address field.
Name label Name Label for the name field.
Consent wording empty Empty means no consent box. Fill it in and the box is shown, and must be ticked before a sign-up is accepted.
Button label Subscribe Text on the submit button.
After the form is sent Thank you. Please open the link in the email we have just sent to confirm your address. Shown in place of the form after a sign-up.
Form stylesheet On Loads the small stylesheet on pages that show the form. Turn it off to style the form in your theme.

Confirmation email

Setting Default What it does
Subject Please confirm your subscription to {site} Subject of the confirmation email.
Message A short plain-text note with the link Body of the confirmation email.

Placeholders in both: {site}, {name}, {email} and {confirm_link}. If the body does not contain the link, it is added at the end, so a confirmation email can never go out without one.

After a link is opened

Setting Default What it does
Confirmed Thank you. Your subscription is confirmed. Wording shown after a confirmation.
Unsubscribed You have been removed from the list. Wording shown after an unsubscribe.
Page to send them to Show a plain message instead A page on this site to open instead. The outcome is added to its address (?pantry-subscribe=confirmed), so a sign-up form on that page shows the message.

When no page is chosen, the plugin shows a plain page with the site's name on it and a link home.

Notification to you

Setting Default What it does
Tell me about confirmations Off Sends you an email when somebody confirms.
Send to empty One address per line. Empty means the site admin address.

Spam and limits

Setting Default What it does
Minimum time to fill in 3 Seconds. A form sent faster than this is rejected. 0 switches the check off.
Sign-ups per hour 5 How many sign-ups one address may send in an hour.

Filters and actions

Every hook is prefixed pantry_subscribe_.

pantry_subscribe_confirm_lifetime

How long a confirmation link works for, in seconds. Seven days by default, one hour at the shortest.

add_filter( 'pantry_subscribe_confirm_lifetime', fn () => 2 * DAY_IN_SECONDS );

pantry_subscribe_email_subject, pantry_subscribe_email_body, pantry_subscribe_email_headers

The confirmation email, after the placeholders have been replaced. The second argument is the subscriber id.

add_filter(
	'pantry_subscribe_email_headers',
	function ( array $headers ): array {
		$headers[] = 'Reply-To: hello@example.com';
		return $headers;
	}
);

pantry_subscribe_notify_to

The addresses told about a new confirmed subscriber.

add_filter( 'pantry_subscribe_notify_to', fn ( array $to ) => array( 'list@example.com' ) );

pantry_subscribe_message

The wording a visitor reads for an outcome code: nonce, spam, too_fast, stale, rate, email, consent, required, already, refused or invalid. The three long messages (sent, confirmed, unsubscribed) are settings instead.

add_filter(
	'pantry_subscribe_message',
	function ( string $message, string $code ): string {
		return 'rate' === $code ? 'Steady on. Try again in an hour.' : $message;
	},
	10,
	2
);

pantry_subscribe_added, pantry_subscribe_confirmed, pantry_subscribe_unsubscribed

Fired when a pending subscriber is stored, when somebody confirms, and when somebody unsubscribes. Each passes the subscriber id and the address.

add_action(
	'pantry_subscribe_confirmed',
	function ( int $id, string $email ): void {
		error_log( $email . ' joined the list' );
	},
	10,
	2
);

pantry_subscribe_rejected

Fired with the reason code when a sign-up is turned away.

What it stores

  • One option, pantry_subscribe_settings, plus pantry_subscribe_version.
  • One private post type, pantry_subscriber, with the meta keys _pantry_subscribe_status, _pantry_subscribe_name, _pantry_subscribe_source, _pantry_subscribe_ip (masked), _pantry_subscribe_consent and _pantry_subscribe_confirmed.
  • Two transients: the hourly rate-limit count per address and the hourly resend guard per pending subscriber.

No tables, no cron events, no outside requests. Uninstalling removes all of it, subscribers included, so export the list before you delete the plugin.

Size

The plugin's own PHP is about 2,000 lines, a little over the 1,500 the Pantry Standard asks for, so here is the reason. Roughly 1,380 of those lines are code and the rest are comments and blank lines. Two thirds of the code is in the two pieces the scope requires and the other Pantry plugins do not have: the subscriber list screen with its status filter, meta box and CSV export, and the signed link handling with its three outcomes for confirming and its three for unsubscribing. The sign-up form, the anti-spam checks and the settings screen are the size they are in Pantry Contact. Nothing here is a second feature looking for a home; cutting anything would cut something in the does list.

What it does not do

  • No newsletter sending, campaigns or templates.
  • No integrations with email services.
  • No segments, tags or automation.
  • No tracking pixels or open rates.
  • No CAPTCHA.

The plugin