commit 78cd0cc55082ff6fa06e68b86307650cc01c75ee Author: Waters Church Date: Tue Sep 22 13:22:26 2026 -0400 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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6fe540d --- /dev/null +++ b/.env.example @@ -0,0 +1,110 @@ +# ============================================================================= +# visit.waterschurch.org — environment configuration (PLACEHOLDERS ONLY) +# +# Two consumers read this file format: +# 1. `npm run build` (Next.js) reads NEXT_PUBLIC_* from ./.env at build time. +# These values are embedded in the static HTML/JS — never put secrets here. +# 2. The PHP API reads EVERYTHING ELSE at runtime from +# /.env on the server (outside the web root). +# Copy this file there as `.env` and fill in real values. +# ============================================================================= + +# ---- Public site ------------------------------------------------------------ +NEXT_PUBLIC_SITE_URL=https://visit.waterschurch.org +PUBLIC_SITE_URL=https://visit.waterschurch.org +# Comma-separated extra origins allowed to call the API (local dev only) +ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +# true = search engines may index. Set false on staging. +NEXT_PUBLIC_ROBOTS_INDEX=true +# Where the browser calls the API. Same-origin "/api" in production; php -S in dev. +NEXT_PUBLIC_API_BASE=/api +# Default landing-page variant label (override per ad with ?v=b) +NEXT_PUBLIC_VARIANT=a +# Legal pages +NEXT_PUBLIC_PRIVACY_URL=https://waterschurch.org/privacy/ +NEXT_PUBLIC_TERMS_URL= +# "Text Us" number before a campus is chosen (must be SMS-enabled on the Sinch account) +NEXT_PUBLIC_CENTRAL_SMS_NUMBER=+15086951300 + +# ---- Integration mode ------------------------------------------------------- +# mock = log what WOULD be sent, send nothing (default). live = real Mailchimp + Sinch calls. +INTEGRATION_MODE=mock +LOG_LEVEL=info +# Random string used to hash IPs for rate limiting / consent records. Generate: openssl rand -hex 32 +HASH_SALT=change-me-to-a-long-random-string +# Set true ONLY if the site is behind Cloudflare or another trusted proxy +TRUST_PROXY_HEADERS=false + +# ---- Lead storage ----------------------------------------------------------- +# Default: SQLite at /data/leads.sqlite (no setup needed). +# To use cPanel MySQL instead, create a database + user in cPanel and set: +DATABASE_DSN= +# e.g. mysql:host=localhost;dbname=cpaneluser_visit;charset=utf8mb4 +DATABASE_USER= +DATABASE_PASSWORD= +# Override the data directory (must be writable by PHP and outside the web root) +DATA_DIR= +# Delete fully-processed leads older than N days (0 = keep forever). Confirm with Waters Church. +LEAD_RETENTION_DAYS=0 + +# ---- Mailchimp -------------------------------------------------------------- +# Account → Extras → API keys. The key ends in "-usXX"; that suffix is the server prefix. +MAILCHIMP_API_KEY= +MAILCHIMP_SERVER_PREFIX=us21 +# Audience → Settings → Audience name and defaults → "Audience ID" +MAILCHIMP_AUDIENCE_ID= +# Optional: API-triggered Customer Journey (Automations → journey → API trigger step) +MAILCHIMP_JOURNEY_ID= +MAILCHIMP_JOURNEY_STEP_ID= +# Tag prefix applied to every contact from this page +MAILCHIMP_TAG_PREFIX=Visit: +# true = new contacts start as "pending" and receive a Mailchimp confirmation email +MAILCHIMP_DOUBLE_OPTIN=false + +# ---- Sinch Engage (formerly MessageMedia) ----------------------------------- +# Sinch Engage portal → Settings → API credentials (Basic auth key + secret). +# Regional host: US https://us.app.api.sinch.com | EU https://eu.app.api.sinch.com | APAC https://au.app.api.sinch.com +SINCH_API_BASE=https://us.app.api.sinch.com +SINCH_API_KEY= +SINCH_API_SECRET= +# Send from each campus's own number/sub-account (config/campuses.json). Recommended. +SINCH_USE_CAMPUS_NUMBERS=true +SINCH_USE_CAMPUS_SUBACCOUNTS=true +# Fallback sender when a campus has no number (must be a 10DLC/toll-free number on the parent account) +SINCH_DEFAULT_SOURCE_NUMBER=+17746436322 +# Fallback sub-account (leave blank to send as the parent account) +SINCH_ACCOUNT_ID= +# Optional staff heads-up text for every new lead (no visitor PII beyond first name) +SINCH_INTERNAL_NOTIFY_NUMBER= +# Optional override of the confirmation template. Placeholders: {{first_name}} {{campus_name}} {{service_or_date}} {{address}} +SINCH_CONFIRMATION_TEMPLATE= + +# ---- Location services ------------------------------------------------------ +# Approximate IP geolocation: none | header | ipapi_co | ipinfo | ip_api_com +GEOIP_PROVIDER=ipapi_co +GEOIP_API_KEY= +GEOIP_CACHE_TTL=3600 +# ZIP lookup: zippopotam | nominatim | google +GEOCODE_PROVIDER=zippopotam +GEOCODE_API_KEY= +# Local testing helpers (never enable in production) +DEBUG_GEO=false + +# ---- Anti-spam ---------------------------------------------------------------- +# Optional Cloudflare Turnstile (free). Leave blank to rely on honeypot + timing + rate limits. +NEXT_PUBLIC_TURNSTILE_SITE_KEY= +TURNSTILE_SECRET_KEY= +MIN_FORM_SECONDS=3 +RATE_LIMIT_SUBMIT_PER_10MIN=5 +RATE_LIMIT_SUBMIT_PER_DAY=60 +RETRY_BATCH_SIZE=25 + +# ---- Analytics & advertising (all optional; nothing loads when blank) ------- +# implied = fire immediately (typical US ad landing page); opt-in = show a consent prompt first +NEXT_PUBLIC_ANALYTICS_CONSENT_MODE=implied +NEXT_PUBLIC_GTM_ID= +NEXT_PUBLIC_GA4_ID= +NEXT_PUBLIC_META_PIXEL_ID= +NEXT_PUBLIC_TIKTOK_PIXEL_ID= +# Google Ads conversion "send_to" value, e.g. AW-123456789/AbC-D_efG-h12_34-567 +NEXT_PUBLIC_GADS_CONVERSION= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07d250a --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# dependencies & build output +node_modules +.next +out +dist/ +tsconfig.tsbuildinfo +next-env.d.ts + +# generated at build time by scripts/optimize-images.mjs (run `npm run images`) +public/images/ +src/config/images.manifest.json + +# secrets & local runtime data (never commit) +.env +.env.* +!.env.example +private/* +!private/README.md +public/api/data/ +tests/php/.tmp/ + +# misc +*.log +.DS_Store +coverage/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..88f6eba --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# visit.waterschurch.org — Plan Your Visit landing page + +A focused, mobile-first advertising landing page for Waters Church that helps +first-time visitors find their closest campus, know what to expect, and plan a +visit. Built to deploy on the church's **cPanel/Apache** hosting as a static +site with a small PHP API. + +``` +Ads (Facebook · Instagram · X · Google · TikTok) + │ utm_* / gclid / fbclid / ttclid / twclid + ▼ +┌──────────────────────────────┐ ┌────────────────────────────────────┐ +│ Static site (Next.js export)│ JSON │ PHP API (public/api) │ +│ hero · campus finder · │ ─────▶ │ validate → store lead (SQLite/MySQL)│ +│ what to expect · kids · form│ │ → Mailchimp → Sinch Engage → retry │ +└──────────────────────────────┘ └────────────────────────────────────┘ +``` + +## Documentation + +| Document | Audience | +| --- | --- | +| [docs/SETUP.md](docs/SETUP.md) | Developers: run locally, test without sending messages, configure Mailchimp / Sinch / tracking, edit campus content | +| [docs/DEPLOY-CPANEL.md](docs/DEPLOY-CPANEL.md) | Site administrator: create the subdomain, SSL, upload, private config, cron, updates | +| [docs/LAUNCH-CHECKLIST.md](docs/LAUNCH-CHECKLIST.md) | Everyone: post-deployment verification and the list of items that need human/legal approval | +| [.env.example](.env.example) | Every configuration key, with placeholders | +| [config/campuses.json](config/campuses.json) | Campus names, addresses, service times, SMS numbers (single source of truth) | + +## Quick start + +```bash +npm install +cp .env.example .env # public build settings (safe to commit? no — .env is git-ignored) +cp .env.example private/.env # local PHP API settings (INTEGRATION_MODE=mock) +npm run dev # Next.js on http://localhost:3000 +npm run dev:api # PHP API on http://127.0.0.1:8080 (needs PHP 8.1+ locally) +``` + +Point the frontend at the local API by setting `NEXT_PUBLIC_API_BASE=http://127.0.0.1:8080/api` +in `.env` (and add `http://localhost:3000` to `ALLOWED_ORIGINS` in `private/.env`). + +Or preview the production build in one process (static files + PHP together): + +```bash +npm run build && php -S 127.0.0.1:8080 -t out +``` + +## Scripts + +| Command | What it does | +| --- | --- | +| `npm run dev` | Next.js dev server | +| `npm run dev:api` | PHP built-in server for the API (`public/`) | +| `npm run build` | Optimize images → `next build` (static export) → post-build (`.htaccess`, campus config) → `out/` | +| `npm run package` | Zip `out/` into `dist/` for upload through cPanel File Manager | +| `npm test` | Vitest: distance, ZIP fallback, validation, consent, attribution, analytics, form flow (keyboard) | +| `npm run test:php` | PHP suite: validation, Mailchimp/Sinch mapping, idempotency, partial failure + retry, HTTP end-to-end | +| `npm run typecheck` / `npm run lint` | TypeScript and ESLint | +| `npm run images` | Regenerate responsive AVIF/WebP/JPEG images from `assets/source` | + +## Project layout + +``` +config/campuses.json campus data (read by TS and PHP) +src/config/ site settings, campus loader, image manifest +src/content/ copy, FAQs, kids info, consent text, testimonials +src/components/ page sections (Hero, CampusFinder, PlanVisitForm, …) +src/lib/ geo, attribution, analytics, schema, api client +public/api/ PHP endpoints + lib (Mailchimp, Sinch, GeoIp, Geocode, LeadProcessor) +public/api/cron/ retry-integrations.php, export-leads.php (CLI only) +deploy/htaccess.root Apache config copied to out/.htaccess at build +assets/source/ original logos/photos (see SOURCES.md) +tests/ Vitest (TS) and tests/php (PHP) +docs/ setup, deployment, launch checklist +``` + +## Architecture decision (cPanel) + +The target host is cPanel on Apache (confirmed: `waterschurch.org:2083` answers, server header `Apache`). +Whether the account offers the Node.js Application Manager could not be verified without logging in, +so the build targets the lowest common denominator that every cPanel account supports: + +* **Frontend:** Next.js App Router + TypeScript + Tailwind, compiled with `output: "export"` to plain + HTML/CSS/JS. No Node process runs in production. +* **Backend:** PHP 8.1+ endpoints (cPanel ships PHP 8.x with cURL and PDO/SQLite). Secrets live in a + `.env` file *outside* the document root. Leads are stored in SQLite (or cPanel MySQL) **before** + Mailchimp/Sinch are called; failures are retried by a cPanel cron job. +* **Location:** approximate server-side IP lookup (city-level, rounded, cached by hashed IP) for the + recommendation; precise browser geolocation only on request; ZIP search always available. + +If Waters Church later confirms Node hosting, the frontend needs no change; the PHP layer could be +swapped for route handlers, but there is no functional reason to. + +## Working in this Google Drive folder + +This repo lives in a Google Drive shared drive. `node_modules/` is large and churns Drive sync; +`.next/` and `out/` are symlinked to `~/.local/share/visit-waterschurch/` for that reason +(`npm install` insists on a real `node_modules`). If you clone elsewhere, delete the two symlinks +first: `rm .next out`. + +## Facts and placeholders + +Campus addresses, service times, phone numbers, kids age groups and safety practices were verified +against waterschurch.org on 2026-09-22. Items that could **not** be verified are `null` in +`config/campuses.json` (listed under each campus's `verify` array) or flagged `placeholder: true` / +`confirm: true` in `src/content/`. Testimonials are intentionally empty. See +[docs/LAUNCH-CHECKLIST.md](docs/LAUNCH-CHECKLIST.md). diff --git a/Visit Page Prompt.md b/Visit Page Prompt.md new file mode 100644 index 0000000..b54d65f --- /dev/null +++ b/Visit Page Prompt.md @@ -0,0 +1,612 @@ +You are a senior product designer, conversion-rate optimization strategist, and full-stack engineer. Build a production-ready advertising landing page for Waters Church that helps first-time visitors feel comfortable, understand what to expect, and take the next step toward attending a service. + +## Primary objective + +Create a modern, edgy, simple, mobile-first landing page that converts traffic from Facebook, Instagram, X, Google, and TikTok ads into prospective first-time visitors. + +The page should: + +1. Identify the Waters Church campus closest to the visitor. +2. Give newcomers clear, reassuring information about their first visit. +3. Capture contact information and visit intent. +4. Add consenting visitors to a Mailchimp email journey. +5. Send consenting visitors a confirmation text through Sinch Engage. +6. Let visitors text the church with questions. +7. Preserve advertising attribution for campaign reporting. + +Do not create a generic church website. Create a focused, high-converting “Plan Your Visit” landing page with one primary conversion goal. + +Add this section to the Claude Code prompt, immediately after “Primary objective”: + +## Confirmed project information + +Use these details throughout the implementation: + +- Production URL: `https://visit.waterschurch.org` +- Main Waters Church website: `https://waterschurch.org` +- Hosting environment: cPanel +- Official information, campus details, service times, brand references, logos, and approved photography can be sourced from `https://waterschurch.org`. + +Treat the main Waters Church website as the primary source of truth for church information. Review it before designing or writing the landing page. Reuse accurate public information where appropriate, but do not copy large sections of text verbatim. Adapt the information into concise, newcomer-focused landing-page copy. + +Download only the Waters Church logos and images needed for this project. Store optimized local copies in the project rather than hotlinking production assets. Preserve image quality, aspect ratio, and transparent backgrounds. Convert oversized photographs to modern web formats such as WebP or AVIF, while retaining fallbacks if needed. Do not alter the logo or invent new brand marks. + +Verify campus addresses, coordinates, service times, contact details, kids information, and other factual claims against `waterschurch.org`. Make uncertain or missing information configurable and flag it for Waters Church to confirm before launch. + +## cPanel hosting requirements + +The finished project must be deployable through the church’s cPanel hosting environment. + +Before selecting the final architecture: + +1. Inspect the existing repository. +2. Determine which runtimes the target cPanel account supports, including its Node.js version, application manager, PHP version, database options, scheduled tasks, SSL, environment-variable management, and reverse-proxy configuration. +3. Do not assume that a continuously running Next.js server is supported. +4. Choose the simplest secure architecture compatible with the available cPanel features. + +Preferred deployment options, in order: + +1. Use the existing application stack if it is already compatible with cPanel. +2. Use a Node.js application through cPanel’s supported Node application manager if the account supports the required runtime and persistent server processes. +3. If Node hosting is unavailable, produce an optimized static frontend with secure PHP server endpoints for form processing, lead persistence, Mailchimp, and Sinch Engage. +4. Do not move secrets or third-party API calls into browser code to work around hosting limitations. + +The deployed page must resolve at: + +`https://visit.waterschurch.org` + +Configure: + +- The canonical URL to use `https://visit.waterschurch.org`. +- Production metadata and social-sharing URLs for that domain. +- HTTPS redirects. +- Non-`www` URL consistency unless Waters Church specifies otherwise. +- cPanel-compatible routing and rewrite rules. +- Secure environment or configuration storage outside the public web root whenever possible. +- Production error logging without exposing personal information or API credentials. +- Appropriate file permissions. +- A custom 404 response. +- Cache headers and compression. +- A deployment process that does not require manually editing generated build files. + +If Apache configuration is required, provide the appropriate `.htaccess` file. If cPanel’s Node.js application manager is used, document the startup file, application root, environment variables, build command, startup command, and restart procedure. + +Do not rely on serverless-platform-specific features such as Vercel functions, edge middleware, or proprietary geolocation headers unless an equivalent cPanel-compatible implementation is provided. + +For approximate location detection, prefer a privacy-conscious server-side IP geolocation service that can operate from cPanel. Keep precise browser geolocation optional and initiated only by the visitor. + +## Updated deployment deliverables + +In addition to the other deliverables, provide: + +- A production build compatible with the target cPanel account. +- Any required `.htaccess`, Node startup, PHP endpoint, or database setup files. +- A cPanel deployment guide written for a site administrator. +- Instructions for creating and pointing the `visit.waterschurch.org` subdomain. +- Instructions for enabling and verifying SSL. +- Instructions for adding production environment variables securely. +- Instructions for configuring scheduled retries or background processing using cPanel cron jobs, if needed. +- Instructions for updating campus details, logos, photographs, service times, and contact numbers. +- A post-deployment checklist covering forms, Mailchimp, Sinch Engage, location detection, analytics, consent records, redirects, SSL, and mobile performance. + +Do not publish or change DNS until explicitly authorized. Prepare the project and deployment documentation so Waters Church can launch it safely through cPanel. + +## First steps + +Before writing code: + +1. Inspect the existing repository, application structure, coding conventions, and design system. +2. Reuse the existing framework and components when practical. +3. Look for existing Waters Church branding, campus data, logos, photography, analytics, environment-variable conventions, and API integrations. +4. If this is a greenfield project, use: + - Next.js with the App Router + - TypeScript + - Tailwind CSS + - Server-side route handlers for integrations + - Zod or equivalent server-side validation +5. Create a brief implementation plan, then build the complete experience. +6. Do not stop at a mockup. Implement the functional page, form flow, campus selection, API integration layer, validation, tracking hooks, loading states, errors, and documentation. + +Do not invent factual information about Waters Church. Put unknown campus details, service times, addresses, phone numbers, and API values into clearly documented configuration or structured placeholder data. + +## Audience and message + +The audience consists mainly of people who: + +- Have never attended Waters Church. +- May not regularly attend any church. +- Are curious but uncertain about what the experience will be like. +- May worry about dress, parking, children, service length, beliefs, or being pressured. +- Are arriving from an advertisement and need an immediate connection between the ad and the page. + +The experience should feel welcoming, confident, energetic, contemporary, and human. Avoid insider church language, clichés, guilt, pressure, and institutional wording. + +Core message: + +“Your first visit doesn’t have to feel uncertain. We’ll help you know what to expect and make it easy to find the Waters Church location nearest you.” + +## Visual direction + +Create an original visual system that feels modern, bold, edgy, and uncluttered. + +Use: + +- Large, heavy display typography. +- Short, direct headlines. +- Strong contrast. +- Generous spacing. +- A restrained color palette aligned with existing Waters Church branding. +- Bold photography or video areas, when appropriate. +- Subtle motion and interaction feedback. +- Clear, repeated calls to action. +- Mobile layouts designed for one-handed use. +- Accessible focus states and large tap targets. + +Avoid: + +- A generic church-template appearance. +- Excessive gradients. +- Overly rounded “startup SaaS” styling. +- Busy navigation. +- Long walls of text. +- Stock religious imagery unless it genuinely fits the brand. +- Carousels, autoplay audio, or distracting animation. + +If brand assets are unavailable, use tasteful placeholders and organize them so they can be replaced without changing the layout. + +## Page structure + +### 1. Minimal header + +Include the Waters Church logo and only essential actions: + +- “Find Your Campus” +- “Text Us” + +Keep users focused on planning a visit. Do not add a full website navigation menu unless one already exists and is necessary. + +### 2. Hero section + +Create a high-impact hero with: + +- A direct headline such as “Church Can Feel Different Here.” +- Supporting copy that addresses first-visit uncertainty. +- Primary CTA: “Plan My Visit” +- Secondary CTA: “Find My Campus” +- A visible “Questions? Text Us” action +- Relevant Waters Church imagery or a well-designed media placeholder +- A compact trust cue such as “No pressure. Just a friendly welcome.” + +The primary CTA should scroll to or open the visit form without sending the user to another page. + +### 3. Closest-campus experience + +Offer the closest relevant Waters Church campus using location-aware logic. + +Use this sequence: + +1. On initial load, make a non-invasive approximate recommendation using server or edge location data if the hosting platform provides it. +2. Clearly label the result as a recommendation. +3. Offer “Use my precise location” as an optional action that triggers the browser permission request only after the user chooses it. +4. Always provide a manual ZIP/postal-code search and campus selector. +5. Never block the experience when location access is denied or unavailable. +6. Save the selected campus through the conversion flow. +7. Explain location use in plain language and do not retain precise coordinates unless truly necessary. + +Calculate the closest campus from latitude and longitude when coordinates are available. Put all campus records in a typed configuration containing: + +- Campus name +- Address +- Latitude and longitude +- Service times +- Campus-specific phone/SMS number +- Directions URL +- Campus page URL +- Parking or arrival notes +- Kids information +- Optional campus image + +The selected-campus card should include: + +- Campus name +- Distance when it can be calculated +- Address +- Upcoming service times +- Directions action +- “Choose This Campus” +- “Text This Campus” +- An easy way to select a different location + +Do not silently claim a campus is closest if the location result is uncertain. + +### 4. “What to Expect” section + +Answer the most common newcomer questions in short, scannable content: + +- What should I wear? +- How long is a service? +- What is the music and teaching like? +- Where should I park? +- What happens when I arrive? +- Is there something for children? +- Will I be singled out or pressured? +- Is the venue accessible? + +Use cards, expandable items, or a clean editorial layout. Keep the answers editable through configuration or content components. + +### 5. Kids and family reassurance + +Give families a concise explanation of: + +- Kids check-in +- Safety and security +- Age groups +- When to arrive +- What parents should bring +- Accessibility or special-needs support, if confirmed + +Do not invent policies. Mark missing information as content placeholders. + +### 6. Social proof + +Add a restrained social-proof section using real testimonials only if they exist in the repository or supplied content. Do not fabricate quotations, attendance numbers, ratings, or claims. + +If real content is unavailable, create an obvious content placeholder that can be removed cleanly before launch. + +### 7. Plan Your Visit form + +Build a short, conversion-oriented multi-step or progressive form. Ask only for information that has a clear purpose. + +Recommended fields: + +- First name — required +- Last name — optional +- Email — required +- Mobile phone — required only if the visitor elects to receive texts +- Selected campus — required +- Expected visit date or service — optional +- Number of adults — optional +- Number and age ranges of children — optional +- “What questions can we answer?” — optional +- Email marketing consent +- SMS consent with a separate, unchecked checkbox + +Do not bundle SMS and email consent. Consent boxes must not be preselected. + +Place concise privacy and consent language beside the relevant controls. The SMS disclosure should explain that the visitor agrees to receive messages from Waters Church, that message and data rates may apply, that frequency may vary, and how to opt out. Link to Privacy Policy and Terms pages or configurable URLs. + +Use accessible labels, inline validation, server-side validation, bot protection, rate limiting, and a honeypot or equivalent anti-spam control. + +The main button should use intent-based language such as: + +- “Plan My Visit” +- “Help Me Get Ready” +- “Send My Visit Details” + +Avoid “Submit.” + +### 8. Confirmation experience + +After a successful submission: + +- Show a warm confirmation message. +- Confirm the selected campus and visit details. +- Display directions and service times. +- Explain that an email is on the way. +- If SMS consent was provided, explain that a text is on the way. +- Provide a prominent “Text Us a Question” action. +- Preserve the page design instead of displaying a generic success alert. +- Fire a conversion event only after the server confirms success. + +If one integration succeeds and another fails, do not lose the lead. Store or log a recoverable integration state, show the user a sensible confirmation, and record the failure for retry or follow-up without exposing technical details. + +## “Text Us” functionality + +Provide a persistent but unobtrusive “Text Us” action throughout the experience. + +On supported devices, use an `sms:` link associated with the selected campus. If no campus has been selected, use a configurable central Waters Church SMS number or ask the visitor to choose a campus. + +Pre-fill a natural message such as: + +“Hi! I’m thinking about visiting Waters Church and have a question.” + +Make sure the visitor can edit the message before sending it. + +The automated confirmation text sent after form submission should be concise and conversational. Example structure: + +“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.” + +Treat this only as editable copy. Ensure the final implementation follows the actual Sinch Engage capabilities, local regulations, and approved Waters Church messaging policies. + +## Sinch Engage integration + +Implement the SMS integration behind a server-only adapter. + +Requirements: + +- Never expose credentials in client-side code. +- Use environment variables. +- Validate and normalize phone numbers, preferably to E.164 format. +- Send an automated message only when explicit SMS consent was provided. +- Store consent timestamp, disclosure version, source, selected campus, and attribution data. +- Include appropriate opt-out handling in the message and downstream workflow. +- Use idempotency protection so retries do not send duplicate texts. +- Handle API errors and timeouts safely. +- Redact personal information and secrets from logs. +- Add a mock/development mode so the flow can be tested without sending a real text. +- Document which Sinch Engage endpoint, sender identity, template, and credentials must be configured. + +Sinch products and account configurations can differ. Use the official documentation matching the project’s actual Sinch Engage account. Isolate provider-specific code so it can be adjusted without changing the form or page. + +Create an adapter interface similar to: + +- `sendVisitConfirmation()` +- `sendInternalLeadNotification()`, only if supported and desired +- `normalizePhoneNumber()` + +Do not pretend that SMS delivery succeeded if the provider did not confirm acceptance. + +## Mailchimp integration + +Implement the Mailchimp connection behind a server-only adapter. + +On valid form submission: + +- Create or update the contact in the correct audience. +- Avoid creating duplicate contacts. +- Apply tags for: + - First-time visitor + - Selected campus + - Landing-page source + - Advertising platform + - Campaign + - Visit date or service, when supplied +- Map useful merge fields. +- Trigger or qualify the contact for the appropriate welcome/first-visit customer journey. +- Respect the email consent state and the audience’s opt-in model. +- Preserve the lead safely if Mailchimp is temporarily unavailable. +- Add idempotency protection. +- Provide a mock/development mode. +- Never expose the Mailchimp API key in the browser. + +Document the required Mailchimp environment variables, audience ID, server prefix, tags, merge fields, and automation prerequisites. + +## Lead storage and reliability + +Do not make Mailchimp or Sinch the only record of the form submission. + +If the existing application has a database, save the lead and integration status there before calling external services. If it does not, implement a clean persistence interface and a development adapter, then clearly document the production storage option that must be connected. + +Track: + +- Submission ID +- Timestamp +- First and last name +- Email and phone +- Consent choices and consent-language version +- Selected campus +- Visit details +- Questions +- UTM and click identifiers +- Landing-page variant +- Referrer +- Mailchimp status +- Sinch status +- Retry state + +Protect personal data, minimize what is retained, and define a deletion or retention approach in the documentation. + +## Advertising attribution and analytics + +Capture and preserve the full attribution context across the form experience, including when the user changes campus or moves between steps. + +Support: + +- `utm_source` +- `utm_medium` +- `utm_campaign` +- `utm_content` +- `utm_term` +- `gclid` +- `gbraid` +- `wbraid` +- `fbclid` +- TikTok click ID when present +- X/Twitter click identifiers when present +- Landing-page variant +- Initial referrer +- First landing URL + +Create a consent-aware analytics layer with clearly named events such as: + +- `landing_page_view` +- `campus_recommended` +- `location_permission_requested` +- `campus_selected` +- `plan_visit_started` +- `plan_visit_step_completed` +- `plan_visit_submitted` +- `plan_visit_success` +- `text_us_clicked` +- `directions_clicked` + +Include event properties such as campus, source, campaign, device category, and landing-page variant, but do not send unnecessary personal information to ad platforms or analytics tools. + +Provide integration hooks for the organization’s chosen analytics and advertising tools. Do not hardcode production tracking IDs. + +## SEO and landing-page marketing + +Although this is an advertising destination, make it technically strong for search and sharing. + +Implement: + +- A focused title and meta description. +- Canonical URL support. +- Open Graph and social-sharing metadata. +- Appropriate robots behavior controlled by environment. +- Organization/Church and location schema using verified information only. +- LocalBusiness or Place schema for campuses when appropriate. +- Semantic heading structure. +- Descriptive link labels and image alt text. +- Fast loading and minimal client-side JavaScript. +- Optimized responsive images. +- Sitemap integration if the project uses a sitemap. +- Clean, readable URL structure. +- Campus-specific copy and metadata where the architecture supports it. + +Do not keyword-stuff. Write for newcomers first. Keep the primary CTA visible and ensure message continuity between ad copy and landing-page content. + +## Performance and accessibility + +Target excellent mobile performance and Core Web Vitals. + +Requirements: + +- Mobile-first responsive design. +- WCAG 2.2 AA-oriented implementation. +- Full keyboard navigation. +- Visible focus styles. +- Proper form labels and error associations. +- Sufficient color contrast. +- Reduced-motion support. +- No layout shifts from images or fonts. +- Optimized font loading. +- Lazy-load content that is below the fold. +- Avoid unnecessary third-party scripts. +- Test at common mobile widths and slow-network conditions. + +## Security and privacy + +Implement: + +- Server-side validation and sanitization. +- Rate limiting. +- Bot protection that does not create needless friction. +- Same-origin and CSRF protections where appropriate. +- Secret management through environment variables. +- Safe error messages. +- Redacted logs. +- Request timeouts. +- Idempotent form processing. +- A configurable consent-disclosure version. +- Privacy Policy and Terms links. +- No automatic precise-location request on page load. +- No email enrollment or SMS delivery without the applicable consent. + +Do not make legal-compliance claims. Flag all consent language, data retention, TCPA-related SMS behavior, and privacy disclosures for review by Waters Church’s legal or compliance adviser before launch. + +## Content and configuration architecture + +Keep frequently changing information outside presentation components. + +Create typed configuration for: + +- Church name and central contact details +- Campuses +- Addresses and coordinates +- Service times +- SMS numbers +- Directions +- Kids information +- FAQs +- Testimonials +- Privacy and Terms URLs +- Consent copy and version +- Analytics IDs +- Mailchimp mapping +- Sinch sender and templates + +If a CMS already exists, use it appropriately. Otherwise, make the configuration simple for a non-developer to update. + +## Testing + +Add meaningful tests for: + +- Campus distance calculation +- Postal-code/manual-location fallback +- Form validation +- Consent enforcement +- Attribution capture +- Mailchimp field and tag mapping +- Sinch message construction +- Idempotency behavior +- Partial integration failure +- Successful submission +- Accessible keyboard operation for the main flow + +Also manually verify: + +- Small and large mobile screens +- Desktop layout +- Denied location permission +- Unsupported geolocation +- No nearby campus result +- Missing or malformed campaign parameters +- API timeout and provider failure +- Duplicate form submission +- Screen-reader-friendly errors +- Text links on iOS and Android where possible + +## Environment variables + +Add a documented `.env.example` containing placeholders only. Include the variables required for: + +- Public site URL +- Database or lead-storage connection +- Mailchimp API key +- Mailchimp server prefix +- Mailchimp audience ID +- Mailchimp journey/tag configuration +- Sinch Engage API base URL +- Sinch project/service identifiers +- Sinch API credentials +- Sinch sender identity +- Central SMS number +- Geocoding provider, if one is required +- Analytics and advertising IDs +- Bot-protection service +- Preview/mock mode + +Never place real secrets in the repository. + +## Deliverables + +Complete the work with: + +1. The functional landing page. +2. Responsive styling and interactions. +3. Campus recommendation and manual selection. +4. The visit-planning form and confirmation state. +5. Mailchimp and Sinch server-side adapter layers. +6. Attribution and analytics hooks. +7. Validation, consent, reliability, and anti-spam protections. +8. Tests. +9. `.env.example`. +10. A concise setup guide explaining: + - How to enter campus details + - How to connect Mailchimp + - How to connect Sinch Engage + - How to configure tracking + - How to run locally + - How to test without sending messages + - How to deploy + - Which content and compliance items require human approval + +## Definition of done + +The project is finished when: + +- A visitor arriving from an ad immediately understands the purpose of the page. +- The page suggests a campus without forcing location permission. +- The visitor can always choose a campus manually. +- The visitor can confidently understand what a first visit will be like. +- The form works on mobile and has clear, separate consent choices. +- Successful submissions are persisted before external integrations run. +- Consenting contacts are correctly prepared for Mailchimp. +- Consenting phone numbers receive one idempotent Sinch confirmation request. +- Visitors can initiate a text conversation with the appropriate church number. +- Campaign attribution reaches the stored lead and integration mapping. +- Failure states are graceful and recoverable. +- The experience is accessible, fast, secure, and visually polished. +- No factual church information, testimonials, legal language, or API success has been fabricated. +- The setup documentation allows another developer to configure and deploy the site. \ No newline at end of file diff --git a/assets/source/SOURCES.md b/assets/source/SOURCES.md new file mode 100644 index 0000000..836dd1d --- /dev/null +++ b/assets/source/SOURCES.md @@ -0,0 +1,24 @@ +# Brand asset sources + +All files were downloaded from the public Waters Church website on 2026-09-22 +and are used with the assumption that Waters Church owns the rights. +Do not alter the logo marks. Replace photos freely; keep the same file names +so the build picks them up, then run `npm run images`. + +| File | Source URL | +| --- | --- | +| logos/waters-mark-teal.png | https://waterschurch.org/wp-content/uploads/2018/03/NewWlogo_3000.png | +| logos/waters-mark-white.svg | https://waterschurch.org/wp-content/uploads/2026/01/wlogo-white.svg | +| logos/waters-church-wordmark-white.svg | https://waterschurch.org/wp-content/uploads/2020/12/WATERS-CHURCH-Text.svg | +| photos/hero-north-attleboro-worship.jpg | https://waterschurch.org/wp-content/uploads/2024/02/2402-North-Attleboro-Stage-Shot-compressed.jpeg (site og:image) | +| photos/campus-north-attleboro.jpg | https://waterschurch.org/wp-content/uploads/2021/05/nacard.jpg | +| photos/campus-norwood.jpg | https://waterschurch.org/wp-content/uploads/2021/05/nwcard.jpg | +| photos/campus-woonsocket.jpg | https://waterschurch.org/wp-content/uploads/2021/05/wscard.jpg | +| photos/campus-southcoast.jpg | https://waterschurch.org/wp-content/uploads/2022/12/southcoast-header.jpg | +| photos/campus-apollo-beach.jpg | https://waterschurch.org/wp-content/uploads/2021/05/ApolloBeachSign.jpg | +| photos/campus-orlando.jpg | https://waterschurch.org/wp-content/uploads/2024/11/orlando-header.jpg | +| photos/worship-southcoast.jpg | https://waterschurch.org/wp-content/uploads/2025/06/3F7A0432.jpeg | +| photos/community-1.jpg | https://waterschurch.org/wp-content/uploads/2022/03/community1.jpg | +| photos/community-woonsocket.jpg | https://waterschurch.org/wp-content/uploads/2022/04/woonsocket-community2.jpg | +| photos/save-a-seat.jpg | https://waterschurch.org/wp-content/uploads/2022/03/saveaseat_editted.jpg | +| photos/gathering-2023.jpg | https://waterschurch.org/wp-content/uploads/2024/02/20230730-Img_9957-1.jpg | diff --git a/assets/source/logos/waters-church-wordmark-white.svg b/assets/source/logos/waters-church-wordmark-white.svg new file mode 100644 index 0000000..455ce1e --- /dev/null +++ b/assets/source/logos/waters-church-wordmark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/source/logos/waters-mark-teal.png b/assets/source/logos/waters-mark-teal.png new file mode 100644 index 0000000..7e8178b Binary files /dev/null and b/assets/source/logos/waters-mark-teal.png differ diff --git a/assets/source/logos/waters-mark-white.svg b/assets/source/logos/waters-mark-white.svg new file mode 100644 index 0000000..abe416d --- /dev/null +++ b/assets/source/logos/waters-mark-white.svg @@ -0,0 +1 @@ +wlogo-white \ No newline at end of file diff --git a/assets/source/photos/campus-apollo-beach.jpg b/assets/source/photos/campus-apollo-beach.jpg new file mode 100644 index 0000000..22a3a62 Binary files /dev/null and b/assets/source/photos/campus-apollo-beach.jpg differ diff --git a/assets/source/photos/campus-north-attleboro.jpg b/assets/source/photos/campus-north-attleboro.jpg new file mode 100644 index 0000000..8855d97 Binary files /dev/null and b/assets/source/photos/campus-north-attleboro.jpg differ diff --git a/assets/source/photos/campus-norwood.jpg b/assets/source/photos/campus-norwood.jpg new file mode 100644 index 0000000..acd34dc Binary files /dev/null and b/assets/source/photos/campus-norwood.jpg differ diff --git a/assets/source/photos/campus-orlando.jpg b/assets/source/photos/campus-orlando.jpg new file mode 100644 index 0000000..2c6d618 Binary files /dev/null and b/assets/source/photos/campus-orlando.jpg differ diff --git a/assets/source/photos/campus-southcoast.jpg b/assets/source/photos/campus-southcoast.jpg new file mode 100644 index 0000000..3f2f0f8 Binary files /dev/null and b/assets/source/photos/campus-southcoast.jpg differ diff --git a/assets/source/photos/campus-woonsocket.jpg b/assets/source/photos/campus-woonsocket.jpg new file mode 100644 index 0000000..5b3ef29 Binary files /dev/null and b/assets/source/photos/campus-woonsocket.jpg differ diff --git a/assets/source/photos/community-1.jpg b/assets/source/photos/community-1.jpg new file mode 100644 index 0000000..632fc28 Binary files /dev/null and b/assets/source/photos/community-1.jpg differ diff --git a/assets/source/photos/community-woonsocket.jpg b/assets/source/photos/community-woonsocket.jpg new file mode 100644 index 0000000..5edfeb3 Binary files /dev/null and b/assets/source/photos/community-woonsocket.jpg differ diff --git a/assets/source/photos/gathering-2023.jpg b/assets/source/photos/gathering-2023.jpg new file mode 100644 index 0000000..3dc3cb0 Binary files /dev/null and b/assets/source/photos/gathering-2023.jpg differ diff --git a/assets/source/photos/hero-north-attleboro-worship.jpg b/assets/source/photos/hero-north-attleboro-worship.jpg new file mode 100644 index 0000000..7e9c5f2 Binary files /dev/null and b/assets/source/photos/hero-north-attleboro-worship.jpg differ diff --git a/assets/source/photos/save-a-seat.jpg b/assets/source/photos/save-a-seat.jpg new file mode 100644 index 0000000..85f4859 Binary files /dev/null and b/assets/source/photos/save-a-seat.jpg differ diff --git a/assets/source/photos/worship-southcoast.jpg b/assets/source/photos/worship-southcoast.jpg new file mode 100644 index 0000000..e2a14f0 Binary files /dev/null and b/assets/source/photos/worship-southcoast.jpg differ diff --git a/config/campuses.json b/config/campuses.json new file mode 100644 index 0000000..8e5184f --- /dev/null +++ b/config/campuses.json @@ -0,0 +1,187 @@ +{ + "$schema": "./campuses.schema.json", + "_readme": "Single source of truth for campus data. Read by the Next.js frontend (src/config/campuses.ts) and by the PHP API (public/api/lib/campuses.php). Facts were verified against waterschurch.org on 2026-09-22 unless listed under `verify`. Edit this file, then rebuild (npm run build) and re-upload.", + "campuses": [ + { + "id": "north-attleboro", + "name": "North Attleboro", + "label": "North Attleboro, MA", + "status": "open", + "isCentral": true, + "address": { + "line1": "57 John L Dietsch Square", + "city": "North Attleboro", + "state": "MA", + "postalCode": "02763" + }, + "coordinates": { "lat": 41.97396, "lng": -71.29631 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["7:30 AM", "9:30 AM", "11:30 AM"] } + ], + "phone": "+15086951300", + "smsNumber": "+15086951300", + "sinchAccountId": "WatersChurch_OLA_0007", + "email": "info@waterschurch.org", + "campusUrl": "https://waterschurch.org/locations/north-attleboro/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=Waters+Church%2C+57+John+L+Dietsch+Square%2C+North+Attleboro%2C+MA+02763", + "leader": { "title": "Lead Pastor", "name": "Tim Hatch" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": "Waters Kids programs run at every service time.", + "image": "north-attleboro", + "verify": ["parkingNotes", "arrivalNotes", "coordinates"] + }, + { + "id": "norwood", + "name": "Norwood", + "label": "Norwood, MA", + "status": "open", + "isCentral": false, + "address": { + "line1": "668 Walpole Street", + "city": "Norwood", + "state": "MA", + "postalCode": "02062" + }, + "coordinates": { "lat": 42.17153, "lng": -71.22482 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["9:30 AM", "11:00 AM"] }, + { "day": "First Wednesday of the month", "times": ["7:00 PM"] } + ], + "phone": "+17817623357", + "smsNumber": "+17817623357", + "sinchAccountId": "WatersChurch_OLA_0002", + "email": "info@waterschurchnorwood.org", + "campusUrl": "https://waterschurch.org/locations/norwood/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=Waters+Church+Norwood%2C+668+Walpole+Street%2C+Norwood%2C+MA+02062", + "leader": { "title": "Senior Pastor", "name": "Tommy Geyser" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": "Waters Kids programs run at every service time.", + "image": "norwood", + "verify": ["parkingNotes", "arrivalNotes", "coordinates"] + }, + { + "id": "woonsocket", + "name": "Woonsocket", + "label": "Woonsocket, RI", + "status": "open", + "isCentral": false, + "address": { + "line1": "1500 Diamond Hill Road", + "city": "Woonsocket", + "state": "RI", + "postalCode": "02895" + }, + "coordinates": { "lat": 42.01634, "lng": -71.47692 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["9:00 AM", "11:00 AM"] } + ], + "phone": "+15086951300", + "smsNumber": "+18332414837", + "sinchAccountId": "WatersChurch_OLA_0005", + "email": "info@waterschurch.org", + "campusUrl": "https://waterschurch.org/locations/woonsocket/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=Waters+Church+Woonsocket%2C+1500+Diamond+Hill+Rd%2C+Woonsocket%2C+RI+02895", + "leader": { "title": "Location Pastor", "name": "Jim Shekleton" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": "Waters Kids programs run at every service time.", + "image": "woonsocket", + "verify": ["parkingNotes", "arrivalNotes", "coordinates", "phone"] + }, + { + "id": "southcoast", + "name": "Southcoast", + "label": "Southcoast, RI/MA", + "status": "open", + "isCentral": false, + "address": { + "line1": "28 Fish Road", + "city": "Tiverton", + "state": "RI", + "postalCode": "02878" + }, + "coordinates": { "lat": 41.66752, "lng": -71.17603 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["10:30 AM"] } + ], + "phone": "+15086951300", + "smsNumber": "+15088093490", + "sinchAccountId": "WatersChurch_OLA_0006", + "email": "info@waterschurch.org", + "campusUrl": "https://waterschurch.org/locations/southcoast/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=Waters+Church+Southcoast%2C+28+Fish+Road%2C+Tiverton%2C+RI+02878", + "leader": { "title": "Location Pastor", "name": "Mehdi Chniouli" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": "Waters Kids programs run at every service time.", + "image": "southcoast", + "verify": ["parkingNotes", "arrivalNotes", "coordinates", "phone"] + }, + { + "id": "apollo-beach", + "name": "Apollo Beach", + "label": "Apollo Beach, FL", + "status": "open", + "isCentral": false, + "address": { + "line1": "5309 North US Hwy 41", + "city": "Apollo Beach", + "state": "FL", + "postalCode": "33572" + }, + "coordinates": { "lat": 27.74286, "lng": -82.41918 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["9:00 AM", "11:00 AM"] } + ], + "phone": "+18136426979", + "smsNumber": "+18136426979", + "sinchAccountId": "WatersChurch_OLA_0003", + "email": "info@waterschurch.org", + "campusUrl": "https://waterschurch.org/locations/apollo-beach/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=Waters+Church+Apollo+Beach%2C+5309+N+US+Hwy+41%2C+Apollo+Beach%2C+FL+33572", + "leader": { "title": "Location Pastor", "name": "Tom Duck" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": "Waters Kids programs run at every service time.", + "image": "apollo-beach", + "verify": ["parkingNotes", "arrivalNotes", "coordinates"] + }, + { + "id": "orlando", + "name": "Orlando", + "label": "Orlando, FL", + "status": "launching", + "isCentral": false, + "address": { + "line1": "Castle Door Studios, 1890 Kentucky Ave", + "city": "Winter Park", + "state": "FL", + "postalCode": "32789" + }, + "coordinates": { "lat": 28.59213, "lng": -81.37501 }, + "timezone": "America/New_York", + "serviceTimes": [ + { "day": "Sunday", "times": ["10:30 AM"], "note": "Doors open 10:00 AM. Currently gathering with the launch team only." } + ], + "phone": "+15086951300", + "smsNumber": null, + "sinchAccountId": null, + "email": "info@waterschurch.org", + "campusUrl": "https://waterschurch.org/locations/orlando/", + "directionsUrl": "https://www.google.com/maps/dir/?api=1&destination=1890+Kentucky+Ave%2C+Winter+Park%2C+FL+32789", + "leader": { "title": "Pastor", "name": "Tim Hatch" }, + "arrivalNotes": null, + "parkingNotes": null, + "kidsNotes": null, + "image": "orlando", + "verify": ["status", "serviceTimes", "smsNumber", "kidsNotes", "coordinates"] + } + ] +} diff --git a/config/campuses.schema.json b/config/campuses.schema.json new file mode 100644 index 0000000..0275e9e --- /dev/null +++ b/config/campuses.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Waters Church campuses", + "type": "object", + "required": ["campuses"], + "properties": { + "campuses": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "name", "label", "status", "address", "coordinates", "serviceTimes", "phone", "campusUrl", "directionsUrl"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, + "name": { "type": "string" }, + "label": { "type": "string" }, + "status": { "enum": ["open", "launching", "hidden"] }, + "isCentral": { "type": "boolean" }, + "address": { + "type": "object", + "required": ["line1", "city", "state", "postalCode"], + "properties": { + "line1": { "type": "string" }, "city": { "type": "string" }, + "state": { "type": "string" }, "postalCode": { "type": "string" } + } + }, + "coordinates": { "type": "object", "required": ["lat", "lng"], "properties": { "lat": { "type": "number" }, "lng": { "type": "number" } } }, + "timezone": { "type": "string" }, + "serviceTimes": { + "type": "array", + "items": { "type": "object", "required": ["day", "times"], "properties": { "day": { "type": "string" }, "times": { "type": "array", "items": { "type": "string" } }, "note": { "type": ["string", "null"] } } } + }, + "phone": { "type": "string", "pattern": "^\\+[1-9][0-9]{6,14}$" }, + "smsNumber": { "type": ["string", "null"], "pattern": "^\\+[1-9][0-9]{6,14}$" }, + "sinchAccountId": { "type": ["string", "null"] }, + "email": { "type": ["string", "null"] }, + "campusUrl": { "type": "string", "format": "uri" }, + "directionsUrl": { "type": "string", "format": "uri" }, + "leader": { "type": ["object", "null"], "properties": { "title": { "type": "string" }, "name": { "type": "string" } } }, + "arrivalNotes": { "type": ["string", "null"] }, + "parkingNotes": { "type": ["string", "null"] }, + "kidsNotes": { "type": ["string", "null"] }, + "image": { "type": ["string", "null"] }, + "verify": { "type": "array", "items": { "type": "string" } } + } + } + } + } +} diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..4226e21 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# ============================================================================= +# Deploy visit.waterschurch.org to the cPanel host over SSH. +# +# deploy/deploy.sh # build + upload + configure (goes live) +# deploy/deploy.sh --no-build # upload the existing ./out +# deploy/deploy.sh --keep-redirect # deploy but keep sending visitors to +# # the Church Center form (pre-launch) +# +# Requires: the SSH alias "waterschurch" from ~/.ssh/config, rsync, node, npm. +# Idempotent: safe to re-run for every content update. +# +# On the server (as the cPanel user) it: +# 1. ensures the subdomain exists with document root ~/visit.waterschurch.org +# 2. creates ~/visit-private/{data,logs} and a .env from .env.example if missing +# 3. rsyncs ./out → ~/visit.waterschurch.org, preserving cPanel's own files +# (.user.ini, php.ini, .well-known/) and rebuilding .htaccess from: +# cPanel-generated PHP blocks + [pre-launch redirect] + deploy/htaccess.root +# 4. installs the 5-minute retry cron job if absent +# 5. requests an AutoSSL check and prints the health endpoint +# It never touches DNS, the main site, or an existing .env. +# ============================================================================= +set -euo pipefail + +SSH_HOST="${SSH_HOST:-waterschurch}" +SUBDOMAIN="visit" +ROOTDOMAIN="waterschurch.org" +FQDN="$SUBDOMAIN.$ROOTDOMAIN" +REMOTE_DOCROOT="visit.waterschurch.org" # relative to $HOME on the server +REMOTE_PRIVATE="visit-private" # relative to $HOME on the server + +BUILD=1 +KEEP_REDIRECT=0 +for arg in "$@"; do + case "$arg" in + --no-build) BUILD=0 ;; + --keep-redirect) KEEP_REDIRECT=1 ;; + *) echo "unknown option: $arg"; exit 2 ;; + esac +done + +cd "$(dirname "$0")/.." + +if [[ $BUILD -eq 1 ]]; then + echo "▶ building" + npm run build +fi +[[ -f out/index.html && -f out/.htaccess && -f out/api/plan-visit.php ]] || { echo "out/ is incomplete — run npm run build"; exit 1; } + +echo "▶ preparing server" +ssh "$SSH_HOST" bash -s -- "$SUBDOMAIN" "$ROOTDOMAIN" "$REMOTE_DOCROOT" "$REMOTE_PRIVATE" <<'REMOTE' +set -euo pipefail +SUB="$1"; ROOT="$2"; DOCROOT="$HOME/$3"; PRIV="$HOME/$4"; FQDN="$SUB.$ROOT" + +if uapi --output=json DomainInfo list_domains 2>/dev/null | grep -q "\"$FQDN\""; then + CUR=$(uapi --output=json DomainInfo single_domain_data domain="$FQDN" 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["data"].get("documentroot",""))' 2>/dev/null || true) + echo " subdomain $FQDN exists (docroot: ${CUR:-unknown})" + if [[ -n "$CUR" && "$CUR" != "$DOCROOT" ]]; then + echo " ⚠ document root differs — pointing it at $DOCROOT" + uapi --output=json SubDomain changedocroot subdomain="$SUB" rootdomain="$ROOT" dir="$DOCROOT" >/dev/null + fi +else + echo " creating subdomain $FQDN → $DOCROOT" + uapi --output=json SubDomain addsubdomain domain="$SUB" rootdomain="$ROOT" dir="$DOCROOT" | python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(" ", "ok" if r["status"] else r["errors"])' +fi +mkdir -p "$DOCROOT" + +# Preserve cPanel-generated .htaccess blocks (PHP ini directives + PHP handler) before we replace the file. +if [[ -f "$DOCROOT/.htaccess" ]]; then + awk '/^# BEGIN cPanel-generated/,/^# END cPanel-generated/ {print} /^# php -- BEGIN cPanel-generated/,/^# php -- END cPanel-generated/ {print}' "$DOCROOT/.htaccess" > "$DOCROOT/.htaccess.cpanel-blocks" + echo " preserved $(grep -c . "$DOCROOT/.htaccess.cpanel-blocks") lines of cPanel-generated .htaccess directives" +fi + +mkdir -p "$PRIV/data" "$PRIV/logs" +chmod 750 "$PRIV" "$PRIV/data" "$PRIV/logs" +echo " private dir: $PRIV" +REMOTE + +echo "▶ uploading build" +rsync -az --delete \ + --exclude '/.htaccess' --exclude '/.htaccess.*' \ + --exclude '/.user.ini' --exclude '/php.ini' --exclude '/.well-known/' \ + --exclude 'api/data/' \ + --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \ + out/ "$SSH_HOST:$REMOTE_DOCROOT/" +scp -q out/.htaccess "$SSH_HOST:$REMOTE_DOCROOT/.htaccess.visit" +scp -q deploy/htaccess.prelaunch-redirect "$SSH_HOST:$REMOTE_DOCROOT/.htaccess.prelaunch" +scp -q .env.example "$SSH_HOST:$REMOTE_PRIVATE/.env.example" + +echo "▶ configuring" +ssh "$SSH_HOST" bash -s -- "$REMOTE_DOCROOT" "$REMOTE_PRIVATE" "$FQDN" "$KEEP_REDIRECT" <<'REMOTE' +set -euo pipefail +DOCROOT="$HOME/$1"; PRIV="$HOME/$2"; FQDN="$3"; KEEP="$4" + +# --- .env (never overwritten) +if [[ ! -f "$PRIV/.env" ]]; then + cp "$PRIV/.env.example" "$PRIV/.env" + SALT=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n') + sed -i "s|^HASH_SALT=.*|HASH_SALT=$SALT|; s|^ALLOWED_ORIGINS=.*|ALLOWED_ORIGINS=|; s|^DEBUG_GEO=.*|DEBUG_GEO=false|" "$PRIV/.env" + echo " created $PRIV/.env (INTEGRATION_MODE=mock — add API keys, then set live)" +else + echo " kept existing $PRIV/.env" +fi +chmod 640 "$PRIV/.env" + +# --- assemble .htaccess: cPanel blocks + [pre-launch redirect] + site config +{ + [[ -f "$DOCROOT/.htaccess.cpanel-blocks" ]] && cat "$DOCROOT/.htaccess.cpanel-blocks" && echo + if [[ "$KEEP" == "1" ]]; then cat "$DOCROOT/.htaccess.prelaunch"; fi + sed "s|^# SetEnv VISIT_PRIVATE_DIR .*|SetEnv VISIT_PRIVATE_DIR $PRIV|" "$DOCROOT/.htaccess.visit" +} > "$DOCROOT/.htaccess.new" +mv -f "$DOCROOT/.htaccess.new" "$DOCROOT/.htaccess" +rm -f "$DOCROOT/.htaccess.visit" "$DOCROOT/.htaccess.prelaunch" "$DOCROOT/.htaccess.cpanel-blocks" +chmod 644 "$DOCROOT/.htaccess" +if [[ "$KEEP" == "1" ]]; then echo " .htaccess: PRE-LAUNCH redirect to Church Center is ACTIVE"; else echo " .htaccess: site is LIVE (no redirect)"; fi + +# --- PHP binary for cron +PHP_BIN=$(command -v php || true) +for c in /usr/local/bin/php /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/ea-php82/root/usr/bin/php; do + [[ -z "$PHP_BIN" && -x "$c" ]] && PHP_BIN="$c" +done +echo " php: ${PHP_BIN:-NOT FOUND} $($PHP_BIN -r 'echo PHP_VERSION;' 2>/dev/null || true)" + +# --- cron (idempotent) +CRON="*/5 * * * * VISIT_PRIVATE_DIR=$PRIV $PHP_BIN $DOCROOT/api/cron/retry-integrations.php >> $PRIV/logs/cron.log 2>&1" +if ! crontab -l 2>/dev/null | grep -qF "retry-integrations.php"; then + ( crontab -l 2>/dev/null || true; echo "$CRON" ) | crontab - + echo " cron installed (every 5 minutes)" +else + echo " cron already present" +fi + +# --- SSL +if uapi --output=json SSL start_autossl_check >/dev/null 2>&1; then echo " AutoSSL check requested"; else echo " (AutoSSL check not available via UAPI; use cPanel → SSL/TLS Status if needed)"; fi + +echo " build: $(head -1 "$DOCROOT/BUILD.txt")" +REMOTE + +echo "▶ verifying" +sleep 2 +printf " https://%s/ → " "$FQDN"; curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" --max-time 20 "https://$FQDN/" || true +printf " /api/health.php → "; curl -s --max-time 20 "https://$FQDN/api/health.php" | head -c 600 || true +echo +if [[ $KEEP_REDIRECT -eq 1 ]]; then + echo "Deployed behind the pre-launch redirect. To go live: deploy/deploy.sh --no-build" +else + echo "Live. Next: fill in $REMOTE_PRIVATE/.env on the server, set INTEGRATION_MODE=live, and run docs/LAUNCH-CHECKLIST.md." +fi diff --git a/deploy/htaccess.prelaunch-redirect b/deploy/htaccess.prelaunch-redirect new file mode 100644 index 0000000..f9fb185 --- /dev/null +++ b/deploy/htaccess.prelaunch-redirect @@ -0,0 +1,15 @@ +# --------------------------------------------------------------------------- +# PRE-LAUNCH: keep sending visitors to the Church Center form until Waters +# Church flips the switch. The /api/ endpoints are exempt so the deployment +# can be verified (they are rate-limited and same-origin protected). To go live, delete this block (or run deploy/deploy.sh +# without --keep-redirect). +# --------------------------------------------------------------------------- + + RewriteEngine On + RewriteCond %{REQUEST_URI} !^/api/ + RewriteCond %{REQUEST_URI} !^/404\.html$ + RewriteCond %{REQUEST_URI} !^/\.well-known/ + RewriteRule ^(.*)$ https://waterschurch.churchcenter.com/people/forms/21186 [R=302,L] + +# --------------------------------------------------------------- end prelaunch + diff --git a/deploy/htaccess.root b/deploy/htaccess.root new file mode 100644 index 0000000..9b760d5 --- /dev/null +++ b/deploy/htaccess.root @@ -0,0 +1,86 @@ +# ============================================================================ +# visit.waterschurch.org — Apache configuration for the subdomain document root +# Copied to out/.htaccess by `npm run build` (scripts/postbuild.mjs). +# ============================================================================ + +Options -Indexes -MultiViews +DirectoryIndex index.html +ErrorDocument 404 /404.html +# cPanel's default 403 page does not exist in this document root; show our branded page instead (status stays 403). +ErrorDocument 403 /404.html + +# --- Private configuration directory for the PHP API ----------------------- +# Uncomment and set to the absolute path of the folder holding your .env file +# (OUTSIDE the document root). If omitted, the API looks for +# /visit-private, e.g. /home/USER/visit-private. +# SetEnv VISIT_PRIVATE_DIR /home/USER/visit-private + + + RewriteEngine On + + # Force HTTPS and the canonical non-www host + RewriteCond %{HTTPS} !=on [OR] + RewriteCond %{HTTP_HOST} ^www\.visit\.waterschurch\.org$ [NC] + RewriteRule ^(.*)$ https://visit.waterschurch.org/$1 [R=301,L] + + # Never serve dot-files (except the ACME challenge folder used for SSL issuance) + RewriteCond %{REQUEST_URI} !^/\.well-known/ + RewriteRule (^|/)\.(?!well-known) - [F,L] + + # Next.js static export: serve /path as /path.html when the file exists + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{DOCUMENT_ROOT}/$1.html -f + RewriteRule ^(.+?)/?$ /$1.html [L] + + # Strip trailing slash on non-directories + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.+)/$ /$1 [R=301,L] + + +# --- Security headers ------------------------------------------------------- + + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Permissions-Policy "geolocation=(self), camera=(), microphone=(), payment=()" + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" "expr=%{HTTPS} == 'on'" + # CSP is report-only until Waters Church confirms its tag/pixel list. See docs/DEPLOY-CPANEL.md. + Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://connect.facebook.net https://analytics.tiktok.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://www.facebook.com https://analytics.tiktok.com https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; base-uri 'self'; form-action 'self' sms:; frame-ancestors 'self'" + + # Immutable hashed assets + + Header set Cache-Control "public, max-age=31536000, immutable" + + # HTML and text files: always revalidate + + Header set Cache-Control "public, max-age=0, must-revalidate" + + + +# --- Compression ----------------------------------------------------------- + + AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml font/woff2 + + + AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml + + +# --- MIME types some cPanel builds miss ------------------------------------- +AddType image/avif .avif +AddType image/webp .webp +AddType font/woff2 .woff2 + +# --- Expires fallback (when mod_headers is unavailable) --------------------- + + ExpiresActive On + ExpiresByType image/avif "access plus 1 year" + ExpiresByType image/webp "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/svg+xml "access plus 1 year" + ExpiresByType font/woff2 "access plus 1 year" + ExpiresByType text/css "access plus 1 year" + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType text/html "access plus 0 seconds" + diff --git a/docs/DEPLOY-CPANEL.md b/docs/DEPLOY-CPANEL.md new file mode 100644 index 0000000..4f246eb --- /dev/null +++ b/docs/DEPLOY-CPANEL.md @@ -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--.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. diff --git a/docs/LAUNCH-CHECKLIST.md b/docs/LAUNCH-CHECKLIST.md new file mode 100644 index 0000000..a3b9754 --- /dev/null +++ b/docs/LAUNCH-CHECKLIST.md @@ -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=