How to display custom fields in WordPress: A developer's blueprint
EN

How to display custom fields in WordPress: A developer's blueprint

Last verified: June 29, 2026
11 min read
Guide
Full-stack developer

#How to Display Custom Fields in WordPress: A Developer’s Blueprint

In WordPress, custom fields are the foundation of structured content. By associating custom metadata with posts, pages, or custom post types, you can extend the platform from a blogging engine into a robust content management system (CMS) capable of handling directory listings, real estate catalogs, and e-commerce inventories. However, managing custom fields at scale requires a deep understanding of database schema design, secure sanitization pipelines, layout integrations, and performance optimizations. This guide provides a developer’s blueprint for query scaling, custom data tables, Block Bindings API integration, and headless REST endpoints.

Learn more about our professional WordPress development services to design high-performance architectures.

Applying these database strategies requires a systematic approach that balances technical database optimization with high-quality content. Here is how to execute each strategy effectively.


#1. Under the Hood: The wp_postmeta Database Schema and Serialization

WordPress uses the Entity-Attribute-Value (EAV) model to store post metadata. In this schema, all metadata is saved in a single table: wp_postmeta.

#Database Schema Structure

The table contains four fields:

  • meta_id: The primary key (auto-increment).
  • post_id: The foreign key referencing the post table.
  • meta_key: The string identifier for the custom field.
  • meta_value: The text field storing the data.

#Serialization Mechanics and Corruption Risks

When you store complex datasets (like arrays or objects) inside a custom field, WordPress serializes the data using PHP’s maybe_serialize() function:

Array in PHP ---> serialize() ---> String stored in wp_postmeta (meta_value)
                                         |
                               [ Query execution ]
                                         |
Read string ---> maybe_unserialize() ---> Reconstruct PHP Array

A typical serialized PHP array looks like this: a:2:{i:0;s:5:"Apple";i:1;s:6:"Banana";}

In this structure:

  • a:2: Defines an Array containing exactly 2 elements.
  • i:0: Represents an Integer index key of 0.
  • s:5:"Apple": Represents a String of length 5 containing "Apple".
  • i:1: Represents an Integer index key of 1.
  • s:6:"Banana": Represents a String of length 6 containing "Banana".

Other common serialization type markers include:

  • b:1 or b:0: Represents a Boolean value (true or false).
  • d:45.99: Represents a Double (float) decimal value.
  • O:8:"stdClass":1:{...}: Represents an instantiated Object of class length 8 ("stdClass") containing 1 property.
  • N;: Represents a Null value.

In this format, type and string-length markers are explicitly defined (e.g., s:6 indicates a string of exactly 6 characters: "Banana"). Because serialized data is stored as raw text, you cannot query individual array elements inside wp_postmeta efficiently using SQL. Running queries that search inside serialized data requires slow LIKE string matching, which causes full-table scans and increases server load times.

Executing direct database search-and-replace SQL commands (for example, renaming a domain or URL path strings) will corrupt serialized arrays. If you replace http://olddomain.com (21 characters) with https://newdomain.com (22 characters) inside a serialized string without updating the string-length indicators (s:21 to s:22), the PHP parser will fail with a fatal warning: unserialize(): Error at offset... and return false, breaking site configuration fields. Always use dedicated serialization-aware migration tools (such as WP-CLI’s wp search-replace) to modify metadata safely.


#2. Retrieving and Sanitizing Metadata

The core function for fetching metadata is get_post_meta().

$value = get_post_meta( int $post_id, string $key = '', bool $single = false );

#Displaying Standard Fields

Always escape values upon rendering to prevent Cross-Site Scripting (XSS) injection:

declare(strict_types=1);

namespace WPPoland\Meta;

/**
 * Renders custom event details safely.
 */
