WordPress.org

WordPress Developer Blog

How to add custom entries to the editor Preview dropdown

How to add custom entries to the editor Preview dropdown

Since WordPress 6.7, plugin developers can extend the Preview dropdown in the editor with custom menu items. This is possible through the PluginPreviewMenuItem component from the @wordpress/editor package. This article walks through building a small plugin that adds a “Social Card Preview” option — showing how a post will look when shared on X.

What is PluginPreviewMenuItem?

The Preview dropdown sits in the editor’s top bar and lets users preview their content in different ways. Before WordPress 6.7, only Core add items there. With PluginPreviewMenuItem component, plugins can register their own entries in that dropdown. It uses the same Slot/Fill pattern that powers other extension points like PluginMoreMenuItem or PluginSidebar.

The component accepts an onClick handler for button behavior or an href for link behavior. You can also pass an optional icon prop. The text you place between the opening and closing <PluginPreviewMenuItem> tags becomes the label shown in the menu.

Social Card Preview example

This plugin adds a Social Card Preview item to the Preview dropdown. When clicked, it opens a modal that shows a mock preview of how the current post would appear as an X card. The modal reads the post title, excerpt, and featured image directly from the editor store, so it always reflects the latest unsaved edits. You can take a peek at the example plugin on GitHub.

Plugin setup

Following along, you start with the file structure. The plugin needs a main PHP file for WordPress to recognize it, a package.json for the build tooling, and the source files in a src/ folder.

custom-preview/
├── custom-preview.php
├── package.json
└── src/
    ├── index.js
    ├── social-card-preview.js
    └── style.css

The PHP bootstrap

The main plugin file, custom-preview.php, registers the script and stylesheet on the enqueue_block_editor_assets hook. The code uses the asset file that @wordpress/scripts generates during the build — it contains the dependency list and a version hash so WordPress can handle caching properly. The below code goes after the PHP plugin header.

function social_card_preview_enqueue_editor_assets() {
	$asset_file = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

	if ( ! file_exists( $asset_file ) ) {
		return;
	}

	$asset = include $asset_file;

	wp_enqueue_script(
		'social-card-preview-editor',
		plugin_dir_url( __FILE__ ) . 'build/index.js',
		$asset['dependencies'],
		$asset['version'],
		true
	);

	wp_enqueue_style(
		'social-card-preview-editor',
		plugin_dir_url( __FILE__ ) . 'build/style-index.css',
		array(),
		$asset['version']
	);
}
add_action( 'enqueue_block_editor_assets', 'social_card_preview_enqueue_editor_assets' );

This is a common pattern in WordPress block editor plugin development. The enqueue_block_editor_assets hook makes sure the script and styles only load inside the editor, not on the frontend.

Build tooling

For the JavaScript build, @wordpress/scripts handles the tooling. The package.json is minimal:

{
	"name": "social-card-preview",
	"version": "1.0.0",
	"scripts": {
		"build": "wp-scripts build",
		"start": "wp-scripts start"
	},
	"devDependencies": {
		"@wordpress/scripts": "^30.0.0"
	}
}

Run npm install to get the dependencies, then npm run build to compile. During development, npm run start watches for changes and rebuilds automatically.

Registering the preview menu item

The entry point src/index.js is where the plugin is registered and the menu item is added to the Preview dropdown. The idea behind it: render a PluginPreviewMenuItem that, when clicked, opens a modal.

import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { PluginPreviewMenuItem } from '@wordpress/editor';
import { useState } from '@wordpress/element';

import SocialCardPreview from './social-card-preview';
import './style.css';

const SocialCardPreviewMenuItem = () => {
	const [ isOpen, setIsOpen ] = useState( false );

	return (
		<>
			<PluginPreviewMenuItem
				onClick={ () => setIsOpen( true ) }
			>
				{ __( 'Social Card Preview', 'social-card-preview' ) }
			</PluginPreviewMenuItem>
			{ isOpen && (
				<SocialCardPreview
					onClose={ () => setIsOpen( false ) }
				/>
			) }
		</>
	);
};

registerPlugin( 'social-card-preview', {
	render: SocialCardPreviewMenuItem,
} );

A few things to note here:

  • The code uses registerPlugin from @wordpress/plugins to register the component.
  • The PluginPreviewMenuItem works like any other menu item — give it an onClick and some children for the label.
  • Modal visibility is managed with a basic useState toggle.

Building the social card modal

The SocialCardPreview component reads data from the editor store and renders a mock X card inside a Modal.

import { __ } from '@wordpress/i18n';
import { Modal } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { store as coreStore } from '@wordpress/core-data';

