# Job Details Meta Box

> Adds employment type, salary range, location, remote option, closing date and apply link to job listings, plus a [job_details] shortcode.

- Type: PHP
- Runs: Run everywhere
- Category: [Custom Post Types & Fields](https://wpide.com/library/category/post-types/)
- Tags: meta-box, jobs, careers
- Author: WPIDE Team
- Installs: 0
- Updated: 2026-10-28
- Page: https://wpide.com/library/metabox-job-details/

## Code

```php
// Post type, and the fields: meta key (saved with a leading underscore) => label and input type.
$wpide_box = array(
    'post_type' => 'job',
    'title'     => 'Job details',
    'fields'    => array(
        'job_type' => array( 'label' => 'Employment type', 'type' => 'select', 'options' => array( 'FULL_TIME' => 'Full-time', 'PART_TIME' => 'Part-time', 'CONTRACTOR' => 'Contract', 'TEMPORARY' => 'Temporary', 'INTERN' => 'Internship' ) ),
        'job_salary_min' => array( 'label' => 'Salary from', 'type' => 'number' ),
        'job_salary_max' => array( 'label' => 'Salary to', 'type' => 'number' ),
        'job_currency' => array( 'label' => 'Currency (e.g. USD)', 'type' => 'text' ),
        'job_salary_unit' => array( 'label' => 'Salary per', 'type' => 'select', 'options' => array( 'YEAR' => 'Year', 'MONTH' => 'Month', 'HOUR' => 'Hour' ) ),
        'job_city' => array( 'label' => 'City', 'type' => 'text' ),
        'job_country' => array( 'label' => 'Country code', 'type' => 'text' ),
        'job_remote' => array( 'label' => 'Remote job', 'type' => 'checkbox' ),
        'job_valid_through' => array( 'label' => 'Applications close', 'type' => 'date' ),
        'job_apply_url' => array( 'label' => 'Apply URL', 'type' => 'url' ),
    ),
);

add_action( 'add_meta_boxes_' . $wpide_box['post_type'], function () use ( $wpide_box ) {
    add_meta_box( 'wpide-job-details', $wpide_box['title'], function ( $post ) use ( $wpide_box ) {
        wp_nonce_field( 'wpide_job_details', 'wpide_job_details_nonce' );
        echo '<div style="display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(220px,1fr))">';

        foreach ( $wpide_box['fields'] as $key => $field ) {
            $value = get_post_meta( $post->ID, '_' . $key, true );
            $id    = 'wpide-' . str_replace( '_', '-', $key );
            echo '<p style="margin:0">';

            if ( 'checkbox' === $field['type'] ) {
                printf( '<label><input type="checkbox" name="%1$s" value="1"%2$s> %3$s</label>', esc_attr( $key ), checked( $value, '1', false ), esc_html( $field['label'] ) );
            } else {
                printf( '<label for="%1$s"><strong>%2$s</strong></label><br>', esc_attr( $id ), esc_html( $field['label'] ) );

                if ( 'textarea' === $field['type'] ) {
                    printf( '<textarea class="widefat" rows="5" id="%1$s" name="%2$s">%3$s</textarea>', esc_attr( $id ), esc_attr( $key ), esc_textarea( $value ) );
                } elseif ( 'select' === $field['type'] ) {
                    printf( '<select class="widefat" id="%1$s" name="%2$s">', esc_attr( $id ), esc_attr( $key ) );
                    foreach ( $field['options'] as $option => $label ) {
                        printf( '<option value="%1$s"%2$s>%3$s</option>', esc_attr( $option ), selected( (string) $value, (string) $option, false ), esc_html( $label ) );
                    }
                    echo '</select>';
                } else {
                    printf( '<input class="widefat" type="%1$s" id="%2$s" name="%3$s" value="%4$s"%5$s>', esc_attr( $field['type'] ), esc_attr( $id ), esc_attr( $key ), esc_attr( $value ), 'number' === $field['type'] ? ' step="any" min="0"' : '' );
                }
            }

            echo '</p>';
        }

        echo '</div>';
    }, $wpide_box['post_type'], 'normal', 'high' );
} );

add_action( 'save_post_' . $wpide_box['post_type'], function ( $post_id ) use ( $wpide_box ) {
    if ( ! isset( $_POST['wpide_job_details_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['wpide_job_details_nonce'] ), 'wpide_job_details' ) ) {
        return;
    }

    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    foreach ( $wpide_box['fields'] as $key => $field ) {
        $raw = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitized below per type.
        $raw = is_string( $raw ) ? $raw : '';

        switch ( $field['type'] ) {
            case 'url':
                $value = esc_url_raw( $raw );
                break;
            case 'email':
                $value = sanitize_email( $raw );
                break;
            case 'number':
                $value = is_numeric( $raw ) ? (string) ( 0 + $raw ) : '';
                break;
            case 'textarea':
                $value = sanitize_textarea_field( $raw );
                break;
            case 'checkbox':
                $value = $raw ? '1' : '';
                break;
            case 'select':
                $value = array_key_exists( $raw, $field['options'] ) ? $raw : '';
                break;
            case 'date':
                $value = preg_match( '/^\d{4}-\d{2}-\d{2}$/', $raw ) ? $raw : '';
                break;
            case 'datetime-local':
                $value = preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $raw ) ? $raw : '';
                break;
            default:
                $value = sanitize_text_field( $raw );
        }

        if ( '' === $value ) {
            delete_post_meta( $post_id, '_' . $key );
        } else {
            update_post_meta( $post_id, '_' . $key, $value );
        }
    }
} );

add_shortcode( 'job_details', function () use ( $wpide_box ) {
    $id = get_the_ID();

    if ( ! $id || 'job' !== get_post_type( $id ) ) {
        return '';
    }

    $get  = function ( $key ) use ( $id ) { return (string) get_post_meta( $id, '_' . $key, true ); };
    $rows = array();

    if ( $get( 'job_type' ) ) {
        $rows['Type'] = $wpide_box['fields']['job_type']['options'][ $get( 'job_type' ) ] ?? $get( 'job_type' );
    }

    $where = $get( 'job_remote' ) ? 'Remote' : '';
    $where = trim( $where . ( $get( 'job_city' ) ? ( $where ? ' / ' : '' ) . $get( 'job_city' ) : '' ) );

    if ( $where ) {
        $rows['Location'] = $where;
    }

    if ( $get( 'job_salary_min' ) ) {
        $rows['Salary'] = trim( number_format_i18n( (float) $get( 'job_salary_min' ) ) . ( $get( 'job_salary_max' ) ? ' – ' . number_format_i18n( (float) $get( 'job_salary_max' ) ) : '' ) . ' ' . $get( 'job_currency' ) ) . ' per ' . strtolower( $get( 'job_salary_unit' ) ? $get( 'job_salary_unit' ) : 'year' );
    }

    if ( $get( 'job_valid_through' ) ) {
        $rows['Apply by'] = wp_date( get_option( 'date_format' ), strtotime( $get( 'job_valid_through' ) . ' 12:00' ) );
    }

    $html = '';

    foreach ( $rows as $label => $value ) {
        $html .= '<dt>' . esc_html( $label ) . '</dt><dd>' . esc_html( $value ) . '</dd>';
    }

    $apply = $get( 'job_apply_url' ) ? '<p><a class="wp-block-button__link wp-element-button" href="' . esc_url( $get( 'job_apply_url' ) ) . '">Apply now</a></p>' : '';

    return ( $html ? '<dl class="job-details">' . $html . '</dl>' : '' ) . $apply;
} );
```

## Notes

Made for the Job Listings post type snippet. The JobPosting schema and auto-expire snippets read these fields.

## How to use it

Install the free WPIDE plugin (https://wordpress.org/plugins/wpide/), find "Job Details Meta Box" in its snippet library and add it in one click; or create a new PHP snippet in WPIDE (or any code snippets plugin) and paste the code above. More snippets: https://wpide.com/library/
