静态网站不运行服务端代码,但联系表单仍然需要它。以下是我如何在通过 SSH 部署到共享主机的 Hugo 网站上接入 PHP/PHPMailer 联系表单的过程——以及途中遇到的问题。
架构概述
网站使用 Hugo 构建,输出通过 rsync 同步到 cPanel 共享主机。服务端有 PHP 可用,联系表单提交到与 Hugo 输出并存于 public_html/ 中的 contact.php 文件。
PHPMailer 负责 SMTP 发送。我直接将库文件放入项目(无需 Composer)——文件存放在 static/vendor/phpmailer/,Hugo 构建时会将其复制到 public/。
防止凭据进入 Git
SMTP 凭据绝不应提交到 git。解决方案:
contact.php调用require __DIR__ . '/mail-config.php';mail-config.php定义常量(SMTP_HOST、SMTP_USER、SMTP_PASS等)static/mail-config.php加入.gitignorestatic/mail-config.example.php提交到 git 作为参考模板
在服务器上,mail-config.php 设置为 chmod 600——仅进程所有者可读。
通过 SSH 部署
部署命令:
hugo && rsync -avz --exclude='mail-config.php' \
-e "ssh -p 7822" \
public/ user@ftp.example.com:~/public_html/ \
&& ssh -p 7822 user@ftp.example.com \
"chmod 600 ~/public_html/mail-config.php"
关键点:
--exclude='mail-config.php'防止 rsync 覆盖服务器上的凭据文件- 每次部署后重新执行
chmod 600作为安全保障
调试 SMTP
mail-config.php 中的凭据最初填写有误。为了诊断问题,我上传了一个测试脚本,在服务器上直接用 SMTPDebug = 2 运行 PHPMailer:
scp -P 7822 smtp_test.php user@ftp.example.com:/tmp/
ssh -p 7822 user@ftp.example.com "php /tmp/smtp_test.php; rm /tmp/smtp_test.php"
调试输出立即显示 535 Incorrect authentication data——确认是凭据问题,而非防火墙或 TLS 问题。更正后输出显示 235 Authentication succeeded,邮件成功发送。
文件权限
共享主机对文件权限要求严格。规则如下:
| 类型 | 权限 |
|---|---|
| 目录 | 755 |
| 文件 | 644 |
| 敏感配置 | 600 |
Hugo 复制 static/ 中的文件时会保留源文件权限。如果源文件权限有误,服务器上也会出错。在源头一次性修复:
find static -type f -exec chmod 644 {} \;
find static -type d -exec chmod 755 {} \;
此后每次 hugo 构建都会生成权限正确的输出,rsync 同步时也会干净地传播——以后的部署无需在服务器端修复权限。
两个需要手动处理的文件:
.htaccess直接存在于public_html/,不由 Hugo 管理。设置一次即可:chmod 644 .htaccess。如果 Apache 无法读取此文件,将无法提供任何页面。mail-config.php被排除在 rsync 之外,必须为chmod 600——部署命令会处理此项。
联系表单:Fetch 代替普通 POST
原始表单使用普通 HTML <form action="..."> POST 提交。这虽然有效,但验证失败时浏览器会在页面上直接渲染原始 JSON——用户看到 {"ok":false,"errors":[...]} 后只能按返回键。
改进版本用 fetch 拦截提交:
- 客户端验证先行——明显的错误不会触达服务器
- 服务器错误映射回对应字段的行内提示
- 成功时,JS 重定向到
/{lang}/contact/?sent=1——一个完整的感谢页面
Hugo 模板的一个陷阱:| jsonify 过滤器会给字符串值加上 JSON 引号。对于 /contact.php 这样的参数,它产生 "/contact.php"——正确。但若重复应用则产生 "\"/contact.php\"",这是一个包含字面引号字符的 URL,fetch 会静默失败。应改用普通插值:
var action = "{{ .Site.Params.contactFormAction }}";
另外,在 contact.php 顶部添加 ini_set('display_errors', '0');。如果 PHP 警告开启,警告内容会被插入 JSON 响应体之前,导致浏览器的 JSON.parse 失败。
总结
| 问题 | 解决方案 |
|---|---|
| 凭据进入 git | 独立的 mail-config.php,加入 gitignore,chmod 600 |
| SMTP 认证失败 | 在服务器运行带 SMTPDebug = 2 的测试脚本 |
| 所有页面 403 | .htaccess 权限为 700——改为 644 |
| 图片 403 | static/ 源文件权限有误——在源头修复 |
| 浏览器 JSON 解析错误 | display_errors=1 将 PHP 警告泄露到响应体中 |
| 提交时"Something went wrong" | ` |