const SocialCardPreview = ( { onClose } ) => {
	const { title, excerpt, imageUrl, siteUrl } = useSelect( ( select ) => {
		const { getEditedPostAttribute } = select( editorStore );
		const featuredMediaId = getEditedPostAttribute( 'featured_media' );

		let featuredImageUrl = '';
		if ( featuredMediaId ) {
			const media = select( coreStore ).getMedia( featuredMediaId );
			featuredImageUrl =
				media?.media_details?.sizes?.large?.source_url ||
				media?.source_url ||
				'';
		}

		return {
			title: getEditedPostAttribute( 'title' ) || '',
			excerpt: getEditedPostAttribute( 'excerpt' ) || '',
			imageUrl: featuredImageUrl,
			siteUrl: select( coreStore ).getSite()?.url || '',
					};
	}, [] );

	const domain = siteUrl ? new URL( siteUrl ).hostname : '';
	const truncatedExcerpt =
		excerpt.length > 200
			? excerpt.substring( 0, 200 ) + '…'
			: excerpt;

	return (
		<Modal title={ __( 'X Preview', 'social-card-preview' ) } 
                       onRequestClose={ onClose } 
                       size="medium">
			{ /* Card preview markup see below */ }
		</Modal>
	);
};

Here is a breakdown. The useSelect hook pulls the required data from two stores. From the editor store, it retrieves the post title, excerpt, and featured media ID. From the core-data store, it resolves the media ID to an actual image URL and fetches the site URL to display the domain name on the card.

For the featured image, the code tries the large size first, falling back to the full source_url. The excerpt gets truncated to 200 characters because that is roughly what X shows.

Because useSelect is reactive, the preview updates in real time as you edit the post. Change the title, and the card updates immediately — no need to save first.

The Card Preview markup

The card markup mirrors how X renders link previews. The featured image sits at the top, followed by a content area with the domain, title, and truncated excerpt. The image only renders when a featured image exists, thanks to the conditional imageUrl && check. 

Each piece of text uses a <span> rather than a heading or paragraph because this is a visual mock — not semantic document content. The class names like social-card-preview__title follow the BEM (Block Element Modifier) naming convention — social-card-preview is the block, and __title, __domain__description are elements within it. This keeps the styles scoped and avoids collisions with editor styles.

The below text goes between the <Modal></Modal> tags in above code example. The complete social-card-preview.js is also on GitHub:

<div className="social-card-preview">
    <div className="social-card-preview__card social-card-preview__card--twitter">
	{ imageUrl && (
		<img className="social-card-preview__image"
			src={ imageUrl }
			alt=""
		/>
	) }
      <div className="social-card-preview__content">
	  <span className="social-card-preview__domain">
		{ domain }
	  </span>
          <span className="social-card-preview__title">
	       { title }
         </span>
         <span className="social-card-preview__description">
	         { truncatedExcerpt }
         </span>
     </div>
  </div>
</div>

Styling the card

The CSS approximates how X renders link cards and follows the naming decision from above.

.social-card-preview__card {
	border: 1px solid #dadce0;
	border-radius: 16px;
	overflow: hidden;
}

.social-card-preview__title {
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

The card uses a 16px border radius to match X’s rounded style. The -webkit-line-clamp rules handle text truncation for the title and description.

Testing with WordPress Playground

You do not need a full WordPress installation to test this. The Playground CLI gives you a disposable WordPress instance in seconds. Make sure you have Node.js 20.18 or later, then from the plugin directory run:

npm install
npm run build
npx @wp-playground/cli@latest server --auto-mount --login

Playground detects the plugin, automatically mounts it and logs in the admin user. Open http://127.0.0.1:9400 in your browser, create or edit a post, and you should see “Social Card Preview” in the Preview dropdown.

Going further

The social card example is one way to use PluginPreviewMenuItem. The slot opens up a range of preview experiences that were previously only available to WordPress core. Here are some other use cases to consider:

  • Accessibility checker — open a modal that scans the post content for common accessibility issues, such as missing alt text or low-contrast headings, before the author hits publish.
  • Reading level preview — show a readability score and a simplified version of the content, useful for publishers targeting a broad or non-specialist audience.
  • External preview service — use the href prop instead of onClick to send the author directly to a third-party staging or QA environment with the post URL pre-filled.
  • Email newsletter preview — for plugins that sync posts to an email list, show a rendered preview of how the content will look in an email client, including any template wrapping applied by the plugin.

Any time your plugin needs to give authors a way to see or check their content before publishing, PluginPreviewMenuItem is the right place to add it.

Props to @greenshadyand @juanmaguitar for reviews.

Leave a Reply

Your email address will not be published. Required fields are marked *