function render_event_metadata(): void {
    $post_id = get_the_ID();

    $price    = get_post_meta( $post_id, 'event_price', true );
    $location = get_post_meta( $post_id, 'event_location', true );
    $link     = get_post_meta( $post_id, 'event_ticket_url', true );

    if ( ! empty( $price ) ) {
        printf( 
            '<p class="event-price">Price: <strong>%s EUR</strong></p>', 
            esc_html( $price ) 
        );
    }

    if ( ! empty( $location ) ) {
        printf( 
            '<p class="event-location">Venue: <strong>%s</strong></p>', 
            esc_html( $location ) 
        );
    }

    if ( ! empty( $link ) ) {
        printf( 
            '<p class="event-tickets"><a href="%s" class="btn-primary">Buy Tickets</a></p>', 
            esc_url( $link ) 
        );
    }
}

#3. Query Scaling and Custom SQL Indexes

When querying posts by metadata using meta_query in WP_Query, WordPress generates SQL JOIN statements linking wp_posts and wp_postmeta. On databases with over 100,000 metadata rows, these queries can degrade database response times.

#Analyzing JOIN Overhead and Database Query Plans

Every key-value constraint inside a meta_query block results in an additional INNER JOIN or LEFT JOIN on the wp_postmeta table in the generated SQL statement. When the MySQL or MariaDB optimizer processes this query:

  1. It must resolve the joins by matching post_id foreign keys.
  2. If the query orders results by a metadata field (using orderby => meta_value), the database engine is forced to perform a filesort operation.
  3. Because the meta_value column is defined as longtext, the database engine cannot use standard indexing for ordering unless a prefix index is configured.
  4. This results in the database creating temporary disk tables to execute the sort operation, leading to high disk I/O and query latency. For high-traffic applications, this can quickly saturate database connections.

#Adding a Custom SQL Composite Index

To optimize metadata lookups, add a composite index on meta_key and the beginning of meta_value. Run this query directly on your database:

CREATE INDEX idx_meta_key_value ON wp_postmeta (meta_key, meta_value(191));

This index allows the database engine to quickly filter keys without performing full-table scans, reducing query execution times from seconds to milliseconds.


#4. Bypassing EAV: Creating Custom Metadata Tables

If your application queries posts by multiple metadata fields simultaneously (e.g., sorting products by price, location, and rating), the EAV model becomes a database bottleneck. In this case, you should create a custom flat SQL table where each custom field is stored in a dedicated, indexed column.

#Implementing a Custom Table Integration

Add this schema migration and write helper class to your theme’s functions.php file to handle custom table inserts, deletions, and performant query wrapping:

declare(strict_types=1);

namespace WPPoland\Database\CustomTable;

/**
 * Creates a custom table to store event metadata.
 */
function create_custom_events_table(): void {
    global $wpdb;
    $table_name = $wpdb->prefix . 'event_metadata_flat';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        post_id bigint(20) NOT NULL,
        event_price decimal(10,2) NOT NULL DEFAULT 0.00,
        event_location varchar(255) NOT NULL,
        event_date datetime DEFAULT NULL,
        PRIMARY KEY  (post_id),
        KEY event_price (event_price),
        KEY event_date (event_date)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}
register_activation_hook( __FILE__, __NAMESPACE__ . '\\create_custom_events_table' );

/**
 * Syncs metadata changes to the custom flat table.
 */
function sync_event_metadata_to_custom_table( int $post_id ): void {
    if ( 'event' !== get_post_type( $post_id ) ) {
        return;
    }

    global $wpdb;
    $table_name = $wpdb->prefix . 'event_metadata_flat';

    $price    = (float) get_post_meta( $post_id, 'event_price', true );
    $location = sanitize_text_field( get_post_meta( $post_id, 'event_location', true ) );
    $date     = sanitize_text_field( get_post_meta( $post_id, 'event_date', true ) );

    $wpdb->replace(
        $table_name,
        [
            'post_id'        => $post_id,
            'event_price'    => $price,
            'event_location' => $location,
            'event_date'     => ! empty( $date ) ? $date : null
        ],
        [ '%d', '%f', '%s', '%s' ]
    );
}
add_action( 'save_post_event', __NAMESPACE__ . '\\sync_event_metadata_to_custom_table', 99 );

