# Apache (.htaccess 或 VirtualHost)
ErrorDocument 404 /404.php
# Nginx
error_page 404 /404.php;
location = /404.php {
internal; # 防止直接訪問
fastcgi_pass php-fpm;
fastcgi_param REQUEST_URI $request_uri; # 傳遞原始 URI
}
<?php
/**
* 友善 404 頁面
* 需放在網站根目錄,並由 Web Server 導向
*/
http_response_code(404); // 必須最先發送
// --- 配置區 ---
$config = [
'site_name' => 'My Awesome Site',
'site_url' => 'https://www.example.com',
'admin_email' => 'webmaster@example.com',
'notify_email' => true, // 是否發送郵件
'notify_level' => 2, // 1=僅本域 Referer, 2=全部
'log_file' => __DIR__ . '/logs/404.log',
'template' => __DIR__ . '/templates/404.html.php', // 可選外部模板
'search_url' => '/search?q=', // 站內搜尋連結前綴
'sitemap_url' => '/sitemap',
'contact_url' => '/contact',
];
// -----------------
// 記錄基本資訊
$info = [
'time' => date('c'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'uri' => $_SERVER['REQUEST_URI'] ?? '/',
'referer' => $_SERVER['HTTP_REFERER'] ?? 'direct',
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
'host' => $_SERVER['HTTP_HOST'] ?? '',
];
// 寫入日誌 (JSON Lines 格式,易於 ELK 收集)
$logLine = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
@file_put_contents($config['log_file'], $logLine, FILE_APPEND | LOCK_EX);
// 判斷是否發送郵件
$shouldMail = $config['notify_email'] &&
($config['notify_level'] === 2 ||
($config['notify_level'] === 1 && stripos($info['referer'], $config['site_url']) !== false));
if ($shouldMail) {
$subject = "[404 Alert] {$config['site_name']} - {$info['uri']}";
$body = "Time: {$info['time']}\n"
. "IP: {$info['ip']}\n"
. "URI: {$config['site_url']}{$info['uri']}\n"
. "Referer: {$info['referer']}\n"
. "User-Agent: {$info['ua']}\n";
$headers = "From: {$config['admin_email']}\r\n"
. "Content-Type: text/plain; charset=UTF-8\r\n";
// 非阻塞發送 (生產建議用隊列 + worker)
@mail($config['admin_email'], $subject, $body, $headers);
}
// --- 輸出 HTML ---
// 若有外部模板且可讀,則 include,否則用內建模板
if (is_readable($config['template'])) {
include $config['template'];
} else {
// 內建響應式模板
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>404 - 頁面不存在 | <?= htmlspecialchars($config['site_name']) ?></title>
<style>
body{font-family:system-ui,sans-serif;background:#f5f5f5;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:1rem}
.card{background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);padding:3rem 2rem;max-width:480px;text-align:center}
h1{font-size:6rem;margin:0;color:#e74c3c;line-height:1}
h2{font-size:1.5rem;margin:1rem 0 0.5rem;color:#2c3e50}
p{color:#7f8c8d;margin:1rem 0}
.btn{display:inline-block;margin:0.5rem;padding:0.75rem 1.5rem;background:#3498db;color:#fff;text-decoration:none;border-radius:4px;transition:background .2s}
.btn:hover{background:#2980b9}
.btn-secondary{background:#95a5a6}
.btn-secondary:hover{background:#7f8c8d}
</style>
</head>
<body>
<div class="card">
<h1>404</h1>
<h2>找不到頁面</h2>
<p>抱歉,您訪問的頁面 <?= htmlspecialchars($info['uri']) ?> 不存在或已移除。</p>
<p>
<a class="btn" href="<?= $config['site_url'] ?>">返回首頁</a>
<a class="btn btn-secondary" href="<?= $config['site_url'] . $config['sitemap_url'] ?>">網站地圖</a>
</p>
<p>或嘗試搜尋您想要的內容:</p>
<form action="<?= $config['site_url'] . $config['search_url'] ?>" method="GET">
<input type="search" name="q" placeholder="關鍵字..." style="padding:0.5rem;border:1px solid #ddd;border-radius:4px;width:70%;max-width:280px">
<button type="submit" class="btn">搜尋</button>
</form>
<p style="margin-top:2rem;font-size:0.875rem;color:#95a5a6">
若您認為這是錯誤,請 聯繫我們。
</p>
</div>
</body>
</html>
<?php
}
?>
<?php // 變數 $config, $info 已在作用域中 ?>
<!DOCTYPE html>
<html><head><title>404 - <?= $config['site_name'] ?></title></head><body>
<h1>頁面走失了 😢</h1>
<p>請求: <code><?= htmlspecialchars($info['uri']) ?></code></p>
<a href="<?= $config['site_url'] ?>">回首頁</a>
</body></html>
將 `$logLine` 直接寫入檔案,配合 Filebeat / Promtail 採集,即可在 Kibana / Grafana 建立儀表板: