$_POST empty on submit (same code, same form submits normally on local server)

The best way to deal with Form Posts in WordPress is to use a special endpoint, /wp-admin/admin-post.php.

POST data can be messed up, both by the WP Query call, and by any redirects that happen.

So you set up your form with this action:

<form action="<?= admin_url('admin-post.php') ?>" method="post">
<input type="hidden" name="action" value="special_action">
<?php wp_nonce_field('special_action_nonce', 'special_action_nonce'); ?>

Then you can handle the form by adding an action to your theme or plugin:

add_action('admin_post_nopriv_special_action', ['My\Plugins\FormController', 'specialAction']);
add_action('admin_post_special_action', ['My\Plugins\FormController', 'specialAction']);

Note that WordPress constructs a special action, based on the action value in the form, admin_post_no_priv_special_action (if you’re logged out) and admin_post_special_action (if you’re logged in). You can point these in different locations.

These action endpoints will always have access to POST, and will never trigger a redirect (which is often what WordPress does for pretty routes… it often routes: site.com/about to site.com/?pageName=about).

Once you’ve handled the form as you want, you can do a wp_redirect() to get to where you need it to be. This is also helpful because an accidental page refresh will not re-send the form.

Much lengthier doco can be found here:
https://www.sitepoint.com/handling-post-requests-the-wordpress-way/