🐛 Fixed email preview always showing the paywall on tier-restricted posts#29059
Conversation
…osts fixes https://linear.app/ghost/issue/ONC-1875/preview-showing-incorrect-cta-in-editor - 28315 made newsletter sends tier-aware, but the email preview and test email path still requested the old free/paid audiences and never loaded the post's tiers relation, so every preview of a tiers-restricted post rendered the public preview + paywall CTA — including "Paid member" - previews now map the paid audience onto the post's tier access segment, matching the browser preview's existing semantics (its paid member shim holds all active paid tiers, i.e. a paid member with access) - also restored the pre-28315 behavior that an unsegmented render (no memberSegment) returns the full content — the send pipeline only produces a null segment for posts without gated content, so this only affects API preview clients
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run ghost:test:ci:integration |
✅ Succeeded | 2m 52s | View ↗ |
nx run ghost:test:integration |
✅ Succeeded | 2m 47s | View ↗ |
nx run ghost:test:legacy |
✅ Succeeded | 2m 58s | View ↗ |
nx run ghost:test:e2e |
✅ Succeeded | 2m 28s | View ↗ |
nx run-many --target=build --projects=tag:publi... |
✅ Succeeded | <1s | View ↗ |
nx run-many -t test:unit -p ghost |
✅ Succeeded | 29s | View ↗ |
nx run-many -t lint -p ghost |
✅ Succeeded | 35s | View ↗ |
nx run @tryghost/admin:build |
✅ Succeeded | 9s | View ↗ |
Additional runs (2) |
✅ Succeeded | ... | View ↗ |
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗
☁️ Nx Cloud last updated this comment at 2026-07-02 17:51:20 UTC
|
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)
WalkthroughChangesThis change introduces a Sequence Diagram(s)See hidden diagrams above illustrating segment mapping and preview/test-send flow. Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ 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.44.0)ghost/core/core/server/services/email-service/email-controller.js[{"text":"logging.warn( ... [truncated 45742 characters] ... (body.email_previews[0].html, new RegExp(selectedNewsletter.name));","charCount":{"leading":62,"trailing":2},"language":"JavaScript","metaVariables":{"single":{"ARG":{"text":"selectedNewsletter.name","range":{"byteOffset":{"start":11470,"end":11493},"start":{"line":240,"column":73},"end":{"line":240,"column":96}}}},"multi":{},"transformed":{}},"ruleId":"regexp-non-literal","severity":"warning","note":"[CWE-1333] Inefficient Regular Expression Complexity. Security best practice. [REFERENCES]\n - https://cwe.mitre.org/data/definitions/1333.html","message":"Do not use variable for regular expressions","labels":[{"text":"new RegExp(selectedNewsletter.name)","range":{"byteOffset":{"start":11459,"end":11494},"start":{"line":240,"column":62},"end":{"line":240,"column":97}},"style":"primary"}]} ghost/core/core/server/services/email-service/email-renderer.js[{"text":"logging.warn( ... [truncated 45742 characters] ... (body.email_previews[0].html, new RegExp(selectedNewsletter.name));","charCount":{"leading":62,"trailing":2},"language":"JavaScript","metaVariables":{"single":{"ARG":{"text":"selectedNewsletter.name","range":{"byteOffset":{"start":11470,"end":11493},"start":{"line":240,"column":73},"end":{"line":240,"column":96}}}},"multi":{},"transformed":{}},"ruleId":"regexp-non-literal","severity":"warning","note":"[CWE-1333] Inefficient Regular Expression Complexity. Security best practice. [REFERENCES]\n - https://cwe.mitre.org/data/definitions/1333.html","message":"Do not use variable for regular expressions","labels":[{"text":"new RegExp(selectedNewsletter.name)","range":{"byteOffset":{"start":11459,"end":11494},"start":{"line":240,"column":62},"end":{"line":240,"column":97}},"style":"primary"}]} ghost/core/core/server/services/email-service/email-service.js[{"text":"logging.warn( ... [truncated 45742 characters] ... (body.email_previews[0].html, new RegExp(selectedNewsletter.name));","charCount":{"leading":62,"trailing":2},"language":"JavaScript","metaVariables":{"single":{"ARG":{"text":"selectedNewsletter.name","range":{"byteOffset":{"start":11470,"end":11493},"start":{"line":240,"column":73},"end":{"line":240,"column":96}}}},"multi":{},"transformed":{}},"ruleId":"regexp-non-literal","severity":"warning","note":"[CWE-1333] Inefficient Regular Expression Complexity. Security best practice. [REFERENCES]\n - https://cwe.mitre.org/data/definitions/1333.html","message":"Do not use variable for regular expressions","labels":[{"text":"new RegExp(selectedNewsletter.name)","range":{"byteOffset":{"start":11459,"end":11494},"start":{"line":240,"column":62},"end":{"line":240,"column":97}},"style":"primary"}]}
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. Comment |

fixes https://linear.app/ghost/issue/ONC-1875/preview-showing-incorrect-cta-in-editor
ref #28315
Problem
Since #28315, newsletter sends correctly gate tier-restricted posts per tier — but the editor's email preview and test emails were never taught about it. For any post with visibility restricted to specific tiers, every email preview selection rendered the paywalled version, including "Paid member" — there was no selection that showed the full post. Actual sends were unaffected; only the preview lied. The Browser preview tab was also unaffected (its "Paid member" simulates a member holding all active paid tiers), so the two preview tabs contradicted each other.
Cause
Two independent defects in the preview/test-email path, plus one adjacent behavior change:
tiersrelation, sogetPostAccessFiltersaw an empty tier list and returned "nobody has access" — every segment of a tiers post got the paywall regardless of its value.status:free/status:-free, and the newsegmentHasPostAccesscheck requires the segment to contain the post's tier filter — which those strings never do.segmentHasPostAccessalso returned false for anullsegment, silently flipping the behavior ofGET /email_previews/posts/:idwithoutmemberSegmentfor paywalled posts (used to return the full body).Change
tiersin the email preview controller'swithRelated, mirroring batch sending.EmailRenderer.getPreviewSegment(post, segment)maps the editor's paid audience choice onto the tier access segment the send pipeline actually renders (status:-free+(product:'…')), so the email preview matches both delivery and the browser preview's existing "paid member with access" semantics.previewEmailandsendTestEmailrender with the mapped segment while still personalizing the example member from the original choice. "Free member" is untouched — it already shows the no-access variant (public preview + paywall).Tests
getPreviewSegmentmapping across visibilities;renderBodyrenders full content for the composite access segment and for null-segment renders of gated posts.Known follow-up (not in this PR)
The preview dropdown has no option for "paid member on a tier this post doesn't include" — a real audience that receives the paywalled version when sending to all members. That option's naming should align with the planned rename of the "Public preview" card divider ("Only visible to members" is inaccurate for tiers posts), so it's tracked as a separate product+engineering issue in Linear.