Developing using a local server and domain is the fastest way until you need to share your work with someone outside your local machine without uploading your code anywhere.
Imagine you need to develop a plugin, that will communicate with a 3rd-party API. That service should be able to ping your local site using a webhook to provide you some information based on the activity on your site. For example, that might be a PayPal IPN sending you the status of the WooCommerce payment.
But as you are working on a local site – remote servers can’t access it by default. There are several ways to fix this issue:
- use PhpStorm “upload to a remote server on file save” feature
- use ngrok (npm package)
- use Expose (composer package)
Using PhpStorm

Uploading a file via PhpStorm to a remote server on explicit save action in the IDE is not a good idea in lots of situations:
- you are evaluating all the changes and generally debugging things – too many risks to crash something if anything goes wrong;
- strong coupling between local microservices, meaning you need to set up that upload for various projects/directories to multiple servers;
- you may make a ton of changes in a short period of time – creating a ton of connections to upload each change in a file resulting in a file being uploaded a gazillion times over and over again.
Using ngrok

ngrok in its free tier won’t allow you to persistently use the same subdomain. It uses random URLs/ports every time when launching it. But you need the same URL to provide to the 3rd-party service you are integrating with, so updating its settings all the time is boring as hell and sometimes might not be possible at all – given you don’t have direct access to the service and need to ping a client to do that for you.
Using Expose by BeyondCode

So I’ve tried Expose by BeyondCode – a free alternative to ngrok with the ability to define your own subdomain when making your local site available to anyone on the internet.
I won’t go through the documentation – it’s quite detailed and has all the needed commands that you need to run locally before starting to use this tool. Just make sure to go through the “Getting Started” and “Client” sections.
Notably, you can also set up your own server (it’s open-sourced), so all requests will go through a reliable server that is under your control, and not the one available for you for free at sharedwithexpose.com
. This is very important when you deal with any kind of confidential information because Expose works as a “Man In The Middle” with access to everything in the HTTP requests, including sent passwords, various keys, page content, etc.
As we are required to have a fixed URL all the time when starting to work locally we simply type this command:
expose share example.local --subdomain=example
It will publish an example.sharedwithexpose.com
URL (if that subdomain is available, otherwise you will see an error) that you can put into the 3rd party system Webhook URL settings.
Here is how it works:
- some 3rd-party service sends a webhook request/alert to the example.sharedwithexpose.com URL;
- sharedwithexpose.com server catches it and maps the subdomain to your local installation;
- sharedwithexpose.com server sends all the received payload to your local Expose admin area that is available for you by default here: http://127.0.0.1:4040;
- sharedwithexpose.com client installed on your local machine retrieves from your local site a response to that request and passes this information back to the server;
sharedwithexpose.com
server replies to a request with that information;- profit.
Expose and WordPress: The Problem

Expose was originally developed with Laravel in mind. When using Laravel you define the URL to your site in a single place: .env
file. And the rule of thumb is to NOT store the full URL in your database of choice. So all URLs on your site are dynamically generated using a value from the config file, and when exposing the domain – everything works easily and without any issues by changing a single predefined value on a fly.
But WordPress has its own history, and the URL is stored in the DB in wp_options
table and lots of other places.
When a theme or plugins are enqueueing assets – various functions are used by the core to retrieve the site URL (example.local
), generate theme URL (wp-content/themes/theme/
), plugin URL (wp-content/plugins/plugin/
), content URL (wp-content/uploads/
), etc. But everything starts with the value from the DB or wp-config.php
.
If you need to render a WordPress page or want to use example.sharedwithexpose.com
in your browser to check how the site works “remotely” – you will face the problem of missing assets. So remotely your site will try to load assets from https://example.local/
– which won’t work. No CSS, no JS, no images on a page will be loaded, just plain HTML.
You may want your client to see your local site in a proper way, or the service you are relying on should parse the content of generated pages – so let’s fix this problem.
Expose and WordPress: The Solution
We should remember, that we want to have a local URL (example.local
) AND exposed URL (example.sharedwithexpose.com
) both working at the same time. That means we can’t hardcode an exposed URL in our code, in wp-config.php
as WP_HOME
or WP_SITEURL
or WP_CONTENT_URL
– because that will fix your issues with the exposed site, but will break your local site.
So now we need to rewrite all those URLs on a fly, and that means a lot of filtering. To do that I’ve created a wp-content/mu-plugins/expose.php
file and put this code there:
I haven’t used PHP 7.4 arrow functions to shorten the code because I wanted to support a wider range of PHP versions. Feel free to adjust it to your needs.
Also, if you find some WordPress functions that should be filtered – please suggest your changes in the comments section and I will gladly update the code so more developers will benefit from those improvements.
Leave a Reply