Archives: Snippets | Page 2

Add a Custom Post Meta Box in WordPress

Add a custom meta box to the post editor for storing additional data without a plugin like ACF. For more on registering custom fields.

add_action( 'add_meta_boxes', function () {
    add_meta_box(
        'savvy_post_subtitle',
        'Post Subtitle',
        'savvy_subtitle_metabox_html',
        'post',
        'normal',
        'high'
    );
} );

function savvy_subtitle_metabox_html( $post ) {
    $value = get_post_meta( $post->ID, '_savvy_subtitle', true );
    wp_nonce_field( 'savvy_subtitle_nonce', 'savvy_subtitle_nonce' );
    printf(
        '<input type="text" name="savvy_subtitle" value="%s" style="width:100%%;" />',
        esc_attr( $value )
    );
}

add_action( 'save_post', function ( $post_id ) {
    if ( ! isset( $_POST['savvy_subtitle_nonce'] ) ||
         ! wp_verify_nonce( $_POST['savvy_subtitle_nonce'], 'savvy_subtitle_nonce' ) ) {
        return;
    }

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    if ( isset( $_POST['savvy_subtitle'] ) ) {
        update_post_meta(
            $post_id,
            '_savvy_subtitle',
            sanitize_text_field( $_POST['savvy_subtitle'] )
        );
    }
} );

Add to functions.php. Change the meta key, label, and input type to match your needs.

Enqueue Scripts Only for Specific Admin Pages

To enqueue a script on a specific admin page in WordPress, hook into admin_enqueue_scripts and check the $hook_suffix parameter before calling wp_enqueue_script(). Without that check, your files load on every screen in wp-admin.

Loading only where you need it avoids clashes with other plugins’ screens and keeps the dashboard light. For the basics of registering assets, see how to properly enqueue CSS and JavaScript in WordPress.

Load Scripts Only on the Post Edit Screen

Add this to your child theme’s functions.php or to a plugin. It loads a stylesheet and a script only when adding or editing a post.

add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) {
    // Only load on the post edit screens.
    if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) {
        return;
    }

    wp_enqueue_style(
        'savvy-admin-custom',
        get_stylesheet_directory_uri() . '/css/admin-custom.css',
        array(),
        '1.0.0'
    );

    wp_enqueue_script(
        'savvy-admin-custom',
        get_stylesheet_directory_uri() . '/js/admin-custom.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
} );

The code uses get_stylesheet_directory_uri(), which points to the child theme when one is active. In a plugin, use plugins_url( 'js/admin-custom.js', __FILE__ ) instead.

How the $hook_suffix Parameter Works

WordPress fires admin_enqueue_scripts in the admin header and passes one argument, $hook_suffix, which identifies the current admin page. Return early when it doesn’t match, and nothing gets enqueued.

For core screens the value is the PHP file name:

  • index.php for the Dashboard
  • edit.php for post lists, post.php and post-new.php for the editor
  • upload.php for the Media Library
  • options-general.php for Settings > General

Pages added with add_menu_page() or add_submenu_page() get a generated value built from the parent menu and the page slug, for example toplevel_page_my-plugin, settings_page_my-plugin or tools_page_my-plugin.

How to Find the Hook Suffix for Any Admin Page

You don’t need to guess the value. Use one of these methods.

  1. Read the body class. WordPress adds a sanitized copy of the hook suffix to the admin <body> class. Inspect the page and look for classes like post-php or settings_page_my-plugin.
  2. Log it. Temporarily add error_log( $hook_suffix ); inside the callback and check debug.log.
  3. Store the return value. The add_*_page() functions return the hook suffix of the page they register.

The third method is the most reliable for your own pages. For submenus under a custom top-level menu, the prefix comes from the sanitized menu title, so it changes if the title is translated.

add_action( 'admin_menu', function () {
    $hook_suffix = add_options_page(
        'Savvy Settings',
        'Savvy Settings',
        'manage_options',
        'savvy-settings',
        'savvy_render_settings_page'
    );

    add_action( 'admin_enqueue_scripts', function ( $current ) use ( $hook_suffix ) {
        if ( $current !== $hook_suffix ) {
            return;
        }
        wp_enqueue_script( 'savvy-settings', plugins_url( 'js/settings.js', __FILE__ ), array(), '1.0.0', true );
    } );
} );

Limit It to One Post Type

post.php matches the editor for every post type, including pages and products. Add a check on the current screen to narrow it down:

add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) {
    $screen = get_current_screen();

    if ( ! in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true )
        || ! $screen || 'product' !== $screen->post_type ) {
        return;
    }

    // Enqueue your product-only files here.
} );

If your script extends the block editor itself (sidebar panels, block styles, editor plugins), use the enqueue_block_editor_assets hook instead. For load order and defer options, see optimizing script loading with in_footer and strategy.

FAQs

Common questions about loading assets on specific admin pages:

