wp_list_pages() exclude category

I think the solution that would work out best for you is just using your own query. Since you have modified pages to have categories and tags taxonomies wp_list_pages doesn’t support excluding the arguments quite like that. You could get an array of all page IDs that are assigned to each category, but you’d be writing a query for that. You might as well just do it once, and forget about using wp_list_pages.

As far as implementation goes – the code above was giving me errors since $string can end up being undefined and attempts to return that. Here’s a working example that I think achieves what you’re going after:

// functions.php

// These hooks are only here as I wasn't sure if you actually meant that you were adding cat/tag to pages as there wasn't any code provided referencing that.
add_action( 'init', 'wpse356728_add_tags_cats_pages' );
add_action( 'pre_get_posts', 'wpse356728_tags_cats_update_queries' );

/**
 * Register the tags and cats tax for pages.
 */
function wpse356728_add_tags_cats_pages() {
    register_taxonomy_for_object_type( 'post_tag', 'page' );
    register_taxonomy_for_object_type( 'category', 'page' );  
}

/**
 * Ensure pages are added to any queries for tags and cats.
 */
function wpse356728_tags_cats_update_queries( $wp_query ) {
    if ( $wp_query->get( 'tag' ) ) {
        $wp_query->set( 'post_type', 'any' );
    }
    if ( $wp_query->get( 'category_name' ) ) {
        $wp_query->set( 'post_type', 'any' );
    }
}

/**
 * Create list of pages that are parents with optional exclude
 * pages by category name.
 * 
 * @param array $cats Array of category names to exclude from query.
 */
function wpse356728_list_child_pages( $cats = [] ) { 
    global $post;

    $current_ID = $post->ID;

    if ( is_page() && $post->post_parent ) {
        $child_of = $post->post_parent;
    } else {
        $child_of = $current_ID;
    }

    // Get the category IDs for passed in category names for query.
    $cats = ( array ) $cats;
    $cats = array_map( 'get_cat_ID', $cats );

    $args = [
        'post_type'      => 'page',
        'posts_per_page' => -1,
        'post_parent'    => $child_of,
        'order'          => 'DESC',
        'orderby'        => 'menu_order',
        'category__not_in' => $cats,
    ];

    $parent = new WP_Query( $args );

    if ( $parent->have_posts() ) : ?>
    <ul>
        <?php while ( $parent->have_posts() ) : $parent->the_post(); ?>
            <?php
                $current = function( $output ) use ( $current_ID ) {
                    return get_the_ID() === $current_ID ? $output : '';
                };
            ?>
                <li class="page_item page_item-<?php the_ID(); echo $current( ' current_page_item' ); ?>">
                    <a href="https://wordpress.stackexchange.com/questions/356728/<?php the_permalink(); ?>" <?php echo $current( 'aria-current="page"' ); ?>><?php the_title(); ?></a>
                </li>
        <?php endwhile; ?>
    </ul>
    <?php endif; wp_reset_postdata();
}

The method wpse356728_list_child_pages is really what you should be looking at. Instead of using wp_list_pages as you had – this shows how to get output from WP_Query that is pretty much identical markup, and allows you to pass in the category names you want to have pages excluded from in the query list returned. This will give more flexibility as theres many other args you can pass to the query.

Usage:

// page.php
<?php wpse356728_list_child_pages( [ 'sports', 'animals', 'movies' ] ); ?>

The above code would omit sports, animals, and movies categories from the query. You can modify the output of wpse356728_list_child_pages to have it returned as a string and create a shortcode if that’s necessary. In my test theme – I just used a hook that existed on page.php and added the code to my functions.php instead like this:

// functions.php

add_action( 'theme_name_after_page_content', function() {
    $cats_to_exclude = [ 'sports', 'animals', 'movies' ];
    wpse356728_list_child_pages( $cats_to_exclude );
} );