I recently wanted to force WordPress to make all my <script> and stylesheet (<link>) tags load relative URLs instead of absolute URLs, and ended up learning a bit about the internals of WordPress (probably more than I wanted to know). For reference, I wanted to turn this:
<link rel=’stylesheet’ id=’joshuatzwp-style-css’ href=’http://joshuatz-wp.test/wp-content/themes/joshuatzwp/style.css‘ type=’text/css’ media=’all’ />
Into this:
<link rel=’stylesheet’ id=’joshuatzwp-style-css’ href=’/wp-content/themes/joshuatzwp/style.css‘ type=’text/css’ media=’all’ />
I wanted to do this because I was working with a local development environment, using Laragon on Windows as an Apache host, and proxying through Ngrok or something similar was not working with absolute domain-tied links, for a number of reasons. Furthermore, relative paths that were being passed to wp_enqueue_style
were not being preserved.
Click here to jump right to the solution, or read on if you are interested in the details of how WordPress forces absolute paths.
First wrinkle: WP forces absolute links
The first issue I ran into was WordPress does not respect the format you use to pass links into the enqueue system, even if you make 100% sure you pass in a relative link. For example, look at this code:
wp_enqueue_style('joshuatzwp-style',wp_make_link_relative(get_stylesheet_uri()),array(),$cacheBustStamp,'all');
You would expect that the use of wp_make_link_relative
would ensure a relative link, but the style tag that this generates actually is absolute with the domain:
<link rel=’stylesheet’ id=’joshuatzwp-style-css’ href=’http://joshuatz-wp.test/wp-content/themes/joshuatzwp/style.css’ type=’text/css’ media=’all’ />
Why is this happening? Well, it is not a fun answer:
WordPress do_item complexity
To figure out what was going on, I ended up tracing backwards from where the <script> and <link> tags are actually echoed out into the page, to see how that HTML is generated. Here is a rough overview of what that looks like:
- Both the script class (wp-includes/class.wp-scripts.php) and the style class (wp-includes/class.wp-styles.php) have a method called
do_item()
that is responsible for actually processing each enqueued “dependency” and echoing it out- In class.wp-scripts.php, there is inline code that prefixes any relative URL with
$base_url
- In class.wp-styles.php, it uses a function called
_css_href()
, which also prefixes relative URLs with$base_url
- In class.wp-scripts.php, there is inline code that prefixes any relative URL with
- The
$base_url
, which is a member of the shared extended class for both scripts and styles, and used to prefix URLs above, is actually filled with a value in wp-includes/script-loader.php- This value ends up either being the result of
site_url()
, orwp_guess_url()
site_url()
pulls it fromget_site_url()
, which pulls it fromget_option('siteurl')
, which comes from your admin settingswp_guess_url()
checks the global constant defined ‘WP_SITEURL’, and if that fails, tries to scrape it off the incoming request
- This value ends up either being the result of
None of this knowledge was strictly necessary to force relative paths, but it gives some insight into how the whole process works. I wouldn’t actually want to change any of these files, since they are part of the core of WP, and would be overwitten on a system update anyways.
The solution for relative paths:
The solution here is to hook into a WordPress filter. I’ve written about this before, when trying to add async/defer attributes to scripts and style tags, in this post. In fact, our solution is going to look pretty similar.
Basically, we will hook into the filter for ‘script_loader_tag’, which is the filter for <script> tags, and ‘style_loader_tag’, which is the filter for stylesheet <link> tags. In our hook, we will take the source that WordPress has prefixed and forced into an absolute URL, and remove the domain part that they added.
Here is the code:
function makeInternalLinkRelative($src){
return str_replace(get_option('siteurl'),'',$src);
}
add_filter('style_loader_src',function($src, $handle){
return makeInternalLinkRelative($src);
},10,4);
add_filter('script_loader_src',function($src, $handle){
return makeInternalLinkRelative($src);
},10,4);
Pretty simple, huh! Just add this code to your theme’s (or child theme’s) function.php file, and you should be good to go!
The Solution for Dynamic Domains
If you don’t want to use relative paths, but just want WordPress to know when you are serving it through a different domain (such as when using Ngrok or swapping domains) and automatically adjust all the links, there is another solution we can employ. We can modify the wp-config.php
file in the root of our WordPress install to dynamically set the “siteurl”, which as my earlier research pointed out, is used internally by WP to auto-prefix relative links.
Here is what I have added to my wp-config.php file:
$host = $_SERVER['HTTP_HOST'];
$protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT']===443)) ? 'https://' : 'http://';
define('WP_SITEURL', $protocol . $host);
define('WP_HOME', $protocol . $host);
Now, here is the really important thing. Make sure this code comes before this line of code that should already be in the file:
require_once(ABSPATH . 'wp-settings.php');
This matters because wp-settings.php is responsible for the chain of events that fills in the $base_url variable we were looking at earlier, and is ultimately responsible for prefixing URLs. If that runs before you set the “siteurl” global, it will have the domain from your admin settings, not from our dynamic override. I found this out the hard way!
Thanks Joshua, that worked for me.
I have since swapped it for this plugin, https://wordpress.org/plugins/make-paths-relative/ as I need all the permalinks to be relative too.
For security and performance I’m making my site static, as it’s basic text blog. I’m planning to run a local copy of wordpress, then export the files with https://wordpress.org/plugins/simply-static/. From reading it looks like the permalinks are there for feeds mainly. The plugin allows me to export all the links as absolute, so I won’t have a problem there.
Hi Matt,
Thanks for the tips! The `make-paths-relative` plugin looks like a really nice configurable option. I took a peek at their code, and it’s nice to see their code seems to take some similar approaches as well.
Thanks for the static site tips too! I’ve been experimenting a little with static sites myself for cheatsheets.joshuatz.com, but I’m using a pure Markdown repository (hosted out of Github) as the content source, and GatsbyJS to transform it into static files. That looks like an interesting plugin!
“Now, here is the really important thing. Make sure this code comes before this line of code that should already be in the file”
LIFE SAVER. I was banging my head on my desk trying to figure out why my site kept loading resources from my internal admin URL and not my CDN. THANK YOU!!
You’re welcome! It definitely took me a while to find that issue when I ran into it haha