没有不值得去解决的问题,也没有不值得去学习的技术!

Migrate WordPress Admin to a Subdomain to Bypass EdgeOne and Fix 524 Timeouts

作者:

,

Previously, I troubleshot a failed WordPress core online upgrade. At that time, the upgrade request went through EdgeOne, and the long-running backend request eventually timed out, leaving core_updater.lock in the database.

Previous record:

Recently, I migrated the English site to:

Plaintext
en.shuijingwanwq.com

and migrated the images and attachments in the uploads directory to:

Plaintext
media.shuijingwanwq.com

Both of these domains currently use Cloudflare, while the Chinese main site continues to be served through EdgeOne.

I originally thought that after completing these two splits, EdgeOne’s traffic pressure would decrease significantly. However, judging from the statistics on July 12, 2026, www.shuijingwanwq.com still generated approximately:

Plaintext
L7 访问流量:2.9 GB
EdgeOne 响应流量:2.8 GB
客户端请求流量:102.66 MB
L7 访问请求数:13.8 万次
[Figure 1, EdgeOne metric analysis showing 2.9 GB of L7 access traffic in a single day]
[Figure 1, EdgeOne metric analysis showing 2.9 GB of L7 access traffic in a single day]

Migrating the backend cannot directly eliminate this 2.9 GB of frontend traffic, because the vast majority of it still comes from Chinese site pages, static files, search engines, and other visitor requests.

However, this at least shows that there is no longer any need to continue routing WordPress backend operations through EdgeOne.

Combined with the previous core upgrade timeout issue, I ultimately decided to migrate the WordPress backend to an independent subdomain:

Plaintext
admin.shuijingwanwq.com

This article fully documents this migration process, including the Nginx virtual host, W3 Total Cache compatibility, WordPress URL rewriting, multilingual domain unified redirection, frontend AJAX interface protection, and the final configuration files used in the production environment.


1. Final Architecture

The access structure after completing the migration is as follows:

Plaintext
www.shuijingwanwq.com

     EdgeOne

WordPress 中文前台


en.shuijingwanwq.com

    Cloudflare

WordPress 英文前台


media.shuijingwanwq.com

    Cloudflare

wp-content/uploads


admin.shuijingwanwq.com

Cloudflare DNS Only
不经过 EdgeOne 或 Cloudflare 代理

独立 Nginx 虚拟主机

PHP-FPM / WordPress 后台

It should be specifically noted that:

Plaintext
admin 使用独立 Nginx 虚拟主机
但不复制第二套 WordPress 程序

admin, www, and en ultimately still share the same WordPress directory:

Plaintext
/data/wwwroot/www.shuijingwanwq.com

This is because backend operations still require access to the same set of:

Plaintext
wp-admin
wp-includes
wp-content
wp-login.php
wp-config.php

The independent virtual host is responsible for the access entry, certificates, logs, and security policies, not for duplicating the application.


2. Why Not Directly Modify WordPress siteurl

The addresses in the WordPress database remain unchanged:

Plaintext
home:
https://www.shuijingwanwq.com

siteurl:
https://www.shuijingwanwq.com

I did not change siteurl to:

Plaintext
https://admin.shuijingwanwq.com

The reason is that directly modifying siteurl could simultaneously affect:

  • Chinese frontend resource addresses;
  • Polylang multidomain recognition;
  • REST API;
  • Plugin and theme resources;
  • W3 Total Cache cache keys;
  • WP-Cron;
  • Yoast SEO;
  • Login and logout redirections;
  • English site resource addresses.

The approach adopted this time is:

Plaintext
Nginx
负责前台域名下旧后台入口的统一跳转

WordPress MU 插件
负责 WordPress 自己生成的后台 URL 和资源地址

数据库 home/siteurl
保持原值不变

3. Create admin DNS Record

Add the following in Cloudflare:

Plaintext
类型:A
名称:admin
IPv4 地址:源站公网 IP
代理状态:仅 DNS
TTL:自动

The grey cloud must be maintained, which is:

Plaintext
DNS Only

Otherwise, backend requests will still go through the Cloudflare proxy, failing to achieve the goal of directly accessing the origin server.

Server-side resolution verification:

Bash
getent ahostsv4 admin.shuijingwanwq.com | awk 'NR==1{print $1}'

Once the origin server’s public IP is returned, it means the resolution has taken effect.


4. Use OneinStack to Create an Independent Virtual Host

I used OneinStack’s built-in vhost.sh to create the virtual host:

Bash
/root/oneinstack/vhost.sh

The main options are as follows:

Plaintext
SSL 类型:
Use Let's Encrypt to Create SSL Certificate and Key

域名:
admin.shuijingwanwq.com

网站目录:
/data/wwwroot/www.shuijingwanwq.com

添加更多域名:
n

HTTP 跳转 HTTPS:
y

证书类型:
ec-256

防盗链:
n

Rewrite:
y

Rewrite 类型:
wordpress

访问日志:
y

Here, I directly set the website directory to the existing WordPress directory:

Plaintext
/data/wwwroot/www.shuijingwanwq.com

OneinStack ultimately created:

Plaintext
Nginx 配置:
/usr/local/nginx/conf/vhost/admin.shuijingwanwq.com.conf

证书:
/usr/local/nginx/conf/ssl/admin.shuijingwanwq.com.crt

私钥:
/usr/local/nginx/conf/ssl/admin.shuijingwanwq.com.key

访问日志:
/data/wwwlogs/admin.shuijingwanwq.com_nginx.log
[Figure 2, OneinStack successfully created the admin virtual host and ECC certificate]
[Figure 2, OneinStack successfully created the admin virtual host and ECC certificate]

5. First Test: WordPress Automatically Redirects Back to www

After the virtual host was set up, I directly tested:

Bash
for u in \
"http://admin.shuijingwanwq.com/" \
"https://admin.shuijingwanwq.com/" \
"https://admin.shuijingwanwq.com/wp-login.php" \
"https://admin.shuijingwanwq.com/wp-admin/"
do
  echo
  echo "========== $u =========="

  curl -sS -o /dev/null -D - \
    --max-time 20 \
    --resolve admin.shuijingwanwq.com:80:127.0.0.1 \
    --resolve admin.shuijingwanwq.com:443:127.0.0.1 \
    "$u" \
  | grep -Ei \
    'HTTP/|location:|set-cookie:|x-redirect-by:|content-type:|server:'
done

The results were:

Plaintext
http://admin.../
→ 301 到 https://admin.../

https://admin.../
→ 301 到 https://www.shuijingwanwq.com/

https://admin.../wp-login.php
→ 301 到 https://www.shuijingwanwq.com/wp-login.php

https://admin.../wp-admin/
→ 301 到 https://www.shuijingwanwq.com/wp-admin/

The response showed:

Plaintext
x-redirect-by: WordPress

However, this was not caused solely by the WordPress core.

I had encountered the same issue previously when migrating the English site from /en/ to an independent domain. It was ultimately confirmed to be caused by W3 Total Cache Page Cache’s:

Plaintext
redirect_on_foreign_domain

which redirected any Host not in the allowed list back to the main site domain.


6. Let W3 Total Cache Allow the admin Domain

There was already an MU plugin:

Plaintext
wp-content/mu-plugins/w3tc-polylang-domain-fix.php

It reads language domains from the Polylang configuration and removes W3TC’s cross-domain redirection.

Since admin.shuijingwanwq.com is not a Polylang language domain, it needs to be explicitly added to the allowed list.

The final complete file is as follows.

wp-content/mu-plugins/w3tc-polylang-domain-fix.php

PHP
<?php
/*
Plugin Name: W3TC Polylang Domain Fix
Description: Prevents W3 Total Cache Page Cache from redirecting valid Polylang language domains back to the main home_url.
*/

add_action('init', function () {
    $host = $_SERVER['HTTP_HOST'] ?? '';

    if ($host === '') {
        return;
    }

    $polylang = get_option('polylang');
    $domains  = $polylang['domains'] ?? array();

    $allowed_hosts = array(
        'admin.shuijingwanwq.com',
    );

    foreach ((array) $domains as $domain) {
        if (!empty($domain)) {
            $parsed = wp_parse_url($domain);

            if (!empty($parsed['host'])) {
                $allowed_hosts[] = strtolower($parsed['host']);
            }
        }
    }

    if (!in_array(strtolower($host), $allowed_hosts, true)) {
        return;
    }

    global $wp_filter;

    if (empty($wp_filter['init']) || empty($wp_filter['init']->callbacks)) {
        return;
    }

    foreach ($wp_filter['init']->callbacks as $priority => $callbacks) {
        foreach ($callbacks as $callback) {
            $function = $callback['function'] ?? null;

            if (
                is_array($function)
                && isset($function[0], $function[1])
                && is_object($function[0])
                && $function[1] === 'redirect_on_foreign_domain'
                && get_class($function[0]) === 'W3TC\PgCache_Plugin'
            ) {
                remove_action('init', $function, $priority);
            }
        }
    }
}, 0);

After the modification, I tested again:

Plaintext
https://admin.shuijingwanwq.com/
→ 200

https://admin.shuijingwanwq.com/wp-login.php
→ 200

W3TC’s cross-domain redirection has been lifted.


7. WordPress-Generated Login URLs Still Point to www

After resolving the W3TC issue, although the login page could now be opened via admin, the login form was still:

HTML
<form
  name="loginform"
  id="loginform"
  action="https://www.shuijingwanwq.com/wp-login.php"
  method="post"
>

The lost password link also pointed to:

Plaintext
https://www.shuijingwanwq.com/wp-login.php?action=lostpassword

Accessing while logged out:

Plaintext
https://admin.shuijingwanwq.com/wp-admin/

would redirect to:

Plaintext
https://www.shuijingwanwq.com/wp-login.php

This part needs to be resolved using WordPress URL filters.


8. Create a Backend Domain Compatibility MU Plugin

I created:

Plaintext
wp-content/mu-plugins/swq-admin-domain.php

This plugin is responsible for:

  • Changing the backend login URL to admin;
  • Changing the backend admin links to admin;
  • Changing backend CSS, JavaScript, and fonts to admin;
  • Changing the backend REST API to admin;
  • Changing backend plugin and theme resources to admin;
  • Dynamically reading newly added language domains from the Polylang configuration;
  • Preserving the frontend’s own admin-ajax.php;
  • Preserving the frontend’s own admin-post.php;
  • Preserving the postpass submission URL for password-protected posts;
  • Keeping uploaded files using media.shuijingwanwq.com.

Below is the final organized complete version. It is logically consistent with what has already been tested in the current production environment, with only comments and formatting cleaned up.

wp-content/mu-plugins/swq-admin-domain.php

PHP
<?php
/*
Plugin Name: SWQ Admin Domain
Description: Routes WordPress login, administration resources and backend APIs through admin.shuijingwanwq.com without changing the database home or siteurl options.
Version: 1.0.0
*/

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if ( ! defined( 'SWQ_ADMIN_BASE_URL' ) ) {
    define( 'SWQ_ADMIN_BASE_URL', 'https://admin.shuijingwanwq.com' );
}

