How to rewrite and shorten WordPress uploads files URLs?

The snippet described below is not currently implemented in Permalink Manager plugin, but I have decided to share the snippet, so it can be easily reused and adjusted by anyone who wants to change the strutcture of WordPress media files permalinks.

Table of contents

    What does it change?

    The code featured in this post allows to shorten and adjust the URLs of media files stored in WordPress Media Library. By default all attachments are stored in wp-content/uploads folder, where all the uploaded files are organized into month- and year-based subfolders, e.g.

    The hollowing snippet allows to change/rewrite the uploads URLs (without moving them) to match the new permastructure, where all the attachments mimic a different directory tree, e.g.

    It might be especially helpful if you would like to mask, rewrite or hide the original permalinks of Media Library items (e.g. for security reasons) or make the attachment files URLs more SEO-friendly.

    Will it break my website?

    Theoretically, it should not, but it may work incorrectly if you are using the same filename for multiple uploads in Media Library (single URL will point to multiple attachments then). Please also note that in some specific cases, implementing this code to your website may slightly decrease its performance and break the behavior of some WordPress plugins using PHP file-handling functions (e.g. it will not be compatible with Aqua-Resizer class).

    Therefore, before you decide to use this snippet, I would recommend to test the performance and ensure that it does not harm your WordPress installation.
    You can revert at anytime the changes, by disabling or removing the snippet. No MySQL data will be altered with this code functional.

    How to implement it?

    You can paste the snippet directly to your theme’s functions.php file or create a mini plugin using the code and following the instructions. If you need any help with integrating this code with your website and you are not a WordPress developer, please contact me via email for a commercial support.

    Step 1. Rewrite uploads directory and check if request URL matches the new permastructure

    Firstly, we will need to define the new directory (e.g. media) that will be used to rewrite/mask the uploads URLs. detect if parsed URL could be treated as a media file attachment. Then, we need to define three functions:

    • first one bis_findfile() will be used to search for the requested file in wp-content/uploads directories
    • the second one bis_get_allowed_extensions() will contain the list of all allowed attachment extensions – the files with different extensions will be ignored
    • the third one bis_detect_image() will check if the correct URL was requested and display the attachment’s contents in the end.

    The simplest and most intuitive solution for examining the requested URLs would be to use plain REGEX rule (regular expressions) and probe the requested URL to check if it ends with specific extension (one of MIME types specified in $mime_types variable).

    Please also do not forget to call for two globals $wp and $wpdb inside bis_findfile() function.

    // You can change the directory name, but please always keep slash in the end.
    DEFINE('NEW_MEDIA_DIR', 'media/');
    // If you want to make the uploads URLs look like:
    // use this instead:
    // DEFINE('NEW_MEDIA_DIR', '');
    function bis_findfile($pattern, $flags = 0) {
     	$files = glob($pattern, $flags);
     	foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
     		$files = array_merge($files, bis_findfile($dir.'/'.basename($pattern), $flags));
     	return $files;
    function bis_get_allowed_extensions() {
    	return array('jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'pdf');
    function bis_detect_image($request) {
    	global $wp, $wpdb;
    	// Allowed MIME types
    	$mime_types_array = bis_get_allowed_extensions();
    	$mime_types = implode("|", $mime_types_array);
     	// Prepare the new directory name for REGEX rule
     	$new_media_dir = preg_quote(NEW_MEDIA_DIR, '/');
    	// Check if requested file is an attachment
    	preg_match("/{$new_media_dir}(.+)\.({$mime_types})/", $wp->request, $is_file);
    	if(!empty($is_file)) {
     	// ... Step 2 & 3 ...

    Step 2. Check if the requested file exists

    To improved the code performance, in the begining we have to make sure that the requested file is stored in WordPress uploads directories and if the filename is used by any attachment uploaded with WordPress Media Library.

    Theoretically we should always use glob() (native PHP function) which would be the most clever way to detect the files’ URLs, but it may not recognize the filenames containing non-ASCII characters (e.g. letters with accents). Therefore, we will check if the filename contains them and use SQL formula for non-standard filenames. It is not a perfect solution, but it is the most generic way to find the files on servers that use various filename encoding. The side effect is that it may affect the pageload time, especially if your database is pretty large, but it applies only to HTTP requests for attachments with non-ASCII characters.

    As you may notice, we defined a variable named $upload_dir. It will be used as a part of LIKE statement to narrow the SQL formula and also in glob() PHP function used in bis_findfile() function.

    For all non-ASCII filename, where we need to use SQL formula, we need to check if the requested URL refers to thumbnail’s file. If it does, we need to separate the thumbnails suffix (e.g. -200x400) from original filenames, before we search for the real path of attachment in the MySQL database.

    All thumbnails filenames ends with the dimensions suffix – for instance, the thumbnail for dummy_image.jpg (original filename) resized to 200x400px will be stored in the server as dummy_image-200x400.jpg. The thumbnail’s dimensions suffix is removed from the filename because WordPress stores in wp_posts table the URLs only for the original files and not for the thumbnails. When the original file (no thumbnail) is found, the suffix is appended back to the original path of found attachment file.

    To sum up, for standard filenames (e.g. abecadlo.jpg) we will use bis_findfile() function based on PHP’s native glob() function. Sometimes, the filenames contain e.g. letters with accents, e.g. ą, ę, ś, ż (e.g. ąbęćądłó.jpg) and then we would need to use SQL formula to find the attachment paths in wp_posts table (guid column, to be more specific).

    // Get the uploads dir used by WordPress to host the media files
    $upload_dir = wp_upload_dir();
    // Decode the URI-encoded characters
    $filename = basename(urldecode($wp->request));
    // Check if filename contains non-ASCII characters. If does, use SQL to find the file on the server
    if(preg_match('/[^\x20-\x7f]/', $filename)) {
    	// Check if the file is a thumbnail
    	preg_match("/(.*)(-[\d]+x[\d]+)([\S]{3,4})/", $filename, $is_thumb);
    	// Prepare the pattern
    	$pattern = "{$upload_dir['baseurl']}/%/{$filename}";
    	// Use the full size URL in SQL query (remove the thumb appendix)
    	$pattern = (!empty($is_thumb[2])) ? preg_replace("/(-[\d]*x[\d]*)/", "", "{$upload_dir['baseurl']}/%/{$filename}") : $pattern;
    	$file_url = $wpdb->get_var($wpdb->prepare("SELECT guid FROM $wpdb->posts WHERE guid LIKE %s", $pattern));
    	if(!empty($file_url)) {
    		// Replace the URL with DIR
    		$file_dir = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_url);
    		// Get the original path
    		$file_dir = (!empty($is_thumb[2])) ? str_replace($is_thumb[1], "{$is_thumb[1]}{$is_thumb[2]}", $file_dir) : $file_dir;
    } else {
    	// Prepare the pattern
    	$pattern = "{$upload_dir['basedir']}/*/{$filename}";
    	$found_files = bis_findfile($pattern);
    	// Get the original path if file is found
    	$file_dir = (!empty($found_files[0])) ? $found_files[0] : false;

    Step 3. Load the found attachment file when the “artificial” URL is requested

    Now, when the full path of the requested attachment is found, we should double check if $file_dir variable is set and it is not empty. Afterwards, we need to get the MIME type of that file using (mime_content_type() function), so we can set-up proper HTTP headers (“Content-type:”) and finally output the contents of the file with readfile() function.

    // Double check if the file exists
    if(!empty($file_dir) && file_exists($file_dir)) {
    	$file_mime = mime_content_type($file_dir);
    	// Set headers
    	header('Content-type: ' . $file_mime);

    Step 4. Filter/rewrite the WordPress uploads’ files URLs

    By now, the “artificial” URLs should work as aliases for original URLs and they both can be used at the same time. works now as an alias for

    To dynamically replace the original URLs in WordPress front-end content, we need to filter two hooks: the_content and wp_get_attachment_url and replace with REGEX the old tree structure (e.g. wp-content/uploads/2017/09) with our new “artificial” directory (media).

    function bis_shorten_media_url($attachment_url) {
    	$mime_types_array = bis_get_allowed_extensions();
    	$extension  = pathinfo($attachment_url, PATHINFO_EXTENSION);
    	// Only the selected file extension should be rewritten
    	if(in_array($extension, $mime_types_array)) {
    		$home_url = preg_quote(rtrim(get_home_url(), "/"), "/");
    		$attachment_url = preg_replace("/(?!{$home_url})(wp-content\/uploads\/[\d]{4}\/[\d]{2}\/)/ui", NEW_MEDIA_DIR, $attachment_url);
    	return $attachment_url;
    add_filter('wp_get_attachment_url', 'bis_shorten_media_url');
    add_filter('the_content', 'bis_shorten_media_url');

    Full snippet

    If you would like to support me in developing Permalink Manager, please consider buying Permalink Manager Pro
    Buy the plugin here.


    • Hi, this is working nice but mess up videos. The media file is not found anymore. So how can we change your code to prevent from rewriting url for .mp4 / .webm files? Thanks a lot!

      • Hi,

        I updated the snippet code and now only the attachments with file extensions selected in bis_get_allowed_extensions() function will be filtered.

    • Hello,
      I cannot activate the plugin. It gives the following error.

      The plugin could not be activated because it caused an unavoidable error.
      Parse error: syntax error, unexpected ‘;’, expecting ‘,’ or ‘)’ in C: \ Inetpub \ vhosts \ \ www \ wp-content \ plugins \ rewrite-wordpress-uploads \ rewrite-wordpress-uploads -urls.php on line 92

      My personal website becomes unavailable when I paste it into your theme’s functions.php file.
      I don’t have any PHP knowledge to fix this error.
      To redeem your codes, my goal is to prevent downloading of photo files from my personal website.
      When urls are rewritten / changed;
      You wrote that it will be like this. When trying to download this image file, it should not be downloaded because it is not actually physically there. Did I get it right?

      If you edit your code as a plug-in for amateurs who do not have PHP knowledge like me, we will be happy to upload it to our website as an extension.

      Thank you, good luck …

      (I don’t know English, it was translated from Turkish to English with Google Translate. I’m not sure how correctly it translated :-))

    • Hello,
      First of all, thank you for your interest.
      I uploaded the file you edited to my website and activated it without any problems.
      I uploaded a photo to the media library. The photo is added to the media library, but the photo is not visible. Photo Url×768-2.jpg gives the following error.
      Disabling the sassy-social-share plugin didn’t solve the problem either.

      Warning: is_readable(): open_basedir restriction in effect. File(C:\Inetpub\vhosts\\www/wp-content/plugins/C:\Inetpub\vhosts\\www\wp-content\plugins\sassy-social-share//languages/ is not within the allowed path(s): (C:/Inetpub/vhosts/\;C:\Windows\Temp\) in C:\Inetpub\vhosts\\www\wp-includes\l10n.php on line 741

      Fatal error: Uncaught Error: Call to undefined function mime_content_type() in C:\Inetpub\vhosts\\www\wp-content\plugins\rewrite-wordpress-uploads-urls\rewrite-wordpress-uploads-urls.php:88 Stack trace:
      #0 C:\Inetpub\vhosts\\www\wp-includes\class-wp-hook.php(287): bis_detect_image(Array)
      #1 C:\Inetpub\vhosts\\www\wp-includes\plugin.php(206): WP_Hook->apply_filters(Array, Array)
      #2 C:\Inetpub\vhosts\\www\wp-includes\class-wp.php(379): apply_filters(‘request’, Array)
      #3 C:\Inetpub\vhosts\\www\wp-includes\class-wp.php(745): WP->parse_request(”)
      #4 C:\Inetpub\vhosts\\www\wp-includes\functions.php(1285): WP->main(”)
      #5 C:\Inetpub\vhosts\\www\wp-blog-header.php(16): wp()
      #6 C:\Inetpub\vhosts\\www\index.php(17): require(‘C:\\Inetpub\\vhos…’)
      #7 {main} thrown in C:\Inetpub\vhosts\\www\wp-content\plugins\rewrite-wordpress-uploads-urls\rewrite-wordpress-uploads-urls.php on line 88

      A critical error has occurred on your website.

    • Hello Maciej,
      How can I tell if “Fileinfo” PHP extension is enabled. Server PHP Version 7.1.33

      Sorry to keep you busy ..

    • Hello Maciej,
      The website is on windows server. Server officials said that I need to move my website to Linux server for “Fileinfo” PHP extension. This is not possible for me now. So I’m so sorry I can’t use your codes. I think there must be another solution.

      Still, thank you very much for your attention.
      Come easy, I wish you success.

    Leave a Reply

    Your email address will not be published. Required fields are marked *