Skip to content

Conversation

@rmgpinto
Copy link
Contributor

@rmgpinto rmgpinto commented Dec 16, 2025

ref https://linear.app/ghost/issue/BER-3095

Extracted the link redirects code from the middleware to the router. This avoids route comparison on every requests Ghost receives.


Note

Moves link redirect handling from middleware to a dedicated router using a new redirectPrefix helper, updating site wiring and tests.

  • Frontend (site app):
    • Replace middleware-based link redirects with a dedicated router via web/routers/link-redirects.js mounting GET on linkRedirects.service.redirectPrefix() + '*'.
    • Update web/site.js to use the new router instead of linkRedirects.service.handleRequest middleware.
  • Service:
    • Add LinkRedirectsService.redirectPrefix() to return full path prefix (handles subdirectory), and simplify handleRequest by removing prefix checks.
  • Tests:
    • Add unit tests for redirectPrefix() and keep redirect behavior tests.

Written by Cursor Bugbot for commit c8861c0. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 16, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new frontend router module that registers a GET route using the link-redirection service’s redirectPrefix() and handleRequest callback. Updates site initialization to import and invoke this router handler instead of mounting the previous middleware. On the service layer, adds redirectPrefix() and routePath() methods and removes the inline prefix pre-check from handleRequest so requests are processed without that early short-circuit. Unit tests were added to cover redirectPrefix() behavior with and without a subdirectory.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • Review the new router file to confirm the GET route is registered with the correct path/prefix and handler binding.
  • Verify site.js now invokes the router setup correctly and that middleware ordering or app-level side effects are unchanged.
  • Inspect LinkRedirectsService.redirectPrefix() and routePath() implementations for correctness (subdirectory handling, trailing slashes).
  • Check handleRequest behavior after removal of the prefix guard to ensure non-matching requests are not improperly processed and no performance regressions occur.
  • Confirm the new unit tests accurately cover redirectPrefix() edge cases.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Changed link redirects from middleware to router' clearly and concisely describes the main architectural change in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The description directly relates to the changeset by explaining the refactoring of link redirects from middleware to a dedicated router with specific implementation details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch link-redirects-router

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (1)

80-86: Well-designed public API for route registration.

The method correctly combines the baseURL pathname with the redirect prefix.