/**
 * Automatically drops custom metadata rows when a post is permanently deleted.
 */
function delete_event_metadata_on_purge( int $post_id ): void {
    global $wpdb;
    $table_name = $wpdb->prefix . 'event_metadata_flat';
    $wpdb->delete( $table_name, [ 'post_id' => $post_id ], [ '%d' ] );
}
add_action( 'deleted_post', __NAMESPACE__ . '\\delete_event_metadata_on_purge' );

/**
 * Retrieves sorted event lists directly from the flat table with transient caching.
 */
function get_flat_events_sorted_by_price( int $limit = 10 ): array {
    global $wpdb;
    $table_name = $wpdb->prefix . 'event_metadata_flat';
    
    $cache_key = 'wppoland_flat_events_price_' . $limit;
    $cached = get_transient( $cache_key );
    if ( false !== $cached ) {
        return $cached;
    }

    $results = $wpdb->get_results( $wpdb->prepare(
        "SELECT post_id, event_price, event_location 
         FROM {$table_name} 
         ORDER BY event_price ASC 
         LIMIT %d",
        $limit
    ), ARRAY_A );

    set_transient( $cache_key, $results, 4 * HOUR_IN_SECONDS );
    return $results;
}

Using this custom database structure, you can query your events with simple, highly performant SQL queries, bypassing wp_postmeta entirely.


#5. Modern Gutenberg Block Bindings API Integration

In WordPress 6.5+, the Block Bindings API allows developers to connect core Gutenberg blocks (like Paragraph, Heading, or Buttons) directly to custom field metadata values without building custom blocks.

#Registering Post Meta for Block Bindings

To expose a custom field to the block editor layout engine, you must first register it using the core register_post_meta() function. Crucially, the configuration array must have show_in_rest set to true. This allows the block editor’s React client framework to fetch, update, and display the metadata field via standard REST requests during editing sessions. If omitted or set to false, the editor client cannot access the value, rendering the binding inactive. You should also define the data type (e.g., string, number, boolean) and implement an authorization callback to verify that only authorized roles can save metadata changes:

declare(strict_types=1);

namespace WPPoland\Gutenberg\Bindings;

function register_bindable_metadata(): void {
    register_post_meta( 'post', 'custom_author_note', [
        'show_in_rest' => true,
        'single'       => true,
        'type'         => 'string',
        'auth_callback'=> function() {
            return current_user_can( 'edit_posts' );
        }
    ] );
}
add_action( 'init', __NAMESPACE__ . '\\register_bindable_metadata' );

#Binding Metadata to a Paragraph Block in HTML Markup

Once registered, you can bind the meta field directly to a Paragraph block in your theme template HTML file:

<!-- wp:paragraph {
  "metadata": {
    "bindings": {
      "content": {
        "source": "core/post-meta",
        "args": {
          "key": "custom_author_note"
        }
      }
    }
  }
} -->
<p>Default fallback text placeholder displayed if post meta is empty.</p>
<!-- /wp:paragraph -->

This native integration allows content creators to edit metadata directly in the sidebar or inline editor, maintaining design consistency.

#Registering Custom Editor-Side Sources

If you need to register a custom source for block bindings (for instance, dynamically formatting dates or currencies inside Gutenberg), write the following registration script on the JavaScript editor side:

// Register a custom source on the block editor side
wp.blocks.registerBlockBindingsSource({
  name: 'wppoland/formatted-currency',
  label: 'Formatted Currency Meta',
  getValues({ args }) {
    // Return custom formatted value to output directly in the editor view
    const rawValue = wp.data.select('core/editor').getEditedPostAttribute('meta')[args.key];
    if (!rawValue) return '';
    return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(rawValue);
  }
});

#6. Advanced ACF Hooks: Encrypting Metadata before Database Saves

If your custom fields store sensitive data (like API tokens or user records), you should encrypt values before saving them to the database.

Here is a PHP implementation using Advanced Custom Fields (ACF) filter hooks to encrypt and decrypt field values:

