Add an “Export CSV” button above the listing template that exports selected post data of posts in the filtered results

<?php
// Add an "Export CSV" button above the listing template that exports selected post data of posts in the filtered results

// Part 1 - Add the "Export CSV" button above the listing
add_action( 'facetwp_scripts', function() {
    ?>
    <script>
        var fwp_export = {
            nonce: '<?php echo wp_create_nonce('fwp_export_csv'); ?>',
            ajaxurl: '<?php echo admin_url('admin-ajax.php'); ?>'
        };
    </script>
    <script>

        (function() {
            document.addEventListener('facetwp-loaded', function() {
                // Only inject the button once
                if (!document.querySelector('.fwp-export-btn')) {
                    var btn = document.createElement('button');
                    btn.className = 'fwp-export-btn';
                    btn.textContent = 'Export CSV';
                    document.querySelector('.facetwp-template').insertAdjacentElement('beforebegin', btn);

                    btn.addEventListener('click', function() {
                        var params = new URLSearchParams();
                        params.set('action', 'fwp_export_csv');
                        params.set('nonce', fwp_export.nonce);
                        window.location.href = fwp_export.ajaxurl + '?' + params.toString();
                    });

                }
            });
        })();

    </script>
    <?php
}, 100 );

// Part 2 - Store filtered post IDs in a transient
add_filter( 'facetwp_filtered_post_ids', function( $post_ids, $class ) {

    // Only store the transient on the relevant listing.
    // Use $class->ajax_params['template'] to check for a FacetWP listing.
    // Or use $class->http_params['uri'] to check the URI if it is another listing template type
    // For examples, see: https://facetwp.com/help-center/developers/hooks/querying-hooks/facetwp_filtered_post_ids/#usage-examples
    if ( 'my_template' !== $class->ajax_params['template'] ) { // Change "my_template" to the name of your template
        return $post_ids;
    }
    $session_id = fwp_get_export_session_id();
    if ( empty( $session_id ) ) {
        return $post_ids;
    }
    set_transient( 'fwp_export_ids_' . $session_id, $post_ids, 5 * MINUTE_IN_SECONDS );

    return $post_ids;
}, 10, 2 );

// Part 3 - Set cookie instead of session ID for logged-out users
// Remove this part if the button is admin-only
add_action( 'init', function() {
    if ( ! is_user_logged_in() && empty( $_COOKIE['fwp_export_session'] ) ) {
        $token = wp_generate_password( 12, false );
        setcookie( 'fwp_export_session', $token, time() + 3600, COOKIEPATH, COOKIE_DOMAIN );
        $_COOKIE['fwp_export_session'] = $token; // make it available in the same request
    }
} );

// Part 4 - Get a session ID from the transient (when logged in) or cookie (when logged out)
function fwp_get_export_session_id() {
    $token = wp_get_session_token();
    if ( ! empty( $token ) ) {
        return $token;
    }
    // Cookie fallback for logged-out users
    // Remove these lines if the button is admin-only
    return ! empty( $_COOKIE['fwp_export_session'] )
        ? sanitize_key( $_COOKIE['fwp_export_session'] )
        : '';
}

// Part 5 - Handle CSV export
add_action( 'wp_ajax_fwp_export_csv', 'fwp_handle_export_csv' );
add_action( 'wp_ajax_nopriv_fwp_export_csv', 'fwp_handle_export_csv' ); // Only for logged-out users. Remove if the button is admin-only.

function fwp_handle_export_csv() {
    ob_start(); // Catch any accidental output

    check_ajax_referer( 'fwp_export_csv', 'nonce' );

    $session_id = fwp_get_export_session_id();
    if ( empty( $session_id ) ) {
        wp_die( 'Session not found.' );
    }

    $post_ids = get_transient( 'fwp_export_ids_' . $session_id );
    if ( empty( $post_ids ) ) {
        wp_die( 'No results to export.' );
    }

    $query = new WP_Query( [
        'post__in'       => $post_ids,
        'orderby'        => 'post__in',
        'posts_per_page' => -1,
        'post_type'      => 'any',
    ] );

    ob_end_clean(); // Discard any buffered output before sending headers

    header( 'Content-Type: text/csv; charset=utf-8' );
    header( 'Content-Disposition: attachment; filename="export.csv"' );

    $out = fopen( 'php://output', 'w' );
    if ( ! $out ) {
        wp_die( 'Could not open output stream.' );
    }

    fputcsv( $out, [ 'Post ID', 'Post Title', 'Post Date', 'My field' ] ); // Add CSV headings for the output below

    foreach ( $query->posts as $post ) {
        fputcsv( $out, [
            $post->ID,
            $post->post_title,
            $post->post_date,
            get_field( 'my_field_name', $post->ID ),// example ACF field
            //.. add any other data
        ] );
    }

    fclose( $out );
    exit;
}