Skip to main content

Documentation

No results found.
Features Members

Marketing (Email, SMS & Web Push Campaigns)

WebProCMS includes a built-in marketing feature — design branded broadcast emails in a familiar block editor, write plain-text SMS campaigns, or send browser push notifications; deliver them to your subscriber list or targeted CRM segments,...

WebProCMS includes a built-in marketing feature — design branded broadcast emails in a familiar block editor, write plain-text SMS campaigns, or send browser push notifications; deliver them to your subscriber list or targeted CRM segments, and measure results. There's no Mailchimp account to pay for, no embed scripts, and your list never leaves your own database.


What it does

  • Design emails visually in a block-based designer with a live preview — the layout mirrors the page builder (preview on the left, editable blocks on the right), so anyone who has edited a page already knows how to build an email.
  • Send to the right people: the whole subscriber list, every CRM contact, or a specific CRM group or status (e.g. "Customers" only).
  • Start from templates: four built-in starting points (Announcement, Monthly Update, Promotion, Simple Note), plus save-your-own templates for recurring sends.
  • Measure results: unique opens, unique clicks, and per-link click totals on every campaign — including link clicks in text messages, via short redirects on your own domain.
  • Set up automated drips: sequences send a timed series ("day 0 welcome, day 3 tips, day 7 offer") to each new subscriber or CRM-enrolled lead automatically — see "Sequences" below.
  • Stay compliant automatically: every email ends with your business name, mailing address, and an unsubscribe link; one-click unsubscribe headers (RFC 8058) are always sent; opt-outs are honored across every audience forever. Optional double opt-in confirms new signups by email.
  • Send through anything: the server's own mail settings, or a bulk email service (Postmark, Resend, Mailgun, SendGrid) by pasting one API key — no extra packages or configuration files.
  • A/B test subject lines: try two subjects on a slice of the audience; the better opener automatically goes to everyone else.
  • Schedule at the best time: the scheduler recommends the weekday/hour your own readers actually open, from your send history.
  • Reach browsers with Web Push: a third channel with no per-message cost and no external account — visitors opt in via a small prompt, notifications deliver straight to their browser.
  • Check deliverability before you send: a domain wizard verifies SPF/DKIM/DMARC/MX records, and every email draft gets a one-click spam-score scan.
  • Automatic blog digests: a recurring "what we published" email assembled from your newest posts — sent (or drafted for review) on a schedule.

Find it under Dashboard → Marketing (enabled by default for members; toggle under Settings → Features). Campaigns come in three channels: Email (the block designer below), Text message (SMS) (see “SMS campaigns”), and Web Push (see “Web Push notifications”).

The email designer

Open an email campaign and you get a two-pane designer:

  • Left: live preview — the actual email HTML rendered in an iframe, refreshed as you type, with sample data filled into personalization tokens.
  • Right: the message and its blocks — subject, preview text (the snippet inboxes show after the subject), audience picker with a live recipient count, and the block list.

Blocks

Emails use their own small library of email-safe blocks rather than the page builder's design library. Email clients (Outlook especially) can't render modern CSS, so each block compiles to bulletproof inline-styled table HTML behind the scenes:

Block What it does
Logo Your site logo (or any image), with width and alignment controls
Heading Large (H1) or medium (H2) headline
Text Body copy — supports paragraphs, basic inline HTML, and personalization tokens
Image Full-width image from the media library, optionally linked
Button Call-to-action button — defaults to your brand's primary color
Divider Thin horizontal rule
Spacer Vertical breathing room (16–64px)

Blocks can be duplicated, removed, and reordered with the same click-to-move pattern as the menus editor: click the move icon on a block, then click the block you want to drop it on (Esc cancels). Everything autosaves as you edit.

Images go through the standard media library picker. Buttons inherit the site's primary brand color automatically (override per button if needed).

Personalization

{{name}} and {{email}} work in any text block and in the subject line. {{name}} falls back to "there" when the recipient has no name ("Hi there,"). The unsubscribe URL is unique per recipient and inserted automatically in the footer.

With the Reviews feature on, {{review_link}} becomes each recipient's own trackable review-funnel link — use it as a button's URL or in a text block or SMS body. At send time it mints (or reuses, while unanswered) a review request for that recipient, so the Reviews dashboard tracks who opened the funnel and who clicked through to a platform. Click tracking deliberately never rewrites it, and with Reviews off it degrades to the site home URL.

