WordPress Dynamic Body Class Manager: Conditional Styling by Page Parent and Taxonomy

This code implements a custom WordPress administration system that allows users to assign specific CSS classes to the HTML body tag based on parent pages or taxonomy terms. It features a modern, repeater-style settings page where administrators can create multiple “Menu Conditions.” Each condition maps a custom class name to selected pages (including their descendants) or specific terms from a “company” taxonomy.

The system uses AJAX for real-time content searching and pre-loading, ensuring a smooth management experience. On the front end, the code dynamically checks the current page’s ancestry and taxonomy associations to inject the designated classes, enabling developers to easily apply unique styling or menu behaviors to entire branches of a website without manual coding.


/**
 * 1. Register the Settings Page under Dashboard (index.php)
 */
add_action( 'admin_menu', 'mc_add_dashboard_submenu' );
function mc_add_dashboard_submenu() {
    add_submenu_page(
        'index.php',          
        'Menu Conditions',             
        'Menu Conditions',             
        'manage_options',              
        'menu-conditions',             
        'mc_render_settings_page'     
    );
}

/**
 * 2. Helper function to get Company Names for display
 */
function mc_get_company_label( $post_id ) {
    $terms = get_the_terms( $post_id, 'company' );
    if ( $terms && ! is_wp_error( $terms ) ) {
        return '[' . implode( ', ', wp_list_pluck( $terms, 'name' ) ) . ']';
    }
    return '';
}

/**
 * 3. Register Settings and Sanitization
 */
add_action( 'admin_init', 'mc_register_settings' );
function mc_register_settings() {
    register_setting( 
        'mc_settings_group', 
        'menu_conditions_settings', 
        array(
            'type'              => 'array',
            'sanitize_callback' => 'mc_sanitize_conditions'
        ) 
    );
}

function mc_sanitize_conditions( $input ) {
    $sanitized_output = array();
    if ( is_array( $input ) ) {
        foreach ( $input as $row ) {
            if ( ! empty( $row['class_name'] ) ) {
                $sanitized_output[] = array(
                    'class_name' => sanitize_html_class( $row['class_name'] ),
                    'pages'      => ! empty( $row['pages'] ) ? array_map( 'intval', $row['pages'] ) : array(),
                    'companies'  => ! empty( $row['companies'] ) ? array_map( 'intval', $row['companies'] ) : array()
                );
            }
        }
    }
    return $sanitized_output;
}

/**
 * 4. AJAX Handler
 */
