New Features and Enhancements with Customizer Changesets in 4.9

In WordPress 4.7 the concept of changesets was introduced in the Customizer (#30937). To understand the new Customizer improvements in 4.9, you must first go back and review what was proposed and implemented a year ago:

Customize Changesets Technical Design Decisions

Changesets are a way to persistently store changes made via the Customizer framework. Changesets contain the pending changes for any number of settings, and a setting can model any object in WordPress—whether options, theme mods, nav menu items, widgets, or even posts/pages and their postmeta. Changesets are identified by UUID (which is the post_name for the customize_changeset post type that stores the data as JSON in post_content). When a request is made to WordPress with the customize_changeset_uuid request param—whether to the frontend or to the REST API—the Customizer framework will bootstrap and all of the values from the changeset will be read and applied to the response via WordPress filters added by the settings’ respective WP_Customize_Setting::preview() methods.

Only an authorized user can write changes into a changeset for a given setting (according to its respective capability). But once it has been written then anyone can preview the site with the changes in the changeset applied: all that is needed is the UUID. Since previewing a changeset is now a readonly operation (whereas before 4.7 it was always a POST request), a changeset can be previewed on a site by authenticated and unauthenticated users alike. With the changeset UUID supplied when opening the Customizer, a user can keep iterating on a set of changes over several days or longer and only publish them once stakeholders are satisfied. Now, freelancers and agencies will be better able to communicate and collaborate on site changes with clients.

Once a customize_changeset post transitions to the publish status then all of the values in the changeset will be passed into their respective WP_Customize_Setting::update() methods to publish (“go live”) on the site: in version control terminology, the staged values from the changeset get committed and pushed. All of the changes go live together in a batch save operation (originally changesets were termed “transactions”).

As noted in the 4.7 merge proposal:

For the initial core merge, no UI changes are being proposed. The feature will only be exposed as the new query parameter on the URL. Adding a UI to this feature will happen in a future release.

The future [release] is now. Where the infrastructure of changesets was merged from the Customize Changesets feature plugin in 4.7, the key UI features from the plugin are now being merged in 4.9 after a significant number of design iterations.

This dev note contains sections on the following:

Changeset Drafting

Similar to the edit post screen, when you open the Customizer and start making changes, those changes will be stored in an auto-draft post. If the post is never published, then it will be garbage-collected after a week. In 4.7 if you wanted to keep a changeset around for longer than a week, then you’d have to bookmark the Customizer URL with the changeset_uuid param and then keep going back to touch the changeset before a week elapses to prevent it from getting auto-deleted. This is no longer the case as of 4.9 (via #39896) as you can now explicitly save a changeset as a persistent draft:

If you decide that you do not want to keep the changes you’ve made, you can also now reset the Customizer to its last published state via the “Discard changes” link. There is now a WP_Customize_Manager::trash_changeset_post() method in PHP and a wp.customize.previewer.trash() in JS to programmatically trash changesets.

Changeset Scheduling

Since the values from a changeset go live whenever the customize_changeset post transitions to the publish status, a changeset can be scheduled merely by setting the status to future and giving it a corresponding future post date. This capability was demonstrated in the 4.7 dev note by using WP-CLI to schedule a change to the site title (also showing how the Customizer framework does not necessitate using the current Customizer app UI at customize.php):

wp post create \
    --post_type=customize_changeset \
    --post_name=$( uuidgen ) \
    --post_status=future \
    --post_date="2018-01-01 00:00:00" \
    --post_content='{"blogname":{"value":"Happy New Year!"}}'

The technical infrastructure to schedule a changeset from inside the Customizer was also implemented in 4.7 via the wp.customize.previewer.save({status: 'future'}) JS API call. However, aside from the Customize Snapshots feature plugin, there was no UI for scheduling a changeset in the Customizer until now in 4.9. As with drafting, you can access scheduling via the gear button that opens the Publish Settings section:

After selecting the future date and time to schedule the changeset, pressing the “Schedule” button will cause the button to update as “Scheduled” while also then being disabled to indicate that the Customizer is no longer in a dirty state. If you have the Customizer open at the scheduled time then the client will proactively publish the settings immediately. Otherwise, if you had closed the Customizer after scheduling (as usual), then the changes will normally publish within a minute of the scheduled time.

If you frequently see missed schedule errors for scheduled posts, you will get missed schedule issues for changesets as well. The problem is frequently caused due to sites getting infrequent visitors, as WP Cron relies on a visitor to determine whether or not it should spawn cron. So if you need to guarantee that your posts or changesets get published at the required scheduled time, you may want to set up system cron to ping wp-cron.php once a minute, or otherwise use an external cron ping service.

Changeset Autoloading and Linear Mode

Above I noted in 4.7 that if you wanted to keep iterating on a changeset over several days that you’d need to bookmark the URL with the changeset_uuid parameter. Without this URL bookmarked there would be no way to get back to it without inspecting the customize_changeset posts in the wp_posts database table to find the UUID. With the introduction of drafting and scheduling in 4.9 this behavior has changed. By default in core if you save a persistent changeset (as drafted or scheduled), this same changeset will be auto-loaded for you the next time you open the Customizer. Because of this, there is no need to bookmark the Customizer URL; the changeset_uuid will not even be added. There is no need to have a list of the current changesets—in core you can only have one changeset saved as a draft or scheduled at a time.

We call this a “linear” changeset mode. We decided it was conceptually easier for the majority of users to understand a linear progression of changesets rather than to allow multiple changesets to be drafted and scheduled in parallel. Remember post forking? I talked with @jorbin about why the effort has largely been discontinued. He said he “found the git based concepts (forking, merging, conflicts) to not really resonate in any sort of meaningful way for most users.” In the Customizer, having concurrent changesets is essentially site forking, so the potential for user confusion would seem to be all the more real.

One key limitation of the linear changeset mode is that if you schedule a changeset to be published in 1 week (say to change the site title) then you will not be able to open the Customizer to just publish today a change to the site tagline. You would have to either discard the previously scheduled changes, or just decide to publish the site title and tagline changes together today. (There is a workaround for power users in that you can open customize.php with a new manually-generated changeset_uuid param to create and publish a changeset concurrently.)

One other limitation with the linear changeset mode is that we do not allow for theme switching to be done when a changeset has been saved as a draft or scheduled. There are a couple technical reasons for this. Most importantly, changesets cannot be scheduled with a theme switch. This is because when a changeset post gets published during WP Cron, the new theme is not active and so none of the theme-specific settings will be registered. We’ll want to eventually allow theme switches to be schedulable along with other Customizer settings, but that capability is not part of 4.9. A secondary reason is that the previewed theme is not actually stored in the changeset, so when returning to the Customizer later the theme being previewed at the time of saving would not then be restored (see #39031).

Changeset Branching Mode

In spite of the linear mode being the default in core for the majority of users, I’m also aware from client work that having concurrent changesets is critical for sites that have multiple users who use the Customizer heavily for site management (customize all the things). For this reason, the previous non-linear branching mode for changesets can be restored with a filter:

add_filter( 'customize_changeset_branching', '__return_true' );

When branching mode is enabled, every time you open the Customizer a new session will be started with a different UUID. After you make your first change, the changeset_uuid parameter will be added to the URL. You can then save the changeset as a persistent draft (see above) and you will not have to come back every few days to touch it to prevent it from being garbage-collected. You will still need to bookmark the URL to be able to return to that changeset as there is no core UI yet for listing out all current changesets. For that changeset listing UI and the ability to give titles to changesets (commit messages), please use the Customize Snapshots feature plugin and try its pending pull request for 4.9 compatibility (since many of the features were merged into 4.9, a plugin update is needed to disable those features from conflicting with what is now in core).

Another reason why branching is not enabled by default is because there is no core facility yet for handling conflicts between concurrent changesets (changeset branching merge conflicts). Consider if one user adds a Text widget to the sidebar in their changeset, but another user adds a Gallery widget to the same sidebar in another changeset. Since there is one sidebars_widgets setting that contains the list of widget IDs that are assigned to the sidebar, whoever publishes their changeset last will end up getting their widget in the sidebar. For work-in-progress on improving the handling of branching changesets, refer to pull requests on the Customize Snapshots feature plugin, including conflict resolution, copying/forking, merging/combining, and splitting.

In addition to the new customize_changeset_branching filter, there are a few API additions:

  • You can instantiate WP_Customize_Manager with a branching argument to its initial value instead of using the filter. The default value is true for back-compat with advanced Customizer plugins, but when core initializes WP_Customize_Manager it will supply false since the linear mode is the default.
  • There is a WP_Customzie_Manager::branching() method that returns the the initial value branching argument with the customize_changeset_branching filter applied.
  • There is a new WP_Customize_Manager::establish_loaded_changeset() method which runs early during after_setup_theme, and this uses the provided branching flag to determine whether or not auto-load (see above) the most recent drafted/scheduled changeset (no if true, yes if false).
  • The branching argument will be exported to the client as wp.customize.settings.changeset.branching.

Autosaving Auto-Drafts and Autosave Revisions

As noted above, changesets were already autosaving when they were introduced in 4.7: if you open the Customizer and start making changes they will get written into an auto-draft changeset post after the autosave interval; the auto-draft will also be updated if you switch tabs or try closing the window without explicitly pressing the Customizer’s close button. Once this auto-draft changeset is created if you return to the Customizer after a accidentally quitting your browser, you will now in 4.9 be prompted with a notification to restore their most recent auto-draft:

Clicking on the restoration link will cause the Customizer to reload with that auto-draft changeset’s UUID supplied so that your previous changes are restored for you to then continue editing and ether save as a persistent draft, schedule, or publish. If, however, you dismiss the notification or exit the Customizer, then the auto-draft changeset will a _customize_restore_dismissed postmeta added to it which will prevent it from being prompted for restoration again. The notification will also dismiss once you make your first change.

The autosaving behavior for auto-draft changesets is similar to the autosave behavior for when you’re creating a post. As long as the post remains in an auto-draft (or draft) status then any changes you make will get autosaved directly on top of the draft post; no autosave revisions get created until you update a post to have a status like pending, private, or future status. If you make a change to a post after it has been saved with a non-draft status then the autosaved changes will be saved in an autosave revision. For posts this both prevents changes from going live prematurely, but it also allows you to make a change but then abandon it by just leaving the edit post screen. The next time you come back to the edit post screen, a notice appears prompting you to restore the autosave revision. Autosave revisions have now been implemented for changesets as well.

In addition to autosave revisions being made for scheduled changesets, they will also be made for drafted changesets (unlike posts). So once you save a a changeset as a draft or scheduled, if you then make a subsequent change it will get written into an autosave revision of the current changeset post. In this state, before hitting the saving Save Draft or Schedule button, any requests in the preview will be made with the customize_autosaved query parameter to cause the changeset data to be sourced from the autosave revision instead of the main changeset post.

Autosave revisions allow you to make a change and preview it in the customizer, but not have it be written into the full changeset until you’re happy. You can likewise just quit the Customizer to abandon those additional changes you made on top of the drafted/scheduled changeset. Similarly to when leaving the Customizer with an auto-draft changeset, if you leave the Customizer after an autosave revision has been created then when you return to that drafted/scheduled changeset you will get the same restore notification:

Clicking the restoration link will likewise reload the Customizer but instead of adding a different changeset_uuid here it adds a customize_autosaved param which then is stripped from the location after the Customizer finishes reloading. Additionally, if you dismiss the notification then the autosave revision will be deleted. The autosave revision will be also deleted and then replaced once you make a change. Note that each user will have their own separate autosave revision, so one user will not get prompted to restore another user’s autosave revision.

Other changes:

  • There is now an autosaved argument which can be passed when constructing WP_Customize_Manager. This is used by WP_Customize_Manager::changeset_data() as a signal that it should source changeset data from the autosave revision if it exists.
  • There is a WP_Customize_Manager::autosaved() method that returns the value of the autosaved argument.
  • The WP_Customize_Manager::save_changeset_post() now has an autosave param which indicates the changes should be written into an autosave revision if the changeset is not an auto-draft.
  • An autosaving message will be sent to the preview when the user makes a change to enter the dirty state; this is used by the preview to ensure it includes customize_autosaved params on any URLs.
  • Autosave restoration notices are not shown when importing starter content.
  • Whether the Customizer was opened with an autosave revision being restored is exported to JS as wp.customize.settings.changeset.autosaved.
  • Whether or not an autosave revision is available is exported to JS as wp.customize.settings.changeset.hasAutosaveRevision.
  • The UUID of the latest auto-draft changeset that the user should be prompted to restore is exported to JS as wp.customize.settings.changeset.latestAutoDraftUuid.
  • The frequency of the changeset update requests has been reduced. In 4.7 a request was made every time the controls window was blurred, including when clicking into the preview window; this incurred additional HTTP POST requests and slowed down site navigation. In 4.9 the blur event has been replaced with visibilitychange event.

Changeset Post Locking

As noted above with changesets defaulting to a linear mode, after a changeset is saved as drafted or scheduled, this same changeset will be autoloaded the next time a user opens the Customizer. This same changeset will autoload not only for the user who first created it but also for any user who accesses the Customizer. Because of this there is a much greater likelihood of two or more users attempting to make changes to the same changeset and overwriting each other. This problem would’t be common before 4.9 because previously you would have to explicitly share a URL to the Customizer with the changeset_uuid in the URL to collaborate on a given changeset. So now that conflicts are a real day-to-day possibility in the Customizer, we’re also including a new changeset locking feature in 4.9.

Changeset locking is implemented similarly to post locking. If a first user is making changes in the Customizer and saves as a draft, then when a second user accesses the Customizer they will be presented with a changeset locked overlay notification. The second user will then have an option to go back or take over. If they decide to take over then the first user will shortly thereafter be presented with a changeset lock overlay notification, with an option to preview the changes on the frontend (see more on frontend previews below).

WP Heartbeat is introduced into the Customizer in order to maintain a user’s lock and also to inform a user if someone else has taken their lock away. In contrast with the post edit screen, locks will be lifted immediately upon leaving if you use the Customizer’s close button.

If a user gets locked out while making changes to a given changeset, their pending changes will be stored in an autosave revision to then be prompted for restoration when the lock releases. If the autosave is restored, it will not presently attempt to merge your changes with the other user’s changes. Nevertheless, initial groundwork has been laid for this by storing a date_modified_gmt with each setting in the changeset so in the future we might be able to merge settings from an autosave revision that are newer than in the full changeset post. For more on this and some additional complexities related to merging revisions, see #42191. When a changeset is published, any autosave revisions for other users will be converted into auto-draft changesets so that they can still be restored via the restore notification described above.

Changeset locking is tied to whatever customize_changeset post is being edited. This means that it will also work as expected when in branching mode: any number of changesets can exist in parallel with different users editing them concurrently, with locking only occurring if two users try to edit the same changeset. It’s important to note as well that since locking is limited to given changeset, it is completely possible that two separate branching changesets could have conflicts between them and these are not currently accounted for. This was a reason why branching is not enabled by default in core. For work on better handling conflicts in concurrent Customizer sessions, see #31436. Eventually wholesale changeset locking should give way to granular control/setting locking similar to how Gutenberg will allow wholesale post blocking to be replaced with granular block locking: this will enable collaborative editing of an entire site.

Additional changes related to locking:

  • Calls to WP_Customize_Manager::save_changeset_post() will fail with a changeset_locked error if it is locked. Nevertheless, the changes will still get saved into the user’s autosave revision.
  • There are new WP_Customize_Manager::set_changeset_lock() and WP_Customize_Manager::refresh_changeset_lock() methods, as well as methods specific to WP Heartbeat and Ajax requests to take over a changeset lock. The core wp_check_post_lock() function is still used for obtaining the lock info.
  • The locking user details are exported to JS as wp.customize.settings.changeset.lockUser.

Frontend Public Preview URLs

A changeset can be previewed on a site by authenticated and unauthenticated users if the customize_changeset_uuid param is present with a valid UUID, as noted above. This has been somewhat of a hidden feature in the Customizer since changesets were introduced in 4.7, but it is something for which the Customize Snapshots feature plugin has provided a UI. This feature of the plugin is also now merged into WordPress 4.9.

A frontend preview URL looks like:

http://example.com/?customize_changeset_uuid=a05c591b-7c19-4706-9360-43bb498435a1

The customize_changeset_uuid will persist as you navigate around the frontend of your site, allowing you see how the customized changes apply in any context. The param will also persist through calls to history.pushState() and history.replaceState().

The link can be obtained via the “Share Preview Link” control in the Publish Settings section accessed by clicking the gear button after having made a change. The preview link is disabled while you have unsaved changes, as changes in autosave revisions are not intended to to be shared:

The preview link will not be available when previewing a theme switch in the Customizer since theme switches require user authentication. In fact, as noted above, the Publish Settings section will be entirely unavailable if you are previewing a theme switch, since drafting and scheduling is not supported for theme switches yet either.

Customization Drafts

In addition to changesets being introduced in 4.7, the ability to create page/posts stubs was also added in that release (see #34923). In the Customizer you could create an auto-draft for a page or post when adding nav menu items or when interacting with the page dropdown control such as adding for the homepage settings. You were only able to provide a title for these stubs, with the intention that you’d then provide content and other properties once you’ve published the changeset, at which point the auto-draft posts/page stubs are also published. With the introduction of changeset drafting and scheduling, however, it became clear that there needed to be a way to edit these stubs before the changeset is published. This is now possible as of #42220.

A page/post stub will still be created with an auto-draft status when first added. However, once the changeset is saved (or updated) as a draft or scheduled, then these stubs will transition to the draft status and will then be shown in the WP admin for editing. These will be marked as “customization drafts” in the post list table and they will be published when its associated changeset is published. A notice will also appear in the publish metabox when editing, with a link back to continue editing its changeset in the Customizer:

A customization draft gets a _customize_changeset_uuid postmeta to link it with its associated changeset. If you change the status from draft then the customization draft will be unlinked from its changeset.

Updating stubs to have a draft status instead of auto-draft also means that we no longer have to hack the post_date to be far in the future to prevent garbage collection. Additionally, now when a changeset is deleted, stubs that have become customization drafts will now get trashed instead of being deleted.

Follow #39752 for eventually adding a full post/page editing flow to the Customizer, something which should rely heavily on the components being developed in Gutenberg.

Conclusion

Key tickets resolved in this release:

  • #39896: Customizer: Allow users to Draft changes before Publishing
  • #28721: Scheduled changes for the customizer
  • #21666: Customizer reset/undo/revert
  • #42220: The draft page cannot see Customizing Homepage Settings makes Add New Page
  • #42024: Customizer: Add “customization locking”

I want to thank @sayedwp in particular for his work on porting features from the Customize Snapshots plugin into 4.9 according to the design iterations from @melchoyce and @joshuawold. Without his help these improvements would not have been possible in this release.

For more details on related Customizer API changes in 4.9, see:

Improvements to the Customize JS API in 4.9

#4-9, #customize, #dev-notes