Contents
Introduction
This tutorial shows a complete, production-ready approach to require a featured image (post thumbnail) before allowing a post to be published in WordPress. The solution contains two complementary layers:
- Client-side (JavaScript) — user-friendly UI prevention in both the Classic Editor and the Block Editor (Gutenberg), so users get immediate feedback and the Publish button is disabled or blocked.
- Server-side (PHP) — enforced validation on post save/insert so posts cannot be published by bypassing the editor (REST API, XML-RPC, WP-CLI, direct DB, etc.). This ensures correctness even when JavaScript is disabled or other clients publish posts.
Both layers are important: JS improves UX and reduces accidental publishes, PHP enforces the rule. The examples below include code you can drop into a plugin file (or your themes functions.php, though a plugin is preferred).
How it works — high level
- Gutenberg: we register a small block-editor script that alters the Publish button behavior (and shows a helpful message) by checking the current posts featured_media attribute via the JS data store.
- Classic editor: we attach a small admin JS to check the hidden _thumbnail_id input and block the #post form submission or prevent clicking the Publish button.
- Server-side: on any attempt to insert/update a post as publish, we check whether a featured image is present via has_post_thumbnail() or the form payload. If missing, we switch the post_status to draft and add an admin notice (or return an appropriate REST response).
Requirements and compatibility notes
- WordPress 5.x for Gutenberg compatibility. The server-side enforcement works on older versions also, but the Gutenberg JS requires the block editor script handles.
- Test with plugins that modify post saving flow, REST or custom publishing workflows. Server-side check will apply to all publishing routes that use wp_insert_post or the REST post endpoints.
- Users with direct DB access or custom code may still circumvent the rule — but standard WP publishing routes will be covered.
Step 1 — Server-side: enforce featured image requirement (PHP)
This PHP code forcibly prevents a post from being published if it has no featured image. It is defensive — if the post is being saved/published and no featured image is present in the payload and the post object does not already have one, the code converts the post_status to draft and adds an admin notice to inform the user why their post remains in draft.
0 ) { payload_has_thumbnail = true } elseif ( isset( _POST[featured_media] ) intval( _POST[featured_media] ) > 0 ) { payload_has_thumbnail = true } elseif ( isset( postarr[meta_input][_thumbnail_id] ) intval( postarr[meta_input][_thumbnail_id] ) > 0 ) { payload_has_thumbnail = true } elseif ( isset( postarr[_thumbnail_id] ) intval( postarr[_thumbnail_id] ) > 0 ) { payload_has_thumbnail = true } // If we do not have a thumbnail anywhere, force status back to draft and signal an admin notice. if ( ! has_thumbnail ! payload_has_thumbnail ) { data[post_status] = draft // Add query var to redirect location so we can show an admin notice after redirect. add_filter( redirect_post_location, function ( location ) { return add_query_arg( rfi_featured_image_required, 1, location ) } ) } return data } add_filter( wp_insert_post_data, rfi_prevent_publish_without_featured_image, 10, 2 ) / Admin notice when a post was prevented from publishing due to missing featured image. / function rfi_admin_notice_missing_featured_image() { if ( ! isset( _GET[rfi_featured_image_required] ) ) { return } // Capability check: only show to users who can edit posts. if ( ! current_user_can( edit_posts ) ) { return } // Output a notice (escape the string) echo ltdiv class=notice notice-error is-dismissiblegt echo ltpgtltstronggtFeatured image required:lt/stronggt This post was not published because it does not have a featured image. Please set a featured image and publish again.lt/pgt echo lt/divgt } add_action( admin_notices, rfi_admin_notice_missing_featured_image )
Notes about the PHP approach
- This filter runs early and applies to both admin and REST insert/update (the REST endpoint eventually uses wp_insert_post internally). That gives broad coverage.
- We attempt to check both existing postmeta via has_post_thumbnail and common POST parameters such as _thumbnail_id and featured_media. This keeps the check robust for Classic and Gutenberg submissions.
- We convert to draft instead of producing an error response because in the admin context the user is redirected back to the edit screen. For API contexts you can expand this to return a WP_Error for REST responses (explain below).
- You can make the list of post types, user roles, or capability checks filterable for customization.
Step 2 — Client-side: Gutenberg (block editor) UI
Gutenberg uses modern JS modules and WP-provided globals. The simplest approach is to register a small block-editor script that wraps the Publish button (or disables the button) and shows a notice in the editor. Below is a self-contained JS file (enqueue it with block editor dependencies).
( function ( wp ) { const { addFilter } = wp.hooks const { createElement } = wp.element const { select } = wp.data const { __ } = wp.i18n / Replace the PostPublishButton with a wrapper that disables Publish when no featured image. This uses the registered Filter editor.PostPublishButton to replace or modify the button. The original button component is passed in (Original). We return a new component that conditionally disables the original or shows a tooltip. / function disablePublishButton( Original ) { return function ( props ) { try { // Get the featured_media id. 0 means none. const featuredMedia = select( core/editor ).getEditedPostAttribute( featured_media ) const isMissing = ! featuredMedia parseInt( featuredMedia, 10 ) === 0 // If missing, render a disabled button with explanatory text/tool-tip if ( isMissing ) { return createElement( button, { className: components-button editor-post-publish-panel__toggle is-primary, disabled: true, // Keep the title accessible — browsers show on hover title: __( A featured image is required before publishing. ), aria-disabled: true }, __( Publish ) ) } // Otherwise render the original button return createElement( Original, props ) } catch ( e ) { // Fail safe: if anything goes wrong, render the original return createElement( Original, props ) } } } // Add the filter that replaces the PostPublishButton addFilter( editor.PostPublishButton, rfi/disable-publish-button, disablePublishButton ) } )( window.wp )
How to enqueue the Gutenberg script
Use the following PHP snippet to enqueue the JS into the block editor. This ensures all dependencies are loaded (wp-hooks, wp-element, wp-data, wp-i18n).
array( wp-hooks, wp-element, wp-data, wp-i18n ), version => 1.0.0, ) // If you compile/bundle your script, provide that file instead. wp_register_script( rfi-block-editor, plugins_url( js/rfi-block-editor.js, __FILE__ ), asset_file[dependencies], asset_file[version], true ) wp_enqueue_script( rfi-block-editor ) } add_action( enqueue_block_editor_assets, rfi_enqueue_block_editor_script )
Notes about the Gutenberg script
- The filter name editor.PostPublishButton is provided by the Gutenberg editor. The wrapper replaced the Publish button rendering with a disabled button when no featured image is set.
- This is non-destructive: users with JavaScript disabled will not see the disabled button, which is why the PHP server-side enforcement is required.
- Rendering a custom disabled button simplifies behavior vs attempting to intercept direct publish actions. It prevents race conditions where people click quickly before state updates.
Step 3 — Client-side: Classic editor (TinyMCE edit form)
For the Classic editor, the featured image is stored in the hidden field _thumbnail_id. The following admin script prevents the publish button from submitting if no featured image is set and shows a native alert or a nicer message.
( function ( window, document, ) { ( document ).ready( function () { var publishBtn = ( #publish ) var thumbnailField = ( #_thumbnail_id ) function hasThumbnail() { return thumbnailField.length parseInt( thumbnailField.val(), 10 ) > 0 } // When publish is clicked, check thumbnail block if missing. ( #post ).on( submit, function ( e ) { // If post new or update attempted to publish (we can check hidden inputs) // But simplest: check if publish button triggered, and prevent if missing if ( publishBtn.length hasThumbnail() === false ) { // Optionally allow other submit buttons like Save Draft to continue check which was clicked // Prevent publish only if the publish button was clicked var clicked = document.activeElement if ( clicked clicked.id === publish ) { e.preventDefault() alert( Please set a featured image before publishing. ) // You may also focus the Featured Image metabox to help user ( #postimagediv )[0].scrollIntoView( { behavior: smooth } ) return false } } } ) } ) } )( window, document, jQuery )
Enqueue classic editor script
Complete plugin example (putting it all together)
Below is a comprehensive single-file plugin example that:
- Enqueues the Gutenberg and Classic admin scripts
- Enforces the featured image on publish server-side
- Shows admin notices when publish is prevented
0 ) { payload_has_thumbnail = true } elseif ( isset( _POST[featured_media] ) intval( _POST[featured_media] ) > 0 ) { payload_has_thumbnail = true } elseif ( isset( postarr[meta_input][_thumbnail_id] ) intval( postarr[meta_input][_thumbnail_id] ) > 0 ) { payload_has_thumbnail = true } elseif ( isset( postarr[_thumbnail_id] ) intval( postarr[_thumbnail_id] ) > 0 ) { payload_has_thumbnail = true } if ( ! has_thumbnail ! payload_has_thumbnail ) { data[post_status] = draft add_filter( redirect_post_location, function ( location ) { return add_query_arg( rfi_featured_image_required, 1, location ) } ) } return data } add_filter( wp_insert_post_data, rfi_prevent_publish_without_featured_image, 10, 2 ) function rfi_admin_notice_missing_featured_image() { if ( ! isset( _GET[rfi_featured_image_required] ) ) { return } if ( ! current_user_can( edit_posts ) ) { return } echo ltdiv class=notice notice-error is-dismissiblegt echo ltpgtltstronggtFeatured image required:lt/stronggt This post was not published because it does not have a featured image. Please set a featured image and publish again.lt/pgt echo lt/divgt } add_action( admin_notices, rfi_admin_notice_missing_featured_image ) / Enqueue Gutenberg script / function rfi_enqueue_block_editor_script() { wp_register_script( rfi-block-editor, plugins_url( js/rfi-block-editor.js, __FILE__ ), array( wp-hooks, wp-element, wp-data, wp-i18n ), 1.0.0, true ) wp_enqueue_script( rfi-block-editor ) } add_action( enqueue_block_editor_assets, rfi_enqueue_block_editor_script ) / Enqueue Classic editor script / function rfi_enqueue_classic_admin_script( hook ) { if ( ! in_array( hook, array( post.php, post-new.php ), true ) ) { return } wp_enqueue_script( rfi-classic-editor, plugins_url( js/rfi-classic-editor.js, __FILE__ ), array( jquery ), 1.0.0, true ) } add_action( admin_enqueue_scripts, rfi_enqueue_classic_admin_script )
Advanced topics and edge cases
REST API and returning structured errors
When posts are created via the REST API, the wp_insert_post_data filter will convert status to draft which results in a successful REST response but with post_status = draft. If you want the REST request to instead receive an error code (HTTP 400/422), you can hook REST-specific endpoints and return a WP_Error when the featured_media is missing.
422 ) ) } } return prepared_post }, 10, 2 )
Bulk edits, Quick Edit, and programmatic publishing
- Bulk edit or quick edit might publish multiple posts. The server-side filter will run for each post, preventing publish when no thumbnail exists.
- If your workflow requires programmatic publishing via code, make sure that code sets the _thumbnail_id or featured_media before calling wp_update_post. Alternatively, provide a filter to bypass the requirement for trusted code.
Allowing bypass for admins or certain roles
If you want to allow administrators to publish without a featured image while preventing other roles, add a capability check in the server-side filter:
Require image dimensions, size or ratio
If your requirement is stricter than any featured image, you can check the attachment metadata and verify width/height or mime type and reject publishing if image constraints are not satisfied. Use wp_get_attachment_metadata() and/or wp_get_attachment_image_src() to inspect dimensions.
Troubleshooting
- If your Gutenberg script appears to do nothing, confirm its enqueued by checking the browser dev tools network tab and verifying that wp-hooks, wp-element, wp-data and wp-i18n are available on the page.
- If posts are still being published without a featured image, verify that your PHP filter runs (add temporary logging) and confirm that the publishing flow uses wp_insert_post (some custom publishing code might bypass WP functions).
- Clear caches and check that other plugins are not modifying post_status after your filter runs. Adjust priority if necessary.
Customization ideas
- Make the list of post types required configurable via a settings page or filter rfi_allowed_post_types.
- Add an option to require a minimum image size or aspect ratio.
- Add a custom pre-publish panel in Gutenberg that explains the rule and links to the featured image panel. Use PluginDocumentSettingPanel if you want more UI inside the editor.
- Create a REST middleware to return structured JSON errors for API clients when the requirement is unmet.
Security and performance notes
- Sanitize and validate any input you use from _POST.
- The wp_insert_post_data filter runs on every post submission keep logic efficient and avoid heavy operations inside it (e.g., dont perform expensive remote calls).
- Use nonces and capability checks where appropriate for custom AJAX endpoints or admin actions.
Summary (what you should implement)
- Implement the PHP server-side check (mandatory) using wp_insert_post_data (example above).
- Enqueue a Gutenberg script that disables the Publish button and shows an explanation (improves UX).
- Enqueue a Classic editor admin script to block #post submission if no featured image (improves UX for non-Gutenberg users).
- Test: try publishing via the admin UI, REST API, and programmatic flows. Confirm the server-side layer prevents publishing when no featured image is present.
The code samples above are full, practical implementations you can place into a plugin file and two small JS files (one for the block editor, one for Classic). Adjust post types, messages, and edge-case behavior as your project requires.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |