From 0de2f28eda12338155ebcba9ecf132bbee6d4875 Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Wed, 27 May 2026 17:35:45 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20broken=20view-in-browser?= =?UTF-8?q?=20link=20for=20email-only=20posts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref INC-290 - forPost's preview-URL override gated on `status !== 'published'` AND `url.match(/\/404\//)`. The second guard worked because the eager UrlService only indexed published resources, so any sent or draft post returned /404/ and reliably triggered the override. - Under `config.lazyRouting`, the lazy service has no implicit status filter — the default unfiltered `/:slug/` collection matches sent posts and returns `/{slug}/`. The /404/ guard silently flipped to false, the override never fired, and emails went out with the slug URL (which 404s in the browser). - Gating on status alone works under both eager and lazy services and restores the intended behaviour for the lazyRouting path. No behaviour change for everyone else. --- .../utils/serializers/output/utils/url.js | 4 +- .../serializers/output/utils/url.test.js | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/url.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/url.js index b99e6133408..f0170c2bae9 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/url.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/utils/url.js @@ -31,7 +31,9 @@ const forPost = (id, attrs, frame, type = 'posts') => { * Needs further discussion. */ if (!localUtils.isContentAPI(frame)) { - if (attrs.status !== 'published' && attrs.url.match(/\/404\//)) { + // Gate on status alone — the previous `/404/` URL check broke + // under `config.lazyRouting` (the lazy service returns `/{slug}/`). + if (attrs.status !== 'published') { if (attrs.posts_meta && attrs.posts_meta.email_only) { attrs.url = urlUtils.urlFor({ relativeUrl: urlUtils.urlJoin('/email', attrs.uuid, '/') diff --git a/ghost/core/test/unit/api/canary/utils/serializers/output/utils/url.test.js b/ghost/core/test/unit/api/canary/utils/serializers/output/utils/url.test.js index 27a3de18ca6..b2b4853ed70 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/output/utils/url.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/output/utils/url.test.js @@ -90,6 +90,59 @@ describe('Unit: endpoints/utils/serializers/output/utils/url', function () { assert.equal(resource.id, 'post-id'); assert.equal(resource.type, 'posts'); }); + + describe('preview URL override for non-published posts', function () { + it('sets /email// for email-only sent posts regardless of the facade URL', function () { + getUrlForResourceStub.returns('http://localhost/the-slug/'); + + const post = { + id: 'post-id', + uuid: 'post-uuid', + slug: 'the-slug', + status: 'sent', + posts_meta: {email_only: true} + }; + + urlUtil.forPost(post.id, post, {options: {}}); + + assert.equal(post.url, 'urlFor'); + const [args] = urlUtils.urlFor.firstCall.args; + assert.equal(args.relativeUrl, '/email/post-uuid/'); + }); + + it('sets /p// for draft posts regardless of the facade URL', function () { + getUrlForResourceStub.returns('http://localhost/the-slug/'); + + const post = { + id: 'post-id', + uuid: 'post-uuid', + slug: 'the-slug', + status: 'draft' + }; + + urlUtil.forPost(post.id, post, {options: {}}); + + assert.equal(post.url, 'urlFor'); + const [args] = urlUtils.urlFor.firstCall.args; + assert.equal(args.relativeUrl, '/p/post-uuid/'); + }); + + it('keeps the facade URL for published posts', function () { + getUrlForResourceStub.returns('http://localhost/the-slug/'); + + const post = { + id: 'post-id', + uuid: 'post-uuid', + slug: 'the-slug', + status: 'published' + }; + + urlUtil.forPost(post.id, post, {options: {}}); + + assert.equal(post.url, 'http://localhost/the-slug/'); + sinon.assert.notCalled(urlUtils.urlFor); + }); + }); }); describe('forTag', function () {