Minor: Consider renaming fullURLWithRedirectPrefix to redirectPath since it's a path segment, not a full URL.

     redirectPrefix() {
-        const fullURLWithRedirectPrefix = `${this.#baseURL.pathname}${this.#redirectURLPrefix}`;
-        return fullURLWithRedirectPrefix;
+        return `${this.#baseURL.pathname}${this.#redirectURLPrefix}`;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 532426d and e29c039.

📒 Files selected for processing (3)
  • ghost/core/core/frontend/web/routers/link-redirects.js (1 hunks)
  • ghost/core/core/frontend/web/site.js (2 hunks)
  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.
📚 Learning: 2025-09-03T12:28:11.174Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.

Applied to files:

  • ghost/core/core/frontend/web/routers/link-redirects.js
  • ghost/core/core/frontend/web/site.js
  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Ghost admin serves admin-x apps from `/ghost/assets/{app-name}/{app-name}.js` URLs

Applied to files:

  • ghost/core/core/frontend/web/site.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Lint
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (3)
ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (1)

95-116: Handler logic is correct for the router-based approach.

The method properly handles the redirect flow: lookup → dispatch event → redirect (or pass to next middleware if not found). The next() call on line 101 when no link is found will correctly result in a 404 via downstream error handlers.

ghost/core/core/frontend/web/site.js (1)

54-54: Correct placement in the middleware/router stack.

The handler registration maintains the same position as the original middleware, ensuring link redirects are processed early before custom redirects and other route handlers.

ghost/core/core/frontend/web/routers/link-redirects.js (1)

1-5: Clean router implementation that achieves the PR objective.

The explicit GET route registration with redirectPrefix() + '*' ensures this handler only runs for redirect URLs, avoiding the middleware overhead on every request.

@rmgpinto rmgpinto force-pushed the link-redirects-router branch from e29c039 to 5369c84 Compare December 16, 2025 10:16
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 3

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e29c039 and 5369c84.

📒 Files selected for processing (3)
  • ghost/core/core/frontend/web/routers/link-redirects.js (1 hunks)
  • ghost/core/core/frontend/web/site.js (2 hunks)
  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/frontend/web/routers/link-redirects.js
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.
📚 Learning: 2025-09-03T12:28:11.174Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.

Applied to files:

  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js
  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-02-06T10:30:53.440Z
Learnt from: allouis
Repo: TryGhost/Ghost PR: 22127
File: .github/scripts/release-apps.js:72-74
Timestamp: 2025-02-06T10:30:53.440Z
Learning: In the Ghost repository, the path to defaults.json is correctly structured as 'ghost/core/core/shared/config/defaults.json' with an intentional double 'core' in the path.

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Core Ghost backend logic should be placed in `ghost/core/core/server/` directory structure organized by purpose (api, services, models, data/schema, etc.)

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Ghost admin serves admin-x apps from `/ghost/assets/{app-name}/{app-name}.js` URLs

Applied to files:

  • ghost/core/core/frontend/web/site.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build & Push Docker Image
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (2)
ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (2)

80-86: LGTM!

The redirectPrefix() method correctly constructs and returns the full redirect path prefix by combining the base URL pathname with the redirect URL prefix.


95-116: Router correctly filters requests by prefix—code change is safe.

The router at ghost/core/core/frontend/web/routers/link-redirects.js properly mounts the handler on linkRedirects.service.redirectPrefix() + '*', ensuring only redirect-prefixed requests reach handleRequest. This satisfies the requirement for prefix filtering without the inline check. The change improves maintainability by delegating path filtering to the router layer as intended.

@rmgpinto rmgpinto force-pushed the link-redirects-router branch from 5369c84 to bbfd884 Compare December 16, 2025 10:20
const linkRedirects = require('../../../server/services/link-redirection');

module.exports = function handleRedirects(siteApp) {
siteApp.get(linkRedirects.service.redirectPrefix() + '*', linkRedirects.service.handleRequest);
Copy link

Choose a reason for hiding this comment

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

Bug: Link redirects fail on subdirectory installations

The route uses redirectPrefix() which returns the full path including the subdirectory (e.g., /blog/r/ for subdirectory installations). However, Express route matching in the siteApp operates on paths without the subdirectory prefix, as evidenced by other routes like sitemap (/sitemap.xml), favicon (/favicon.ico), and frontend-caching checks (req.path.startsWith('/p/')). The old middleware approach worked because it checked req.originalUrl which includes the subdirectory, but Express routing uses req.path which doesn't. For subdirectory installations, the route /blog/r/* won't match requests where req.path is /r/abc.

Additional Locations (1)

Fix in Cursor Fix in Web

@rmgpinto rmgpinto force-pushed the link-redirects-router branch from bbfd884 to 88cd61d Compare December 16, 2025 10:36
@rmgpinto rmgpinto force-pushed the link-redirects-router branch from 88cd61d to 38d312c Compare December 16, 2025 10:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/link-redirection/LinkRedirectsService.test.js (1)

68-88: Add test coverage for the routePath() method.

The test suite adds coverage for redirectPrefix() but the service also introduces a routePath() method (line 95-97 in LinkRedirectsService.js) that has no corresponding tests. Since this is a new public API, it should have test coverage to verify it returns the expected value (/r/).

Add a test case for routePath():

     });