if ( ! defined( 'SWQ_ADMIN_HOST' ) ) {
    define( 'SWQ_ADMIN_HOST', 'admin.shuijingwanwq.com' );
}

/**
 * Normalize an HTTP host for comparison.
 */
function swq_admin_domain_normalize_host( $host ) {
    $host = strtolower( trim( (string) $host ) );
    $host = preg_replace( '/:\d+$/', '', $host );

    return rtrim( $host, '.' );
}

/**
 * Return all WordPress frontend and administration hosts.
 *
 * Future Polylang language domains are read automatically from the
 * Polylang option after they have been configured in WordPress.
 */
function swq_admin_domain_get_managed_hosts() {
    static $hosts = null;

    if ( $hosts !== null ) {
        return $hosts;
    }

    $hosts = array(
        'shuijingwanwq.com'     => true,
        'www.shuijingwanwq.com' => true,
        SWQ_ADMIN_HOST          => true,
    );

    foreach ( array( 'home', 'siteurl' ) as $option_name ) {
        $option_url = get_option( $option_name );

        if ( is_string( $option_url ) && $option_url !== '' ) {
            $option_host = wp_parse_url(
                $option_url,
                PHP_URL_HOST
            );

            $option_host = swq_admin_domain_normalize_host(
                $option_host
            );

            if ( $option_host !== '' ) {
                $hosts[ $option_host ] = true;
            }
        }
    }

    $polylang = get_option( 'polylang' );

    $domains = is_array( $polylang )
        ? ( $polylang['domains'] ?? array() )
        : array();

    foreach ( (array) $domains as $domain ) {
        if ( ! is_string( $domain ) || $domain === '' ) {
            continue;
        }

        $domain_host = wp_parse_url(
            $domain,
            PHP_URL_HOST
        );

        $domain_host = swq_admin_domain_normalize_host(
            $domain_host
        );

        if ( $domain_host !== '' ) {
            $hosts[ $domain_host ] = true;
        }
    }

    /*
     * The uploads hostname is intentionally independent and must not be
     * rewritten to the administration hostname.
     */
    unset( $hosts['media.shuijingwanwq.com'] );

    return $hosts;
}

/**
 * Determine whether a host belongs to the WordPress/Polylang installation.
 */
function swq_admin_domain_is_managed_host( $host ) {
    $host  = swq_admin_domain_normalize_host( $host );
    $hosts = swq_admin_domain_get_managed_hosts();

    return $host !== '' && isset( $hosts[ $host ] );
}

/**
 * Replace the origin of a managed WordPress URL while preserving its
 * path, query string and fragment.
 */
function swq_admin_domain_replace_origin( $url ) {
    if ( ! is_string( $url ) || $url === '' ) {
        return $url;
    }

    $parts = wp_parse_url( $url );

    if (
        ! is_array( $parts )
        || empty( $parts['host'] )
        || ! swq_admin_domain_is_managed_host( $parts['host'] )
    ) {
        return $url;
    }

    $port = isset( $parts['port'] )
        ? ':' . (int) $parts['port']
        : '';

    $pattern = '#^https?://'
        . preg_quote( $parts['host'], '#' )
        . preg_quote( $port, '#' )
        . '(?=/|$)#i';

    return preg_replace(
        $pattern,
        SWQ_ADMIN_BASE_URL,
        $url,
        1
    );
}

/**
 * Return the path portion of a relative WordPress URL.
 */
function swq_admin_domain_get_path_only( $path ) {
    $path     = ltrim( (string) $path, '/' );
    $position = strpos( $path, '?' );

    if ( $position !== false ) {
        $path = substr( $path, 0, $position );
    }

    return $path;
}

/**
 * Return the action query argument from a relative WordPress URL.
 */
function swq_admin_domain_get_url_action( $path ) {
    $path     = (string) $path;
    $position = strpos( $path, '?' );

    if ( $position === false ) {
        return '';
    }

    $query = array();

    parse_str(
        substr( $path, $position + 1 ),
        $query
    );

    return strtolower(
        (string) ( $query['action'] ?? '' )
    );
}

/**
 * Determine whether a backend-looking path is also a public frontend
 * endpoint and therefore must remain on the frontend hostname.
 */
function swq_admin_domain_is_public_frontend_endpoint( $path ) {
    $path_only = swq_admin_domain_get_path_only( $path );

    if (
        in_array(
            $path_only,
            array(
                'wp-admin/admin-ajax.php',
                'wp-admin/admin-post.php',
            ),
            true
        )
    ) {
        return true;
    }

    return (
        $path_only === 'wp-login.php'
        && swq_admin_domain_get_url_action( $path ) === 'postpass'
    );
}

/**
 * Determine whether a site URL belongs to login or administration.
 */
function swq_admin_domain_is_backend_url(
    $path,
    $scheme = null
) {
    if ( swq_admin_domain_is_public_frontend_endpoint( $path ) ) {
        return false;
    }

    $path_only = swq_admin_domain_get_path_only( $path );

    if (
        in_array(
            $scheme,
            array(
                'login',
                'login_post',
                'admin',
            ),
            true
        )
    ) {
        return true;
    }

    return (
        $path_only === 'wp-login.php'
        || $path_only === 'wp-admin'
        || strpos( $path_only, 'wp-admin/' ) === 0
    );
}

/**
 * Determine whether the current request uses the dedicated admin host.
 */
function swq_admin_domain_is_current_request() {
    $host = swq_admin_domain_normalize_host(
        $_SERVER['HTTP_HOST'] ?? ''
    );

    return $host === SWQ_ADMIN_HOST;
}

/**
 * Filter site_url().
 *
 * On the admin host, WordPress application URLs must use the admin host.
 * On frontend hosts, only true login and management URLs are rewritten.
 */
function swq_admin_domain_filter_site_url(
    $url,
    $path,
    $scheme,
    $blog_id
) {
    if (
        swq_admin_domain_is_current_request()
        || swq_admin_domain_is_backend_url(
            $path,
            $scheme
        )
    ) {
        return swq_admin_domain_replace_origin( $url );
    }

    return $url;
}

add_filter(
    'site_url',
    'swq_admin_domain_filter_site_url',
    999,
    4
);

/**
 * Filter network_site_url(), including lost-password URLs.
 */
function swq_admin_domain_filter_network_site_url(
    $url,
    $path,
    $scheme
) {
    if (
        swq_admin_domain_is_current_request()
        || swq_admin_domain_is_backend_url(
            $path,
            $scheme
        )
    ) {
        return swq_admin_domain_replace_origin( $url );
    }

    return $url;
}

add_filter(
    'network_site_url',
    'swq_admin_domain_filter_network_site_url',
    999,
    3
);

/**
 * Replace a generated URL origin with the current managed frontend host.
 */
function swq_admin_domain_replace_origin_with_current_host( $url ) {
    if ( ! is_string( $url ) || $url === '' ) {
        return $url;
    }

    $host = swq_admin_domain_normalize_host(
        $_SERVER['HTTP_HOST'] ?? ''
    );

    if (
        $host === ''
        || $host === SWQ_ADMIN_HOST
        || ! swq_admin_domain_is_managed_host( $host )
    ) {
        return $url;
    }

    $parts = wp_parse_url( $url );

    if (
        ! is_array( $parts )
        || empty( $parts['host'] )
    ) {
        return $url;
    }

    $port = isset( $parts['port'] )
        ? ':' . (int) $parts['port']
        : '';

    $pattern = '#^https?://'
        . preg_quote( $parts['host'], '#' )
        . preg_quote( $port, '#' )
        . '(?=/|$)#i';

    $origin = ( is_ssl() ? 'https' : 'http' )
        . '://'
        . $host;

    return preg_replace(
        $pattern,
        $origin,
        $url,
        1
    );
}

/**
 * Filter ordinary administration URLs.
 *
 * Public AJAX and form endpoints remain on the current frontend host.
 * Inside the dedicated admin host they use the admin hostname.
 */
function swq_admin_domain_filter_admin_url(
    $url,
    $path = '',
    $blog_id = null,
    $scheme = null
) {
    $path_only = swq_admin_domain_get_path_only( $path );

    if (
        ! swq_admin_domain_is_current_request()
        && in_array(
            $path_only,
            array(
                'admin-ajax.php',
                'admin-post.php',
            ),
            true
        )
    ) {
        return swq_admin_domain_replace_origin_with_current_host(
            $url
        );
    }

    return swq_admin_domain_replace_origin( $url );
}

add_filter(
    'admin_url',
    'swq_admin_domain_filter_admin_url',
    999,
    4
);

/**
 * Filter network administration URLs.
 */
function swq_admin_domain_filter_network_admin_url(
    $url,
    $path = '',
    $scheme = null
) {
    return swq_admin_domain_replace_origin( $url );
}

add_filter(
    'network_admin_url',
    'swq_admin_domain_filter_network_admin_url',
    999,
    3
);

/**
 * Authentication URLs.
 */
add_filter(
    'login_url',
    'swq_admin_domain_replace_origin',
    999
);

add_filter(
    'lostpassword_url',
    'swq_admin_domain_replace_origin',
    999
);

add_filter(
    'logout_url',
    'swq_admin_domain_replace_origin',
    999
);

add_filter(
    'register_url',
    'swq_admin_domain_replace_origin',
    999
);

/**
 * Rewrite final CSS and JavaScript URLs only while rendering admin.
 */
function swq_admin_domain_filter_asset_src(
    $src,
    $handle = ''
) {
    if ( ! swq_admin_domain_is_current_request() ) {
        return $src;
    }

    return swq_admin_domain_replace_origin( $src );
}

add_filter(
    'style_loader_src',
    'swq_admin_domain_filter_asset_src',
    999,
    2
);

add_filter(
    'script_loader_src',
    'swq_admin_domain_filter_asset_src',
    999,
    2
);

/**
 * Keep REST API requests generated inside wp-admin off the frontend CDN.
 */
function swq_admin_domain_filter_rest_url(
    $url,
    $path,
    $blog_id,
    $scheme
) {
    if ( ! swq_admin_domain_is_current_request() ) {
        return $url;
    }

    return swq_admin_domain_replace_origin( $url );
}

add_filter(
    'rest_url',
    'swq_admin_domain_filter_rest_url',
    999,
    4
);

/**
 * Rewrite WordPress-generated plugin, content, include and theme
 * resources while serving the dedicated administration hostname.
 */
function swq_admin_domain_filter_generated_resource_url( $url ) {
    if ( ! swq_admin_domain_is_current_request() ) {
        return $url;
    }

    return swq_admin_domain_replace_origin( $url );
}

add_filter(
    'plugins_url',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'content_url',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'includes_url',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'theme_root_uri',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'template_directory_uri',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'stylesheet_directory_uri',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'theme_file_uri',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

add_filter(
    'parent_theme_file_uri',
    'swq_admin_domain_filter_generated_resource_url',
    999
);

After saving, I ran a syntax check:

Bash
php -l wp-content/mu-plugins/swq-admin-domain.php

It returned:

Plaintext
No syntax errors detected in wp-content/mu-plugins/swq-admin-domain.php

The final server-side write and syntax check records have also been preserved.


9. Backend Resources Initially Still Went Through www

After logging into admin, although the page URL remained:

Plaintext
https://admin.shuijingwanwq.com/wp-admin/

the browser developer tools showed that many backend resources were still being loaded from:

Plaintext
www.shuijingwanwq.com

.

These included:

  • load-styles.php;
  • Plugin CSS;
  • Plugin JavaScript;
  • Font files;
  • wp-content/plugins resources;
  • Some theme resources.
[Figure 3, admin backend opened successfully, but the network panel still shows a large number of www resource requests]
[Figure 3, admin backend opened successfully, but the network panel still shows a large number of www resource requests]

Because the backend page itself was located on the admin domain, these cross-domain fonts and scripts also triggered:

Plaintext
CORS Missing Allow Origin

The correct approach was not to open CORS on www, but to ensure that backend resources themselves were loaded via admin.

The MU plugin ultimately supplemented these filters:

Plaintext
site_url
network_site_url
admin_url
network_admin_url
login_url
lostpassword_url
logout_url
register_url
style_loader_src
script_loader_src
rest_url
plugins_url
content_url
includes_url
theme_root_uri
template_directory_uri
stylesheet_directory_uri
theme_file_uri
parent_theme_file_uri

After completion, filtering in the developer tools for:

Plaintext
www.shuijingwanwq.com

and:

Plaintext
en.shuijingwanwq.com

both showed:

Plaintext
没有请求
[Figure 4, Filtering for www and en in the final backend network requests yields no results]
[Figure 4, Filtering for www and en in the final backend network requests yields no results]

10. Unify Backend Entry Points Across All Frontend Domains

It was not enough to just make admin work properly itself.

The original frontend domains could still access:

Plaintext
https://www.shuijingwanwq.com/wp-login.php
https://www.shuijingwanwq.com/wp-admin/

https://en.shuijingwanwq.com/wp-login.php
https://en.shuijingwanwq.com/wp-admin/

Previously, there was also an old rule in Nginx:

Nginx
# Redirect non-www admin/login requests to the main www domain.

It would first redirect backend requests from en and the bare domain to www.

Since the backend has been uniformly migrated to admin, this logic should be replaced with:

Plaintext
所有前台语言域名的后台管理入口

admin.shuijingwanwq.com

At the same time, three frontend interfaces must be preserved:

Plaintext
/wp-admin/admin-ajax.php
/wp-admin/admin-post.php
/wp-login.php?action=postpass

The reasons are:

  • admin-ajax.php might be called by frontend themes or plugins;
  • admin-post.php might handle frontend form submissions;
  • postpass is used for password-protected posts, and the Cookie should remain on the current frontend domain.

11. Backend Redirection Map Configuration

Create:

Plaintext
/usr/local/nginx/conf/include/shuijingwan_admin_redirect_map.conf

The complete content is as follows.

shuijingwan_admin_redirect_map.conf

Nginx
# 所有前台语言域名共用的 WordPress 后台入口判断。
#
# 以下前台接口保留在原域名:
# - /wp-admin/admin-ajax.php
# - /wp-admin/admin-post.php
# - /wp-login.php?action=postpass

map "$uri|$arg_action" $swq_frontend_admin_redirect {
    default 0;

    # 前台 AJAX 与表单接口。
    ~*^/wp-admin/admin-(?:ajax|post)\.php\| 0;

    # 密码保护文章提交接口。
    ~*^/wp-login\.php\|postpass$ 0;

    # 其他登录和后台管理入口。
    ~*^/wp-login\.php\| 1;
    ~*^/wp-admin(?:/.*)?\| 1;
}

Using map here keeps the path matching clear and makes it convenient to extend with new exception interfaces in the future.


12. Server Context Redirection Configuration

Create:

Plaintext
/usr/local/nginx/conf/include/shuijingwan_admin_redirect_server.conf

The complete content is as follows.

shuijingwan_admin_redirect_server.conf

Nginx
# 将前台语言域名下的登录和后台管理入口统一转到独立后台域名。
# 307 可保留旧登录表单 POST 请求的方法与正文。

if ($swq_frontend_admin_redirect = 1) {
    return 307 https://admin.shuijingwanwq.com$request_uri;
}

This uses:

Plaintext
307 Temporary Redirect

instead of 301 or a standard 302.

The main reason is that if a user still submits a POST form from the old login page, 307 can preserve the original request method and body, preventing the username, password, or other form contents from being lost during the redirection.


13. Final www Virtual Host Configuration

Currently, the Chinese, English, and bare domains still share:

Plaintext
/usr/local/nginx/conf/vhost/www.shuijingwanwq.com.conf

Below is the final complete content.

The original 3,900+ lines of tag mappings remain in a separate:

Plaintext
shuijingwan_nginx_redirect.conf

file, so there is no need to reiterate these site-specific rules that are not directly related to the backend migration in this article.

/usr/local/nginx/conf/vhost/www.shuijingwanwq.com.conf

Nginx
# 引入生成的 map 规则(位于 server 外部,属于 http 上下文)
include /usr/local/nginx/conf/include/shuijingwan_nginx_redirect.conf;

# 引入 en.shuijingwanwq.com 独立旧路径规则
include /usr/local/nginx/conf/include/shuijingwan_en_redirect.conf;

# 引入所有前台语言域名共用的后台入口判断
include /usr/local/nginx/conf/include/shuijingwan_admin_redirect_map.conf;

server {
  limit_req zone=site_limit burst=50 nodelay;
  limit_conn conn_limit 50;
  limit_req_status 429;

  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_certificate /usr/local/nginx/conf/ssl/www.shuijingwanwq.com.crt;
  ssl_certificate_key /usr/local/nginx/conf/ssl/www.shuijingwanwq.com.key;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;

  ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256;

  ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
  ssl_conf_command Options PrioritizeChaCha;

  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache shared:SSL:10m;
  ssl_buffer_size 2k;

  add_header Strict-Transport-Security max-age=15768000;

  ssl_stapling on;
  ssl_stapling_verify on;

  server_name
    www.shuijingwanwq.com
    shuijingwanwq.com
    en.shuijingwanwq.com;

  access_log /data/wwwlogs/www.shuijingwanwq.com_nginx.log combined;

  index index.html index.htm index.php;
  root /data/wwwroot/www.shuijingwanwq.com;

  if ($ssl_protocol = "") {
    return 301 https://$host$request_uri;
  }

  # 所有前台语言域名共用的后台入口跳转
  include /usr/local/nginx/conf/include/shuijingwan_admin_redirect_server.conf;

  if ($host = shuijingwanwq.com) {
    return 301 $scheme://www.shuijingwanwq.com$request_uri;
  }

  include /usr/local/nginx/conf/rewrite/wordpress.conf;

  #error_page 404 /404.html;
  #error_page 502 /502.html;

  location ~ .*\.(wma|wmv|asf|mp3|mmf|zip|rar|jpg|gif|png|swf|flv|mp4)$ {
    valid_referers
      none
      blocked
      *.shuijingwanwq.com
      www.shuijingwanwq.com
      shuijingwanwq.com;

    if ($invalid_referer) {
      return 403;
    }
  }

  # 批量重定向所有 /amp 后缀链接至原规范页面
  location ~* ^(.+)/amp/?$ {
    return 301 $1/;
  }

  # 旧标签、旧专题先映射,再一次跳转到最终中文或英文域名
  if ($www_final_redirect_uri != "") {
    return 301 $www_final_redirect_uri$is_args$args;
  }

  # 未命中旧规则的普通 /en/ 页面迁移到英文子域名
  if ($www_en_migration_uri != "") {
    return 301 $www_en_migration_uri$is_args$args;
  }

  # en 子域名自己的旧标签、旧专题路径跳转
  if ($en_final_redirect_uri != "") {
    return 301 $en_final_redirect_uri$is_args$args;
  }

  location ~ [^/]\.php(/|$) {
    #fastcgi_pass remote_php_ip:9000;
    fastcgi_pass unix:/dev/shm/php-cgi.sock;
    fastcgi_index index.php;
    fastcgi_read_timeout 150s;
    include fastcgi.conf;
  }

  location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
    expires 30d;
    access_log off;
  }

  location ~ .*\.(js|css)?$ {
    expires 7d;
    access_log off;
  }

  location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
    deny all;
  }

  location /.well-known {
    allow all;
  }
}

14. Final admin Virtual Host Configuration

admin uses independent certificates, logs, and a virtual host, but shares the WordPress directory with the main site.

To prevent search engines from indexing frontend pages potentially accessible under the admin domain as duplicate content, I added the following in Nginx:

Plaintext
X-Robots-Tag: noindex, nofollow, noarchive

/usr/local/nginx/conf/vhost/admin.shuijingwanwq.com.conf

Nginx
server {
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_certificate /usr/local/nginx/conf/ssl/admin.shuijingwanwq.com.crt;
  ssl_certificate_key /usr/local/nginx/conf/ssl/admin.shuijingwanwq.com.key;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;

  ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256;

  ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
  ssl_conf_command Options PrioritizeChaCha;

  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache shared:SSL:10m;
  ssl_buffer_size 2k;

  add_header Strict-Transport-Security max-age=15768000;

  ssl_stapling on;
  ssl_stapling_verify on;

  server_name admin.shuijingwanwq.com;

  add_header X-Robots-Tag "noindex, nofollow, noarchive" always;

  access_log /data/wwwlogs/admin.shuijingwanwq.com_nginx.log combined;

  index index.html index.htm index.php;
  root /data/wwwroot/www.shuijingwanwq.com;

  if ($ssl_protocol = "") {
    return 301 https://$host$request_uri;
  }

  include /usr/local/nginx/conf/rewrite/wordpress.conf;

  #error_page 404 /404.html;
  #error_page 502 /502.html;

  location ~ [^/]\.php(/|$) {
    #fastcgi_pass remote_php_ip:9000;
    fastcgi_pass unix:/dev/shm/php-cgi.sock;
    fastcgi_index index.php;
    include fastcgi.conf;
  }

  location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
    expires 30d;
    access_log off;
  }

  location ~ .*\.(js|css)?$ {
    expires 7d;
    access_log off;
  }

  location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
    deny all;
  }

  location /.well-known {
    allow all;
  }
}

Configuration test and reload:

Bash
/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload

Response header verification:

Bash
curl -sS -o /dev/null -D - \
  --resolve admin.shuijingwanwq.com:443:127.0.0.1 \
  "https://admin.shuijingwanwq.com/" \
| grep -Ei \
  'HTTP/|x-robots-tag:|strict-transport-security:'

It returned:

Plaintext
HTTP/2 200
strict-transport-security: max-age=15768000
x-robots-tag: noindex, nofollow, noarchive

15. Frontend Interface and Backend Entry Verification

Direct origin server connection test:

Bash
for host in \
"www.shuijingwanwq.com" \
"en.shuijingwanwq.com" \
"shuijingwanwq.com"
do
  for path in \
  "/wp-login.php" \
  "/wp-admin/" \
  "/wp-admin/update-core.php" \
  "/wp-admin/admin-ajax.php" \
  "/wp-admin/admin-post.php" \
  "/wp-login.php?action=postpass"
  do
    echo
    echo "========== https://$host$path =========="

    curl -sS -o /dev/null -D - \
      --max-time 20 \
      --resolve "$host:443:127.0.0.1" \
      "https://$host$path" \
    | grep -Ei \
      'HTTP/|location:|x-redirect-by:|set-cookie:'
  done
done

Final results:

Plaintext
www / en / 裸域:

/wp-login.php
→ 307 到 admin

/wp-admin/
→ 307 到 admin

/wp-admin/update-core.php
→ 307 到 admin

/wp-admin/admin-ajax.php
→ 保留在当前前台域名

/wp-admin/admin-post.php
→ 保留在当前前台域名

/wp-login.php?action=postpass
→ 保留在当前前台域名

The bare domain’s public interfaces continue to be first canonicalized to www, which is part of the original bare domain redirection behavior.


16. EdgeOne and Cloudflare Public Network Verification

Without using --resolve, I directly tested the public CDN link:

Bash
for u in \
"https://www.shuijingwanwq.com/wp-login.php" \
"https://www.shuijingwanwq.com/wp-admin/" \
"https://www.shuijingwanwq.com/wp-admin/admin-ajax.php" \
"https://en.shuijingwanwq.com/wp-login.php" \
"https://en.shuijingwanwq.com/wp-admin/" \
"https://en.shuijingwanwq.com/wp-admin/admin-ajax.php"
do
  echo
  echo "========== $u =========="

  curl -sS -o /dev/null -D - \
    --max-time 20 \
    "$u?swq_test=$(date +%s)" \
  | grep -Ei \
    'HTTP/|location:|server:|eo-cache-status:|cf-cache-status:'
done

EdgeOne returned:

Plaintext
wp-login.php:
HTTP/2 307
location: https://admin.shuijingwanwq.com/wp-login.php
eo-cache-status: MISS

wp-admin/:
HTTP/2 307
location: https://admin.shuijingwanwq.com/wp-admin/
eo-cache-status: MISS

admin-ajax.php:
HTTP/2 400
eo-cache-status: MISS

Cloudflare returned:

Plaintext
wp-login.php:
HTTP/2 307
location: https://admin.shuijingwanwq.com/wp-login.php
cf-cache-status: DYNAMIC

wp-admin/:
HTTP/2 307
location: https://admin.shuijingwanwq.com/wp-admin/
cf-cache-status: DYNAMIC

admin-ajax.php:
HTTP/2 400
cf-cache-status: DYNAMIC

admin-ajax.php returning 400 when the action parameter is not included is a normal WordPress response.


17. Runtime URL Verification

I used PHP to simulate the www, en, and admin Hosts respectively:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

for host in \
"www.shuijingwanwq.com" \
"en.shuijingwanwq.com" \
"admin.shuijingwanwq.com"
do
  echo
  echo "========== $host =========="

  SWQ_TEST_HOST="$host" php -r '
  $host = getenv("SWQ_TEST_HOST");

  $_SERVER["HTTP_HOST"]   = $host;
  $_SERVER["SERVER_NAME"] = $host;
  $_SERVER["HTTPS"]       = "on";
  $_SERVER["SERVER_PORT"] = "443";
  $_SERVER["REQUEST_URI"] = "/";

  require "wp-load.php";

  echo "admin_url  : " . admin_url() . "\n";
  echo "admin-ajax : " . admin_url("admin-ajax.php") . "\n";
  echo "admin-post : " . admin_url("admin-post.php") . "\n";
  echo "login_url  : " . wp_login_url() . "\n";
  echo "rest_url   : " . rest_url() . "\n";
  '
done

The final output was:

Plaintext
www.shuijingwanwq.com

admin_url:
https://admin.shuijingwanwq.com/wp-admin/

admin-ajax:
https://www.shuijingwanwq.com/wp-admin/admin-ajax.php

admin-post:
https://www.shuijingwanwq.com/wp-admin/admin-post.php

login_url:
https://admin.shuijingwanwq.com/wp-login.php

rest_url:
https://www.shuijingwanwq.com/wp-json/
Plaintext
en.shuijingwanwq.com

admin_url:
https://admin.shuijingwanwq.com/wp-admin/

admin-ajax:
https://en.shuijingwanwq.com/wp-admin/admin-ajax.php

admin-post:
https://en.shuijingwanwq.com/wp-admin/admin-post.php

login_url:
https://admin.shuijingwanwq.com/wp-login.php

rest_url:
https://en.shuijingwanwq.com/wp-json/
Plaintext
admin.shuijingwanwq.com

admin_url:
https://admin.shuijingwanwq.com/wp-admin/

admin-ajax:
https://admin.shuijingwanwq.com/wp-admin/admin-ajax.php

admin-post:
https://admin.shuijingwanwq.com/wp-admin/admin-post.php

login_url:
https://admin.shuijingwanwq.com/wp-login.php

rest_url:
https://admin.shuijingwanwq.com/wp-json/

This shows that the frontend interfaces, language domain REST APIs, and backend admin URLs have been correctly separated.


18. Successfully Upgrading WordPress via admin

After the migration was completed, I went to:

Plaintext
https://admin.shuijingwanwq.com/wp-admin/

and re-ran the WordPress core upgrade.

This time the upgrade completed smoothly. The EdgeOne 524 error did not occur again, and it did not get stuck on the upgrade page.

Final version:

Plaintext
WordPress 7.0.1
[Figure 5, WordPress 7.0.1 core upgrade successfully completed via the admin subdomain]
[Figure 5, WordPress 7.0.1 core upgrade successfully completed via the admin subdomain]

Server-side check:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

php -r '
require "wp-includes/version.php";

echo "WordPress 版本:{$wp_version}\n";

