Architecture
This is a developer-facing reference for how Super Speedy Coming Soon Products is put together. It covers the major components, how a request flows through them, the parts that are unusual or easy to get wrong, and then the detailed design of each notable component.
The plugin is deliberately small and procedural — a single main file
(super-speedy-coming-soon.php), one front-end template, one JavaScript file, and
one stylesheet. There are no classes; everything is plain functions wired to
WordPress/WooCommerce hooks.
Table of Contents
Component summary
| Component | Where | Purpose |
|---|---|---|
| Product admin fields | sscs_add_custom_fields / sscs_save_custom_fields |
Per-product configuration (coming-soon flag, arrival date, offer HTML, provider, MailerLite group / SSE form), stored as post meta. |
| Add-to-cart replacement | sscs_maybe_replace_add_to_cart + helpers |
Swaps WooCommerce’s add-to-cart output for the coming-soon template, theme-agnostically, by discovering the hook at runtime. |
| Coming-soon template | templates/sscs-coming-soon-template.php |
The front-end UI: countdown, offer description, signup form (MailerLite or SSE), share buttons. |
| Provider abstraction | template branch + class_exists('SSE_Forms') |
Each product collects signups via MailerLite (built-in) or Super Speedy Emails (optional sibling plugin). |
| AJAX signup handler | sscs_ajax_signup_handler |
The live signup endpoint used by the MailerLite form. Public (nopriv). |
| Legacy POST handler | sscs_process_signup_form |
An older non-AJAX signup path on init. Effectively dead code (see below). |
| Abuse-protection layer | honeypot + sscs_check_rate_limit + sscs_verify_turnstile |
Defends the public endpoint: honeypot, per-IP rate limit, optional Cloudflare Turnstile. |
| MailerLite client | sscs_add_subscriber_to_mailerlite |
Three-step MailerLite REST flow: upsert subscriber, then assign to group. |
| Referral system | JS (sscs-ajax-signup.js) + referrer_id column |
Cookie + URL-param sharing scheme that records who referred whom. |
| Countdown timer | sscs-ajax-signup.js |
Pure client-side countdown to the arrival date. |
| Settings page | sscs_settings_page |
Global options: MailerLite API key + the spam/abuse controls. |
| Signups table | wp_sscs_signups + sscs_create_custom_table |
Local ledger of every signup (email, MailerLite id, referrer, WP user). |
| Test suite | .tests/ |
wp eval-file harness. |
Unusual or interesting aspects
A few things here are worth knowing before you change anything:
-
The add-to-cart replacement discovers its hook at runtime. Rather than assuming WooCommerce’s default hook, it checks a list of known locations and, failing that, walks the entire
$wp_filtergraph looking forwoocommerce_template_single_add_to_cartand rebinds the coming-soon template to the same hook/priority. This makes it theme-independent but is the single most surprising piece of the codebase. See Add-to-cart replacement. -
There are two signup code paths, and one is dead. The live path is the AJAX handler. The older
sscs_process_signup_form()oninitstill runs on every request but is effectively unreachable, because the form it expects (with thesscs_signupnonce) is never rendered by the current template. Keep this in mind so you don’t “fix a bug” in code that never executes — or, better, delete it. See The two signup paths. -
The nopriv AJAX nonce is not a security boundary. Because the endpoint is public, WordPress hands a valid
sscs_signup_nonceto every visitor viawp_localize_script. The nonce only prevents trivially-replayed cross-site posts; it does not stop abuse. That is why the abuse-protection layer (rate limit + honeypot + optional CAPTCHA) exists and is the real defence. -
The referral identity is the visitor’s own MailerLite subscriber id. After signup, the handler returns the MailerLite id, stores it in the
sscs_mailerlite_user_idcookie, and the JS builds share links carrying it as?shared_by=<id>. The next visitor’s browser turns that into thesscs_shared_by_idcookie, which is posted back and saved asreferrer_id. It’s an entirely client-mediated loop. See Referral system. -
Provider selection is per-product and the SSE provider is optional. The Super Speedy Emails option only appears (and only renders) when the
SSE_Formsclass exists. There is no hard dependency. -
Watch the CSS class typo. The body class added for styling is
scss-coming-soon-product(notescss, notsscs). It’s inconsistent with the rest of thesscs_/sscs-naming but is load-bearing for the stylesheet — don’t “correct” it without updating the CSS. -
Security posture is documented separately. The H1/M1 fixes and the L1 secret-masking are implemented; a couple of lower-severity items (e.g. the MailerLite-id leak) remain open.
Request / data flow
Rendering a coming-soon product page:
wp (is_product + _sscs_coming_soon=yes)
└─ sscs_maybe_replace_add_to_cart()
├─ fast path: known hook locations
└─ slow path: scan $wp_filter for woocommerce_template_single_add_to_cart
└─ remove_action(core) + add_action(sscs_render_coming_soon_template)
wp_enqueue_scripts
└─ enqueue sscs-ajax-signup.js + css (+ Turnstile if enabled)
└─ wp_localize_script → sscs_ajax { ajax_url, nonce, post_id, arrival_date, captcha }
template renders countdown + offer + (MailerLite form | SSE form) + share buttons
Submitting the MailerLite signup form (AJAX):
POST admin-ajax.php?action=sscs_ajax_signup
└─ sscs_ajax_signup_handler()
1. honeypot (sscs_honeypot_enabled + $_POST['name'])
2. nonce (check_ajax_referer)
3. rate limit (sscs_check_rate_limit, per-IP transient)
4. CAPTCHA (sscs_captcha_enabled + sscs_verify_turnstile)
5. email valid? (is_email)
6. product valid? (product + _sscs_coming_soon=yes + group set)
7. dedupe + insert into wp_sscs_signups
8. sscs_add_subscriber_to_mailerlite() → MailerLite REST
9. set sscs_mailerlite_user_id cookie
10. wp_send_json_success({ mailerlite_id })
The numbered order in step list matters: the cheap, abuse-blocking checks run before any database query or outbound HTTP.
Component details
Product admin fields
Two functions on WooCommerce product hooks:
sscs_add_custom_fields()onwoocommerce_product_options_general_product_datarenders the fields in the Product data → General tab using thewoocommerce_wp_*helpers. An inline jQuery snippet toggles the MailerLite vs SSE fields based on the selected provider.sscs_save_custom_fields( $post_id )onwoocommerce_process_product_metapersists them as post meta.
Stored meta keys:
| Meta key | Sanitisation on save | Notes |
|---|---|---|
_sscs_coming_soon |
'yes'/'no' |
Master switch; gates almost everything else. |
_sscs_arrival_date |
sanitize_text_field |
YYYY-MM-DD; consumed by the JS countdown. |
_sscs_offer_description |
wp_kses_post |
HTML allowed; sanitised again on output. |
_sscs_email_provider |
sanitize_text_field |
mailerlite (default) or sse. |
_sscs_mailerlite_group_id |
sanitize_text_field |
Target MailerLite group. |
_sscs_sse_form_id |
absint |
Only shown/used when SSE is active. |
Capability and nonce handling are inherited from WooCommerce’s product save flow,
so the plugin doesn’t re-check them here. The offer description is the only field
that accepts HTML, and it’s filtered through wp_kses_post on both save and
render, so it can’t carry script.
Add-to-cart replacement
This is the cleverest and most fragile part. The goal: replace WooCommerce’s add-to-cart output with the coming-soon template without depending on a specific theme’s hook layout.
sscs_maybe_replace_add_to_cart() runs on wp. After confirming we’re on a
coming-soon single-product page, it:
-
Fast path — iterates
$known_locations(currently justwoocommerce_single_product_summary@ 30, with commented examples for themes like REHub). For each,sscs_try_replace_add_to_cart()checks whetherwoocommerce_template_single_add_to_cartis actually bound there at that priority; if so it removes the core callback and bindssscs_render_coming_soon_templatein its place, then returns. -
Slow path — if no known location matched, it walks the entire
$wp_filterarray (every hook, every priority, every callback) to find wherewoocommerce_template_single_add_to_cartis registered, and rebinds there.
sscs_render_coming_soon_template() simply includes the template file.
Design implications:
– It works across themes that move the add-to-cart to a custom hook — the slow
path finds it wherever it lives.
– It only recognises the standard woocommerce_template_single_add_to_cart
callback. A theme that renders add-to-cart through a fully custom mechanism
won’t be detected. The remedy is to
add the theme’s hook to $known_locations.
– The slow path is O(all registered hooks); the fast path exists precisely to
avoid paying that on every coming-soon page view.
Coming-soon template
templates/sscs-coming-soon-template.php reads the product meta and renders:
- A countdown scaffold (
#days/#hours/#minutes/#seconds) populated by JS. - The offer description, run through
wp_kses_post+wpautop. - A provider branch:
- If provider is
sse, a form id is set, andSSE_Formsexists →SSE_Forms::render( (int) $sse_form_id, 'inline' )delegates the entire form (markup, fields, AJAX) to Super Speedy Emails. - Otherwise → the built-in MailerLite form: a hidden honeypot
namefield, the email input, the optional Turnstile widget (only whensscs_captcha_enabled()), and a submit button. Below it sit the share buttons (hidden until signup succeeds).
The template is pure output; all behaviour lives in the JS.
Provider abstraction
There is no formal interface — provider choice is a per-product meta value plus a
runtime capability check (class_exists('SSE_Forms')). MailerLite is always
available and is the default; SSE is offered only when the sibling plugin is
active. When SSE is selected the plugin steps out of the way entirely and lets
SSE_Forms::render() own the form, so the AJAX handler, honeypot, rate limit and
MailerLite client in this plugin are only exercised on the MailerLite path.
The two signup paths
There are two server-side entry points for a signup, and only one is live:
- AJAX (
sscs_ajax_signup_handler, live). Registered on bothwp_ajax_nopriv_sscs_ajax_signupandwp_ajax_sscs_ajax_signup. This is what the template’s MailerLite form posts to. See below. initPOST (sscs_process_signup_form, legacy/dead). Listens for$_POST['sscs_submit']and asscs_signupnonce, then redirects with WC notices. The current template never renders a form with that field or nonce, so this path doesn’t fire in normal use. It’s retained history; treat it as dead code (a candidate for removal) rather than an active surface.
AJAX signup handler
sscs_ajax_signup_handler() is the heart of the live flow. Its checks run in a
deliberate cheap-to-expensive order so abuse is rejected before any DB/HTTP work:
- Honeypot — if
sscs_honeypot_enabled()and the hiddennamefield is non-empty, return a “Spam detected” error. - Nonce —
check_ajax_referer('sscs_signup_nonce','nonce'). (Public nonce; see the interesting-aspects note — not a real abuse control.) - Rate limit —
sscs_check_rate_limit()per IP. - CAPTCHA — if
sscs_captcha_enabled(), requiresscs_verify_turnstile(). - Email —
sanitize_email+is_email. - Product —
post_idmust resolve to aproductpost type marked_sscs_coming_soon = yesand have a MailerLite group configured. This is the M1 fix; it blocks blind hammering against arbitrary post IDs. - Dedupe + persist — look up the email in
wp_sscs_signups; insert a row if new (the uniqueemailkey also enforces this at the DB level). - MailerLite —
sscs_add_subscriber_to_mailerlite(). - Cookie + response — set
sscs_mailerlite_user_idand return the MailerLite id in the JSON success payload (feeds the referral system).
Every failure exits via wp_send_json_error; success via wp_send_json_success.
Abuse-protection layer
Three independent, settings-driven controls, all defined alongside the handler:
- Honeypot (
sscs_honeypot_enabled, optionsscs_honeypot_enabled, default on). A hiddennamefield; if a bot fills it, the request is dropped. - Per-IP rate limit (
sscs_check_rate_limit, always on). A transient keyedsscs_rl_<md5(ip)>counts submissions; defaults to 5 per 10 minutes, tunable via thesscs_rate_limit_maxandsscs_rate_limit_windowfilters. Fails open when noREMOTE_ADDRis available (won’t lock out real users). Note: transient-backed, so a persistent object cache makes it effective under load. - Cloudflare Turnstile (
sscs_verify_turnstile, option-gated, default off).sscs_captcha_enabled()returns true only when the toggle is on and both site and secret keys are set. The front-end renders the widget and loads Cloudflare’s script; the handler verifies the token server-side againstchallenges.cloudflare.com/turnstile/v0/siteverifyand fails closed on any error. This is the only layer that defeats a distributed, IP-rotating attacker.
MailerLite client
sscs_add_subscriber_to_mailerlite( $email, $group_id ) talks to the MailerLite
v2 (“connect”) REST API using the global API key from sscs_mailerlite_api_key.
If no key is set it no-ops (signups still land in the local table). The flow:
POST /api/subscriberswith the email andgroups: [group_id]— upserts the subscriber. 200 = already existed, 201 = created.- Read
data.idfrom the response → this becomes the returned subscriber id. POST /api/subscribers/{id}/groups/{group_id}— belt-and-braces group assignment in case step 1 didn’t attach it.
Errors are logged via error_log and swallowed (the user still sees success).
The returned subscriber id flows back into the handler’s response, the cookie, and
the mailerlite_id column.
Referral system
Entirely client-mediated, implemented in js/sscs-ajax-signup.js plus the
referrer_id column:
- On a successful signup the handler returns
mailerlite_idand sets thesscs_mailerlite_user_idcookie. The JS then reveals the share buttons and rewrites the Facebook/X share URLs to include?shared_by=<that id>. - When a referred visitor lands with
?shared_by=<id>in the URL, the JS stores it in thesscs_shared_by_idcookie. - On that visitor’s own signup, the JS posts
shared_byback; the handler sanitises it and stores it asreferrer_id.
So referrer_id records which existing subscriber’s share link brought this
person in. The plugin stores the relationship but does not itself compute any
“priority” from it — that’s left to whatever you do with the data downstream.
Countdown timer
Pure JavaScript. arrival_date is passed to the page via wp_localize_script
(sscs_ajax.arrival_date) and parsed with new Date(...). A 1-second interval
updates the day/hour/minute/second boxes and hides the timer once the date
passes. An empty/invalid arrival date yields NaN (documented in troubleshooting)
— there is no server-side guard for that.
Settings page
sscs_settings_page() under WooCommerce → Super Speedy Settings (registered
with the manage_options capability). It’s a single nonce-protected form that
saves five options:
| Option | Default | Purpose |
|---|---|---|
sscs_mailerlite_api_key |
'' |
Global MailerLite key. |
sscs_honeypot_enabled |
yes |
Honeypot toggle. |
sscs_captcha_enabled |
no |
Turnstile toggle. |
sscs_turnstile_site_key |
'' |
Turnstile front-end key. |
sscs_turnstile_secret_key |
'' |
Turnstile server key. |
(The MailerLite API key and the Turnstile secret key are write-only in the UI:
they render as masked password fields with an empty value, the stored secret is
never sent to the browser, and a blank submission keeps the existing value. The
Turnstile site key stays a plain text field — it’s public by design.)
Signups table
sscs_create_custom_table() runs on activation (register_activation_hook) and
builds wp_sscs_signups via dbDelta:
id BIGINT UNSIGNED PK, auto-increment
email VARCHAR(255) NOT NULL, UNIQUE
mailerlite_id VARCHAR(255) NOT NULL
referrer_id VARCHAR(255) NULL -- the shared_by id, if referred
wp_user_id BIGINT UNSIGNED NULL -- logged-in user, if any
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
It is a local ledger independent of MailerLite — the source of truth for “who
signed up, when, and who referred them.” The UNIQUE email key is what makes the
handler’s dedupe safe even under races.
Asset loading
sscs_enqueue_scripts() and sscs_enqueue_styles() both early-return unless the
current page is a coming-soon single product, so nothing is loaded site-wide. The
script enqueue also conditionally loads Cloudflare’s Turnstile script and localises
the sscs_ajax object (ajax url, public nonce, post id, arrival date, captcha
flag). dashicons is enqueued for the share-button icons.
Where to look first
- Changing what’s collected per product → Product admin fields.
- The form not appearing on a theme → Add-to-cart replacement (
$known_locations). - Signup behaviour, validation, spam → AJAX signup handler + Abuse-protection layer.
- MailerLite quirks → MailerLite client.
- Sharing/referrals → Referral system (mostly JS).