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>
149 lines
6.9 KiB
Bash
Executable File
149 lines
6.9 KiB
Bash
Executable File
#!/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
|