通过 SSH 在 Hugo 上部署 PHPMailer 联系表单

静态网站不运行服务端代码,但联系表单仍然需要它。以下是我如何在通过 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_HOSTSMTP_USERSMTP_PASS 等)
  • static/mail-config.php 加入 .gitignore
  • static/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" `