WordPress show_in_rest: Expose Custom Post Types and Meta to the REST API and Block Editor

By default, WordPress custom post types are not exposed to the REST API — they do not appear at /wp-json/wp/v2/ and their posts cannot be fetched, created, or updated by REST clients. The show_in_rest argument on register_post_type() opts the CPT into the REST API, and a matching show_in_rest argument on register_meta() or register_post_meta() makes custom fields available in the meta object of each post response. This combination powers the block editor — Gutenberg requires show_in_rest => true to enable the block editor interface for a custom post type, and individual meta fields must be registered with show_in_rest to be readable and writable in the editor sidebar. Getting this configuration right is necessary for any CPT that needs a block editor experience or a JavaScript/mobile client.

Problem: A custom post type event was registered without show_in_rest. The block editor is not available for events, and the REST API endpoint /wp-json/wp/v2/events does not exist. Custom meta fields _event_date and _event_location need to be readable in REST responses.

Solution: Add 'show_in_rest' => true to the CPT registration arguments, and call register_post_meta() for each meta field that should appear in REST responses — with 'show_in_rest' => true and a 'sanitize_callback'.

<?php
// ── CPT with block editor and REST API support ────────────────────────
add_action( 'init', function () {
    register_post_type( 'event', [
        'label'        => 'Events',
        'public'       => true,
        'show_in_rest' => true,         // enables REST API + block editor
        'rest_base'    => 'events',     // → /wp-json/wp/v2/events (default: post type slug)
        'supports'     => [ 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ],
        // 'custom-fields' in 'supports' is required for meta to appear in block editor sidebar
        'rewrite'      => [ 'slug' => 'events' ],
    ] );
} );

// ── Register meta fields for REST API + block editor ──────────────────
add_action( 'init', function () {
    register_post_meta( 'event', '_event_date', [
        'type'              => 'string',
        'description'       => 'Event date in Y-m-d format',
        'single'            => true,
        'sanitize_callback' => function ( $value ) {
            // Validate Y-m-d format
            $dt = DateTime::createFromFormat( 'Y-m-d', $value );
            return ( $dt && $dt->format( 'Y-m-d' ) === $value ) ? $value : '';
        },
        'auth_callback'     => fn() => current_user_can( 'edit_posts' ),
        'show_in_rest'      => true,    // included in REST responses + editable via REST
    ] );

    register_post_meta( 'event', '_event_location', [
        'type'              => 'string',
        'single'            => true,
        'sanitize_callback' => 'sanitize_text_field',
        'auth_callback'     => fn() => current_user_can( 'edit_posts' ),
        'show_in_rest'      => true,
    ] );

    // Meta with a complex REST schema (e.g. an object/array value)
    register_post_meta( 'event', '_event_speakers', [
        'type'         => 'array',
        'single'       => true,
        'show_in_rest' => [
            'schema' => [
                'type'  => 'array',
                'items' => [ 'type' => 'string' ],
            ],
        ],
    ] );
} );

// ── Reading from the REST API ──────────────────────────────────────────
// GET /wp-json/wp/v2/events?_fields=id,title,meta
// Response: { "id": 42, "title": { "rendered": "..." }, "meta": { "_event_date": "2021-06-15" } }

// ── Writing via REST API ───────────────────────────────────────────────
// POST /wp-json/wp/v2/events/42  with body: { "meta": { "_event_date": "2021-07-01" } }
// Requires valid authentication + auth_callback returning true

// ── Reading in the block editor via useEntityProp ──────────────────────
/*
import { useEntityProp } from '@wordpress/core-data';
const [ meta, setMeta ] = useEntityProp( 'postType', 'event', 'meta' );
const eventDate = meta['_event_date'];
const setEventDate = (val) => setMeta({ ...meta, '_event_date': val });
*/

NOTE: Meta fields with names starting with an underscore (_event_date) are treated as "protected" by WordPress — they are hidden from the custom fields meta box in the classic editor. In the block editor they are still accessible via useEntityProp when registered with show_in_rest => true. Also, 'custom-fields' must be listed in the CPT's 'supports' array for the block editor's custom fields panel to be available — this is a separate requirement from show_in_rest. Without it, registered meta fields are exposed in REST API responses but not editable in the sidebar panel.