Skip to content

🎨 Improved sign-in to return you to the comment you were replying to#28439

Merged
jonatansberg merged 6 commits into
TryGhost:mainfrom
luissazevedo:BER-3648/reply-sign-in-scroll-position
Jun 11, 2026
Merged

🎨 Improved sign-in to return you to the comment you were replying to#28439
jonatansberg merged 6 commits into
TryGhost:mainfrom
luissazevedo:BER-3648/reply-sign-in-scroll-position

Conversation

@luissazevedo

Copy link
Copy Markdown
Contributor

ref https://linear.app/tryghost/issue/BER-3648/

What

When a logged-out reader clicks Reply on a comment and signs in, the page used to reload and drop them at the top of the post. Now it returns them to the comment they were replying to (scrolled + highlighted).

How

  • The reply CTA's Sign in opens Portal with a same-site redirect: #/portal/signin?redirect=<comment permalink>.
  • Portal already threads pageData.redirect through the magic-link round-trip and core preserves the URL fragment, so the existing permalink path does the scroll/highlight — no client-side storage, no new restore logic.
  • Scope: sign-in only. Free/paid sign-up is deferred (same mechanism — see follow-up).

🔍 Reviewer notes — please weigh in on these two

1. closePopup clears a one-shot redirect (shared Portal action)apps/portal/src/actions.js
To stop a dismissed reply-sign-in from leaking its pageData.redirect into a later, unrelated sign-in on the same page, closePopup now strips redirect from pageData (other keys preserved). This runs on every popup close across Portal — please confirm no existing flow relies on pageData.redirect surviving a close. Account-page / feedback redirects consume their redirect during sign-in (not after a close), so I believe it's safe; covered by a new actions.test.ts test, and existing signin-flow tests still pass.

2. getPageUrl strips a generic action paramapps/comments-ui/src/index.tsx
We strip success/action so they don't leak into comment permalinks (and the redirect built from the page URL). success is unambiguously a Ghost auth marker, but action is generic — a publisher could legitimately use ?action=… and we'd silently drop it. Options: only strip action when success is present, or only strip action=signin or not implement this

Security

The sign-in link can now carry a redirect (where to send the reader after they log in). It's validated same-site only — via a new isSameOriginUrl() helper that compares URL origins rather than a string prefix, so it can't be fooled by look-alike hosts or a missing trailing slash — meaning it can't bounce anyone to an external site. This makes "redirect after sign-in" usable by any page on the site, not just comments, and Ghost core has an older, looser version of the same check elsewhere. Neither is exploitable in this PR, but flagging in case we want to tighten or standardise it.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR implements a feature enabling users to be redirected to a specific comment after signing in from comment CTAs. Changes span comments-ui and portal: ReplyButton and Like/Dislike include commentId when opening the CTA popup; CTAPopup/CTABox forward commentId and CTABox builds and encodes a comment permalink into the Portal signin redirect; getPageUrl and cleanPageUrl remove transient auth markers from permalinks; Portal adds isSameOriginUrl and merges a validated redirect into signin pageData; closePopup clears one-shot redirects. Tests (unit and E2E) exercise these paths.

Suggested labels

ok to merge for me

Suggested reviewers

  • EvanHahn
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improved sign-in to return you to the comment you were replying to' clearly and concisely summarizes the main objective of the changeset.
Description check ✅ Passed The description thoroughly explains the feature, implementation approach, reviewer notes, and security considerations, all of which align with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/comments-ui/src/index.tsx`:
- Around line 51-54: The code unconditionally deletes the 'action' query param
in apps/comments-ui/src/index.tsx which strips publisher-specific values; change
this to only remove Ghost auth markers by checking
url.searchParams.get('action') and deleting it only when it equals known Ghost
values (e.g., 'signin', 'signup', 'giftRedeem' or whatever markers your Ghost
install uses); leave 'success' handling as-is if it should always be removed,
and ensure this conditional targets the same location where you build permalinks
(referencing index.tsx and the buildCommentPermalink flow) so other publisher
query params are preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a9012cd-e0cb-4b32-9d70-83806e884f4a

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1085f and c7357a4.

📒 Files selected for processing (11)
  • apps/comments-ui/src/components/content/buttons/reply-button.tsx
  • apps/comments-ui/src/components/content/comment.tsx
  • apps/comments-ui/src/components/content/cta-box.tsx
  • apps/comments-ui/src/components/popups/cta-popup.tsx
  • apps/comments-ui/src/index.tsx
  • apps/comments-ui/test/e2e/reply-signin-redirect.test.ts
  • apps/portal/src/actions.js
  • apps/portal/src/app.js
  • apps/portal/src/utils/helpers.js
  • apps/portal/test/actions.test.ts
  • apps/portal/test/utils/helpers.test.js

Comment thread apps/comments-ui/src/index.tsx Outdated
@luissazevedo luissazevedo force-pushed the BER-3648/reply-sign-in-scroll-position branch from c7357a4 to 8c71734 Compare June 9, 2026 13:52
ref https://linear.app/tryghost/issue/BER-3648/

- clicking Reply while logged out, then signing in, used to reload the post and
  drop the reader at the top; now it returns them to the comment they wanted to
  reply to (scrolled + highlighted)
- the reply CTA's "Sign in" asks Portal to return to that comment via a
  same-site redirect (#/portal/signin?redirect=<comment permalink>); Portal
  threads pageData.redirect through the magic-link round trip and core preserves
  the fragment, so the existing permalink path does the scroll/highlight — no
  client-side storage needed
- portal: the signin route reads a redirect from its hash query, validated
  same-origin (isSameOriginUrl) to prevent open redirects
- portal: closePopup clears a one-shot redirect from pageData so a dismissed
  sign-in can't leak its redirect into a later, unrelated sign-in on the same page
- comments-ui: getPageUrl strips success/action auth markers so they don't leak
  into comment permalinks (or the redirect built from the page URL)
- scope: sign-in only; free/paid sign-up deferred (same redirect mechanism)
@luissazevedo luissazevedo force-pushed the BER-3648/reply-sign-in-scroll-position branch from 8c71734 to db53d44 Compare June 9, 2026 14:02
ref https://linear.app/tryghost/issue/BER-3648/

- review follow-up: likes/dislikes opened the same sign-in CTA as replies but without the comment id, so readers signing in from a like lost their place — they now get the same redirect treatment
- gated the post-auth `action` param strip on `success` being present, since Ghost only ever sets them together — a publisher's own standalone `?action=…` param is no longer dropped from permalinks
- extracted the stripping into a pure cleanPageUrl() helper so it could be unit tested
…-in-scroll-position

# Conflicts:
#	apps/comments-ui/package.json
#	apps/portal/package.json
ref https://linear.app/tryghost/issue/BER-3648/

- the pre-commit hook auto-bumped @tryghost/activitypub to 3.1.37 during a non-interactive merge of main, because the merge staged main's activitypub changes and the hook's tty prompt defaulted to yes
- this PR doesn't touch activitypub, so the version should stay at main's 3.1.36
@jonatansberg jonatansberg force-pushed the BER-3648/reply-sign-in-scroll-position branch 2 times, most recently from 02fbb7d to 870be62 Compare June 11, 2026 10:21
ref https://linear.app/tryghost/issue/BER-3648/

- permalinks are rendered as target="_parent" hrefs and thread navigation relies on the parent hashchange event, which is only reload-free when the href differs from the address bar in the fragment alone; fragment-only hrefs make that true by construction, because the browser resolves them against the live page URL at click time (srcdoc iframes inherit the parent document's base URL)
- this replaces cleanPageUrl and the captured pageUrl plumbing entirely: there is no longer a URL snapshot that can disagree with the address bar after Portal or comments-ui scrub auth params via history.replaceState
- the Portal sign-in redirect is the one URL that must stay absolute (validated and emailed server-side), so it is built from the live URL at click time — by then handleTokenUrl has already scrubbed any one-time token, and the result matches what a plain sign-in would send via the request referrer anyway
ref https://linear.app/tryghost/issue/BER-3648/

- Ghost already refuses off-site redirects at the only moment they could do harm: the members middleware only honours the magic link's r param when it matches the site URL, and a direct POST to sendMagicLink bypasses Portal entirely, so the client-side isSameOriginUrl check was duplicate surface area rather than a security boundary
- an off-site redirect now simply no-ops at exchange time (member lands on the homepage), same as before — Portal just no longer pre-filters what the server validates anyway
@jonatansberg jonatansberg force-pushed the BER-3648/reply-sign-in-scroll-position branch from fe17ca1 to 5c3e5f8 Compare June 11, 2026 10:45
@jonatansberg jonatansberg merged commit 388504b into TryGhost:main Jun 11, 2026
44 checks passed
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.

2 participants