How to redirect WordPress media file URLs to custom permalink structure

I’m trying to change how WordPress handles attachment URLs but running into issues. Right now my site uses the default format with attachment_id parameter, but I want to redirect these to a cleaner URL structure.

Current URL format: example.com/?attachment_id=123
Desired URL format: example.com/files/attachment-name

I’ve been working with this code but it’s not functioning properly:

// Create custom rewrite rule
function media_url_rewrite( $rewrite ) {
    $new_rule = array(
        'files/(.+)' => 'index.php?attachment=' . $rewrite->preg_index(1)
    );
    
    $rewrite->rules = $new_rule + $rewrite->rules;
}
add_filter( 'generate_rewrite_rules', 'media_url_rewrite' );

// Handle redirection from old URLs
function handle_attachment_redirect() {
    global $wp;
    
    if( !preg_match( '/^files\/.*$/', $wp->request ) && isset( $wp->query_vars['attachment'] ) ) {
        wp_redirect( site_url( '/files/' . $wp->query_vars['attachment'] ), 301 );
        exit();
    }
}
add_filter( 'template_redirect', 'handle_attachment_redirect' );

// Modify attachment permalink structure
function custom_attachment_permalink( $url, $id ){
    $attachment = get_post( $id );
    return home_url( '/files/' . $attachment->post_name );
}
add_filter( 'attachment_link', 'custom_attachment_permalink', 15, 2 );

The goal is to automatically redirect old attachment URLs to the new format. Any suggestions on what might be wrong with this approach?

The Problem:

You’re trying to redirect WordPress attachment URLs from the default format (example.com/?attachment_id=123) to a cleaner format (example.com/files/attachment-name), but your current code isn’t working correctly. The core issue is an over-reliance on manual PHP rewrite rule manipulation within the WordPress framework, leading to conflicts and inconsistencies. Debugging these types of redirects within WordPress can be extremely challenging.

:thinking: Understanding the “Why” (The Root Cause):

WordPress has a robust, but sometimes complex, system for managing URLs and redirects. Directly manipulating the generate_rewrite_rules filter and attempting to handle redirects entirely within PHP often leads to conflicts and unexpected behavior. This approach often bypasses crucial WordPress internal functions, resulting in failures. The system expects a coordinated effort between .htaccess rules (or equivalent server configurations) and WordPress’s internal rewrite engine.

Your approach tries to manage both the rewrite rules and the redirect logic manually, leading to potential timing issues and conflicts with WordPress’s internal mechanisms. It’s more efficient and robust to leverage existing WordPress functionality or to handle the redirects outside of the core WordPress routing system.

:gear: Step-by-Step Guide:

  1. Automate Redirects Outside WordPress: Instead of modifying WordPress’s internal rewrite rules directly, build a separate system to handle the redirects. This could be a simple script (e.g., using PHP, Python, or Node.js) that runs periodically or is triggered by events. This script would:

    • Monitor incoming requests for the old attachment URL format (example.com/?attachment_id=123).
    • Extract the attachment_id.
    • Use the WordPress API (or direct database query) to retrieve the corresponding attachment’s filename.
    • Construct the new URL (example.com/files/attachment-name).
    • Perform a 301 redirect to the new URL.
  2. Implement the Redirect Script: The following example demonstrates a basic PHP script approach. Remember to replace placeholders with your actual database credentials and paths. This script should be placed outside your WordPress installation and accessible to your webserver.

<?php
// Database credentials
$db_host = 'your_db_host';
$db_user = 'your_db_user';
$db_pass = 'your_db_password';
$db_name = 'your_db_name';

// Connect to database
$conn = new mysqli($db_host, $db_user, $db_pass, $db_name);
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

// Get the attachment ID from the URL
$attachment_id = isset($_GET['attachment_id']) ? intval($_GET['attachment_id']) : 0;

if ($attachment_id > 0) {
    // Query the database to get the attachment filename
    $sql = "SELECT post_name FROM wp_posts WHERE ID = $attachment_id AND post_type = 'attachment'";
    $result = $conn->query($sql);

    if ($result && $result->num_rows > 0) {
        $row = $result->fetch_assoc();
        $attachment_name = $row['post_name'];
        $new_url = home_url('/files/' . $attachment_name);

        // Perform the 301 redirect
        header("HTTP/1.1 301 Moved Permanently");
        header("Location: $new_url");
        exit();
    }
}

// If no attachment_id or attachment not found, handle it appropriately (e.g., 404).
$conn->close();
?>
  1. Configure Your Web Server: Set up your web server (Apache or Nginx) to route requests for the old URL format to this script. This usually involves adding a rewrite rule in your server’s configuration file. The exact configuration depends on your server setup.

  2. Testing: Thoroughly test your redirect script with different attachment IDs to ensure it functions correctly. Monitor your server logs to check for any errors or unexpected behavior.

:mag: Common Pitfalls & What to Check Next:

  • Database Connection: Ensure your database credentials are correct and that the script has the necessary permissions to access the WordPress database.
  • Error Handling: Implement robust error handling in your script to gracefully handle cases where the attachment ID is invalid or the attachment is not found.
  • File Paths: Double-check the paths to your script and any files it uses.
  • Caching: Clear any caching mechanisms (browser cache, server cache, CDN cache) to ensure you’re seeing the latest redirects.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

switch to wp_query instead of global $wp - it’s more compatible. your attachment redirect function works, but you need to add flush_rewrite_rules() after registering new rules or they won’t kick in. also make sure your .htaccess file is writable since WordPress has to update it for custom permalinks to function.

Your rewrite rules have timing issues and aren’t handling query variables correctly. I ran into the exact same problem migrating a media-heavy site two years ago. You’re using generate_rewrite_rules which bypasses WordPress’s standard system and conflicts with attachment queries. Don’t manipulate the rewrite object directly. Instead, register your rules during the init action: add_rewrite_rule('files/([^/]+)/?$', 'index.php?attachment_id=$matches[1]', 'top'). Use attachment_id instead of attachment - that’s what WordPress expects. Your redirect function needs fixing too. Check for the old format first using get_query_var('attachment_id') instead of accessing query vars through the global wp object. Flush permalinks once after making these changes or the new rules won’t work and you’ll keep getting 404s.

Your main issue is how you’re handling rewrite rule generation. The generate_rewrite_rules filter doesn’t work like you’ve set it up. You need to flush rewrite rules after adding custom rules, and your parameter matching is wrong too. I dealt with something similar last year when restructuring media URLs for a client. What fixed it was using add_rewrite_rule() in the init hook instead of trying to modify the rewrite object directly. Also, you’re using attachment as the query var but WordPress expects attachment_id for attachment queries. Your redirect logic might create loops too. Check if the current request already uses the old attachment_id format before redirecting. Otherwise you’ll get redirect chains that mess up SEO and user experience. Don’t forget to flush permalinks in admin after you make these changes, and test both direct media file access and attachment page URLs.

Your rewrite rule syntax is wrong, and you’re not handling query variables properly. $rewrite->preg_index(1) isn’t how you access capture groups in WordPress rewrites. Use add_rewrite_rule() with proper query variables during initialization instead. I hit this same issue migrating a photography site last year. What worked: create a proper rewrite endpoint first, then handle redirects separately. You’ve also got a timing problem - your rewrite rules aren’t flushing properly, so WordPress doesn’t recognize the new URL structure. Wrap your rewrite additions in an init action hook and call flush_rewrite_rules() once after activation. Make sure your attachment query variable matches what WordPress expects internally. If it doesn’t, redirects fail silently and users get 404s instead of media content.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.