What is the $hook parameter in admin_enqueue_scripts?
It's the $hook_suffix of the current admin page, such as edit.php or toplevel_page_my-plugin. The name of the variable in your callback doesn't matter, $hook and $hook_suffix receive the same value.
Should I compare $hook_suffix or get_current_screen()->id?
Either works, but they differ. $hook_suffix for the Posts list is edit.php, while the screen ID is edit-post. Use the screen object when you also need the post type or taxonomy.
What is the 'common' handle in wp-admin?
Core registers both a script and a stylesheet named common (wp-admin/js/common.js and wp-admin/css/common.css). You can list it as a dependency so your admin CSS or JS loads after it.
Why doesn't my script load on my plugin's settings page?
The hook suffix is probably not what you expect. Check the <body> class or log $hook_suffix on that page, then compare against the exact value.

Add Custom Bulk Actions to the Posts List in WordPress

Add a custom option to the bulk actions dropdown on the posts list screen. This example marks selected posts as “Featured”. For more on WordPress hooks.

add_filter( 'bulk_actions-edit-post', function ( $actions ) {
    $actions['mark_featured'] = 'Mark as Featured';
    return $actions;
} );

add_filter( 'handle_bulk_actions-edit-post', function ( $redirect_to, $action, $post_ids ) {
    if ( $action !== 'mark_featured' ) {
        return $redirect_to;
    }

    foreach ( $post_ids as $post_id ) {
        update_post_meta( $post_id, '_is_featured', '1' );
    }

    return add_query_arg( 'marked_featured', count( $post_ids ), $redirect_to );
}, 10, 3 );

add_action( 'admin_notices', function () {
    if ( empty( $_REQUEST['marked_featured'] ) ) {
        return;
    }

    $count = intval( $_REQUEST['marked_featured'] );
    printf(
        '<div class="notice notice-success is-dismissible"><p>%d post(s) marked as featured.</p></div>',
        $count
    );
} );

Add to functions.php. Change mark_featured and the meta key to fit your use case.

Create a Simple Shortcode with Attributes in WordPress

Create a reusable shortcode that accepts attributes. This pattern is the foundation for any custom shortcode in WordPress. For more on WordPress hooks.

function savvy_button_shortcode( $atts ) {
    $atts = shortcode_atts( array(
        'url'   => '#',
        'text'  => 'Click Here',
        'color' => 'blue',
    ), $atts, 'savvy_button' );

    return sprintf(
        '<a href="%s" class="savvy-btn savvy-btn-%s">%s</a>',
        esc_url( $atts['url'] ),
        esc_attr( $atts['color'] ),
        esc_html( $atts['text'] )
    );
}

add_shortcode( 'savvy_button', 'savvy_button_shortcode' );

Add to functions.php. Usage: [savvy_button url="/contact/" text="Contact Us" color="gold"].

Register a Custom REST API Endpoint in WordPress

Register a custom REST API endpoint to expose data from your theme or plugin. For more on securing the REST API.

add_action( 'rest_api_init', function () {
    register_rest_route( 'savvy/v1', '/recent-posts/', array(
        'methods'             => 'GET',
        'callback'            => 'savvy_get_recent_posts',
        'permission_callback' => '__return_true',
    ) );
} );

function savvy_get_recent_posts() {
    $posts = get_posts( array(
        'numberposts' => 5,
        'post_status' => 'publish',
    ) );

    $data = array();
    foreach ( $posts as $post ) {
        $data[] = array(
            'id'    => $post->ID,
            'title' => $post->post_title,
            'link'  => get_permalink( $post ),
        );
    }

    return rest_ensure_response( $data );
}

Add to functions.php. Access at /wp-json/savvy/v1/recent-posts/. Set permission_callback to restrict access if needed.

Add Custom Admin Notices in WordPress

To add an admin notice in WordPress, hook a function into admin_notices and print a div with the notice class. It’s the same mechanism core and plugins use for success messages, warnings and errors at the top of wp-admin screens.

Use it for reminders to your content team, configuration warnings in a plugin, or a confirmation after a custom action.

Add a Custom Admin Notice

Add this to your child theme’s functions.php or to a plugin. It shows a dismissible warning on the Posts list screen only.

add_action( 'admin_notices', function () {
    $screen = get_current_screen();

    if ( ! $screen || 'edit-post' !== $screen->id ) {
        return;
    }

    echo '<div class="notice notice-warning is-dismissible">';
    echo '<p>' . esc_html__( 'Remember to set a featured image before publishing.', 'your-textdomain' ) . '</p>';
    echo '</div>';
} );

Change the screen ID check and the message to fit your case. Remove the screen check entirely to show the notice on every admin page.

How It Works

WordPress fires admin_notices near the top of every regular admin screen, before the page content. Core JavaScript then moves notices so they sit right below the page title.