add_action( 'wp_ajax_mc_get_info', 'mc_ajax_get_info' );
function mc_ajax_get_info() {
    if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Unauthorized' );

    $id   = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
    $type = isset( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : 'post';

    if ( $id > 0 ) {
        if ( $type === 'taxonomy' ) {
            $term = get_term( $id, 'company' );
            if ( $term && ! is_wp_error( $term ) ) {
                wp_send_json_success( array( 
                    'id'    => $term->term_id, 
                    'title' => $term->name, 
                    'url'   => get_term_link($term), 
                    'is_tax' => true 
                ) );
            }
        } else {
            $post = get_post( $id );
            if ( $post ) {
                wp_send_json_success( array(
                    'id'      => $post->ID,
                    'title'   => $post->post_title,
                    'url'     => get_permalink($post->ID),
                    'company' => mc_get_company_label( $post->ID ),
                    'is_tax'  => false
                ) );
            }
        }
    }
    wp_send_json_error( 'Not found.' );
}

/**
 * 5. Render Settings Page
 */
function mc_render_settings_page() {
    if ( ! current_user_can( 'manage_options' ) ) return;

    $settings = get_option( 'menu_conditions_settings', array() );
    
    $post_types = get_post_types( array( 'public' => true ), 'names' );
    if ( isset( $post_types['produkt-list'] ) ) unset( $post_types['produkt-list'] );
    
    $all_content = get_posts( array( 
        'post_type'      => array_values($post_types), 
        'posts_per_page' => 500, 
        'post_status'    => 'publish', 
        'post_parent'    => 0, 
        'orderby'        => 'title', 
        'order'          => 'ASC' 
    ) ); 

    $all_companies = get_terms( array( 'taxonomy' => 'company', 'hide_empty' => false ) );

    $preloaded = array( 'posts' => array(), 'terms' => array() );
    foreach ( (array)$settings as $row ) {
        foreach ( (array)($row['pages'] ?? []) as $pid ) {
            $p = get_post($pid);
            if($p) $preloaded['posts'][$pid] = array( 
                'id' => $p->ID, 
                'title' => $p->post_title, 
                'url' => get_permalink($p->ID), 
                'company' => mc_get_company_label($p->ID) 
            );
        }
        foreach ( (array)($row['companies'] ?? []) as $tid ) {
            $t = get_term($tid, 'company');
            if($t && !is_wp_error($t)) $preloaded['terms'][$tid] = array( 
                'id' => $t->term_id, 
                'title' => $t->name, 
                'url' => get_term_link($t) 
            );
        }
    }
    ?>
    <style>
        .mc-repeater-row { max-width:850px; background: #fff; border: 1px solid #ccd0d4; padding: 0; margin-bottom: 5px; border-radius: 4px; overflow: hidden; box-shadow: 0 1px 1px rgba(0,0,0,.04); }
        .mc-row-header { background: #f6f7f7; padding: 12px 20px; border-bottom: 1px solid #ccd0d4; display: flex; justify-content: space-between; align-items: center; }
        .mc-row-header h3 { margin: 0; font-size: 14px; }
        .mc-row-body { padding:  4px; }
        .mc-config-collapsible { background: #fafafa; border: 1px solid #e5e5e5; border-radius: 4px; margin-bottom: 5px; }
        .mc-config-collapsible summary { padding: 5px; cursor: pointer; font-weight: 600; color: #2271b1; outline: none; }
        .mc-config-content { padding: 15px; border-top: 1px solid #e5e5e5; }
        .mc-form-group { margin-bottom: 15px; }
        .mc-form-group label { display: block; font-weight: 600; margin-bottom: 5px; }
        .mc-search-flex { display: flex; gap: 8px; margin-bottom: 8px; }
        .mc-selected-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
        .mc-selected-list li { background: #fff; padding: 8px 12px; border-radius: 4px; border: 1px solid #dcdcde; display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
        .mc-tag-tax { background: #f0f6fb !important; border-color: #d1e3ef !important; }
        .mc-item-title { flex-grow: 1; font-weight: 500; font-size: 13px; }
        .mc-view-page { text-decoration: none; font-size: 11px; padding: 3px 8px; border: 1px solid #ccc; border-radius: 3px; background: #fff; color: #2271b1; }
        .mc-remove-item { color: #d63638; cursor: pointer; font-size: 20px; line-height: 1; }
        .mc-remove-row { color: #d63638; border-color: #d63638; cursor: pointer; background: transparent; padding: 2px 8px; border-radius: 3px; border: 1px solid #d63638; }
        .mc-remove-row:hover { background: #d63638; color: #fff; }
        .mc-preview-section { margin-top: 5px; }
        .mc-preview-label { font-size: 11px; text-transform: uppercase; color: #646970; font-weight: 700; margin-bottom: 5px; display: block; }
    </style>

    <div class="wrap">
        <h1>Menu Conditions</h1>
        <form action="options.php" method="post">
            <?php settings_fields( 'mc_settings_group' ); ?>
            <div id="mc-repeater-container"></div>
            <p><button type="button" id="mc-add-row" class="button button-secondary">+ Add New Class Condition</button></p>
            <?php submit_button( 'Save Conditions' ); ?>
        </form>
    </div>

    <datalist id="mc-post-datalist"><?php foreach($all_content as $p) echo '<option value="'.$p->ID.'">'.esc_html($p->post_title).' '.esc_html(mc_get_company_label($p->ID)).' ('.$p->post_type.')</option>'; ?></datalist>
    <datalist id="mc-tax-datalist"><?php foreach($all_companies as $t) echo '<option value="'.$t->term_id.'">'.esc_html($t->name).'</option>'; ?></datalist>

    <script>
    document.addEventListener('DOMContentLoaded', function() {
        const preloaded = <?php echo json_encode( $preloaded ); ?>;
        const settings = <?php echo json_encode( $settings ); ?>;
        const container = document.getElementById('mc-repeater-container');
        let rowIndex = 0;

        function createRow(data = { class_name: '', pages: [], companies: [] }) {
            const rowDiv = document.createElement('div');
            rowDiv.className = 'mc-repeater-row';
            
            // Initial title logic
            const displayTitle = data.class_name ? `Class: .${data.class_name}` : `New Condition #${rowIndex + 1}`;

            rowDiv.innerHTML = `
                <div class="mc-row-header">
                    <h3 class="mc-row-title">${displayTitle}</h3>
                    <button type="button" class="mc-remove-row">Delete</button>
                </div>
                <div class="mc-row-body">
                    <details class="mc-config-collapsible">
                        <summary>Configure Settings & Rules</summary>
                        <div class="mc-config-content">
                            <div class="mc-form-group">
                                <label>Body Class Name:</label>
                                <input type="text" name="menu_conditions_settings[${rowIndex}][class_name]" 
                                       value="${data.class_name}" class="regular-text mc-class-input" 
                                       placeholder="e.g. my-custom-style" required>
                            </div>
                            <div class="mc-form-group">
                                <label>Search & Add Parent Content:</label>
                                <div class="mc-search-flex">
                                    <input type="text" list="mc-post-datalist" class="mc-post-search" placeholder="ID or Title...">
                                    <button type="button" class="button mc-add-btn" data-type="post">Add</button>
                                </div>
                            </div>
                            <div class="mc-form-group">
                                <label>Search & Add Company:</label>
                                <div class="mc-search-flex">
                                    <input type="text" list="mc-tax-datalist" class="mc-tax-search" placeholder="Search Company...">
                                    <button type="button" class="button mc-add-btn" data-type="taxonomy">Add</button>
                                </div>
                            </div>
                        </div>
                    </details>

                    <div class="mc-preview-section">
                        <span class="mc-preview-label"></span>
                        <ul class="mc-selected-list mc-combined-list"></ul>
                    </div>
                </div>
            `;

            container.appendChild(rowDiv);
            const curIdx = rowIndex;
            const classInput = rowDiv.querySelector('.mc-class-input');
            const titleEl = rowDiv.querySelector('.mc-row-title');

            // Live Title Update
            classInput.addEventListener('input', (e) => {
                const val = e.target.value.trim();
                titleEl.textContent = val ? `Class: .${val}` : `Condition #${curIdx + 1}`;
            });

            // Populate existing data
            const listContainer = rowDiv.querySelector('.mc-combined-list');
            (data.pages || []).forEach(id => { if(preloaded.posts[id]) addItem(preloaded.posts[id], listContainer, curIdx, 'pages'); });
            (data.companies || []).forEach(id => { if(preloaded.terms[id]) addItem(preloaded.terms[id], listContainer, curIdx, 'companies', true); });

            // Button Logic
            rowDiv.querySelectorAll('.mc-add-btn').forEach(btn => {
                btn.addEventListener('click', function() {
                    const type = this.dataset.type;
                    const input = type === 'post' ? rowDiv.querySelector('.mc-post-search') : rowDiv.querySelector('.mc-tax-search');
                    const id = parseInt(input.value.split(' ')[0], 10);
                    if(isNaN(id)) return;

                    fetch(ajaxurl, {
                        method: 'POST',
                        body: new URLSearchParams({ action: 'mc_get_info', id: id, type: type }),
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
                    })
                    .then(res => res.json())
                    .then(res => {
                        if (res.success) {
                            addItem(res.data, listContainer, curIdx, type === 'post' ? 'pages' : 'companies', type === 'taxonomy');
                            input.value = '';
                        }
                    });
                });
            });

            rowDiv.querySelector('.mc-remove-row').addEventListener('click', () => rowDiv.remove());
            rowIndex++;
        }

        function addItem(data, ul, idx, fieldName, isTax = false) {
            if(ul.querySelector(`input[value="${data.id}"][name*="${fieldName}"]`)) return;
            const li = document.createElement('li');
            if(isTax) li.className = 'mc-tag-tax';
            
            li.innerHTML = `
                <span class="mc-item-title">
                    <strong>${isTax ? '🏢' : '📄'}</strong> ${data.title} ${data.company || ''} 
                    <small style="opacity:0.5">(ID: ${data.id})</small>
                </span>
                <a href="${data.url}" target="_blank" class="mc-view-page">View</a>
                <span class="mc-remove-item" title="Remove">×</span>
                <input type="hidden" name="menu_conditions_settings[${idx}][${fieldName}][]" value="${data.id}">
            `;
            li.querySelector('.mc-remove-item').addEventListener('click', () => li.remove());
            ul.appendChild(li);
        }

        if (settings.length) settings.forEach(s => createRow(s)); else createRow();
        document.getElementById('mc-add-row').addEventListener('click', () => createRow());
    });
    </script>
    <?php
}

/**
 * 6. Apply classes to <body>
 */
add_filter( 'body_class', 'mc_apply_body_classes' );
function mc_apply_body_classes( $classes ) {
    $settings = get_option( 'menu_conditions_settings' );
    if ( empty( $settings ) || ! is_array( $settings ) || ! is_singular() ) return $classes;

    $post_id   = get_queried_object_id();
    $ancestors = get_post_ancestors( $post_id );

    foreach ( $settings as $condition ) {
        $match = false;
        
        $saved_posts = (array)($condition['pages'] ?? []);
        if ( in_array( $post_id, $saved_posts ) || (!empty($ancestors) && array_intersect($ancestors, $saved_posts)) ) {
            $match = true;
        }

        if ( !$match && !empty($condition['companies']) ) {
            if ( has_term( $condition['companies'], 'company', $post_id ) ) {
                $match = true;
            }
        }

        if ( $match ) $classes[] = sanitize_html_class( $condition['class_name'] );
    }

    return array_unique($classes);
}