From 0cdf8a1d1596062de38af4acf0b4a2ae5e3126cd Mon Sep 17 00:00:00 2001 From: Dognose Date: Fri, 31 Jul 2026 14:52:40 +0800 Subject: [PATCH 1/2] Banner: absolutize root-relative hrefs in wp-admin (Odyssey) Root-relative hrefs on Banner/UpsellNudge are Calypso routes, but wp-admin resolves them against the site's own domain - every upgrade nudge rendered in Odyssey Stats linked to a 404 (the SEO preview nudge was patched at its call site in #113144; stats-no-content-banner's /post link is still broken today). Absolutize at the Banner chokepoint - computed defaults, caller-provided hrefs, and the secondary CTA all pass through it - via getCalypsoUrl(), a no-op outside wp-admin. Co-Authored-By: Claude Fable 5 --- client/components/banner/index.jsx | 41 ++++++++++++---- .../components/banner/test/to-calypso-href.js | 49 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 client/components/banner/test/to-calypso-href.js diff --git a/client/components/banner/index.jsx b/client/components/banner/index.jsx index 9ca0e32b6c0a..07e15224effa 100644 --- a/client/components/banner/index.jsx +++ b/client/components/banner/index.jsx @@ -1,3 +1,4 @@ +import config from '@automattic/calypso-config'; import { planMatches, isBloggerPlan, @@ -8,6 +9,7 @@ import { GROUP_JETPACK, GROUP_WPCOM, } from '@automattic/calypso-products'; +import { getCalypsoUrl } from '@automattic/calypso-url'; import { Button, Card, Gridicon, PlanPrice } from '@automattic/components'; import { isMobile } from '@automattic/viewport'; import clsx from 'clsx'; @@ -28,6 +30,23 @@ import './style.scss'; const noop = () => {}; +// In wp-admin (Odyssey) a root-relative href is a Calypso route, which the +// browser would resolve against the site's own domain — a 404. Point it at +// Calypso absolutely there; getCalypsoUrl() falls back to +// https://wordpress.com when the current origin isn't a Calypso one, as in +// wp-admin. Everywhere else (Calypso proper, Jetpack Cloud) this is a no-op. +export function toCalypsoHref( href ) { + if ( + href && + href.startsWith( '/' ) && + ! href.startsWith( '//' ) && + config.isEnabled( 'is_odyssey' ) + ) { + return getCalypsoUrl() + href; + } + return href; +} + export class Banner extends Component { static propTypes = { callToAction: PropTypes.oneOfType( [ PropTypes.string, PropTypes.element ] ), @@ -107,21 +126,23 @@ export class Banner extends Component { if ( ! href && siteSlug && canUserUpgrade ) { if ( customerType ) { - return `/plans/${ siteSlug }?customerType=${ customerType }`; + return toCalypsoHref( `/plans/${ siteSlug }?customerType=${ customerType }` ); } const baseUrl = `/plans/${ siteSlug }`; if ( feature || plan ) { - return addQueryArgs( - { - feature, - plan, - }, - baseUrl + return toCalypsoHref( + addQueryArgs( + { + feature, + plan, + }, + baseUrl + ) ); } - return baseUrl; + return toCalypsoHref( baseUrl ); } - return href; + return toCalypsoHref( href ); } handleClick = ( e ) => { @@ -302,7 +323,7 @@ export class Banner extends Component { { secondaryCallToAction && (