echo "维护模式文件:";
echo file_exists(".maintenance")
    ? "存在\n"
    : "不存在\n";

$_SERVER["HTTP_HOST"] = "admin.shuijingwanwq.com";
$_SERVER["HTTPS"] = "on";

require "wp-load.php";

echo "core_updater.lock:";
echo get_option("core_updater.lock") === false
    ? "不存在\n"
    : "仍然存在\n";
'

Output:

Plaintext
WordPress 版本:7.0.1
维护模式文件:不存在
core_updater.lock:不存在

The results of this upgrade practically prove that the previous failures were primarily related to backend requests going through the CDN link, rather than WordPress file permissions, core files, or the server environment itself being corrupted.


19. Do CDN Cache Rules Need to be Modified

After completing this migration, I did not continue to modify the cache rules for EdgeOne or Cloudflare.

The existing rules already ensure that:

Plaintext
/wp-login.php
/wp-admin/*
/wp-admin/admin-ajax.php
/wp-admin/admin-post.php
/wp-json/*

are not page cached.

The public verification also already showed:

Plaintext
EdgeOne:
eo-cache-status: MISS

Cloudflare:
cf-cache-status: DYNAMIC

Therefore, there is no need to set up backend redirections separately in EdgeOne and Cloudflare.

The unified backend entry redirection continues to be handled by Nginx. The advantages are:

  • EdgeOne and Cloudflare use the same set of origin server logic;
  • There will be no inconsistency between two sets of CDN rules;
  • admin-ajax.php can be precisely preserved;
  • admin-post.php can be precisely preserved;
  • postpass can be precisely preserved;
  • It can be automatically inherited when new language domains are added in the future.

admin.shuijingwanwq.com itself uses DNS Only and does not go through a CDN, so it does not require separate cache rules.


20. How to Handle Adding New Language Domains in the Future

The current Nginx backend redirection rules do not hardcode www or en.

As long as newly added language domains in the future:

  1. Have DNS configured;
  2. Are added to the server certificate;
  3. Are added to the current frontend virtual host’s server_name;
  4. Are configured as language domains in Polylang;

they will automatically inherit:

Plaintext
/wp-login.php
/wp-admin/*

admin.shuijingwanwq.com

The PHP MU plugin will also read from:

PHP
get_option( 'polylang' )

to dynamically read Polylang’s domains configuration, so there is no need to manually add:

Plaintext
fr.shuijingwanwq.com
de.shuijingwanwq.com
es.shuijingwanwq.com

This is better suited for long-term maintenance than hardcoding in PHP:

PHP
(?:www|en|admin)

.


21. What This Migration Solved

Ultimately, it has been confirmed that:

  • admin uses an independent Nginx virtual host;
  • admin uses an independent ECC HTTPS certificate;
  • admin connects directly to the origin server via DNS Only;
  • Backend pages no longer go through EdgeOne;
  • Backend resources no longer go through EdgeOne;
  • Backend resources no longer go through the English site’s Cloudflare;
  • WordPress login, lost password, and logout URLs uniformly use admin;
  • www, en, and the bare domain backend entries uniformly redirect to admin;
  • Frontend AJAX, forms, and post password submissions were not inadvertently affected;
  • Polylang’s future new language domains can be dynamically accommodated;
  • W3 Total Cache no longer redirects admin back to www;
  • admin has been added to noindex, nofollow, noarchive;
  • WordPress 7.0.1 core upgrade succeeded;
  • .maintenance does not exist;
  • core_updater.lock does not exist;
  • Chinese site, English site, and old /en/ redirections all remain normal.

22. What This Migration Did Not Solve

It must be kept objective that this backend migration will not directly eliminate EdgeOne’s approximately 2.9 GB of daily traffic.

Backend access usually involves only the administrator, generating far less traffic than frontend visitors, search engines, and static resource requests.

Therefore, the main value of this migration is:

Plaintext
提高后台稳定性
解决长请求超时
降低后台操作对 CDN 的依赖
减少一部分不必要的 EdgeOne 流量
建立清晰的前后台网络边界

If we want to further reduce EdgeOne usage later, we still need to separately analyze:

  • Chinese frontend HTML traffic;
  • Static files outside of uploads;
  • Font files;
  • Crawler requests;
  • Tag and pagination pages;
  • Cache hit rate;
  • High-frequency access sources;
  • Whether there are other resources that can be migrated to another static domain.

23. Summary

Initially, I just wanted to solve a failed WordPress core upgrade.

But upon actually troubleshooting, I found that migrating the backend could not be accomplished simply by adding a DNS record and an Nginx virtual host.

The complete chain also involved:

Plaintext
DNS
HTTPS 证书
Nginx 虚拟主机
W3 Total Cache 跨域名跳转
WordPress site_url
WordPress admin_url
登录表单
后台静态资源
REST API
Polylang 多域名
前台 AJAX
前台表单
文章密码 Cookie
搜索引擎隔离
EdgeOne
Cloudflare

The final division of responsibilities adopted is:

Plaintext
Nginx:
统一前台语言域名下的后台入口

MU 插件:
修正 WordPress 生成的后台 URL 和资源

W3TC 补丁:
放行合法的多域名 Host

CDN:
保持后台路径不缓存,不负责核心跳转

数据库:
home 和 siteurl 保持不变

After completing these adjustments, the WordPress backend has been truly separated from the frontend CDN link.

And the most convincing verification result is not that a specific configuration check passed, but rather:

Plaintext
WordPress 7.0.1 已经通过
admin.shuijingwanwq.com
成功完成在线升级

The 524 timeout that previously occurred when going through EdgeOne did not happen again.

系列导航

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理