You never build the compliance footer — every email automatically ends with the business name, the mailing address from Marketing Settings, a "you're receiving this because you subscribed" line, and the recipient's unsubscribe link. Anti-spam laws (CAN-SPAM, GDPR) require the address and the opt-out link in marketing email.

Audiences & CRM targeting

The Send to picker offers:

  • All subscribers — everyone on the newsletter list who hasn't opted out (and has confirmed, when double opt-in is on).
  • Subscribers + all CRM contacts — the combined reach.
  • Any CRM group — e.g. a "VIP" or "Wholesale" group.
  • Any CRM status — e.g. only "Customer" contacts.

Recipients are deduped by email across sources, and the designer shows the live count after suppression. When a CRM contact is targeted, they get a subscriber record (source crm) at send time so their unsubscribe link works like anyone else's.

Opt-outs are global. Anyone who unsubscribes is suppressed from every future audience — including CRM-targeted sends — until they explicitly opt back in. Admins can also unsubscribe or fully remove a person from Marketing → Subscribers (e.g. on request).

Subscribers

Dashboard → Marketing → Subscribers lists everyone with search and a status filter (Subscribed / Pending confirmation / Unsubscribed / Everyone). From here you can add people manually (optionally with a phone number and a recorded SMS opt-in), unsubscribe/resubscribe them, toggle their SMS consent, or remove them entirely.

People join the list from the site through the newsletter signup rows in the page builder — Contact Newsletter, Hero Newsletter, CTA Newsletter Signup, the Blog Detail Newsletter CTA, and the sidebar Newsletter widget all post to the built-in signup endpoint. The forms work on statically cached pages (plain POST, no JavaScript required), are rate-limited, and carry an invisible honeypot field that silently drops naive bots. Successful signups land on a "You're subscribed!" confirmation page.

Every signup row also ships an optional phone + SMS opt-in block (off by default — enable it per form with the "Show Phone & SMS Opt-in" toggle in the page editor). It adds a phone field and an explicit "Also text me…" consent checkbox; the checked box is the express SMS consent (TCPA), so a phone submitted without it is stored but never texted.

CSV import: the Subscribers page has an Import CSV button (headers: email, name, phone, sms_opt_in; sample file downloadable). Existing subscribers are matched by email and never resubscribed if they opted out; sms_opt_in is only honored on rows that also carry a phone. Pending (double-opt-in) subscribers get a Resend confirmation action in their row menu.

Double opt-in

Off by default; enable under Marketing → Settings → Signup. With it on:

  • New form signups are held as Pending and emailed a branded confirmation link (rendered through the same email engine, so it matches your branding).
  • Pending people are excluded from every send until they click the link.
  • A previously-unsubscribed person who signs up again must also reconfirm — nothing silently reactivates an opt-out.
  • Subscribers added manually from the dashboard are confirmed immediately (an admin adding someone is the consent check).

Double opt-in proves consent (recommended under GDPR), keeps typo'd and forged addresses off the list, and protects sender reputation.

Templates

Dashboard → Marketing → Templates holds the starting points (email campaigns only — SMS has no blocks to template):

  • Built-in: Announcement, Monthly Update, Promotion, Simple Note — generic block layouts for the most common sends.
  • Your own: in the designer, Save as Template snapshots the current block layout for reuse — ideal for recurring formats like a monthly roundup.

"Use Template" creates a fresh draft from the template's blocks. Built-ins can't be deleted; custom templates can.

