301 重導向是網站遷移、域名變更、HTTPS 強制的關鍵 SEO 技術,正確實作能保留搜尋引擎權重。本文提供 PHP 多種場景的 301 實作、測試方法與常見錯誤避坑指南。
<?php
// 必須在任何輸出之前呼叫
header('HTTP/1.1 301 Moved Permanently');
header('Location: https://www.new-domain.com' . $_SERVER['REQUEST_URI']);
exit; // 關鍵:防止後續代碼執行
?>
<?php
// 301_redirect.php - 放在舊網站入口檔案最頂端
$oldDomains = [
'old-domain.com',
'www.old-domain.com',
'legacy.example.com'
];
$newDomain = 'www.new-domain.com';
$currentHost = $_SERVER['HTTP_HOST'] ?? '';
if (in_array($currentHost, $oldDomains, true)) {
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$targetUrl = "https://{$newDomain}{$requestUri}";
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
?>
<?php
// 強制 HTTPS(建議在 Web 伺服器層面設定,PHP 作為備援)
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
$host = $_SERVER['HTTP_HOST'] ?? '';
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$targetUrl = "https://{$host}{$requestUri}";
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
// HSTS 標頭(建議在伺服器設定)
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
?>
<?php
// 統一為 www
$host = $_SERVER['HTTP_HOST'] ?? '';
if (!str_starts_with($host, 'www.')) {
$targetUrl = "https://www.{$host}" . ($_SERVER['REQUEST_URI'] ?? '/');
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
// 或統一為非 www(二選一)
if (str_starts_with($host, 'www.')) {
$targetUrl = "https://" . substr($host, 4) . ($_SERVER['REQUEST_URI'] ?? '/');
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
?>
<?php
// 舊結構:/product.php?id=123 -> 新結構:/product/123
$redirectMap = [
'/product.php' => '/product/',
'/category.php' => '/category/',
'/article.php' => '/article/'
];
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($requestUri, PHP_URL_PATH);
$query = parse_url($requestUri, PHP_URL_QUERY);
foreach ($redirectMap as $oldPath => $newPath) {
if ($path === $oldPath) {
$id = $_GET['id'] ?? '';
if ($id !== '') {
$targetUrl = "https://{$_SERVER['HTTP_HOST']}{$newPath}{$id}";
if ($query) {
// 保留其他參數(排除 id)
parse_str($query, $params);
unset($params['id']);
if ($params) {
$targetUrl .= '?' . http_build_query($params);
}
}
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
}
}
?>
<?php
// 使用資料庫或快取儲存對照表
$redirects = [
'/old-page-1' => '/new-category/page-1',
'/old-page-2' => '/new-category/page-2',
// ... 數千筆
];
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($requestUri, PHP_URL_PATH);
if (isset($redirects[$path])) {
$targetUrl = "https://{$_SERVER['HTTP_HOST']}{$redirects[$path]}";
// 保留查詢字串
$query = parse_url($requestUri, PHP_URL_QUERY);
if ($query) {
$targetUrl .= '?' . $query;
}
header('HTTP/1.1 301 Moved Permanently');
header("Location: {$targetUrl}");
exit;
}
// 效能優化:大量對照表用 Redis 或 APCu
// $targetPath = apcu_fetch("redirect:{$path}");
// if ($targetPath) { ... }
?>
<?php
// Middleware/Redirect301.php
class Redirect301Middleware {
private array $rules = [];
private array $domainRules = [];
public function addDomainRule(string $fromHost, string $toHost): self {
$this->domainRules[strtolower($fromHost)] = $toHost;
return $this;
}
public function addPathRule(string $fromPath, string $toPath, bool $keepQuery = true): self {
$this->rules[$fromPath] = ['to' => $toPath, 'keepQuery' => $keepQuery];
return $this;
}
public function addRegexRule(string $pattern, string $replacement): self {
$this->rules[$pattern] = ['regex' => true, 'replacement' => $replacement];
return $this;
}
public function handle(Request $request, callable $next): Response {
$host = strtolower($request->getHost());
$path = $request->getPathInfo();
$query = $request->getQueryString();
// 域名規則
if (isset($this->domainRules[$host])) {
$targetHost = $this->domainRules[$host];
$targetUrl = "https://{$targetHost}{$path}" . ($query ? "?{$query}" : '');
return new RedirectResponse($targetUrl, 301);
}
// 精確路徑匹配
if (isset($this->rules[$path])) {
$rule = $this->rules[$path];
$targetPath = $rule['to'];
$targetQuery = $rule['keepQuery'] ? $query : '';
$targetUrl = "https://{$host}{$targetPath}" . ($targetQuery ? "?{$targetQuery}" : '');
return new RedirectResponse($targetUrl, 301);
}
// 正則規則
foreach ($this->rules as $pattern => $rule) {
if (($rule['regex'] ?? false) && preg_match($pattern, $path, $matches)) {
$targetPath = preg_replace($pattern, $rule['replacement'], $path);
$targetUrl = "https://{$host}{$targetPath}" . ($query ? "?{$query}" : '');
return new RedirectResponse($targetUrl, 301);
}
}
return $next($request);
}
}
// 註冊規則
$middleware = new Redirect301Middleware()
->addDomainRule('old.example.com', 'www.new-example.com')
->addPathRule('/product.php', '/product/', true)
->addRegexRule('#^/user/(\d+)$#', '/profile/$1');
?>
# 檢查回應碼與 Location 標頭
curl -I "https://old-domain.com/product.php?id=123"
# 預期輸出:
# HTTP/1.1 301 Moved Permanently
# Location: https://www.new-domain.com/product/123
# Content-Type: text/html; charset=UTF-8
<?php
// test_redirects.php
$testCases = [
['url' => 'https://old.com/page1', 'expected' => 'https://www.new.com/page1', 'code' => 301],
['url' => 'https://old.com/page2?utm=src', 'expected' => 'https://www.new.com/page2?utm=src', 'code' => 301],
['url' => 'https://www.new.com/page3', 'expected' => null, 'code' => 200], // 不應重導向
];
foreach ($testCases as $case) {
$ch = curl_init($case['url']);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_NOBODY => true,
CURLOPT_FOLLOWLOCATION => false, // 關鍵:不自動追蹤
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$location = '';
if (preg_match('/^Location:\s*(.+)$/im', $response, $m)) {
$location = trim($m[1]);
}
curl_close($ch);
$pass = $httpCode === $case['code']
&& ($case['expected'] === null || $location === $case['expected']);
echo ($pass ? '✓' : '✗') . " {$case['url']} -> {$httpCode} {$location}\n";
}
?>
| 錯誤 | 後果 | 正確做法 |
|---|---|---|
| 用 302 代替 301 | 搜尋引擎不傳遞權重,舊頁面保留索引 | 永久遷移必須用 301 |
| 重導向鏈過長(A→B→C) | 爬蟲放棄追蹤,權重流失,載入慢 | 直接導向最終目標,最多 1 次跳轉 |
| 導向無關頁面(如首頁) | 軟 404、用戶體驗差、權重不傳遞 | 一對一對應相關內容 |
| 查詢參數丟失 | UTM 追蹤失效、分頁/篩選狀態消失 | 保留或合理轉換查詢字串 |
| HTTP/HTTPS 混合導向 | 安全性警告、SEO 損害 | 統一 HTTPS,HSTS 預載 |
| 輸出內容前未 exit | 可能輸出雜訊、導向失效 | header 後立即 exit |
# 域名遷移
server {
server_name old-domain.com www.old-domain.com;
return 301 https://www.new-domain.com$request_uri;
}
# 強制 HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
# 路徑對照
location /product.php {
rewrite ^/product\.php$ /product/$arg_id? permanent;
}
RewriteEngine On
# 域名遷移
RewriteCond %{HTTP_HOST} ^(www\.)?old-domain\.com$ [NC]
RewriteRule ^(.*)$ https://www.new-domain.com/$1 [R=301,L]
# 強制 HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# 強制 WWW
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
**最佳實踐**:優先在 Web 伺服器層面設定 301(效能最好、不經 PHP)、PHP 作為複雜邏輯備援、建立完整對照表與自動化測試、上線後持續監控 Search Console 索引狀態、保留舊域名至少 1 年並續約 SSL 憑證。