0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Convert portal links to relative to avoid homepage flash on click

closes https://linear.app/tryghost/issue/PLG-190

- often when adding portal links to your own site pages the URLs are added as absolute on the site's homepage due to copy+paste from displayed URLs in Admin
- when clicking absolute portal URLs the homepage is first loaded before the Portal popup is shown resulting in a slower and flashier experience
- added a transform for all local portal URLs on the page when Portal is initialized so links open the Portal popup immediately on the current page
This commit is contained in:
Kevin Ansfield 2024-09-02 12:13:46 +01:00
parent a3e498106d
commit 06058db6ae
No known key found for this signature in database
4 changed files with 112 additions and 5 deletions

View file

@ -5,12 +5,13 @@ import Notification from './components/Notification';
import PopupModal from './components/PopupModal';
import setupGhostApi from './utils/api';
import AppContext from './AppContext';
import {hasMode} from './utils/check-mode';
import NotificationParser from './utils/notifications';
import * as Fixtures from './utils/fixtures';
import {hasMode} from './utils/check-mode';
import {transformPortalAnchorToRelative} from './utils/transform-portal-anchor-to-relative';
import {getActivePage, isAccountPage, isOfferPage} from './pages';
import ActionHandler from './actions';
import './App.css';
import NotificationParser from './utils/notifications';
import {hasRecommendations, allowCompMemberUpgrade, createPopupNotification, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnlySite, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {handleDataAttributes} from './data-attributes';
@ -184,10 +185,9 @@ export default class App extends React.Component {
};
window.addEventListener('hashchange', this.hashHandler, false);
// spike ship - to test if we can show / hide signup forms inside post / page
// the signup card will ship hidden by default,
// so we need to show it if the member is not logged in
if (!member) {
// the signup card will ship hidden by default, so we need to show it if the user is not logged in
// not sure why a user would have more than one form on a post, but just in case we'll find them all
const formElements = document.querySelectorAll('[data-lexical-signup-form]');
if (formElements.length > 0){
formElements.forEach((element) => {
@ -195,7 +195,11 @@ export default class App extends React.Component {
});
}
}
this.setupRecommendationButtons();
// avoid portal links switching to homepage (e.g. from absolute link copy/pasted from Admin)
this.transformPortalLinksToRelative();
} catch (e) {
/* eslint-disable no-console */
console.error(`[Portal] Failed to initialize:`, e);
@ -956,6 +960,16 @@ export default class App extends React.Component {
}
}
/**
* Transform any portal links to use relative paths
*
* Prevents unwanted/unnecessary switches to the home page when opening the
* portal. Especially useful for copy/pasted links from Admin screens.
*/
transformPortalLinksToRelative() {
document.querySelectorAll('a[href*="#/portal"]').forEach(transformPortalAnchorToRelative);
}
render() {
if (this.state.initStatus === 'success') {
return (

View file

@ -0,0 +1,34 @@
import App from '../App';
import setupGhostApi from '../utils/api';
import {appRender} from '../utils/test-utils';
import {site as FixtureSite, member as FixtureMember} from '../utils/test-fixtures';
describe('App', function () {
beforeEach(function () {
// Stub window.location with a URL object so we have an expected origin
const location = new URL('http://example.com');
delete window.location;
window.location = location;
});
test('transforms portal links on render', async () => {
const link = document.createElement('a');
link.setAttribute('href', 'http://example.com/#/portal/signup');
document.body.appendChild(link);
const ghostApi = setupGhostApi({siteUrl: 'http://example.com'});
ghostApi.init = jest.fn(() => {
return Promise.resolve({
site: FixtureSite.singleTier.basic,
member: FixtureMember.free
});
});
const utils = appRender(
<App api={ghostApi} />
);
await utils.findByTitle(/portal-popup/i);
expect(link.getAttribute('href')).toBe('#/portal/signup');
});
});

View file

@ -0,0 +1,37 @@
import {transformPortalAnchorToRelative} from '../../utils/transform-portal-anchor-to-relative';
// NOTE: window.location.origin = http://localhost:3000
describe('transformPortalAnchorToRelative', function () {
test('ignores non-portal links', function () {
const anchor = document.createElement('a');
anchor.setAttribute('href', 'http://localhost:3000/#/signup');
transformPortalAnchorToRelative(anchor);
expect(anchor.getAttribute('href')).toBe('http://localhost:3000/#/signup');
});
test('ignores already-relative links', function () {
const anchor = document.createElement('a');
anchor.setAttribute('href', '#/portal/signup');
transformPortalAnchorToRelative(anchor);
expect(anchor.getAttribute('href')).toBe('#/portal/signup');
});
test('ignores external links', function () {
const anchor = document.createElement('a');
anchor.setAttribute('href', 'https://example.com/#/portal/signup');
transformPortalAnchorToRelative(anchor);
expect(anchor.getAttribute('href')).toBe('https://example.com/#/portal/signup');
});
test('converts absolute to a relative link', function () {
const anchor = document.createElement('a');
anchor.setAttribute('href', 'http://localhost:3000/#/portal/signup');
transformPortalAnchorToRelative(anchor);
expect(anchor.getAttribute('href')).toBe('#/portal/signup');
});
});

View file

@ -0,0 +1,22 @@
export function transformPortalAnchorToRelative(anchor) {
const href = anchor.getAttribute('href');
const url = new URL(href, window.location.origin);
// ignore non-portal links
if (!url.hash || !url.hash.startsWith('#/portal')) {
return;
}
// ignore already-relative links
if (href.startsWith('#/portal')) {
return;
}
// ignore external links
if (url.origin !== window.location.origin) {
return;
}
// convert to a relative link
anchor.setAttribute('href', url.hash);
}