Sending

  • Send now — a confirmation dialog shows the audience and exact recipient count before anything goes out.
  • Schedule — pick a date/time; a background task (LazyCron, 60-second cadence) dispatches due campaigns, so scheduled sends work even on hosts without a real cron. When there's enough open history (30+ opens in the last 90 days), the schedule dialog also shows the best send time for your list — the weekday and hour your own readers open most — with a one-click "use it" button. No history yet, no guess: the suggestion simply doesn't appear.
  • Send a test — email the current draft to yourself at any time (marked [Test], never tracked, doesn't touch the list).

A/B subject-line testing

On any email campaign (not SMS/push/sequence steps), flip on A/B Test the Subject in the designer sidebar:

  • Enter Subject B, pick the test group size (10–50% of the audience) and how long to wait before picking the winner (30 minutes–24 hours).
  • On send, the test slice gets subject A or B (split evenly, assigned deterministically so an interrupted send never reshuffles anyone); the rest of the audience is held back.
  • After the wait window, the subject with more unique opens wins (clicks break ties; a dead heat falls back to A) and the remainder sends automatically with the winning subject — no one has to come back and press anything.
  • The sidebar shows each variant's sent/opens/clicks live, plus the winner badge once decided. The campaign stays in "Sending" between phases; that's normal.

Guard rails: audiences under 10 recipients skip the test (everyone gets subject A — a split that small is noise), and the designer warns if open tracking is off, since the winner is measured by opens.

Sends run as a queued job with a per-recipient delivery log. Individual failures are recorded (visible on the campaign) without aborting the run, and an interrupted send can be retried without double-sending anyone. Campaign cards show delivered/failed counts live.

Sending providers

Marketing → Settings → Sending Provider picks how email leaves the server:

  • Server email (default) — whatever the site's mail settings already use. Fine for small lists.
  • Postmark / Resend / Mailgun / SendGrid — bulk email services with proper deliverability tooling. Paste the service's API key under Settings → API Keys → Email Sending, then select the provider here. The calls go straight to each provider's HTTP API — no extra server packages.

The settings page shows a "Configured / Needs API key" badge per provider. Sender identity (From name/email, Reply-to) is set on the same page; leave blank to inherit the site-wide mail settings.

For deliverability at any real volume: use a bulk provider, send from a domain you've verified with that provider (SPF/DKIM), and keep the postal address filled in.

Open & click tracking

Each can be toggled under Marketing → Settings → Open & Click Tracking. Opens are on by default; both click switches are off by default (link rewriting changes the visible link to a redirect, which security/spam filters scrutinize, so it's opt-in for deliverability).

  • Opens — an invisible 1×1 image per recipient. First load marks the recipient as opened and bumps the campaign's unique-opens count. Email only — a text and a notification have nowhere to put a pixel, so neither reports opens, ever.
  • Clicks — links are rewritten through a short redirect on your own domain. The destination is stored server-side (nothing user-controlled in the URL, so the redirect can't be abused), and each recipient's first click counts once toward unique clicks; per-link totals accumulate on every hit.
  • Clicks in text messages — a separate switch, because SMS pays for tracking by the character. See below.

Turning on Track clicks in text messages rewrites every http(s) link in an SMS campaign to a short per-recipient redirect on your own domain (/r/{code}/{link}) — never a third-party shortener, which carriers filter far harder than a link to the site the message is about. Texts then report unique clicks, per-link totals, and top links exactly like emails, and SMS clicks feed Analytics attribution with utm_source=sms.

The cost is length. A tracked link runs about 30 characters, and carriers bill per 160-character segment, so a rewrite can push a message into another segment. The composer's character/segment counter already accounts for this — it measures the message as it will actually be sent, not as typed — so watch that number rather than guessing.

Details worth knowing:

  • Trailing punctuation stays out of the link. "Book now: https://example.com/book." tracks …/book, not …/book..
  • {{review_link}} is never rewritten. The review funnel does its own per-recipient tracking; wrapping it would break that.
  • No opens. An SMS click is recorded as a click only — unlike email, it does not imply an open, because a campaign that can't measure opens shouldn't report an open rate.
  • Existing campaigns aren't retroactively changed. Tracking applies at send time, so a text already sent keeps whatever links it went out with.

Sent campaigns show a Performance card in the designer — delivered count, open rate, click rate, send time, and a top-links table — and the campaigns index shows opens/clicks per row. A text campaign shows its click numbers only when it was sent with link tracking on; without it the card stays a delivered/failed count, as before.

What's deliberately not tracked: the unsubscribe link (always a direct URL, so one-click unsubscribe never routes through tracking), and test sends/previews. Tracking hits are also invisible to the site's own Analytics feature.

Bot filtering. Security gateways like Outlook SafeLinks fetch every link in an email at delivery time to scan it, and phone messaging apps fetch a link preview the moment a text arrives — on a raw counter both would register as a click (and, for email, an implied open) the recipient never made, so a corporate-heavy list would show implausibly high click rates within minutes of sending. Tracking hits are therefore filtered: a hit is ignored when it lands within about ten seconds of the send (the delivery-scan window — a real click needs the message opened and read first), when it's a bare HEAD probe, or when its user agent identifies a known scanner, crawler, HTTP library, or link-preview bot. The recipient is never affected — the redirect (or pixel) is served either way; only the counting is skipped, and a skipped open is still recorded by any later one. The filter is heuristic by nature: a gateway that scans minutes after delivery with a stock browser user agent still slips through, so corporate-heavy lists can still read slightly high — true of every email platform.

Caveat to set expectations: image-blocking mail apps undercount opens, and Apple Mail privacy protection prefetches pixels regardless of whether the message is read, which overcounts them. This is true of every email platform.

SMS campaigns

The New Campaign button offers Email campaign or Text message (SMS). SMS campaigns share the whole pipeline — audiences, scheduling, the queued send job with its per-recipient delivery log, test sends — but compose differently:

  • Plain-text composer with a live phone-style preview, a character/segment counter (carriers bill per 160-character GSM segment, 70 for unicode), and {{name}} personalization.
  • Attach an image to send as MMS — pick from the media library and the campaign goes out as a picture message (Twilio fetches the image from your site). The composer flags the trade-offs: MMS costs roughly 2–4× per message and is best supported on US/Canadian numbers.
  • "Reply STOP to opt out." is appended automatically unless the message already mentions STOP — carrier rules require opt-out language in marketing texts.
  • The Subject field doubles as an internal campaign name (it never appears in the text).

Provider setup

SMS needs a texting gateway — unlike email there is no server fallback. Configure it under Marketing → Settings → Text Messages (SMS):

  1. Pick Twilio and paste the Account SID, auth token, and a From value (an E.164 phone number you rent from Twilio, or a Messaging Service SID starting with MG).
  2. US senders: register an A2P 10DLC brand + campaign in the Twilio console first — carriers won't reliably deliver unregistered marketing traffic. This is form-filling on Twilio's side, not configuration here.
  3. Point the Twilio number's "A message comes in" webhook at POST /marketing/sms/webhook so STOP/START replies update the opt-out list. Requests are verified with the X-Twilio-Signature header.
  4. Optional: turn on Track clicks in text messages under Open & Click Tracking to measure link clicks in texts (see Link tracking in text messages).

Texting requires its own consent (TCPA in the US) — having someone's email permission does not allow texting them:

  • The All subscribers audience only includes people with a phone number and a recorded SMS opt-in (the checkbox when adding a subscriber, or the per-row "Record SMS opt-in" action).
  • CRM-targeted audiences resolve to contacts with phone numbers — same rule as email: targeting a CRM segment is the sender's consent responsibility.
  • Opt-outs always stick. A STOP reply (or dashboard opt-out) suppresses that phone number from every future SMS audience, no matter how it was targeted. STOP suppression is phone-keyed (marketing_sms_optouts), so it covers CRM contacts who never had a subscriber row. START re-enables.
  • Twilio also blocks STOPped numbers at the carrier level; the webhook keeps your dashboard in agreement with that.

Web Push notifications

Marketing → Settings → Web Push Notifications turns on the third channel. Enabling it mints the install's own VAPID key pair (the cryptographic sender identity browsers verify) — there is no push provider account, no API key, and no per-message cost; notifications go straight from your server to each browser's push service, end-to-end encrypted per subscriber (RFC 8291).

  • How visitors subscribe: a small, dismissable prompt appears on the public site (title, message, and delay are configurable). Accepting triggers the browser's native permission dialog; granted browsers are stored as anonymous subscriptions. Dismissals re-offer after 30 days; the prompt never renders for browsers that already decided. Push requires HTTPS.
  • Composing: New Campaign → Push notification gives a title, a short message, and the page to open on click — with a live notification mock as the preview. The site logo is the icon automatically.
  • Sending & stats: push campaigns send to every subscribed browser (subscriptions are anonymous, so audience targeting doesn't apply). Delivered/failed counts work like the other channels; with click tracking on, the notification's link routes through the standard campaign click redirect, so clicks land in the same stats and pick up auto-UTM attribution. There is no open tracking for notifications. Expired subscriptions (uninstalled browsers, revoked permission) are pruned automatically on send.
  • Testing: the designer's Send test to this browser subscribes your own browser and delivers the draft to it for real.

Deliverability wizard & spam score

Dashboard → Marketing → Deliverability verifies the DNS records mailbox providers use to trust your mail, checked live against the domain of your from-address:

  • SPF — record present, single (multiple v=spf1 records are invalid), and containing your bulk provider's include (Postmark/Mailgun/SendGrid/Resend hints built in). Missing records get a copy-ready suggestion.
  • DKIM — probes your provider's usual selectors plus common ones; a miss is a warning (selectors are account-specific), with a pointer to verify in the provider dashboard.
  • DMARC — presence + policy readout; missing gets a starter p=none record to copy.
  • MX and a freemail check (sending bulk mail "from" gmail.com et al. fails DMARC at the big receivers).

Content-side, every email campaign has a Check spam score button in the designer: a local, deterministic scan for the classic filter signals — ALL-CAPS/exclamation subjects, trigger phrases ("act now", "100% free", …), link shorteners, raw-IP links, image-only bodies, "click here" anchors, a missing postal address, freemail senders. Issues come back as a rated list (Excellent → Poor) of concrete fixes. Nothing leaves the server; no third-party scoring service is involved.

Automatic blog digest

Marketing → Settings → Automatic Blog Digest assembles a recurring "what we published" email with zero composing:

  • Pick the content type (blog by default — any content type works), cadence (weekly / every two weeks / monthly), day + hour, audience, and how many posts per edition (max 10).
  • Each edition includes the posts published since the previous edition — featured image, title, excerpt, and a "Read more" button per post, with your logo and a configurable heading/intro on top. The subject line is a template with {site}, {count}, {first_title}, and {date} tokens.
  • Delivery mode: Send automatically, or Save a draft for review — drafts appear in the campaigns list (badged Digest) ready to tweak and send by hand.
  • A cycle with nothing new is skipped — no empty email — and those posts roll into the next edition. Enabling the feature never fires immediately; the first edition waits for the next scheduled slot. Auto-UTM is on, so digest visits attribute in Analytics.

Sequences (automated drips)

Dashboard → Marketing → Sequences. A sequence is an ordered series of delayed emails and texts — "day 0 welcome, day 3 tips, day 7 offer" — that sends automatically to everyone who enrolls, one person at a time on their own clock. Broadcasts answer "send this to everyone now"; sequences answer "send these, spaced out, to each new person from the moment they arrive."

Building one

Create a sequence (name + who enters it) — or pick Start from: Template: Review follow-up in the create modal to get the classic post-job flow prebuilt as a draft: day-1 "does everything look good?" check-in, day-4 thank-you with a Leave a review button ({{review_link}}), day-8 nudge that only sends to people who haven't left a review yet. Then add or tweak steps. Each step is a wait (minutes / hours / days — the first step's wait counts from enrollment, later ones from the previous step) plus a message. Edit content opens the step in the same designer campaigns use — same blocks, live preview, personalization tokens, and Send a Test. Steps reorder with up/down arrows and can be mixed email/SMS. A sequence is draft until you Activate it, and can be paused any time — paused sequences stop enrolling and stop sending; enrollments resume where they left off when reactivated.

Under the hood each step's content is a hidden campaign record, which is why the designer, per-recipient delivery log, and open/click tracking all work identically. Step campaigns never appear in the campaigns list and can't be blast-sent.

Who enters it, and when

  • When someone joins the mailing list — new confirmed subscribers from the public signup form or a dashboard add are enrolled the moment they join (after email confirmation when double opt-in is on). CSV imports and rows auto-created by campaign sends never trigger a drip.
  • CRM automations — the CRM's rule engine (CRM Settings → Automations) has an Enroll in a marketing sequence action that composes with every CRM trigger: contact captured (per source), status changed, deal stage changed, deal won, inactive N days, course enrolled/completed, and the five commerce moments — store order placed, donation received, invoice paid, appointment booked, event tickets bought. "Lead captured from the real-estate form → start the buyer nurture drip" is one rule; "bought tickets for the gala → run the pre-event logistics drip" is another. See CRM → Automations for the trigger list and how each one is scoped.
  • By hand — the sequence editor has an enroll-by-email box, and every CRM contact page has an Enroll in Sequence button.
  • Project Tracking — completed projects enroll their client in a configurable completion sequence (Projects → Settings default, per-project override). See project-tracking.md.

Enrollment is once per person per sequence, forever — completed or exited enrollments block re-entry, so a re-captured lead is never welcomed twice. Only active sequences enroll, and activation is not retroactive (people already on the list don't get back-filled into a new sequence).

Branching on engagement

Every step after the first can carry a send condition: always send (default), or only if the person opened / didn't open / clicked / didn't click the previous email. Conditions are evaluated when the step comes due — so the step's own wait time is the decision window ("wait 2 days, then: if they opened…"). A step whose condition isn't met is skipped and the sequence continues, which makes two-path branching a matter of pairing steps: step 3 "only if opened" + step 4 "only if didn't open" sends different content down each path, and both paths rejoin at step 5.

"The previous message" means the most recent unconditional ("always send") step before the conditional one — conditional steps never anchor each other, so a branch pair both measure the same main-line message. Which steps count depends on what you're measuring: open conditions look only at email steps (texts and notifications have no opens), while click conditions also accept a text step, provided that text was sent with link tracking on. An untracked text in between is ignored rather than read as "didn't click", so turning SMS link tracking on later never silently rewires a sequence that's already running. If no eligible earlier step exists — or it was never sent to that person — the "didn't open/click" conditions pass and the positive ones skip.

With the Reviews feature on, two more conditions appear: only if they left a review or gave feedback and only if they haven't left a review yet. These don't look at email clicks at all — they check the person's review requests (the {{review_link}} funnel) since enrollment: rating privately, submitting a first-party review, or clicking through to Google/Yelp/Facebook all count as "responded". That's what makes the Review follow-up template's nudge step precise.

  • An email step for someone who has unsubscribed exits the enrollment immediately — no send, drip over.
  • An SMS step without SMS consent is skipped and the drip continues with the next step (SMS consent is a separate grant; its absence shouldn't cancel someone's emails). Subscriber-enrolled people need the explicit SMS opt-in; STOP suppression always sticks for everyone.
  • Every sequence email carries the same automatic compliance footer, unsubscribe link, and one-click unsubscribe headers as a broadcast; every text carries the STOP notice.

Delivery mechanics

Due steps send on the same no-cron background schedule as scheduled campaigns (LazyCron, 60-second cadence, lock-guarded, capped per pass so big backlogs spread across ticks). Sends are resume-safe — an interrupted pass never double-sends a step. A failed send is recorded on the step and the enrollment still advances, so one bouncing address can't wedge a drip. The sequence editor shows active/completed/exited counts and recent enrollments; each step's designer shows its own delivered/opens/clicks Performance card.

Privacy & data

  • The list lives in your database; nothing is shared with third parties unless you choose a bulk sending provider (which then processes the emails it sends, like any SMTP relay would).
  • Unsubscribe is honored three ways: the footer link (with a resubscribe option if they change their mind), RFC 8058 one-click unsubscribe from the mail client's native button, and admin action in the dashboard.
  • Tracking can be disabled entirely for a no-tracking newsletter program.

Technical notes (for developers)

  • Module: app/Features/Marketing/ (key marketing, default ON, gated by the standard feature middleware). Campaigns carry a channel of email, sms, or push, and an origin of null (dashboard) or digest (auto-assembled).
  • A/B subject tests: state machine in Jobs/SendCampaignJob.php (ab_* columns on campaigns, ab_variant on campaign_sends); the campaign stays sending between phases and the marketing:send-scheduled LazyCron tick advances it. Winner = unique opens, clicks tiebreak, dead heat → A; audiences < AB_MIN_RECIPIENTS (10) skip the test.
  • Send-time recommendation: Support/SendTimeOptimizer.php — PHP-side histogram of campaign_sends.opened_at (last 90 days, ≥30 opens) so it runs identically on MySQL and sqlite.
  • Web Push: Support/WebPushCrypto.php (pure-PHP RFC 8291 aes128gcm + RFC 8292 VAPID on ext-openssl, unit-tested against the RFC's Appendix A vector), Support/WebPushSender.php (delivery + 404/410 pruning), Models/PushSubscription.php (marketing_push_subscriptions). VAPID keys live in marketing.vapid_public/private Settings; enabling mints them once and never rotates silently. Public pieces: partials/push-prompt.blade.php + resources/js/push.js (conditionally included in layouts/public.blade.php, cached-page-safe — all per-visitor state is client-side), the static service worker public/push-sw.js, and CSRF-exempt POST /push/subscribe|unsubscribe. New-browser subscriptions record the push_subscriber Analytics goal.
  • Sequence branching: send_condition on marketing_sequence_steps (opened_previous, not_opened_previous, clicked_previous, not_clicked_previous), evaluated in Support/SequenceRunner.php against the previous email step's campaign_sends row at due time; unmet ⇒ advance without sending.
  • Deliverability: Support/DeliverabilityChecker.php (DNS via overridable txtRecords()/mxRecords() seams for tests) behind dashboard/marketing/deliverability; content scan Support/SpamScoreAnalyzer.php (local heuristics, no external service).
  • Blog digest: Support/BlogDigestBuilder.php + marketing:send-digest (LazyCron, 900s). Cycle clock in marketing.digest_last_run_at; the posts window is anchored to the previous digest campaign's created_at (origin digest), so skipped cycles never drop posts. Assembles standard EmailBlocks and sends through the ordinary SendCampaignJob.
  • Blocks registry: Support/EmailBlocks.php; HTML compiler: Support/EmailRenderer.php (600px inline-styled tables; brand color from branding.colors.primary).
  • Audience resolution + suppression: Support/MarketingAudience.php. Audience strings: subscribers, all, crm_group:{id}, crm_status:{id}, crm_view:{userId}:{id}. SMS campaigns resolve phone-keyed recipients (smsActive subscribers / CRM contacts with phones) with phone-keyed suppression.
  • Email providers: Support/CampaignMailer.php — Setting-first credentials (mail.postmark_token, mail.resend_token, mail.mailgun_token + mail.mailgun_domain, mail.sendgrid_token) with config/services.php fallback.
  • SMS provider: Support/SmsSender.php (Twilio via plain HTTP, credentials in marketing.twilio_* Settings, webhook signature validation). STOP suppression model: Models/SmsOptout.php.
  • Tracking: Support/CampaignTracking.php; endpoints /newsletter/o/{token} and /newsletter/c/{token}/{link}; per-URL totals in campaign_links. Scanner/bot hits are kept out of every counter by Support/TrackingBotFilter.php (HEAD probes, known scanner/crawler/preview user agents, and any hit inside the 10-second delivery-scan window after campaign_sends.sent_at) — the redirect/pixel is still served, only the accounting is skipped, on all three tracking endpoints (email open, email click, SMS click).
  • SMS link tracking: Support/SmsLinkTracking.php (marketing.track_sms_clicks) — same two-phase instrument() / forSend() shape as CampaignTracking but over plain text, sharing campaign_links so stats and the top-links table need no separate path. Its redirect /r/{code}/{link} keys on campaign_sends.short_code, a 7-char code minted in the model's creating hook for phone-keyed sends only (email keeps the 40-char token, where URL length is free); both redirect routes run through one MarketingPublicController::recordClick(), with the implied-open step passed in rather than assumed. Click branching on an SMS step is gated on that campaign having campaign_links rows, so enabling the setting can't retroactively rewire a running sequence — see SequenceRunner::conditionMet().
  • CampaignUtm::channelSource() derives utm_source from the channel (email / sms / push). Push clicks previously tagged as email.
  • Public endpoints (newsletter/subscribe, newsletter/unsubscribe/*, marketing/sms/webhook) are CSRF-exempt because they're hit by plain form posts from cached pages, mail clients, and Twilio. The public URLs deliberately kept the newsletter/ prefix — they're baked into design-library signup rows and they still serve the email list.
  • Sequences: models Models/{Sequence,SequenceStep,SequenceEnrollment}.php; enrollment gate Support/SequenceEnroller.php (subscriber hooks + the CRM enroll_sequence automation action); sender Support/SequenceRunner.php driven by marketing:run-sequences (LazyCron, 60s, lock-guarded, 200 enrollments/pass). Step content lives on hidden campaigns rows with status sequence — excluded from the index list, refused by SendCampaignJob, deleted with their step.
  • Tables: campaigns, marketing_subscribers, campaign_sends, campaign_templates, campaign_links, marketing_sms_optouts, marketing_sequences, marketing_sequence_steps, marketing_sequence_enrollments, marketing_push_subscriptions.
  • Tests: tests/Feature/Marketing*.php, tests/Feature/Campaign*.php (designer, audiences, send job, tracking, double opt-in, settings, SMS, SMS link tracking, sequences, A/B + send-time, push, branching, deliverability, digest).