Plan Your Visit landing page for visit.waterschurch.org
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>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Launch checklist
|
||||
|
||||
## A. Items that require Waters Church approval before launch
|
||||
|
||||
Nothing below was fabricated; each item is either unverified or a legal/policy decision.
|
||||
|
||||
### Facts to confirm (config/campuses.json → `verify`)
|
||||
- [ ] **Coordinates** for every campus (derived from Google Maps links / OpenStreetMap; spot-check each pin).
|
||||
- [ ] **North Attleboro service times** — campus page says 7:30, 9:30, 11:30 AM; the locations index page shows "11:30p" (assumed typo).
|
||||
- [ ] **Parking and arrival notes** for each campus (currently empty; generic copy is shown instead).
|
||||
- [ ] **Woonsocket and Southcoast phone numbers** — the main office number is used because the campus pages list none.
|
||||
- [ ] **Orlando** — marked `launching` (launch team only) and excluded from recommendations. Flip `status` to `open` and add an SMS number when it opens.
|
||||
- [ ] **Central "Text Us" number** (`NEXT_PUBLIC_CENTRAL_SMS_NUMBER`, default +1 508-695-1300) — who answers general texts?
|
||||
- [ ] **Norwood** uses `info@waterschurchnorwood.org` and 781-762-3357 per its own page — confirm these are current.
|
||||
|
||||
### Content placeholders (src/content)
|
||||
- [ ] `faqs.ts` items flagged `confirm: true`: service length ("a little over an hour"), parking ("free on-site parking at every location"), accessibility ("step-free entrances").
|
||||
- [ ] `kids.ts` items flagged `placeholder: true`: first-time check-in steps, "arrive 15 minutes early", what to bring, special-needs accommodations.
|
||||
- [ ] `testimonials.ts` is empty → section is hidden. Add real, permissioned quotes or leave hidden.
|
||||
- [ ] Headline and copy tone (`copy.ts`).
|
||||
|
||||
### Legal / compliance (not legal advice — route to Waters Church's adviser)
|
||||
- [ ] **SMS consent language** (`consent.ts` → `sms.detail`): TCPA/CTIA disclosures, frequency, "Msg & data rates may apply", STOP/HELP, consent not a condition. Confirm the number is registered (10DLC campaign / toll-free verification) for this use case.
|
||||
- [ ] **Email consent language** and single vs. double opt-in (`MAILCHIMP_DOUBLE_OPTIN`).
|
||||
- [ ] **Privacy Policy** at waterschurch.org/privacy covers this subdomain, SMS, geolocation estimate, analytics and ad pixels. Add a **Terms** URL (`NEXT_PUBLIC_TERMS_URL`) if one exists.
|
||||
- [ ] **Data retention**: set `LEAD_RETENTION_DAYS` (0 = keep forever). Decide who may run `export-leads.php` and where exports live.
|
||||
- [ ] **Analytics consent mode**: `implied` (default) vs `opt-in` banner.
|
||||
- [ ] **Location estimation** wording (`consent.ts` → `locationBlurb`).
|
||||
- [ ] Bump `consent.version` if any consent text changes.
|
||||
|
||||
## B. Post-deployment verification
|
||||
|
||||
### Hosting
|
||||
- [ ] `https://visit.waterschurch.org/` loads; `http://` and `www.` redirect (301) to `https://visit.waterschurch.org/`.
|
||||
- [ ] SSL padlock valid (AutoSSL) — check with https://www.ssllabs.com/ssltest/.
|
||||
- [ ] `/BUILD.txt` shows the expected build; `/404.html` and a random URL return the branded 404.
|
||||
- [ ] `/api/health.php` → `"ok": true`, `integration_mode: "live"`.
|
||||
- [ ] `/api/lib/Config.php`, `/api/config/campuses.json`, `/api/cron/retry-integrations.php` → **403**.
|
||||
- [ ] Response headers include `Strict-Transport-Security`, `X-Content-Type-Options`, cache headers (`immutable` on `/_next/static`, `must-revalidate` on HTML), `Content-Encoding: br|gzip`.
|
||||
- [ ] Old Church Center redirect removed.
|
||||
|
||||
### Location detection
|
||||
- [ ] On a phone off Wi-Fi: a campus is *recommended* with the "based on your general area" badge.
|
||||
- [ ] Deny "Use my precise location" → friendly message, page still usable.
|
||||
- [ ] ZIP search: 02763 → North Attleboro, 02062 → Norwood, 02895 → Woonsocket, 02878 → Southcoast, 33572 → Apollo Beach, 32789 → Apollo Beach (far away notice), 90210 → far away notice.
|
||||
- [ ] Bad ZIP (`abc`, `1234`) → inline error, no crash.
|
||||
- [ ] Choose a different campus → form step 1 updates to match.
|
||||
|
||||
### Form, consent and integrations (use your own email/phone)
|
||||
- [ ] Email consent only → Mailchimp contact created with tags; **no** text sent.
|
||||
- [ ] SMS consent without phone → blocked with an inline error.
|
||||
- [ ] SMS + phone → confirmation text arrives from the **campus** number within a minute; Mailchimp `Visit:SMS Opt-In` tag present.
|
||||
- [ ] Neither box → lead stored (export shows it), nothing sent.
|
||||
- [ ] Reply STOP to the text → opt-out confirmed by Sinch; resubscribe afterwards.
|
||||
- [ ] Double-tap "Send" / refresh and resubmit → one lead, one text (`duplicate: true` in the network response).
|
||||
- [ ] Temporarily set a wrong `SINCH_API_SECRET` → submit → confirmation still shows, `sms_status = retry`, cron log shows retry; fix the secret → text arrives on the next cron run.
|
||||
- [ ] Confirmation screen shows campus, times, directions, "Text Us a Question" with the campus number.
|
||||
- [ ] Mailchimp journey/automation fires for the new contact.
|
||||
|
||||
### Attribution and analytics
|
||||
- [ ] Visit with `?utm_source=facebook&utm_campaign=test&fbclid=abc&v=b`, submit → export shows the attribution JSON and `variant = b`; Mailchimp tags include `Visit:Platform Facebook`, `Visit:Campaign test`.
|
||||
- [ ] Visit with no parameters and with malformed ones (`?utm_source=<script>`) → still works, values sanitised.
|
||||
- [ ] GTM/GA4 debug view shows `landing_page_view`, `campus_recommended`, `campus_selected`, `plan_visit_started`, `plan_visit_step_completed`, `plan_visit_submitted`, `plan_visit_success`, `text_us_clicked`, `directions_clicked` with `campus`, `platform`, `campaign`, `device_category`, `variant`.
|
||||
- [ ] Conversion events fire **only** after `plan_visit_success` (check Meta Events Manager / Google Ads test conversion).
|
||||
- [ ] No email/phone/name appears in any analytics payload.
|
||||
|
||||
### Text Us
|
||||
- [ ] iPhone and Android: header, hero, floating button and campus cards open Messages with the pre-filled text, editable, to the right number.
|
||||
- [ ] Desktop: the `sms:` link degrades gracefully (Messages app on macOS, otherwise no-op) — consider a visible phone number if desktop traffic is significant.
|
||||
|
||||
### Consent records
|
||||
- [ ] Export a lead: `consent_version`, `consent_at`, `email_consent`, `sms_consent`, hashed IP present; no raw IP or precise coordinates stored.
|
||||
|
||||
### Performance & accessibility
|
||||
- [ ] Lighthouse mobile (Chrome DevTools, "Slow 4G"): Performance ≥ 90, Accessibility ≥ 95, no CLS from hero/fonts.
|
||||
- [ ] 320 px, 375 px, 430 px, 768 px, 1280 px widths: no horizontal scroll, tap targets ≥ 44 px.
|
||||
- [ ] Keyboard only: skip link → header → campus cards → FAQ accordions → form steps → submit; focus always visible.
|
||||
- [ ] Screen reader: form errors announced (`role="alert"`), step heading focused on step change, confirmation announced (`role="status"`).
|
||||
- [ ] `prefers-reduced-motion`: no animation.
|
||||
|
||||
### Operations
|
||||
- [ ] Cron job runs every 5 minutes (`cron.log` grows with `retry:` lines).
|
||||
- [ ] Someone owns `app.log` review and the leads export cadence.
|
||||
- [ ] `.env` backed up somewhere safe (it is not in the repo).
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# Setup guide (developers)
|
||||
|
||||
## 1. Requirements
|
||||
|
||||
* Node.js 20+ and npm
|
||||
* PHP 8.1+ with `curl`, `pdo_sqlite`, `mbstring`, `json` (macOS: `brew install php`)
|
||||
* `zip` (for `npm run package`)
|
||||
|
||||
## 2. Run locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
cp .env.example private/.env
|
||||
```
|
||||
|
||||
Edit `.env` (frontend build settings):
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_BASE=http://127.0.0.1:8080/api
|
||||
```
|
||||
|
||||
Edit `private/.env` (PHP API; stays in mock mode by default):
|
||||
|
||||
```
|
||||
INTEGRATION_MODE=mock
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
DEBUG_GEO=true # lets you fake a location: /api/locate.php?debug_lat=42.19&debug_lng=-71.19
|
||||
```
|
||||
|
||||
Then in two terminals:
|
||||
|
||||
```bash
|
||||
npm run dev # http://localhost:3000
|
||||
npm run dev:api # http://127.0.0.1:8080/api/health.php
|
||||
```
|
||||
|
||||
Open http://localhost:3000. Submit the form: in mock mode the API stores the lead in
|
||||
`private/data/leads.sqlite` and logs *what it would have sent* to `private/logs/app.log`
|
||||
without contacting Mailchimp or Sinch.
|
||||
|
||||
### Preview the exact production build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
php -S 127.0.0.1:8080 -t out
|
||||
```
|
||||
|
||||
This serves the static export and the PHP API from one origin, like cPanel will.
|
||||
|
||||
## 3. Test without sending messages
|
||||
|
||||
* `INTEGRATION_MODE=mock` (default) never calls Mailchimp or Sinch. Responses carry
|
||||
`"status": "mock"` so the confirmation screen still renders correctly.
|
||||
* `npm test` runs the TypeScript suite (jsdom).
|
||||
* `npm run test:php` spins up fake Mailchimp/Sinch/geocode servers on `127.0.0.1:8099`
|
||||
and the real API on `127.0.0.1:8098`, then checks validation, consent enforcement,
|
||||
idempotency, partial failure + cron retry, and every HTTP endpoint end to end.
|
||||
* To inspect stored leads: `php public/api/cron/export-leads.php > leads.csv`
|
||||
(run with `VISIT_PRIVATE_DIR=private` locally).
|
||||
|
||||
## 4. Enter campus details
|
||||
|
||||
Everything campus-related is in **`config/campuses.json`**. Each entry:
|
||||
|
||||
| Field | Notes |
|
||||
| --- | --- |
|
||||
| `id`, `name`, `label` | `id` is stable and appears in analytics + Mailchimp tags |
|
||||
| `status` | `open` (selectable), `launching` (shown as "coming soon"), `hidden` |
|
||||
| `address`, `coordinates` | Coordinates drive the "closest campus" math. Verify on Google Maps (right-click → copy coordinates). |
|
||||
| `serviceTimes[]` | `{ day, times[], note? }` — times are display strings like `"9:30 AM"` |
|
||||
| `phone`, `smsNumber` | E.164. `smsNumber` must be SMS-enabled on the Sinch Engage account |
|
||||
| `sinchAccountId` | Sinch Engage sub-account used as the sender (`Account` header) |
|
||||
| `directionsUrl`, `campusUrl` | Google Maps directions link; campus page on waterschurch.org |
|
||||
| `arrivalNotes`, `parkingNotes`, `kidsNotes` | Optional short text shown on the campus card |
|
||||
| `image` | Photo key; put the file at `assets/source/photos/campus-<id>.jpg`, run `npm run images` |
|
||||
| `verify[]` | Reminder list of fields Waters Church still needs to confirm (documentation only) |
|
||||
|
||||
After editing: `npm run build` and upload `out/` again (the API reads `out/api/config/campuses.json`).
|
||||
|
||||
Copy and FAQ text live in `src/content/` (`copy.ts`, `faqs.ts`, `kids.ts`, `consent.ts`,
|
||||
`testimonials.ts`). Site-wide settings (phone, email, privacy URL, central SMS number) are in
|
||||
`src/config/site.ts` and can be overridden by `NEXT_PUBLIC_*` variables.
|
||||
|
||||
## 5. Connect Mailchimp
|
||||
|
||||
1. In Mailchimp: **Profile → Extras → API keys → Create a key**. The key ends in `-usXX`; that is your
|
||||
`MAILCHIMP_SERVER_PREFIX`.
|
||||
2. **Audience → Settings → Audience name and defaults** → copy the **Audience ID**.
|
||||
3. Create these **merge fields** in the audience (Settings → Audience fields): `CAMPUS`, `VISITDATE`,
|
||||
`SERVICE`, `ADULTS`, `KIDS`, `KIDAGES`, `QUESTIONS`, `UTMSOURCE`, `UTMCAMP`, `LPVARIANT`, `SUBID`,
|
||||
`PHONE` (text). Unknown fields are ignored (`skip_merge_validation=true`), but they only store data
|
||||
if they exist.
|
||||
4. Tags applied automatically (prefix configurable via `MAILCHIMP_TAG_PREFIX`, default `Visit:`),
|
||||
matching the church's existing `Visit:<Campus>` convention:
|
||||
`Visit:<Campus>` (e.g. `Visit:Norwood`), `Visit:First-Time Visitor`, `Visit:Source visit.waterschurch.org`,
|
||||
`Visit:Platform <Google|Facebook|Instagram|TikTok|X|…>`, `Visit:Campaign <utm_campaign>`,
|
||||
`Visit:Visit <date>` or `Visit:Service <time>`, `Visit:Has Kids`, `Visit:SMS Opt-In`.
|
||||
5. Welcome journey — two options:
|
||||
* **Tag-triggered** (simplest): build a Customer Journey whose starting point is
|
||||
"Tag added → `Visit:First-Time Visitor`" (all campuses) or `Visit:<Campus>` (one per campus).
|
||||
The "New Visit" journey created 2026-09-22 triggers on `Visit:Apollo Beach` only.
|
||||
* **API-triggered**: add an "API trigger" starting point, copy the journey id and step id from
|
||||
the trigger's URL into `MAILCHIMP_JOURNEY_ID` / `MAILCHIMP_JOURNEY_STEP_ID`.
|
||||
6. Consent: contacts are only created when the visitor ticks the **email** box. `status_if_new` is
|
||||
`subscribed` (single opt-in). Set `MAILCHIMP_DOUBLE_OPTIN=true` to require Mailchimp's confirmation
|
||||
email instead. Existing contacts are updated, never duplicated (upsert by email hash).
|
||||
|
||||
## 6. Connect Sinch Engage
|
||||
|
||||
The church's Sinch Engage account (formerly MessageMedia) has one sub-account per campus with an
|
||||
SMS-enabled number; these are already in `config/campuses.json`:
|
||||
|
||||
| Campus | Sub-account | Number |
|
||||
| --- | --- | --- |
|
||||
| North Attleboro | `WatersChurch_OLA_0007` | +1 508-695-1300 |
|
||||
| Norwood | `WatersChurch_OLA_0002` | +1 781-762-3357 |
|
||||
| Woonsocket | `WatersChurch_OLA_0005` | +1 833-241-4837 (toll-free) |
|
||||
| Southcoast | `WatersChurch_OLA_0006` | +1 508-809-3490 |
|
||||
| Apollo Beach | `WatersChurch_OLA_0003` | +1 813-642-6979 |
|
||||
| Parent (fallback) | `WatersChurch_OLA_0001` | +1 774-643-6322, +1 781-762-4007 (10DLC) |
|
||||
|
||||
1. In the Sinch Engage web portal, create **API credentials** on the *parent* account
|
||||
(Basic auth: key + secret). Parent keys may send on behalf of sub-accounts via the `Account` header,
|
||||
which the adapter sets automatically (`SINCH_USE_CAMPUS_SUBACCOUNTS=true`).
|
||||
2. Set `SINCH_API_BASE` to the regional host for the account (US: `https://us.app.api.sinch.com`).
|
||||
3. Endpoint used: `POST /v1/messages` with `content`, `destination_number`, `source_number`,
|
||||
`format: "SMS"`, `delivery_report: true`, `metadata`, `message_expiry_timestamp`.
|
||||
Success = HTTP 202 **and** a `message_id` in the response; anything else is recorded as failed
|
||||
and retried by cron if transient.
|
||||
4. Confirmation text (edit via `SINCH_CONFIRMATION_TEMPLATE`; placeholders `{{first_name}}`,
|
||||
`{{campus_name}}`, `{{service_or_date}}`, `{{address}}`):
|
||||
|
||||
> Hi {{first_name}} — we're excited that you're considering Waters Church {{campus_name}}.
|
||||
> Your visit details: {{service_or_date}}. Have a question? Reply to this message. Reply STOP to opt out.
|
||||
|
||||
5. **Opt-out handling:** confirm in the Sinch Engage portal that STOP/HELP keyword handling is enabled
|
||||
for each campus number, and that replies route to the campus inbox. The landing page never
|
||||
sends to a number that has not explicitly ticked the SMS box on this form.
|
||||
6. Optional: `SINCH_INTERNAL_NOTIFY_NUMBER` sends staff a short heads-up per lead (first name + campus).
|
||||
|
||||
## 7. Configure tracking
|
||||
|
||||
All tracking is off until an ID is present. Set in `.env` before `npm run build`:
|
||||
|
||||
| Variable | Loads |
|
||||
| --- | --- |
|
||||
| `NEXT_PUBLIC_GTM_ID` | Google Tag Manager (recommended: manage every pixel there) |
|
||||
| `NEXT_PUBLIC_GA4_ID` | GA4 directly (if not using GTM) |
|
||||
| `NEXT_PUBLIC_GADS_CONVERSION` | Google Ads conversion (`AW-xxx/label`) fired on `plan_visit_success` |
|
||||
| `NEXT_PUBLIC_META_PIXEL_ID` | Meta Pixel (`PageView` + `Lead` on success) |
|
||||
| `NEXT_PUBLIC_TIKTOK_PIXEL_ID` | TikTok Pixel (`SubmitForm` on success) |
|
||||
| `NEXT_PUBLIC_ANALYTICS_CONSENT_MODE` | `implied` (default) or `opt-in` (shows an Allow/Decline prompt) |
|
||||
|
||||
Every event is pushed to `window.dataLayer` with these properties: `variant`, `source`, `medium`,
|
||||
`campaign`, `content`, `platform`, `device_category`, plus event-specific ones such as `campus`
|
||||
and `selection_source`. Events: `landing_page_view`, `campus_recommended`,
|
||||
`location_permission_requested`, `campus_selected`, `plan_visit_started`,
|
||||
`plan_visit_step_completed`, `plan_visit_submitted`, `plan_visit_success` (server-confirmed),
|
||||
`plan_visit_error`, `text_us_clicked`, `directions_clicked`. No names, emails or phone numbers are
|
||||
ever sent to analytics.
|
||||
|
||||
In GTM, create a Custom Event trigger for `plan_visit_success` and attach your conversion tags.
|
||||
|
||||
Attribution captured per visitor (session storage) and stored with the lead: `utm_source`,
|
||||
`utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `gclid`, `gbraid`, `wbraid`, `fbclid`,
|
||||
`ttclid`, `twclid`, landing-page `variant` (`?v=b`), initial referrer, first landing URL.
|
||||
|
||||
## 8. Location services
|
||||
|
||||
* `GEOIP_PROVIDER` — `ipapi_co` (default, no key up to ~1,000 lookups/day), `ipinfo` (key),
|
||||
`ip_api_com` (key for HTTPS), `header` (Cloudflare geolocation headers), or `none`.
|
||||
Results are rounded to ~1 km and cached for an hour by hashed IP. They are shown to the visitor as
|
||||
a *recommendation*, never stored with the lead.
|
||||
* `GEOCODE_PROVIDER` — `zippopotam` (default, free), `nominatim`, or `google` (key).
|
||||
ZIP centroids are cached for 30 days.
|
||||
* Precise browser geolocation is requested **only** when the visitor taps "Use my precise location"
|
||||
and is never sent to the server.
|
||||
|
||||
## 9. Anti-spam
|
||||
|
||||
Honeypot field + minimum time-on-form (`MIN_FORM_SECONDS`) + per-IP rate limits
|
||||
(`RATE_LIMIT_SUBMIT_PER_10MIN`, `RATE_LIMIT_SUBMIT_PER_DAY`) are always on. Optional Cloudflare
|
||||
Turnstile: set `NEXT_PUBLIC_TURNSTILE_SITE_KEY` (build) and `TURNSTILE_SECRET_KEY` (server).
|
||||
|
||||
## 10. Deploy
|
||||
|
||||
See [DEPLOY-CPANEL.md](DEPLOY-CPANEL.md).
|
||||
Reference in New Issue
Block a user