Skip to content

🐛 Fixed paid members on other tiers receiving gated newsletter content#28315

Merged
9larsons merged 1 commit into
mainfrom
slars/tier-email-visibility
Jun 19, 2026
Merged

🐛 Fixed paid members on other tiers receiving gated newsletter content#28315
9larsons merged 1 commit into
mainfrom
slars/tier-email-visibility

Conversation

@9larsons

@9larsons 9larsons commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #27893
Fixes https://linear.app/ghost/issue/ONC-1725
Related: #25046 · Forum: paid tier without post access still has access

Problem

When a post's visibility is restricted to specific tiers, the website correctly gates the content — a paying member on a tier not included in the post sees the public preview and paywall. Email newsletters did not honor this: the sending pipeline only understood a free/paid split, so any paying member received the full members-only content in their inbox, regardless of which tier they were on.

This contradicts the documented behavior that post access settings determine how members see content "on your site, or in their inbox as an email newsletter."

Cause

The email pipeline reasoned only about status:free vs status:-free:

  • getSegments() returned a hardcoded free/paid pair and never produced tier-based segments.
  • renderBody() applied the paywall only to the status:free segment — so every paying member, on any tier, got the full body.
  • The post's tiers relation wasn't even loaded during batch sending.

Change

  • Single source of truth for access — extracted getPostAccessFilter(post) in content-gating.js; checkPostAccess (web) now delegates to it, so web and email derive "who can read this post" the same way.
  • Tier-aware segmentationgetSegments() now splits a tier-restricted post into an access segment (members on an allowed tier → full content) and its complement (everyone else → preview + paywall). Reduces to the existing behavior for paid/members posts.
  • Access-driven paywallrenderBody() paywalls any segment lacking post access, instead of keying off status:free. Per-block data-gh-segment (free/paid) card visibility remains keyed on free/paid status, independent of tier access.
  • Load the relation — batch sending now loads the post's tiers.

Behavior

Post restricted to Tier A, sent to all members:

Recipient Receives
Member on Tier A Full content
Paying member on Tier B Public preview + paywall
Free member Public preview + paywall

paid and members posts are unchanged.

Tests

  • Unit: getPostAccessFilter across visibilities; getSegments tier (2-cell and 3-cell) cases; renderBody paywalls the no-access tier segment, not the access one.
  • Integration: seeds members across tiers (including a multi-tier member) and asserts the access/no-access segments partition recipients exactly against a real DB — no double-sends, no drops.
  • Verified manually end-to-end via a real newsletter send.

Known follow-up (not in this PR)

The email preheader/excerpt still regenerates from gated HTML only for the status:free segment, so a wrong-tier paid member sees members-only text in the preview text (the body is correctly gated). Same free/paid-vs-tier assumption, in #getEmailPreheader(); tracked for a separate change.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a777d3f0-fc4f-4dd6-8c4e-0632896e19d9

📥 Commits

Reviewing files that changed from the base of the PR and between ba08301 and 56cca09.

📒 Files selected for processing (6)
  • ghost/core/core/server/services/email-service/batch-sending-service.js
  • ghost/core/core/server/services/email-service/email-renderer.js
  • ghost/core/core/server/services/members/content-gating.js
  • ghost/core/test/integration/services/email-service/tier-segmentation.test.js
  • ghost/core/test/unit/server/services/email-service/email-renderer.test.js
  • ghost/core/test/unit/server/services/members/content-gating.test.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • ghost/core/core/server/services/email-service/batch-sending-service.js
  • ghost/core/test/unit/server/services/members/content-gating.test.js
  • ghost/core/test/unit/server/services/email-service/email-renderer.test.js
  • ghost/core/test/integration/services/email-service/tier-segmentation.test.js
  • ghost/core/core/server/services/members/content-gating.js
  • ghost/core/core/server/services/email-service/email-renderer.js

Walkthrough