The CSS classes control how the notice looks:

  • notice is required on every notice.
  • notice-success (green), notice-error (red), notice-warning (yellow) or notice-info (blue) set the color of the left border.
  • is-dismissible adds a close button.

get_current_screen() returns the current screen object. Useful IDs include dashboard, edit-post (Posts list), post (post editor), upload, plugins and options-general.

The wp_admin_notice() Function (WordPress 6.4+)

Since WordPress 6.4, wp_admin_notice() builds the markup for you. This is the same notice as above, without hand-written HTML:

add_action( 'admin_notices', function () {
    $screen = get_current_screen();

    if ( ! $screen || 'edit-post' !== $screen->id ) {
        return;
    }

    wp_admin_notice(
        esc_html__( 'Remember to set a featured image before publishing.', 'your-textdomain' ),
        array(
            'type'        => 'warning',
            'dismissible' => true,
        )
    );
} );

The type argument takes success, error, warning or info and adds the matching notice- class. Other arguments are id, additional_classes, attributes and paragraph_wrap, which is true by default. Output passes through wp_kses_post(), so links and <strong> survive but scripts don’t.

If your plugin still supports versions older than 6.4, wrap the call in function_exists( 'wp_admin_notice' ) or keep the manual markup.

Why the Notice Doesn’t Show in the Block Editor

When the block editor is active, it hides the classic notice area, so anything printed on admin_notices doesn’t appear while editing a post. Use the editor’s JavaScript notices store instead:

wp.data.dispatch( 'core/notices' ).createNotice(
    'warning',
    'Remember to set a featured image before publishing.',
    { isDismissible: true }
);

Enqueue that script only on the editor screens, as shown in Enqueue Scripts Only for Specific Admin Pages, with wp-data and wp-notices as dependencies. For more on actions and filters, see WordPress hooks explained.

FAQs

Common questions about WordPress admin notices:

Why does my dismissible notice come back after reload?
is-dismissible only removes the notice from the page with JavaScript. To keep it hidden, save the dismissal yourself, for example in user meta via an AJAX request, and check that value before printing the notice.
How do I show a success message after saving a custom form?
Redirect back with a query argument such as &saved=1, then check for it inside your admin_notices callback and print a notice-success. Settings API forms that submit to options.php get a "Settings saved." notice automatically.
Why don't my add_settings_error() messages appear?
WordPress calls settings_errors() automatically only on pages under the Settings menu. On a top-level or other custom admin page, call settings_errors() yourself at the top of the page output.
How do I show a notice in the Network Admin on Multisite?
Network Admin screens fire network_admin_notices instead of admin_notices. Use all_admin_notices if the notice should appear in both.

Schedule Cron to Delete Expired Transients

Over time, expired transients pile up in the wp_options table and slow down queries. This snippet schedules a weekly cleanup. For more on improving WordPress performance.

if ( ! wp_next_scheduled( 'savvy_cleanup_transients' ) ) {
    wp_schedule_event( time(), 'weekly', 'savvy_cleanup_transients' );
}

add_action( 'savvy_cleanup_transients', function () {
    global $wpdb;
    $wpdb->query(
        "DELETE a, b FROM {$wpdb->options} a
        LEFT JOIN {$wpdb->options} b
            ON b.option_name = REPLACE( a.option_name, '_transient_timeout_', '_transient_' )
        WHERE a.option_name LIKE '\_transient\_timeout\_%'
            AND a.option_value < UNIX_TIMESTAMP()"
    );
} );

Add to functions.php. The cron runs once a week and removes only expired transients.

Remove Shortlink, RSD and wlwmanifest from Header

WordPress adds meta tags for shortlink, RSD (Really Simple Discovery), and wlwmanifest (Windows Live Writer). Most sites do not need them. Add to your functions.php file. For more on securing WordPress.

remove_action( 'wp_head', 'wp_shortlink_wp_head' );
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );
remove_action( 'wp_head', 'wp_generator' );

Remove jQuery Migrate

WordPress loads jQuery Migrate for backward compatibility. If your site and plugins support modern jQuery, you can remove Migrate and save an HTTP request. Add to your functions.php file. For more on performance optimization.

add_action( 'wp_default_scripts', function( $scripts ) {
	if ( ! is_admin() && isset( $scripts->registered['jquery'] ) ) {
		$script = $scripts->registered['jquery'];
		if ( $script->deps ) {
			$script->deps = array_diff( $script->deps, array( 'jquery-migrate' ) );
		}
	}
} );

Add Custom Image Sizes

You can define custom image sizes for your site. Images are generated on upload or when regenerating thumbnails. Add to your functions.php file. For more on image optimization.

add_action( 'after_setup_theme', function() {
	add_image_size( 'card-thumbnail', 400, 300, true );
	add_image_size( 'hero-banner', 1920, 600, true );
} );

Replace names and dimensions as needed. Use true for crop or false for scale.