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

Troubleshooting Persistent EdgeOne 301 Requests After Moving WordPress Images to a Media Subdomain

【图3,EdgeOne 301 请求的 URL Path 排行,集中在 /wp-content/uploads/ 图片路径】

作者:

在

Recently, I moved my WordPress image assets to a dedicated media subdomain:

Plaintext
https://media.shuijingwanwq.com/

The goal was to reduce the static asset load on the main site, www.shuijingwanwq.com, and let image files be handled through a separate CDN path.

After the migration, newly written posts were already using image URLs under media.shuijingwanwq.com correctly. However, in EdgeOne access analytics, I noticed something odd: several hours after switching to the media subdomain, there were still quite a few 301 requests in the most recent one-hour window.

At first, I thought this might be caused by EdgeOne cache, browser cache, or leftover crawler requests from search engines. After checking further, I confirmed that the main cause was old image URLs hardcoded in historical post content.

[Figure 1: In the EdgeOne status code ranking for the most recent one hour, 301 still accounts for 5.59 MB, about 2.93%]
[Figure 1: In the EdgeOne status code ranking for the most recent one hour, 301 still accounts for 5.59 MB, about 2.93%]

1. The symptom: EdgeOne still showed 301 responses in the most recent one hour

In EdgeOne metric analysis, the latest one-hour data looked roughly like this:

Plaintext
200:183.2 MB,占 96.16%
301:5.59 MB,占 2.93%
404:1.55 MB,占 0.81%

Judging only by the percentage, 301 responses were not extremely high. But since the image migration had already been completed for several hours, the ideal behavior was that newly served pages should directly output the media subdomain instead of frequently requesting the old /wp-content/uploads/ URLs.

So I filtered the status code further:

Plaintext
边缘响应状态码 = 301

After applying the filter, the 301 requests alone in the most recent one-hour window were:

Plaintext
L7 访问流量:7.25 MB
L7 访问请求数:966 次
[Figure 2: EdgeOne data after filtering by status code 301]
[Figure 2: EdgeOne data after filtering by status code 301]

Then I checked the URL Path ranking and found that many 301 responses were concentrated on old image paths, for example:

Plaintext
/wp-content/uploads/2026/07/21-2-1024x175.png
/wp-content/uploads/2026/07/23-2-1024x180.png
/wp-content/uploads/2018/04/14-3-520x245.png
/wp-content/uploads/2023/12/3-6.png
/wp-content/uploads/2021/08/5-3.png
[Figure 3: EdgeOne 301 URL Path ranking, concentrated on /wp-content/uploads/ image paths]
[Figure 3: EdgeOne 301 URL Path ranking, concentrated on /wp-content/uploads/ image paths]

This showed that although the image files had been moved to the media subdomain, many requests were still hitting the old image URLs.

2. First, confirm whether the 301 requests were going back to the origin

After seeing the 301 responses, the first key question was:

Were these 301 responses handled directly by the EdgeOne edge nodes, or were they being forwarded back to WordPress / Nginx every time?

If every request went back to the origin, the origin server would still be under pressure.
If the edge node returned the 301 directly, the issue would be much less serious.

I tested one of the old image URLs with curl:

Bash
curl -I https://www.shuijingwanwq.com/wp-content/uploads/2026/07/21-2-1024x175.png

The important parts of the response were:

Plaintext
HTTP/2 301
location: https://media.shuijingwanwq.com/2026/07/21-2-1024x175.png
content-length: 0
server: TencentEdgeOne
x-cache-lookup: Return Directly

This result means:

The 301 response for the old image URL was returned directly by the EdgeOne edge node and did not go back to WordPress.

In particular:

Plaintext
x-cache-lookup: Return Directly

This was the key signal. It indicates that EdgeOne returned the redirect response directly instead of forwarding the request to the origin and then returning the result.

So even though this group of 301 responses still appeared in EdgeOne analytics, it was not an origin-server pressure problem by itself.

3. Then confirm whether images on the media subdomain were being cached properly

Next, I tested the redirected media image URL:

Bash
curl -I https://media.shuijingwanwq.com/2026/07/21-2-1024x175.png

The first request returned:

Plaintext
HTTP/2 200
server: cloudflare
cache-control: max-age=2592000
cf-cache-status: MISS

After requesting it again, the second response showed:

Plaintext
HTTP/2 200
server: cloudflare
age: 10
cf-cache-status: HIT

This confirmed that image assets under the media subdomain could be cached by Cloudflare correctly.

So the request flow at that point could be understood as:

Plaintext
旧图片地址:
https://www.shuijingwanwq.com/wp-content/uploads/...

EdgeOne 边缘直接 301:
location: https://media.shuijingwanwq.com/...

新图片地址:
https://media.shuijingwanwq.com/...

Cloudflare:
第一次 MISS,第二次 HIT

In other words, the image file itself was not repeatedly hitting the origin, and caching on the media subdomain was working normally.

4. The real issue: old posts were still outputting old image URLs in their HTML

Since the EdgeOne 301 responses were handled directly at the edge, and the media image cache was also working, the remaining question was:

Why were there still so many requests to old image URLs?

I first checked the homepage HTML:

Bash
curl -s https://www.shuijingwanwq.com/ | grep -o "/wp-content/uploads/[^\"') ]*" | head

The homepage produced no output, which meant it was basically clean.

Then I checked an older post:

Bash
curl -s https://www.shuijingwanwq.com/2021/08/04/5122/ | grep -o "/wp-content/uploads/[^\"') ]*" | head

The command returned many old paths:

Plaintext
/wp-content/uploads/2021/08/1-2.png
/wp-content/uploads/2021/08/1-2.png
/wp-content/uploads/2021/08/2-2.png
/wp-content/uploads/2021/08/2-2.png
/wp-content/uploads/2021/08/3-1.png

This confirmed that the issue was not just historical cache. The old post page itself was still outputting old image URLs.

I also viewed the source code of that old post in the browser and found many occurrences of:

Plaintext
/wp-content/uploads/
[Figure 4: /wp-content/uploads/ could still be found in the page source of the old post]
[Figure 4: /wp-content/uploads/ could still be found in the page source of the old post]

This explained why EdgeOne still had 301 responses:

Plaintext
用户或爬虫访问旧文章
↓
旧文章 HTML 中仍然包含 /wp-content/uploads/ 图片地址
↓
浏览器继续请求旧图片地址
↓
EdgeOne 边缘返回 301
↓
跳转到 media.shuijingwanwq.com

5. Why did new posts work correctly while old posts did not?

At first, I wondered whether this was related to differences between the Classic Editor and Gutenberg.

After checking, I found that it had nothing to do with the editor type.

The root cause was:

Image URLs inside WordPress post content are essentially HTML strings stored in wp_posts.post_content.

For images inserted before the migration, the post content stored URLs like this:

HTML
<img src="https://www.shuijingwanwq.com/wp-content/uploads/2021/08/1-2.png">

For images uploaded and inserted after the migration, the post content stored URLs like this:

HTML
<img src="https://media.shuijingwanwq.com/2026/07/1-22.png">

So the media configuration only affects newly generated image URLs after the migration. It does not automatically go back and rewrite the HTML in historical posts.

[Figure 5: In the old post code editor before the migration, the image URL was still www.shuijingwanwq.com/wp-content/uploads]
[Figure 5: In the old post code editor before the migration, the image URL was still www.shuijingwanwq.com/wp-content/uploads]
[Figure 6: In the new post code editor after the migration, the image URL had already become media.shuijingwanwq.com]
[Figure 6: In the new post code editor after the migration, the image URL had already become media.shuijingwanwq.com]

6. Use Better Search Replace for a dry run first

Because Better Search Replace was already installed on my site, I did not install WP-CLI separately. I used the plugin directly to check the data first.

At first, I searched for:

Plaintext
Search for:
/wp-content/uploads/

Replace with:
https://media.shuijingwanwq.com/

I selected only:

Plaintext
wp_posts

And enabled:

Plaintext
Run as dry run

The dry run showed:

Plaintext
6455 cells were found that need to be updated
[Figure 7: Better Search Replace dry run for /wp-content/uploads/ showing 6,455 cells]
[Figure 7: Better Search Replace dry run for /wp-content/uploads/ showing 6,455 cells]

However, this search condition was too broad and was not suitable for direct replacement.

There were two types of content in old posts.

The first type was a full image URL:

Plaintext
https://www.shuijingwanwq.com/wp-content/uploads/2021/08/1-2.png

This type could be safely replaced with:

Plaintext
https://media.shuijingwanwq.com/2021/08/1-2.png

The second type was technical explanation, path documentation, or server directory notes inside the article body, for example:

Plaintext
/wp-content/uploads/2021/07/

This might not be an image loading URL at all. It could simply be part of the article content. Replacing it directly could damage the original troubleshooting context.

So I should not blindly replace every occurrence of /wp-content/uploads/.

7. Use the full old image domain for the actual replacement

A safer search condition was the complete old image URL prefix:

Plaintext
Search for:
https://www.shuijingwanwq.com/wp-content/uploads/

Replace with:
https://media.shuijingwanwq.com/

I continued to select only:

Plaintext
wp_posts

And first enabled:

Plaintext
Run as dry run

This dry run showed:

Plaintext
3304 cells were found that need to be updated
[Figure 8: Better Search Replace dry run using the full old image domain, with matches reduced to 3,304 cells]
[Figure 8: Better Search Replace dry run using the full old image domain, with matches reduced to 3,304 cells]

This result was much closer to what I expected.

Because it only targeted complete old image URLs explicitly hardcoded in historical posts:

Plaintext
https://www.shuijingwanwq.com/wp-content/uploads/

It would not accidentally modify relative paths recorded in normal article content:

Plaintext
/wp-content/uploads/

Before running the real replacement, I created a database backup with UpdraftPlus. After confirming that the database backup was complete, I disabled Run as dry run and executed the actual replacement.

The replacement result was:

Plaintext
During the search/replace, 1 tables were searched, with 3304 cells changed in 3299 updates.
[Figure 9: Better Search Replace completed successfully, with 3,304 cells changed in 3,299 updates]
[Figure 9: Better Search Replace completed successfully, with 3,304 cells changed in 3,299 updates]

8. Clear W3 Total Cache and verify again

After the actual replacement finished, I first checked the code editor of the old post and found that the image URL had changed to:

Plaintext
https://media.shuijingwanwq.com/2021/08/1-2.png
[Figure 10: In the old post code editor, the image URL had been replaced from www to media]
[Figure 10: In the old post code editor, the image URL had been replaced from www to media]

However, when I initially used curl to access the frontend page, I could still see the old URL.

This meant the database replacement had succeeded, but the frontend page cache had not been refreshed yet.

So I cleared W3 Total Cache in the WordPress dashboard:

Plaintext
Performance → Purge All Caches

After clearing the cache, I ran the check again:

Bash
curl -s "https://www.shuijingwanwq.com/2021/08/04/5122/?_nocache=$(date +%s)" | grep -o "https://www.shuijingwanwq.com/wp-content/uploads/[^\"') ]*" | head

This time there was no output.

Then I checked the relative path pattern:

Bash
curl -s "https://www.shuijingwanwq.com/2021/08/04/5122/?_nocache=$(date +%s)" | grep -o "/wp-content/uploads/[^\"') ]*" | head

There was no output either.

This confirmed that the frontend HTML of this old post was no longer outputting the old image URL.

9. Why I did not continue replacing the remaining /wp-content/uploads/ occurrences

After replacing the first batch of complete old image URLs, I ran another Better Search Replace dry run:

Plaintext
Search for:
/wp-content/uploads/

Replace with:
https://media.shuijingwanwq.com/

The result showed that the remaining count had dropped significantly.

[Figure 11: After replacing complete URLs, another dry run checked the remaining /wp-content/uploads/ occurrences]
[Figure 11: After replacing complete URLs, another dry run checked the remaining /wp-content/uploads/ occurrences]

But I did not run the actual replacement for those remaining matches.

The reason was that read-only checks showed many remaining /wp-content/uploads/ occurrences were not image loading URLs. They were technical notes in article content, attachment titles, historical revisions, or server paths recorded during earlier troubleshooting.

For example, some articles were discussing things like:

Plaintext
通过 FlashFXP 上传文件至 /wp-content/uploads/2021/07/

If this kind of content were forcefully replaced with:

Plaintext
https://media.shuijingwanwq.com/2021/07/

it would change the meaning of the article.

So my rule was:

Plaintext
完整旧图片 URL 可以批量替换;
单独的 /wp-content/uploads/ 不再无脑替换。

10. Conclusion from this troubleshooting process

The core problem this time was not an abnormal EdgeOne 301 issue, nor was it a failed cache setup for the media subdomain. The real issue was:

After moving to a media subdomain, historical image URLs saved inside old WordPress post content do not update automatically.

The final handling logic was:

Plaintext
1. EdgeOne 仍然有 301
2. 筛选发现 301 集中在 /wp-content/uploads/ 图片路径
3. curl 确认 301 是 EdgeOne 边缘直接返回,没有回源
4. curl 确认 media 子域名图片可以正常 Cloudflare HIT
5. 检查旧文章 HTML,发现仍然输出旧图片地址
6. 用 Better Search Replace dry run 检查完整旧图片 URL
7. 备份数据库
8. 正式替换完整旧图片 URL
9. 清理 W3 Total Cache
10. 重新验证旧文章前台 HTML 已经不再输出旧图片地址

The URL that really needed to be replaced was:

Plaintext
https://www.shuijingwanwq.com/wp-content/uploads/

It was replaced with:

Plaintext
https://media.shuijingwanwq.com/

Instead of directly replacing every occurrence of:

Plaintext
/wp-content/uploads/

across the database.

11. Follow-up observation

After the replacement was completed, the 301 count in EdgeOne would not immediately drop to zero.

Possible reasons include:

Plaintext
搜索引擎仍然可能请求旧图片 URL;
浏览器缓存中可能还保存旧地址;
外部引用可能仍然访问旧图片路径;
EdgeOne 的统计数据也有时间窗口延迟。

But when real users access old posts later, the page HTML now directly outputs image URLs under the media subdomain. It no longer asks the browser to request the old /wp-content/uploads/ path first, so the 301 responses triggered by article pages should gradually decrease.

For me, this troubleshooting process was another reminder:

WordPress media URL settings affect images inserted in the future, but they do not automatically rewrite historical post content.

If a site has been running for a long time and has many historical posts, fixed URLs inside old post content should always be included in the checklist when migrating image domains, CDN domains, or static asset domains.

系列导航

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系