🐛 Fixed paid members on other tiers receiving gated newsletter content#28315
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
WalkthroughThe PR fixes tier-based email segmentation for posts with Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.jsThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| 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
ba08301 to
56cca09
Compare
…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.

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:freevsstatus:-free:getSegments()returned a hardcoded free/paid pair and never produced tier-based segments.renderBody()applied the paywall only to thestatus:freesegment — so every paying member, on any tier, got the full body.tiersrelation wasn't even loaded during batch sending.Change
getPostAccessFilter(post)incontent-gating.js;checkPostAccess(web) now delegates to it, so web and email derive "who can read this post" the same way.getSegments()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 forpaid/membersposts.renderBody()paywalls any segment lacking post access, instead of keying offstatus:free. Per-blockdata-gh-segment(free/paid) card visibility remains keyed on free/paid status, independent of tier access.tiers.Behavior
Post restricted to Tier A, sent to all members:
paidandmembersposts are unchanged.Tests
getPostAccessFilteracross visibilities;getSegmentstier (2-cell and 3-cell) cases;renderBodypaywalls the no-access tier segment, not the access one.Known follow-up (not in this PR)
The email preheader/excerpt still regenerates from gated HTML only for the
status:freesegment, 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.