The PR fixes tier-based email segmentation for posts with visibility: 'tiers' and a public preview paywall. A new getPostAccessFilter(post) helper is extracted from content-gating.js and exported, centralizing NQL filter derivation for paid and tiers visibility. The BatchSendingService is updated to eager-load the tiers relation. The email renderer gains four helper functions for tier-access logic and a rewritten getSegments that produces per-tier access/no-access NQL segment variants instead of the previous binary status:free/status:-free split. The renderBody paywall and data-gh-segment card stripping logic are updated to use the new tier-aware helpers. Unit tests and a new integration test verify the recipient partitioning and paywall rendering.

Possibly related PRs

  • TryGhost/Ghost#27938: Both PRs modify BatchSendingService.sendEmail() to expand the post lazy withRelated list for downstream URL/renderer logic.
  • TryGhost/Ghost#28743: Both PRs modify BatchSendingService.sendEmail's lazy post relation loading by changing the constructed withRelated list.

Suggested reviewers

  • acburdine
  • troyciesco
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preventing paid members on excluded tiers from receiving tier-restricted newsletter content.
Description check ✅ Passed The description comprehensively explains the problem, root causes, changes made, and testing approach, all directly related to the changeset.
Linked Issues check ✅ Passed All coding requirements from issue #27893 are met: getPostAccessFilter extracts access logic, getSegments returns tier-aware segments, renderBody applies paywalls based on access not free/paid status, and tiers relation is loaded during batch sending.
Out of Scope Changes check ✅ Passed All changes directly address the three identified locations in issue #27893 and are necessary for the fix; no unrelated refactoring or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slars/tier-email-visibility

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.43.0)
ghost/core/test/unit/server/services/email-service/email-renderer.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@9larsons 9larsons marked this pull request as ready for review June 18, 2026 17:28
@nx-cloud

nx-cloud Bot commented Jun 18, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 56cca09

Command Status Duration Result
nx run ghost:test:ci:integration:no-coverage ✅ Succeeded 2m 33s View ↗
nx run ghost:test:ci:integration ✅ Succeeded 2m 4s View ↗
nx run ghost:test:ci:e2e:no-coverage ✅ Succeeded 10m 16s View ↗
nx run ghost:test:ci:e2e ✅ Succeeded 8m 27s View ↗
nx run ghost:test:ci:legacy ✅ Succeeded 3m 33s View ↗
nx build @tryghost/sodo-search ✅ Succeeded <1s View ↗
nx build @tryghost/portal ✅ Succeeded <1s View ↗
nx build @tryghost/signup-form ✅ Succeeded <1s View ↗
Additional runs (10) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-19 17:47:34 UTC

fixes #27893
ref #25046

- the website gated tier-restricted posts correctly, but newsletters only
  understood a free/paid split, so any paying member received the full
  members-only content regardless of which tier the post was restricted to
- extracted getPostAccessFilter() so web (checkPostAccess) and email derive
  "who can read this post" from a single source and can't drift apart
- getSegments() now splits a tier-restricted post into an access segment and
  its complement; renderBody() paywalls any segment lacking post access rather
  than keying off status:free; batch sending loads the post's tiers relation
- paid/members posts reduce to the existing behaviour
@9larsons 9larsons force-pushed the slars/tier-email-visibility branch from ba08301 to 56cca09 Compare June 19, 2026 17:31
@9larsons 9larsons enabled auto-merge (squash) June 19, 2026 17:37
@9larsons 9larsons merged commit f1679ec into main Jun 19, 2026
49 checks passed
@9larsons 9larsons deleted the slars/tier-email-visibility branch June 19, 2026 17:47
9larsons added a commit that referenced this pull request Jul 3, 2026
…osts (#29059)

fixes https://linear.app/ghost/issue/ONC-1875/preview-showing-incorrect-cta-in-editor
ref #28315

Since #28315, the content preview card has properly respected the post's access (tiers, not just paid/free), but the preview pane was not wired up to the segment/audience; it was still using a naive paid/free entry.

This change updates the Preview behavior to match the segment, however we still have an open design question about the language around 'Free' and 'Paid' when it comes to Tier access vs. simply being on any free or any paid tier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Public preview emails ignore tier-specific visibility

2 participants