declare(strict_types=1);

namespace WPPoland\ACF\Security;

/**
 * Simple key/iv helper for encryption.
 */
function get_encryption_key(): string {
    return defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : 'fallback-key-value-string';
}

/**
 * Encrypts ACF custom field values.
 */
function encrypt_acf_field_value( $value, $post_id, $field ) {
    if ( empty( $value ) ) {
        return $value;
    }

    $key = get_encryption_key();
    $method = 'aes-256-cbc';
    $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $method ) );
    
    $encrypted = openssl_encrypt( (string) $value, $method, $key, 0, $iv );
    
    // Return payload containing base64 data and the IV string
    return base64_encode( $iv . '::' . $encrypted );
}
add_filter( 'acf/update_value/name=sensitive_api_token', __NAMESPACE__ . '\\encrypt_acf_field_value', 10, 3 );

/**
 * Decrypts ACF custom field values.
 */
function decrypt_acf_field_value( $value, $post_id, $field ) {
    if ( empty( $value ) ) {
        return $value;
    }

    $payload = base64_decode( $value );
    if ( false === $payload || strpos( $payload, '::' ) === false ) {
        return $value; // Return raw value if not encrypted
    }

    list( $iv, $encrypted ) = explode( '::', $payload, 2 );
    $key = get_encryption_key();
    $method = 'aes-256-cbc';

    return openssl_decrypt( $encrypted, $method, $key, 0, $iv );
}
add_filter( 'acf/load_value/name=sensitive_api_token', __NAMESPACE__ . '\\decrypt_acf_field_value', 10, 3 );

#7. Exposing Custom Fields in the REST API

For decoupled headless frontend stacks, expose custom fields as native elements in REST API post objects:

declare(strict_types=1);

namespace WPPoland\API\Meta;

function register_rest_custom_fields(): void {
    register_rest_field( 'post', 'author_note', [
        'get_callback' => function( array $post_object ) {
            return get_post_meta( $post_object['id'], 'custom_author_note', true );
        },
        'update_callback' => function( $value, \WP_Post $post_object ) {
            return update_post_meta( $post_object->ID, 'custom_author_note', sanitize_text_field( $value ) );
        },
        'schema' => [
            'type'        => 'string',
            'description' => 'Author custom note note metadata',
            'context'     => [ 'view', 'edit' ]
        ]
    ] );
}
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_rest_custom_fields' );

#8. Action Plan: A 90-Day Custom Fields Optimization Roadmap

Follow this timeline to optimize, extend, and secure your site’s custom fields:

  • Days 1–30: Add composite SQL indexes to wp_postmeta, escape all metadata output on theme templates, and register fields in the REST API.
  • Days 31–60: Integrate Gutenberg Block Bindings API to connect metadata fields to core blocks, and apply security filter hooks to encrypt sensitive keys.
  • Days 61–90: Migrate high-volume query endpoints to custom flat database tables, run load tests to measure query speeds, and configure caching.

Need help building structured custom data architectures? Our WordPress development team can audit your database, construct flat custom tables, and secure sensitive field payloads. Contact us to discuss your project requirements.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

How does get_post_meta handle serialized arrays?#
It automatically checks for serialized data and runs maybe_unserialize() before returning the value, returning a clean PHP array.
Why are complex meta_query filters slow on large databases?#
Because every sub-query in a meta_query generates an SQL JOIN and a wildcard scan. Without indexes, this forces full-table scans.
Can I bind custom fields directly to core blocks in Gutenberg?#
Yes. WordPress 6.5+ supports the Block Bindings API, allowing you to bind post meta values directly to image, paragraph, or heading blocks.
Is it safe to output custom field values directly?#
No. Always run output escaping (e.g. esc_html, esc_url, or wp_kses_post) to mitigate Cross-Site Scripting (XSS) risks.
What is the difference between update_post_meta and add_post_meta?#
The add_post_meta function registers a new metadata row and can create duplicate rows for the same key. The update_post_meta function updates the row if it exists, and falls back to registering a new one if not.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles