Next.js 16 static export + PHP 8 API for cPanel/Apache hosting. - Location-aware campus finder (IP estimate, optional precise location, ZIP search) - Three-step Plan Your Visit form with separate email/SMS consent - PHP API: validation, anti-spam, rate limiting, SQLite lead store, Mailchimp and Sinch Engage adapters, idempotent retries via cron - Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and consent-aware analytics hooks - Vitest and PHP test suites, cPanel deployment script and docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
195 lines
9.9 KiB
Markdown
195 lines
9.9 KiB
Markdown
# Deploying to cPanel (site administrator guide)
|
||
|
||
This guide assumes the standard cPanel layout `/home/USER/`. Replace `USER` with the account's
|
||
username throughout. Nothing here changes DNS or publishes anything until step 8.
|
||
|
||
> **Current state (2026-09-22):** `visit.waterschurch.org` already resolves to the same server as
|
||
> waterschurch.org (108.160.152.27) and currently 301-redirects to a Church Center form
|
||
> (`waterschurch.churchcenter.com/people/forms/21186`). That redirect lives either in the main
|
||
> site's `.htaccess` or in a cPanel **Redirects** entry and must be removed when this site goes live.
|
||
|
||
## Fastest path: `deploy/deploy.sh` (SSH)
|
||
|
||
If you have SSH access (alias `waterschurch` in `~/.ssh/config`), one command does steps 2, 5, 6 and 7 below:
|
||
|
||
```bash
|
||
deploy/deploy.sh --keep-redirect # deploy, but keep visitors on the Church Center form (pre-launch)
|
||
deploy/deploy.sh # build, deploy and go live (removes the redirect)
|
||
deploy/deploy.sh --no-build # re-upload the existing ./out
|
||
```
|
||
|
||
It preserves cPanel's own `.htaccess` blocks and `.user.ini`, never overwrites an existing
|
||
`~/visit-private/.env`, and prints the health check when done. The manual steps below describe
|
||
what it does, for File Manager users or troubleshooting.
|
||
|
||
**Status 2026-09-22 (evening): LIVE.** Deployed with `deploy/deploy.sh`; the Church Center redirect is
|
||
gone. `INTEGRATION_MODE=live` with Mailchimp (audience "Waters Church", datacenter `us2`) and Sinch
|
||
(all five campus sub-accounts verified). The 12 merge fields exist in the audience. A real email-only
|
||
test lead succeeded end to end; the SMS path is verified against Sinch auth but a live text has not yet
|
||
been sent. Journey "New Visit" (id 5842) is still a draft and triggers only on `Visit:Apollo Beach`.
|
||
|
||
## 0. What you will need
|
||
|
||
* cPanel login for the waterschurch.org account
|
||
* The build package: run `npm run build && npm run package` → `dist/visit-waterschurch-<version>-<date>.zip`
|
||
* Filled-in `.env` values (Mailchimp key, Sinch key/secret, etc.) — see `.env.example`
|
||
|
||
## 1. Check the account's PHP
|
||
|
||
cPanel → **Software → MultiPHP Manager** (or *Select PHP Version*).
|
||
|
||
* Set the domain (once created) to **PHP 8.1, 8.2 or 8.3**.
|
||
* **MultiPHP INI Editor / Select PHP Version → Extensions**: ensure `curl`, `pdo_sqlite`, `sqlite3`,
|
||
`mbstring`, `json`, `openssl` are enabled. (`pdo_mysql` only if you choose MySQL storage.)
|
||
|
||
Node.js is **not** required. If the account has "Setup Node.js App", ignore it.
|
||
|
||
## 2. Create the private configuration folder (outside the web root)
|
||
|
||
cPanel → **Files → File Manager** → navigate to `/home/USER/` (the home directory, *not*
|
||
`public_html`).
|
||
|
||
1. Create folder **`visit-private`**.
|
||
2. Inside it create folders **`data`** and **`logs`**.
|
||
3. Upload `.env.example` from the package (`dist/env.example.txt`), rename it to **`.env`**, and edit
|
||
it in File Manager: fill in real values, set `INTEGRATION_MODE=live` only when ready to go live,
|
||
and set a long random `HASH_SALT`.
|
||
4. Permissions (right-click → *Change Permissions*): folders `0750`, the `.env` file `0640`.
|
||
PHP runs as your cPanel user under suPHP/LSAPI, so it can read and write these while the web
|
||
server cannot serve them.
|
||
|
||
If your host runs PHP as a different user (rare), use `0755`/`0644` and rely on the folder being
|
||
outside the document root.
|
||
|
||
## 3. Create the subdomain
|
||
|
||
cPanel → **Domains → Domains** (or *Subdomains* on older versions) → **Create a New Domain**:
|
||
|
||
* Domain: `visit.waterschurch.org`
|
||
* Untick "Share document root"; set **Document Root** to `/home/USER/visit.waterschurch.org`
|
||
* Submit. cPanel creates the folder and the DNS `A` record automatically because the zone is hosted
|
||
here (`ns1/ns2.waterschurch.org`). The record already exists and points at this server, so nothing
|
||
external changes.
|
||
|
||
Then remove the old redirect:
|
||
|
||
* cPanel → **Domains → Redirects**: delete any entry for `visit.waterschurch.org`, **or**
|
||
* File Manager → `public_html/.htaccess`: remove the `RewriteCond %{HTTP_HOST} ^visit\.` block that
|
||
redirects to churchcenter.com.
|
||
|
||
Do this at the moment you are ready to launch (step 8); until then the redirect keeps working.
|
||
|
||
## 4. Enable SSL
|
||
|
||
cPanel → **Security → SSL/TLS Status** → tick `visit.waterschurch.org` → **Run AutoSSL**.
|
||
Wait for the green padlock (usually under 5 minutes). If AutoSSL fails with a DCV error, make sure
|
||
the document root exists and nothing blocks `/.well-known/` (our `.htaccess` explicitly allows it).
|
||
|
||
Verify: `https://visit.waterschurch.org/BUILD.txt` shows the build id once files are uploaded
|
||
(step 5). Force-HTTPS is handled by the site's own `.htaccess`; you do **not** need cPanel's
|
||
"Force HTTPS Redirect" toggle, but enabling it is harmless.
|
||
|
||
## 5. Upload the build
|
||
|
||
1. File Manager → open `/home/USER/visit.waterschurch.org/`. Delete the placeholder `cgi-bin`
|
||
folder if present.
|
||
2. **Upload** the zip from `dist/`, then right-click → **Extract** into this folder. Delete the zip.
|
||
3. Enable **Settings → Show Hidden Files (dotfiles)** and confirm `.htaccess` is present at the root
|
||
and inside `api/lib`, `api/cron`, `api/config`.
|
||
4. Tell the API where the private folder is. Either:
|
||
* leave the default (`/home/USER/visit-private` — the folder *next to* the document root), **or**
|
||
* edit `/home/USER/visit.waterschurch.org/.htaccess` and uncomment
|
||
`SetEnv VISIT_PRIVATE_DIR /home/USER/visit-private` with the real path.
|
||
5. Permissions: folders `0755`, files `0644` (File Manager's defaults after extraction are fine).
|
||
|
||
The upload never requires editing generated files. To update the site later, rebuild locally and
|
||
repeat steps 5.2–5.3 (or use *File Manager → Upload* with "Overwrite existing files"). An alternative
|
||
for frequent updates is cPanel's **Git Version Control** pointed at a repo whose `out/` is committed,
|
||
or SFTP with `rsync --delete out/ USER@host:visit.waterschurch.org/`.
|
||
|
||
## 6. Verify the API
|
||
|
||
Open `https://visit.waterschurch.org/api/health.php`. You should see `"ok": true` and:
|
||
|
||
```
|
||
"private_dir_found": true, "env_file_found": true, "data_dir_writable": true,
|
||
"database": "ok (sqlite)", "campuses_loaded": 6, "https": true
|
||
```
|
||
|
||
`integration_mode` shows `mock` until you change it in `.env`. Fix anything `false` before continuing
|
||
(most often: wrong `VISIT_PRIVATE_DIR` path or `data` folder not writable).
|
||
|
||
Confirm private paths are blocked (each must return **403**):
|
||
|
||
* `https://visit.waterschurch.org/api/lib/Config.php`
|
||
* `https://visit.waterschurch.org/api/config/campuses.json`
|
||
* `https://visit.waterschurch.org/api/cron/retry-integrations.php`
|
||
|
||
## 7. Cron job for retries
|
||
|
||
cPanel → **Advanced → Cron Jobs** → add, every 5 minutes (`*/5 * * * *`):
|
||
|
||
```
|
||
VISIT_PRIVATE_DIR=/home/USER/visit-private /usr/local/bin/php /home/USER/visit.waterschurch.org/api/cron/retry-integrations.php >> /home/USER/visit-private/logs/cron.log 2>&1
|
||
```
|
||
|
||
Check the PHP binary path with **MultiPHP Manager** (often `/usr/local/bin/php`, `/opt/cpanel/ea-php82/root/usr/bin/php`, or `/usr/local/bin/ea-php82`). The job re-sends any Mailchimp/Sinch
|
||
delivery that failed transiently (up to 5 attempts with backoff) and, if `LEAD_RETENTION_DAYS` is set,
|
||
deletes fully-processed leads older than that.
|
||
|
||
Optional weekly export for staff (contains personal data — download and delete promptly):
|
||
|
||
```
|
||
VISIT_PRIVATE_DIR=/home/USER/visit-private /usr/local/bin/php /home/USER/visit.waterschurch.org/api/cron/export-leads.php --since=$(date -d '7 days ago' +%F) > /home/USER/visit-private/data/leads-week.csv
|
||
```
|
||
|
||
## 8. Go live
|
||
|
||
1. Set `INTEGRATION_MODE=live` in `/home/USER/visit-private/.env`.
|
||
2. Remove the Church Center redirect (step 3).
|
||
3. Submit one real test lead with your own email and mobile: confirm the Mailchimp contact + tags
|
||
and the confirmation text arrive. Reply STOP to test opt-out routing, then re-subscribe.
|
||
4. Run through [LAUNCH-CHECKLIST.md](LAUNCH-CHECKLIST.md).
|
||
|
||
## 9. Updating content later
|
||
|
||
| Change | Where | Then |
|
||
| --- | --- | --- |
|
||
| Service times, address, phone, SMS number, campus status | `config/campuses.json` | `npm run build` → re-upload |
|
||
| Logo | `assets/source/logos/` (same file names) | `npm run build` → re-upload |
|
||
| Photos | `assets/source/photos/` (same file names) | `npm run build` → re-upload |
|
||
| FAQs, kids info, headlines, consent text | `src/content/*.ts` (bump `consent.version` if consent text changes) | `npm run build` → re-upload |
|
||
| Central contact number, privacy URL | `.env` (`NEXT_PUBLIC_*`) | `npm run build` → re-upload |
|
||
| API keys, mode, providers | `/home/USER/visit-private/.env` | takes effect immediately, no rebuild |
|
||
| SMS template | `SINCH_CONFIRMATION_TEMPLATE` in server `.env` | immediate |
|
||
|
||
## 10. Files, permissions and logs
|
||
|
||
```
|
||
/home/USER/
|
||
├── visit.waterschurch.org/ document root (0755) — everything in out/
|
||
│ ├── .htaccess HTTPS + canonical redirects, caching, security headers, 404
|
||
│ ├── index.html, 404.html, _next/, images/
|
||
│ └── api/ PHP endpoints (public) + lib/ cron/ config/ (403)
|
||
└── visit-private/ NOT web accessible (0750)
|
||
├── .env secrets (0640)
|
||
├── data/leads.sqlite lead store (created automatically)
|
||
├── data/retry.lock
|
||
└── logs/app.log, cron.log JSON lines; personal data and secrets are redacted
|
||
```
|
||
|
||
Rotate `app.log` occasionally (cPanel cron: `find /home/USER/visit-private/logs -name '*.log' -size +20M -delete`)
|
||
or simply download and clear it.
|
||
|
||
## 11. Optional: MySQL instead of SQLite
|
||
|
||
cPanel → **Databases → MySQL Database Wizard**: create `USER_visit` + a user with ALL PRIVILEGES.
|
||
In `.env`: `DATABASE_DSN=mysql:host=localhost;dbname=USER_visit;charset=utf8mb4`,
|
||
`DATABASE_USER`, `DATABASE_PASSWORD`. Tables are created automatically on first request.
|
||
|
||
## 12. Content Security Policy
|
||
|
||
`.htaccess` ships a **report-only** CSP covering GTM, GA4, Meta, TikTok and Turnstile. After
|
||
confirming the tags you actually use, change `Content-Security-Policy-Report-Only` to
|
||
`Content-Security-Policy` and remove hosts you do not need.
|