-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Editor: Hide SEO description for private/hidden sites #11579
Conversation
@@ -246,7 +247,10 @@ const EditorDrawer = React.createClass( { | |||
|
|||
const { plan } = this.props.site; | |||
const hasBusinessPlan = isBusiness( plan ) || isEnterprise( plan ); | |||
if ( ! hasBusinessPlan ) { | |||
const { privacySetting = 1 } = this.props; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't appear that the default value here would ever be used; partly because the condition above ! this.props.privacySetting
would abort on a falsey value, but also because mapStateToProps
always provides at least a null
value based on the behavior of getSiteSetting
. This would not trigger the default (see example).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're expecting to use site settings, you should render a <QuerySiteSettings />
in the component's render so you're not relying on circumstances of other components on the page.
Also, would the isPrivateSite
selector be an appropriate alternative? It uses information from the site object, which is more readily available.
e4ed244
to
7d5ef86
Compare
@aduth Thanks for the review. I'm now rendering in the |
Yeah, based on my understanding, this is expected but could be improved. I ran into a similar case with site icon, in that it's duplicated between both the settings endpoint and the sites endpoint. In my case, I deferred to having the sites state be the preferred (canonical) source, tracking updates to site settings in the sites reducer. We should probably do something similar for privacy settings. |
7d5ef86
to
93af5bb
Compare
Good call. I like that approach. I modified the sites reducer to monitor changes in blog privacy as well. I left comments to this effect in the code for clarity, but since the settings endpoint returns a numerical value for Extra sanity check since it broke prod last time, with the updates to Tests for |
Tests for
CircleCI still doesn't like me. Investigating. |
529176d
to
7d08e9c
Compare
Tests should all be passing now. Long story (hopefully) short: With the updated behavior in We use |
@@ -328,7 +331,8 @@ export default connect( | |||
return { | |||
canJetpackUseTaxonomies: isJetpackMinimumVersion( state, siteId, '4.1' ), | |||
jetpackVersionSupportsSeo: isJetpackMinimumVersion( state, siteId, '4.4-beta1' ), | |||
typeObject: getPostType( state, siteId, type ) | |||
typeObject: getPostType( state, siteId, type ), | |||
isSitePrivate: isPrivateSite( state, siteId ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing cartwheels to avoid no-shadow
? 😛 Seems fine as isPrivate
too if that would be clearer than the word shuffling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha I take ESLint rules very seriously :)
|
||
if ( settings ) { | ||
// Site settings returns a numerical value for blog_public. | ||
return parseInt( settings.blog_public, 10 ) !== 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we rely on settings.blog_public
to always be a string? My hunch is maybe not (via optimistic state updates), in which case what you have here is probably good for safety. If we could be confident, we might be able to simply return '1' !== settings.blog_public
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep the current behavior. I feel like I've seen multiple behaviors while testing this out.
client/state/sites/reducer.js
Outdated
}; | ||
} else if ( settings.hasOwnProperty( 'site_icon' ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't a settings
payload have both blog_public
and site_icon
? In which case we'd only update one or the other because of else if
. Probably better as a separate if
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't a
settings
payload have bothblog_public
andsite_icon
? In which case we'd only update one or the other because ofelse if
. Probably better as a separateif
.
To this point, we should probably add a test for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't a settings payload have both blog_public and site_icon? In which case we'd only update one or the other because of else if. Probably better as a separate if.
I ran through some scenarios, and I don't think it's likely that the settings payload will actually ever contain both a blog_public
and site_icon
update. When the site icon is updated, we call saveSiteSettings
immediately which automatically saves the settings with only the icon property. You can see this if you change the blog privacy (but don't save), update the icon (see the "Settings Saved" dialogue), and then try to leave the page. You'll see an error because the blog privacy setting is still queued.
I'm going to update it either way just in case this behavior is changed in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the more likely scenario is that we receive an entire fresh set of site settings via SITE_SETTINGS_RECEIVE
, which would always include both keys, one or the other or both potentially having been updated.
client/state/sites/reducer.js
Outdated
if ( settings.hasOwnProperty( 'blog_public' ) ) { | ||
// Site settings returns a numerical value for blog_public. | ||
// Site state expects a true/false value for is_private. | ||
const sitePrivacy = parseInt( settings.blog_public, 10 ) !== 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Might be nice to indicate boolean value by prefixing with is
: const isPrivate = ...
client/state/sites/reducer.js
Outdated
@@ -138,34 +138,49 @@ export function items( state = {}, action ) { | |||
|
|||
// A site settings update may or may not include the icon property. | |||
// If not, we should simply return state unchanged. | |||
if ( ! settings.hasOwnProperty( 'site_icon' ) ) { | |||
if ( ! settings.hasOwnProperty( 'site_icon' ) && ! settings.hasOwnProperty( 'blog_public' ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what gotchas you might encounter, but doing a reduce
on the keys we're monitoring might be a nicer way to encapsulate the separate logic.
Something like:
return reduce( [ 'blog_public', 'site_icon' ], ( memo, key ) => {
if ( ! settings.hasOwnProperty( key ) ) {
return memo;
}
switch ( key ) {
case 'blog_public':
// ...
case 'site_icon':
// ...
}
return memo;
}, state );
(SITE_RECEIVE
reducer above is similar in how it reduces using state as initial value while avoiding direct mutations)
7d08e9c
to
3db5409
Compare
3db5409
to
148ef73
Compare
client/state/sites/test/reducer.js
Outdated
@@ -412,7 +412,7 @@ describe( 'reducer', () => { | |||
} | |||
} ); | |||
|
|||
expect( state ).to.equal( original ); | |||
expect( state ).to.eql( original ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're now returning memo, which is a copy of state, it was necessary to change these to eql
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That we needed to change the tests here implies that we're returning a new state reference in the reducer where we shouldn't be (no changes occurred). I think in your reducer's switch
where you're breaking upon finding that there are no differences, we should return memo;
instead to bypass the rest of the reduce
and ensure that we only create a copy of the state object (return a new reference) if we're certain that a change has actually occurred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. Updated.
client/state/sites/reducer.js
Outdated
if ( null === mediaId ) { | ||
// Unset icon | ||
nextSite = omit( nextSite, 'icon' ); | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This break;
is redundant since there's no other logic in the case
to be run except the final break;
anyways.
client/state/sites/reducer.js
Outdated
switch ( key ) { | ||
case 'blog_public': | ||
const isPrivate = parseInt( settings.blog_public, 10 ) !== 1; | ||
nextSite.is_private = isPrivate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'll want to check (and add test case for) whether the value has actually changed. In this case we're always updating the site state even if the value we've received is the same as what we're currently tracking.
client/state/sites/reducer.js
Outdated
return state; | ||
} | ||
|
||
const mediaId = settings.site_icon; | ||
let nextSite = { ...site }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor-ish: We're creating a shallow clone of the site even if we never end up making any changes. We could do this at the point we need it instead:
let nextSite = site;
// ...
case 'blog_public':
const isPrivate = parseInt( settings.blog_public, 10 ) !== 1;
if ( site.is_private === isPrivate ) {
return memo;
}
nextSite = { ...nextSite, is_private: isPrivate };
break;
Only downside of this is that if multiple keys have new values, we'd clone the site multiple times. I think it's the much more likely scenario that nothing will have changed (thus zero cloning) than multiple changed values though.
e3ce71e
to
87946cc
Compare
I went back and forth on whether or not to clone nextSite = { ...nextSite, // ... } You make a good point about frequency. I updated the behavior and added a test case for:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good and works well for me 👍
I did notice that, after changing my business site to private, the next SITE_RECEIVE
caused a couple other changes to state. Of them, options.default_sharing_status
is probably one that makes sense as being affected by the change in blog_public
. Not something that we necessarily need to change here, but maybe in a subsequent pull request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, actually, I just noticed that the SEO Description field shows for a hidden business site. Is that expected? I mean, it makes sense given what we're testing (a hidden site does not mean site.is_private
). Which means our reducer logic for testing whether blog_public
is equal to 1 is not actually accurate.
I'm not seeing the SEO description field on my side when a site is changed to hidden. Here's a GIF of what I'm experiencing with a hidden site: |
The problem is that the reducer logic for In this pull request, we test: const isPrivate = parseInt( settings.blog_public, 10 ) !== 1; Server-side, we test: function is_private() {
return ( -1 == get_option( 'blog_public' ) );
} Key difference being that our reducer is treating |
87946cc
to
0b6cb5d
Compare
Per a Slack convo with @aduth, I updated this PR to treat if ( ! hasBusinessPlan || isPrivate || isHidden ) {
return;
}
|
0b6cb5d
to
6ca148d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
We disable the SEO settings for any site that is private. This commit disables the SEO description drawer in the Editor when a site is private as well. This is the second attempt after #11476 broke production. Both the site and site settings endpoints return a site privacy setting. This PR establishes the site state as the preferred source of truth and updates the isPrivateSite selector as a result. The SEO description will still appear for hidden sites since .is_private will return false. I'll fix that up in another PR with an addition selector for isHiddenSite(). Resolves: #11406
6ca148d
to
3d24435
Compare
Really happy to see this one. Nice work! 🎉 |
We disable the SEO settings for any site that is private. This commit disables the SEO description drawer in the Editor when a site is private as well. This is the second attempt after #11476 broke production (see p4TIVU-5UZ-p2).
The previous attempt broke production because it assumed
this.props.siteSettings
would exist when someone tried to load the editor. With a clear cache, this isn't necessarily true. We fetch site settings in specific instances like when someone tries to visit the settings page directly. When users tried to load the editor with an empty initial state, they received an error becauseblog_public
wasn't defined yet leading to a broken experience.In the revised approach:
getSiteSetting
selector understate/selectors/
.this.props.privacySetting
. Then, we set a default value of1
as a fallback.I did test this on a cleared cache in my regular browser and an incognito window. I didn't experience any of the errors from the previous attempt.
To test
Resolves: #11406