Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Settings: Allow Users to Insert Header Code #86079

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/components/inline-support-link/context-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ const contextLinks = {
link: 'https://wordpress.com/support/export/',
post_id: 2087,
},
'insert-header-code': {
link: 'https://wordpress.com/support/adding-code-to-headers/',
post_id: 187480,
},
'introduction-to-woocommerce': {
link: 'https://wordpress.com/support/introduction-to-woocommerce/',
post_id: 176336,
Expand Down
6 changes: 6 additions & 0 deletions client/my-sites/site-settings/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selecto
import DeleteSite from './delete-site';
import DisconnectSite from './disconnect-site';
import ConfirmDisconnection from './disconnect-site/confirm';
import HeaderCodeSettings from './header-code';
import ManageConnection from './manage-connection';
import SiteOwnerTransfer from './site-owner-transfer/site-owner-transfer';
import StartOver from './start-over';
Expand Down Expand Up @@ -111,6 +112,11 @@ export function startSiteOwnerTransfer( context, next ) {
next();
}

export function headerCode( context, next ) {
context.primary = <HeaderCodeSettings />;
Copy link
Member

Choose a reason for hiding this comment

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

Will all site types support this? I see Jetpack sites will, but what about WP.com simple / Atomic / etc.?

Copy link
Contributor

Choose a reason for hiding this comment

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

The code in Automattic/jetpack#34881 explicitly excludes Simple. As things stand I don't see anything stopping it from Atomic.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, we should make sure to redirect in Calypso if someone tries to access this with a WP.com simple site.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my comments below!

next();
}

export function legacyRedirects( context, next ) {
const { section } = context.params;
const redirectMap = {
Expand Down
182 changes: 182 additions & 0 deletions client/my-sites/site-settings/header-code/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { FEATURE_INSTALL_PLUGINS, getPlan, PLAN_BUSINESS } from '@automattic/calypso-products';
import { Button, Card, Gridicon } from '@automattic/components';
import { html } from '@codemirror/lang-html';
import CodeMirror from '@uiw/react-codemirror';
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
import classNames from 'classnames';
import { localize } from 'i18n-calypso';
import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import illustration404 from 'calypso/assets/images/illustrations/illustration-404.svg';
import UpsellNudge from 'calypso/blocks/upsell-nudge';
import DocumentHead from 'calypso/components/data/document-head';
import QuerySiteSettings from 'calypso/components/data/query-site-settings';
import EmptyContent from 'calypso/components/empty-content';
import HeaderCake from 'calypso/components/header-cake';
import InlineSupportLink from 'calypso/components/inline-support-link';
import Main from 'calypso/components/main';
import NavigationHeader from 'calypso/components/navigation-header';
import { successNotice, errorNotice } from 'calypso/state/notices/actions';
import { canCurrentUser } from 'calypso/state/selectors/can-current-user';
import getSiteSetting from 'calypso/state/selectors/get-site-setting';
import siteHasFeature from 'calypso/state/selectors/site-has-feature';
import { saveSiteSettings } from 'calypso/state/site-settings/actions';
import {
isRequestingSiteSettings,
isSavingSiteSettings,
isSiteSettingsSaveSuccessful,
getSiteSettingsSaveError,
} from 'calypso/state/site-settings/selectors';
import { isJetpackSite } from 'calypso/state/sites/selectors';
import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
import './style.scss';

const HeaderCodeSettings = ( {
errorMessage,
hasPlanFeature,
headCode,
isAdmin,
isJetpack,
isRequestingSettings,
isSaveFailure,
isSaveSuccess,
isSavingSettings,
saveSettings,
siteId,
siteSlug,
successMessage,
translate,
} ) => {
const [ htmlCode, setHtmlCode ] = useState( '' );
Aurorum marked this conversation as resolved.
Show resolved Hide resolved

useEffect( () => {
if ( 'string' === typeof headCode && headCode.length ) {
setHtmlCode( headCode );
}

// Necessary for if switching to a site without any code.
if ( ! headCode ) {
setHtmlCode( '' );
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
}
}, [ headCode, siteId ] );

useEffect( () => {
if ( isSaveSuccess ) {
successMessage( translate( 'Code updated successfully!' ), {
duration: 4000,
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
} );
} else if ( isSaveFailure && ! isSavingSettings ) {
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
errorMessage( translate( 'There was an error saving your code.' ), {
duration: 4000,
} );
}
}, [ isSaveSuccess, isSavingSettings, isSaveFailure ] );

const handleSaveSettings = () => {
saveSettings( siteId, {
jetpack_header_code: htmlCode,
} );
};

const isSupported = isJetpack || hasPlanFeature;

return (
<Main className="header-code">
<DocumentHead title={ translate( 'Header Code' ) } />
{ siteId && <QuerySiteSettings siteId={ siteId } /> }
{ ! isAdmin && (
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
<EmptyContent
illustration={ illustration404 }
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
title={ translate( 'You are not authorized to view this page' ) }
/>
) }

{ isAdmin && (
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
<>
<NavigationHeader
title={ translate( 'Header Code' ) }
subtitle={ translate( 'Insert code within the Header element of your site.' ) }
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean head element?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't actually sure! This refers to it as the Header element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header

Happy to change if you'd prefer. :)

Copy link
Member

Choose a reason for hiding this comment

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

"Header element" is definitely confusing when we talk about the "head" element. So we might want to change it indeed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking about this a little further, and I feel like it repeats the text below! What do you think about something like this which explains why this feature exists and why most people might use it? Might be more informative from a user's perspective. :)

Insert HTML meta tags and JavaScript code to your site.

/>
<HeaderCake backHref={ `/settings/general/${ siteSlug }` } isCompact>
<h1>{ translate( 'Header Code' ) }</h1>
</HeaderCake>
<Card>
<UpsellNudge
plan={ PLAN_BUSINESS }
Copy link
Member

Choose a reason for hiding this comment

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

This leads to the WP.com business plan and not a Jetpack plan though. Are you sure this was intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes - all Jetpack sites should be able to use this, though it's primarily intended for WP.com Atomic, but WordPress.com Simple sites shouldn't be able to.

Copy link
Member

Choose a reason for hiding this comment

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

But PLAN_BUSINESS is the WP.com plan, not the Jetpack plan. That upsell wouldn't make sense for a non-Atomic Jetpack site, because a non-Atomic Jetpack site can't purchase a WP.com plan. Does that makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we're redirecting unsupported sites to the General Settings page instead, this upsell is defunct and I've removed it.

But just to clarify, Jetpack sites wouldn't have seen that notice - unless I'm missing something? It was hidden from them.

title={ translate( 'Add custom code to your site with the %(planName)s plan.', {
Copy link
Member

Choose a reason for hiding this comment

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

Is this really the case? Do we really need a business plan to insert the header code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it'll be stripped out on WP.com Simple sites.

Copy link
Member

Choose a reason for hiding this comment

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

What about Personal (AKA Starter) and Premium (AKA Explorer) plans? Does only the WP.com business plan have this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, indeed. I believe that's for security reasons since Simple sites share the platform alongside product reasons (but of course, that's been determined well above me!).

Users wouldn't even be able to insert such code with the Custom HTML block: https://wordpress.com/support/wordpress-editor/blocks/custom-html-block/#supported-html-tags

args: { planName: getPlan( PLAN_BUSINESS ).getTitle() },
} ) }
description={ translate(
'Upgrade to insert JavaScript and HTML meta tags to your site.'
) }
showIcon
forceDisplay={ ! isSupported }
/>
<p className="header-code__text">
{ translate(
'The following code will be inserted within {{code}}<head>{{/code}} on your site. {{link}}Learn more{{/link}}.',
{
components: {
code: <code />,
link: (
<InlineSupportLink supportContext="insert-header-code" showIcon={ false } />
),
},
}
) }
</p>
<div
className={ classNames( 'header-code__editor', {
'is-loading': isRequestingSettings,
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
} ) }
>
<CodeMirror
value={ htmlCode }
onChange={ ( value ) => setHtmlCode( value ) }
height="235px"
extensions={ [ html() ] }
/>
</div>
<div className="header-code__warning">
Copy link
Member

Choose a reason for hiding this comment

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

Instead of introducing a new component, can we use the existing FormSettingExplanation component?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely! However, I feel quite strongly that italic text looks weird on this screen. I also noticed that there's a few cases of an icon with non-italic text around FormSettingExplanation (eg. Connections section).

Probably out of scope of this PR, but might be nice one day to have FormSettingExplanation accept an icon as a prompt and choose whether the text should be italicised.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have strong feelings on using FormSettingExplanation but I think ideally we should reuse what is already in place on other screens. This just doesn't feel like a special case that needs special handling.

<Gridicon icon="info" size={ 18 } />
<p className="header-code__warning-text">
{ translate( "This code will be run for all your site's viewers." ) }{ ' ' }
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
{ translate(
'Please make sure that you understand any code and trust its source before adding it here.'
) }
</p>
</div>
<div className="header-code__button">
<Button
disabled={ headCode === htmlCode || ! isSupported || isRequestingSettings }
primary
busy={ isSavingSettings }
onClick={ handleSaveSettings }
>
{ translate( 'Save' ) }
</Button>
</div>
</Card>
</>
) }
</Main>
);
};

export default connect(
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
( state ) => {
const siteId = getSelectedSiteId( state );
return {
hasPlanFeature: siteHasFeature( state, siteId, FEATURE_INSTALL_PLUGINS ),
headCode: getSiteSetting( state, siteId, 'jetpack_header_code' ),
isAdmin: canCurrentUser( state, siteId, 'manage_options' ),
isJetpack: isJetpackSite( state, siteId ),
isRequestingSettings: isRequestingSiteSettings( state, siteId ),
isSaveFailure: getSiteSettingsSaveError( state, siteId ),
isSaveSuccess: isSiteSettingsSaveSuccessful( state, siteId ),
isSavingSettings: isSavingSiteSettings( state, siteId ),
siteId,
siteSlug: getSelectedSiteSlug( state, siteId ),
};
},
{ saveSettings: saveSiteSettings, successMessage: successNotice, errorMessage: errorNotice }
)( localize( HeaderCodeSettings ) );
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
51 changes: 51 additions & 0 deletions client/my-sites/site-settings/header-code/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.header-code__warning {
display: flex;
margin-bottom: 1.5em;

.gridicon {
fill: var(--color-text-subtle);
margin-right: 6px;
}

.header-code__warning-text {
color: var(--color-text-subtle);
font-size: $font-body-extra-small;
margin: 0;
}
}

.header-code__editor .cm-editor {
border: 1px solid var(--studio-gray-5);
font-size: $font-body-small;
margin-bottom: 1em;
outline: none;

&.cm-focused {
border-color: var(--studio-gray-10);
}

* {
font-family: $code;
}
}

.header-code__editor.is-loading .cm-editor {
@include placeholder();
pointer-events: none;

&:after {
display: none;
}

.cm-line {
visibility: hidden;
}
}

.header-code__text {
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
margin-bottom: 1em;
}

.header-code__button {
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
text-align: right;
}
11 changes: 11 additions & 0 deletions client/my-sites/site-settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
disconnectSite,
disconnectSiteConfirm,
general,
headerCode,
legacyRedirects,
manageConnection,
redirectIfCantDeleteSite,
Expand Down Expand Up @@ -111,6 +112,16 @@ export default function () {
clientRender
);

page(
'/settings/header-code/:site_id',
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
siteSelection,
navigation,
setScroll,
headerCode,
makeLayout,
clientRender
);

page( '/settings/traffic/:site_id', redirectToTraffic );
page( '/settings/analytics/:site_id?', redirectToTraffic );
page( '/settings/seo/:site_id?', redirectToTraffic );
Expand Down
13 changes: 13 additions & 0 deletions client/my-sites/site-settings/site-tools/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ class SiteTools extends Component {
showClone,
showDeleteContent,
showDeleteSite,
showHeaderCode,
showManageConnection,
showStartSiteTransfer,
siteId,
} = this.props;

const changeAddressLink = `/domains/manage/${ siteSlug }`;
const headerCodeLink = `/settings/header-code/${ siteSlug }`;
const startOverLink = `/settings/start-over/${ siteSlug }`;
const startSiteTransferLink = `/settings/start-site-transfer/${ siteSlug }`;
const deleteSiteLink = `/settings/delete-site/${ siteSlug }`;
Expand All @@ -74,6 +76,9 @@ class SiteTools extends Component {
"Remove all posts, pages, and media to start fresh while keeping your site's address."
);

const headerCode = translate( 'Header code' );
const headerCodeText = translate( 'Insert code within the Header element of your site.' );
Aurorum marked this conversation as resolved.
Show resolved Hide resolved

const deleteSite = translate( 'Delete your site permanently' );
const deleteSiteText = translate(
"Delete all your posts, pages, media, and data, and give up your site's address."
Expand Down Expand Up @@ -117,6 +122,13 @@ class SiteTools extends Component {
description={ copyText }
/>
) }
{ showHeaderCode && (
<SiteToolsLink
href={ headerCodeLink }
title={ headerCode }
description={ headerCodeText }
/>
) }
{ showClone && (
<SiteToolsLink href={ cloneUrl } title={ cloneTitle } description={ cloneText } />
) }
Expand Down Expand Up @@ -219,6 +231,7 @@ export default compose( [
showClone: 'active' === rewindState.state && ! isAtomic,
showDeleteContent: isAtomic || ( ! isJetpack && ! isVip && ! isP2Hub ),
showDeleteSite: ( ! isJetpack || isAtomic ) && ! isVip && sitePurchasesLoaded,
showHeaderCode: isJetpack || isAtomic,
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Should we show this only to admins? (if the current user has the capability to manage_options)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not necessary in this case! The entire page is blocked for non-admins.

showManageConnection: isJetpack && ! isAtomic,
showStartSiteTransfer,
siteId,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"@automattic/whats-new": "workspace:^",
"@automattic/wp-babel-makepot": "workspace:^",
"@automattic/wpcom-template-parts": "workspace:^",
"@codemirror/lang-html": "^6.4.7",
"@types/cookie": "^0.4.1",
"@types/debug": "^4.1.7",
"@types/fast-json-stable-stringify": "^2.0.0",
Expand All @@ -173,6 +174,7 @@
"@types/validator": "^13.7.1",
"@types/webpack-env": "^1.18.4",
"@types/wordpress__block-editor": "^11.5.8",
"@uiw/react-codemirror": "^4.21.21",
Aurorum marked this conversation as resolved.
Show resolved Hide resolved
"@wordpress/base-styles": "^4.38.0",
"@wordpress/block-editor": "^12.16.0",
"@wordpress/block-library": "^8.25.0",
Expand Down
Loading
Loading