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>
9.9 KiB
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.orgalready 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.htaccessor 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:
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
.envvalues (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,opensslare enabled. (pdo_mysqlonly 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).
- Create folder
visit-private. - Inside it create folders
dataandlogs. - Upload
.env.examplefrom the package (dist/env.example.txt), rename it to.env, and edit it in File Manager: fill in real values, setINTEGRATION_MODE=liveonly when ready to go live, and set a long randomHASH_SALT. - Permissions (right-click → Change Permissions): folders
0750, the.envfile0640. 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
Arecord 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 theRewriteCond %{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
- File Manager → open
/home/USER/visit.waterschurch.org/. Delete the placeholdercgi-binfolder if present. - Upload the zip from
dist/, then right-click → Extract into this folder. Delete the zip. - Enable Settings → Show Hidden Files (dotfiles) and confirm
.htaccessis present at the root and insideapi/lib,api/cron,api/config. - 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/.htaccessand uncommentSetEnv VISIT_PRIVATE_DIR /home/USER/visit-privatewith the real path.
- leave the default (
- Permissions: folders
0755, files0644(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.phphttps://visit.waterschurch.org/api/config/campuses.jsonhttps://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
- Set
INTEGRATION_MODE=livein/home/USER/visit-private/.env. - Remove the Church Center redirect (step 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.
- Run through 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.