+
+    describe('routePath', function () {
+        it('returns the Express route path', function () {
+            const instance = new LinkRedirectsService({
+                linkRedirectRepository: {},
+                config: {
+                    baseURL: new URL('https://localhost:2368/blog/')
+                }
+            });
+            assert.equal(instance.routePath(), '/r/');
+        });
+    });
 
     describe('handleRequest', function () {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5369c84 and 38d312c.

📒 Files selected for processing (4)
  • ghost/core/core/frontend/web/routers/link-redirects.js (1 hunks)
  • ghost/core/core/frontend/web/site.js (2 hunks)
  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (1 hunks)
  • ghost/core/test/unit/server/services/link-redirection/LinkRedirectsService.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/frontend/web/routers/link-redirects.js
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.
📚 Learning: 2025-09-03T12:28:11.174Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24779
File: ghost/core/core/server/services/members/members-api/controllers/RouterController.js:34-51
Timestamp: 2025-09-03T12:28:11.174Z
Learning: The extractRefererOrRedirect function in Ghost's RouterController (ghost/core/core/server/services/members/members-api/controllers/RouterController.js) is working as intended according to maintainer kevinansfield. The current handling of autoRedirect, redirect parameter parsing, and referrer logic does not need the security-related changes suggested around autoRedirect coercion, relative URL handling, or same-origin checks.

Applied to files:

  • ghost/core/test/unit/server/services/link-redirection/LinkRedirectsService.test.js
  • ghost/core/core/server/services/link-redirection/LinkRedirectsService.js
  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Core Ghost backend logic should be placed in `ghost/core/core/server/` directory structure organized by purpose (api, services, models, data/schema, etc.)

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-02-06T10:30:53.440Z
Learnt from: allouis
Repo: TryGhost/Ghost PR: 22127
File: .github/scripts/release-apps.js:72-74
Timestamp: 2025-02-06T10:30:53.440Z
Learning: In the Ghost repository, the path to defaults.json is correctly structured as 'ghost/core/core/shared/config/defaults.json' with an intentional double 'core' in the path.

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-04-23T15:44:52.549Z
Learnt from: 9larsons
Repo: TryGhost/Ghost PR: 21866
File: ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js:10-19
Timestamp: 2025-04-23T15:44:52.549Z
Learning: The existing implementation in `ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js` using `path.parse(req.url).base` is secure against path traversal attacks as it properly extracts only the filename component without any directory parts.

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • ghost/core/core/frontend/web/site.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Ghost admin serves admin-x apps from `/ghost/assets/{app-name}/{app-name}.js` URLs

Applied to files:

  • ghost/core/core/frontend/web/site.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build & Push Docker Image
  • GitHub Check: Setup
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
ghost/core/core/server/services/link-redirection/LinkRedirectsService.js (2)

80-97: LGTM! Well-structured methods for router integration.

The redirectPrefix() method correctly composes the full path including subdirectory, which is used by the router for route registration. The implementation properly leverages the baseURL pathname that was normalized in the constructor.


106-127: LGTM! Clean handler implementation for route-mounted usage.

The handleRequest() method correctly delegates prefix filtering to the router level (via route pattern matching) and focuses on redirect resolution and execution. The URL construction and error handling are appropriate.

ghost/core/core/frontend/web/site.js (1)

18-18: Router implementation correctly exports and registers link redirect routes.

The integration at lines 18 and 54 of site.js properly imports and invokes the link-redirects router. The router correctly exports a function that accepts siteApp, registers a GET route at the redirect prefix, and invokes the link redirection service handler. The change successfully replaces the previous global middleware approach with a router-based handler.

Comment on lines 90 to 97
/**
* Returns the route path for Express routing (always /r/)
* Express routing uses req.path which excludes subdirectory
* @return {string}
**/
routePath() {
return `/${this.#redirectURLPrefix}`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Search for calls to routePath() method
echo "=== Searching for routePath() calls ==="
rg -n --type js '\.routePath\s*\(' 

echo ""
echo "=== Searching for routePath reference ==="
rg -n --type js 'routePath' 

echo ""
echo "=== Checking link-redirects router file ==="
if [ -f "ghost/core/core/frontend/web/routers/link-redirects.js" ]; then
  echo "File found, showing content:"
  cat -n ghost/core/core/frontend/web/routers/link-redirects.js
else
  echo "Router file not found at expected location"
  # Try to find any link-redirects router file
  echo ""
  echo "Searching for link-redirects router file..."
  fd -t f "link-redirects" --type f 2>/dev/null | head -20
fi

Repository: TryGhost/Ghost

Length of output: 576


Remove unused routePath() method or document its purpose.

The routePath() method (lines 90-97) is defined but never called anywhere in the codebase. The router implementation uses redirectPrefix() instead. If this method is not intended for future use, remove it; otherwise, add a comment explaining why it exists.

🤖 Prompt for AI Agents
In ghost/core/core/server/services/link-redirection/LinkRedirectsService.js
around lines 90 to 97, the routePath() method is defined but unused; either
remove the method entirely to avoid dead code or keep it and add a clear comment
explaining its purpose and why it differs from redirectPrefix() (e.g., reserved
for Express routing that expects a path excluding subdirectory) so future
readers know it’s intentional; update tests or references if any rely on this
API and run linting to ensure no unused-private warnings remain.
ref https://linear.app/ghost/issue/BER-3095

Extracted the link redirects code from the middleware to the router.
This avoids route comparison on every requests Ghost receives.
@rmgpinto rmgpinto force-pushed the link-redirects-router branch from 38d312c to c8861c0 Compare December 16, 2025 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants