13

The introduction of the Block Editor killed all plugins which offered publishing conditions, such as minimum word counts, featured image requirements etc.

But the Block Editor did introduce the pre-publish checks:

Pre-publish checks

Beautiful. How can we disable the Publish button until a set amount of conditions have been fulfilled?

Examples of four (very) different conditions:

  1. Minimum word count (example: 500 words)
  2. Min/max tags (example: 3-5 tags)
  3. Min category (that isn't uncategorized)
  4. Featured image is assigned

What we have so far

As expected, the documentation is non-existent. But leads are scattered across the web.

In core/editor, we can use .lockPostSaving() to disabled the Publish button, and unlock it via .unlockPostSaving().

We can add a panel to the pre-publish screen via PluginPrePublishPanel. Example (by MadMaardigan):

var PluginPrePublishPanel = wp.editPost.PluginPrePublishPanel;
var registerPlugin = wp.plugins.registerPlugin;

function Component() {
    // lock post saving
    wp.data.dispatch('core/editor').lockPostSaving()

    // unlock post saving
    // wp.data.dispatch('core/editor').unlockPostSaving()

    return wp.element.createElement(
        PluginPrePublishPanel,
        {   
            className: 'my-plugin-publish-panel',
            title: 'Panel title',
            initialOpen: true,
        },  
        'Panel content'
    );  
}

registerPlugin( 'my-plugin', {
  render: Component,
});

It works:

Custom pre-publish panel

And we have great discussions on GitHub: #7020, #7426, #13413, #15568, #10649...

4
  • It looks like you have the approach nailed down - are you looking for the code that implements this approach?
    – Welcher
    May 30 '19 at 11:30
  • @Welcher Yes, absolutely. With the (four) conditions.
    – Christine Cooper
    May 30 '19 at 11:40
  • Please check this ostraining.com/blog/wordpress/blog-post-checklists. This may help you.
    – Bhupen
    Jun 5 '19 at 5:39
  • @Bhupen Thanks, but all plugins listed in that article doesn't work with Gutenberg, beside perhaps one, which is a paid plugin.
    – Christine Cooper
    Jun 5 '19 at 8:45
14
+200

EDIT Sept 2021:

An updated version of the answer that uses hooks.

This version tracks changes in the editor much better. It also uses import statement instead of importing directly from the wp global. This approach when used with the @wordpress/scripts package will correctly add dependencies for this file when being enqueued. Accessing the wp global will still work but you will have to be sure you're managing your script dependencies manually.

Thanks to everyone in the comments!

import { useState, useEffect } from '@wordpress/element';
import { registerPlugin } from '@wordpress/plugins';
import { PluginPrePublishPanel } from '@wordpress/edit-post';
import { useSelect, useDispatch } from '@wordpress/data';
import { count } from '@wordpress/wordcount';
import { serialize } from '@wordpress/blocks';

const PrePublishCheckList = () => {
    // Manage the messaging in state.
    const [wordCountMessage, setWordCountMessage] = useState('');
    const [catsMessage, setCatsMessage] = useState('');
    const [tagsMessage, setTagsMessage] = useState('');
    const [featuredImageMessage, setFeaturedImageMessage] = useState('');

    // The useSelect hook is better for retrieving data from the store.
    const { blocks, cats, tags, featuredImageID } = useSelect((select) => {
        return {
            blocks: select('core/block-editor').getBlocks(),
            cats: select('core/editor').getEditedPostAttribute('categories'),
            tags: select('core/editor').getEditedPostAttribute('tags'),
            featuredImageID:
                select('core/editor').getEditedPostAttribute('featured_media'),
        };
    });

    // The useDispatch hook is better for dispatching actions.
    const { lockPostSaving, unlockPostSaving } = useDispatch('core/editor');

    // Put all the logic in the useEffect hook.
    useEffect(() => {
        let lockPost = false;
        // Get the WordCount
        const wordCount = count(serialize(blocks), 'words');
        if (wordCount < 500) {
            lockPost = true;
            setWordCountMessage(`${wordCount} - Minimum of 500 required.`);
        } else {
            setWordCountMessage(`${wordCount}`);
        }

        // Get the category count
        if (!cats.length || (cats.length === 1 && cats[0] === 1)) {
            lockPost = true;
            setCatsMessage('Missing');
            // Check that the cat is not Uncategorized - this assumes that the ID of Uncategorized is 1, which it would be for most installs.
            if (cats.length === 1 && cats[0] === 1) {
                setCatsMessage('Cannot use Uncategorized');
            }
        } else {
            setCatsMessage('Set');
        }

        // Get the tags
        if (tags.length < 3 || tags.length > 5) {
            lockPost = true;
            setTagsMessage('Required 3 - 5 tags');
        } else {
            setTagsMessage('Set');
        }
        // Get the featured image
        if (featuredImageID === 0) {
            lockPost = true;
            setFeaturedImageMessage('Not Set');
        } else {
            setFeaturedImageMessage(' Set');
        }

        if (lockPost === true) {
            lockPostSaving();
        } else {
            unlockPostSaving();
        }
    }, [blocks, cats, tags, featuredImageID]);

    return (
        <PluginPrePublishPanel title={'Publish Checklist'}>
            <p>
                <b>Word Count:</b> {wordCountMessage}
            </p>
            <p>
                <b>Categories:</b> {catsMessage}
            </p>
            <p>
                <b>Tags:</b> {tagsMessage}
            </p>
            <p>
                <b>Featured Image:</b> {featuredImageMessage}
            </p>
        </PluginPrePublishPanel>
    );
};

registerPlugin('pre-publish-checklist', { render: PrePublishCheckList });








const { registerPlugin } = wp.plugins;
const { PluginPrePublishPanel } = wp.editPost;
const { select, dispatch } = wp.data;
const { count } = wp.wordcount;
const { serialize } = wp.blocks;
const { PanelBody } = wp.components;

const PrePublishCheckList = () => {
    let lockPost = false;

    // Get the WordCount
    const blocks = select( 'core/block-editor' ).getBlocks();
    const wordCount = count( serialize( blocks ), 'words' );
    let wordCountMessage = `${wordCount}`;
    if ( wordCount < 500 ) {
        lockPost = true;
        wordCountMessage += ` - Minimum of 500 required.`;
    }

    // Get the cats
    const cats = select( 'core/editor' ).getEditedPostAttribute( 'categories' );
    let catsMessage = 'Set';
    if ( ! cats.length ) {
        lockPost = true;
        catsMessage = 'Missing';
    } else {
        // Check that the cat is not uncategorized - this assumes that the ID of Uncategorized is 1, which it would be for most installs.
        if ( cats.length === 1 && cats[0] === 1 ) {
            lockPost = true;
            catsMessage = 'Cannot use Uncategorized';
        }
    }

    // Get the tags
    const tags = select( 'core/editor' ).getEditedPostAttribute( 'tags' );
    let tagsMessage = 'Set';
    if ( tags.length < 3 || tags.length > 5 ) {
        lockPost = true;
        tagsMessage = 'Required 3 - 5 tags';
    }

    // Get the featured image
    const featuredImageID = select( 'core/editor' ).getEditedPostAttribute( 'featured_media' );
    let featuredImage = 'Set';

    if ( featuredImageID === 0 ) {
        lockPost = true;
        featuredImage = 'Not Set';
    }

    // Do we need to lock the post?
    if ( lockPost === true ) {
        dispatch( 'core/editor' ).lockPostSaving();
    } else {
        dispatch( 'core/editor' ).unlockPostSaving();
    }
    return (
        <PluginPrePublishPanel title={ 'Publish Checklist' }>
            <p><b>Word Count:</b> { wordCountMessage }</p>
            <p><b>Categories:</b> { catsMessage }</p>
            <p><b>Tags:</b> { tagsMessage }</p>
            <p><b>Featured Image:</b> { featuredImage }</p>
        </PluginPrePublishPanel>
    )
};

registerPlugin( 'pre-publish-checklist', { render: PrePublishCheckList } );

Display:

enter image description here

The solution above addresses the requirements listed in the question. One thing that can be expanded on is the category checking, I am making some assumptions about the category ID.

I have kept all of the checks in the same component for the sake of brevity and readability here. I would recommend moving each portion into a separate component and potentially making them Higher Order Components ( i.e withWordCount ).

I have inline comments that explain what is being done but am happy to explain further if there are any questions.

EDIT: Here's how I'm enqueuing the script

function enqueue_block_editor_assets() {
    wp_enqueue_script(
        'my-custom-script', // Handle.
        plugin_dir_url( __FILE__ ) . '/build/index.js',
        array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor', 'wp-edit-post', 'word-count' ) // Dependencies, defined above.
    );
}
add_action( 'enqueue_block_editor_assets', 'enqueue_block_editor_assets' );

Adding some more details about the build process. I am using @wordpress/scripts and running the following scripts:

"scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },

Edit 2:

You can get the attachment data via:

wp.data.select('core').getMedia( ID )
17
  • There was a minor issue with the operator for the tags (edited your answer), but works like a charm. I now know how little I know about developing with Gutenberg. Thank you so much for this answer! PS: The reason why it was not displaying originally for me was because users on contributor level can't see the pre-publish panel properly, see this bug report.
    – Christine Cooper
    Jun 17 '19 at 13:56
  • Update: my bug report is successful and they are fixing it!
    – Christine Cooper
    Jul 12 '19 at 11:37
  • Locking the post directly in this component is an anti pattern. The PluginPrePublishPanel is exactly what it says it is. It's a panel to render custom things in the sidebar that is shown before publishing. It's not supposed to be the place where you lock/unlock the post saving. Renders should never have this kind of direct effect and doing this will lead to bugs.
    – inwerpsel
    Mar 19 at 15:54
  • As this is rendered in the last step before publishing is done, I'd argue this is the best place to do it. I'm not sure how this is an anti-pattern either, as the only way to introduce functionality to the BlockEditor is done via SlotFill - so by your logic, anything rendered anywhere cannot do anything but render static content.
    – Welcher
    Mar 19 at 16:23
  • It's an anti-pattern because it's not the moment you want this post locking to happen. A component function is only rerun when its props change (or when it uses custom hooks or state). There may be forced re-renders from time to time, but you can't rely on those keeping on happening. I actually tried your solution on WP 5.6, and the post save button stays disabled after making my post valid again. So I can't trigger the panel to render again. I need to close and re-open the whole sidebar using the cog icon to enable the button, because only then it reruns the validation logic and post locking.
    – inwerpsel
    Mar 20 at 13:11
4

Update 29.02.2020

You have to replace select( 'core/editor' ).getBlocks() with select( 'core/block-editor' ).getBlocks() in order for this to work

This worked for me:

path\to\theme\pre-publish-checklist\src\index.js

const { registerPlugin } = wp.plugins;
const { PluginPrePublishPanel } = wp.editPost;
const { select, dispatch } = wp.data;
const { count } = wp.wordcount;
const { serialize } = wp.blocks;
const { PanelBody } = wp.components;

const PrePublishCheckList = () => {
    let lockPost = false;

    // Get the WordCount
    const blocks = select( 'core/block-editor' ).getBlocks();
    const wordCount = count( serialize( blocks ), 'words' );
    let wordCountMessage = `${wordCount}`;
    if ( wordCount < 500 ) {
        lockPost = true;
        wordCountMessage += ` - Minimum of 500 required.`;
    }

    // Get the cats
    const cats = select( 'core/editor' ).getEditedPostAttribute( 'categories' );
    let catsMessage = 'Set';
    if ( ! cats.length ) {
        lockPost = true;
        catsMessage = 'Missing';
    } else {
        // Check that the cat is not uncategorized - this assumes that the ID of Uncategorized is 1, which it would be for most installs.
        if ( cats.length === 1 && cats[0] === 1 ) {
            lockPost = true;
            catsMessage = 'Cannot use Uncategorized';
        }
    }

    // Get the tags
    const tags = select( 'core/editor' ).getEditedPostAttribute( 'tags' );
    let tagsMessage = 'Set';
    if ( tags.length < 3 || tags.length > 5 ) {
        lockPost = true;
        tagsMessage = 'Required 3 - 5 tags';
    }

    // Get the featured image
    const featuredImageID = select( 'core/editor' ).getEditedPostAttribute( 'featured_media' );
    let featuredImage = 'Set';

    if ( featuredImageID === 0 ) {
        lockPost = true;
        featuredImage = 'Not Set';
    }

    // Do we need to lock the post?
    if ( lockPost === true ) {
        dispatch( 'core/editor' ).lockPostSaving();
    } else {
        dispatch( 'core/editor' ).unlockPostSaving();
    }
    return (
        <PluginPrePublishPanel title={ 'Publish Checklist' }>
            <p><b>Word Count:</b> { wordCountMessage }</p>
            <p><b>Categories:</b> { catsMessage }</p>
            <p><b>Tags:</b> { tagsMessage }</p>
            <p><b>Featured Image:</b> { featuredImage }</p>
        </PluginPrePublishPanel>
    )
};

registerPlugin( 'pre-publish-checklist', { render: PrePublishCheckList } );

Full steps to create the panel with @wordpress/scripts

  1. Create a folder pre-publish-checklist in your theme
  2. Create inside the folder package.json file with
{
 "scripts": {
   "build": "wp-scripts build",
   "check-engines": "wp-scripts check-engines",
   "check-licenses": "wp-scripts check-licenses",
   "format:js": "wp-scripts format-js",
   "lint:css": "wp-scripts lint-style",
   "lint:js": "wp-scripts lint-js",
   "lint:md:docs": "wp-scripts lint-md-docs",
   "lint:md:js": "wp-scripts lint-md-js",
   "lint:pkg-json": "wp-scripts lint-pkg-json",
   "packages-update": "wp-scripts packages-update",
   "start": "wp-scripts start",
   "test:e2e": "wp-scripts test-e2e",
   "test:unit": "wp-scripts test-unit-js"
 },
 "dependencies": {
   "@wordpress/scripts": "^7.1.2"
 }
}
  1. Create a file in the folder with the path 'src/index.js' and place the code in the file
  2. yarn
  3. yarn build
  4. Add this code to functions.php to enqueue the file
function enqueue_block_editor_assets() {
    wp_enqueue_script(
        'pre-publish-checklist', // Handle.
        get_template_directory_uri(). '/pre-publish-checklist/build/index.js',
        array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor', 'wp-edit-post', 'word-count' ) // Dependencies, defined above.
    );
}
add_action( 'enqueue_block_editor_assets', 'enqueue_block_editor_assets' );
3
  • You only replaced one instance of core/editor with core/block-editor in your code. Was this intentional? Aug 29 at 20:56
  • Gutenberg changed over time. @HashimAziz do I need to update the code?
    – BenB
    Aug 30 at 16:15
  • No, I finally managed to get this working after setting up a build process, the step-by-step instructions helped. Sep 1 at 21:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.