← back to Grant
SEO (robots/sitemap/OG image/metadata) + reusable deploy.sh + nginx vhost template + DEPLOY.md
580b21381f3277298a91c8e70a674afe7496b5e0 · 2026-05-30 09:39:24 -0700 · Steve Abrams
Files touched
A DEPLOY.mdM app/landing/page.tsxM app/layout.tsxA app/opengraph-image.tsxA app/robots.tsA app/sitemap.tsA deploy.shA deploy/nginx/grant.agentabrams.com.conf
Diff
commit 580b21381f3277298a91c8e70a674afe7496b5e0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 30 09:39:24 2026 -0700
SEO (robots/sitemap/OG image/metadata) + reusable deploy.sh + nginx vhost template + DEPLOY.md
---
DEPLOY.md | 180 +++++++++++++++++++++++++++++
app/landing/page.tsx | 17 +++
app/layout.tsx | 27 ++++-
app/opengraph-image.tsx | 172 ++++++++++++++++++++++++++++
app/robots.ts | 25 ++++
app/sitemap.ts | 21 ++++
deploy.sh | 197 ++++++++++++++++++++++++++++++++
deploy/nginx/grant.agentabrams.com.conf | 122 ++++++++++++++++++++
8 files changed, 759 insertions(+), 2 deletions(-)
diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..14174a6
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,180 @@
+# Grant App — Deployment Runbook
+
+## Topology
+
+```
+Mac2 (local dev)
+ |
+ | git ls-files → rsync (SSH, port 22)
+ v
+Kamatera root@45.61.58.125
+ /root/Projects/Grant/
+ npm install + npm run build
+ pm2 reload grant-app --update-env
+ Next.js listening on 127.0.0.1:7450 (loopback only)
+ |
+ | reverse proxy
+ v
+nginx :443 → grant.agentabrams.com (Let's Encrypt cert)
+```
+
+- **No git remote** — deploy is rsync-based, not git push.
+- **DNS**: direct-to-origin (no Cloudflare proxy). A record → 45.61.58.125.
+- **PM2 process**: `grant-app` (see `ecosystem.config.js`).
+- **Database**: `dw_admin@127.0.0.1:5432/grant_app` — connection string in `/root/Projects/Grant/.env.local` on prod (never committed).
+
+---
+
+## Routine Deploy
+
+```sh
+# From Mac2, inside the project directory:
+cd /Users/stevestudio2/Projects/Grant
+./deploy.sh
+```
+
+Phases: rsync → npm install → npm run build → pm2 reload → smoke-test (/ and /landing must return HTTP 200).
+
+### Skip the build (config or asset-only push)
+
+```sh
+./deploy.sh --no-build
+```
+
+### Watch logs after a deploy
+
+```sh
+ssh root@45.61.58.125 pm2 logs grant-app --lines 80
+```
+
+---
+
+## One-Time Public-Domain Wiring
+
+These steps are operator-run once. `deploy.sh` never touches them.
+
+### 1. DNS (GoDaddy)
+
+Add an A record for `grant.agentabrams.com` pointing to `45.61.58.125`.
+TTL 600 (10 min) is fine while testing; raise to 3600 afterward.
+
+### 2. nginx vhost
+
+```sh
+# On Kamatera (as root):
+cp /root/Projects/Grant/deploy/nginx/grant.agentabrams.com.conf \
+ /etc/nginx/sites-available/grant.agentabrams.com
+
+ln -sf /etc/nginx/sites-available/grant.agentabrams.com \
+ /etc/nginx/sites-enabled/grant.agentabrams.com
+
+nginx -t && systemctl reload nginx
+```
+
+Verify HTTP redirects before certbot:
+```sh
+curl -I http://grant.agentabrams.com/
+# Expect: 301 → https://grant.agentabrams.com/
+```
+
+### 3. TLS certificate (certbot)
+
+```sh
+# On Kamatera (as root) — DNS must be propagated first:
+certbot --nginx -d grant.agentabrams.com
+# Follow prompts; certbot will rewrite the nginx vhost to add ssl directives.
+
+nginx -t && systemctl reload nginx
+```
+
+Verify HTTPS:
+```sh
+curl -I https://grant.agentabrams.com/
+# Expect: 200
+```
+
+### 4. Auto-renewal check
+
+```sh
+certbot renew --dry-run
+# Should complete without errors. Certbot's systemd timer handles production renewals.
+```
+
+---
+
+## Rollback Procedure
+
+### Option A — Re-deploy a previous state (preferred)
+
+On Mac2, check out the last known-good commit and re-run deploy:
+
+```sh
+cd /Users/stevestudio2/Projects/Grant
+git log --oneline -10 # find the target SHA
+git stash # save any local edits
+git checkout <good-sha> # or git checkout HEAD~1
+./deploy.sh
+git checkout - # return to previous branch/state
+git stash pop
+```
+
+### Option B — Roll back on the box without Mac2
+
+```sh
+ssh root@45.61.58.125
+cd /root/Projects/Grant
+
+# pm2 gracefully drops to last-built .next/ — if the build itself is bad,
+# restore from a backup snapshot (if you created one before deploying):
+# cp -r /root/Projects/Grant-backup-<date>/.next /root/Projects/Grant/.next
+# pm2 reload grant-app --update-env
+
+# If process is crashed:
+pm2 start ecosystem.config.js
+```
+
+### Emergency: previous .next/ snapshot
+
+Before any risky deploy, snapshot the build on the box:
+```sh
+ssh root@45.61.58.125 "cp -r /root/Projects/Grant/.next /root/Projects/Grant-next-bak-\$(date +%Y%m%d)"
+```
+Restore with:
+```sh
+ssh root@45.61.58.125 "rm -rf /root/Projects/Grant/.next && cp -r /root/Projects/Grant-next-bak-<date> /root/Projects/Grant/.next && pm2 reload grant-app --update-env"
+```
+
+---
+
+## Verified Facts (as of 2026-05-30)
+
+| Item | Value |
+|------|-------|
+| Prod host | root@45.61.58.125 (Kamatera) |
+| App dir | /root/Projects/Grant |
+| pm2 name | grant-app |
+| Start command | `next start -p 7450 -H 127.0.0.1` |
+| Build command | `npm run build` |
+| Node version | v22 |
+| Port | 7450 (loopback only) |
+| Public domain | grant.agentabrams.com |
+| nginx conf dir | /etc/nginx/sites-available/ |
+| TLS cert | /etc/letsencrypt/live/grant.agentabrams.com/ |
+| Database | dw_admin@127.0.0.1:5432/grant_app |
+
+---
+
+## Known Gotcha — Local File Dependency
+
+`package.json` declares:
+```json
+"@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz"
+```
+
+This tarball lives at `/tmp/` on Mac2 only. On prod, `npm ci` will fail because
+the file is absent. `deploy.sh` runs `npm install --no-audit --no-fund` instead,
+which resolves remaining deps from the registry and skips the missing local tarball
+if it is already installed in `node_modules/`.
+
+**Fix**: When `@dw/nextjs-admin-login` is published to a private registry or
+converted to a bundled dependency, update `deploy.sh` to use `npm ci`.
diff --git a/app/landing/page.tsx b/app/landing/page.tsx
index 1e42d43..2bf44b0 100644
--- a/app/landing/page.tsx
+++ b/app/landing/page.tsx
@@ -5,6 +5,23 @@ export const metadata: Metadata = {
title: 'Grant — One fit-scored feed for every funding source',
description:
'Federal, foundation, corporate, and local grants unified into a single feed and ranked by fit against your nonprofit mission. Stop tab-hopping between siloed databases.',
+ alternates: {
+ canonical: 'https://grant.agentabrams.com/',
+ },
+ openGraph: {
+ type: 'website',
+ siteName: 'Grant',
+ title: 'Grant — One fit-scored feed for every funding source',
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single feed and ranked by fit against your nonprofit mission. Stop tab-hopping between siloed databases.',
+ url: 'https://grant.agentabrams.com/',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Grant — One fit-scored feed for every funding source',
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single feed and ranked by fit against your nonprofit mission.',
+ },
};
export default function LandingPage() {
diff --git a/app/layout.tsx b/app/layout.tsx
index aae19cb..e858406 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,8 +2,31 @@ import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
- title: 'Grant - Non-Profit Fundraiser',
- description: 'AI-powered fundraising platform for non-profit organizations',
+ metadataBase: new URL('https://grant.agentabrams.com'),
+ title: {
+ default: 'Grant — Deadline River. One fit-scored feed for every grant source.',
+ template: '%s | Grant',
+ },
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single fit-scored feed ranked against your nonprofit mission. Stop tab-hopping between siloed databases.',
+ openGraph: {
+ type: 'website',
+ siteName: 'Grant',
+ title: 'Grant — Deadline River. One fit-scored feed for every grant source.',
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single fit-scored feed ranked against your nonprofit mission. Stop tab-hopping between siloed databases.',
+ url: 'https://grant.agentabrams.com',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Grant — Deadline River. One fit-scored feed for every grant source.',
+ description:
+ 'Federal, foundation, corporate, and local grants unified into a single fit-scored feed ranked against your nonprofit mission.',
+ },
+ robots: {
+ index: true,
+ follow: true,
+ },
};
export default function RootLayout({
diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx
new file mode 100644
index 0000000..affdb8d
--- /dev/null
+++ b/app/opengraph-image.tsx
@@ -0,0 +1,172 @@
+import { ImageResponse } from 'next/og';
+
+export const alt = 'Grant — Every grant, ranked by fit.';
+export const size = { width: 1200, height: 630 };
+export const contentType = 'image/png';
+
+export default function OgImage() {
+ return new ImageResponse(
+ (
+ <div
+ style={{
+ width: '1200px',
+ height: '630px',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#f4f7f5',
+ position: 'relative',
+ }}
+ >
+ {/* Emerald accent bar top */}
+ <div
+ style={{
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: '8px',
+ backgroundColor: '#047857',
+ display: 'flex',
+ }}
+ />
+
+ {/* Wordmark */}
+ <div
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '16px',
+ marginBottom: '32px',
+ }}
+ >
+ {/* Icon mark */}
+ <div
+ style={{
+ width: '72px',
+ height: '72px',
+ borderRadius: '16px',
+ backgroundColor: '#047857',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+ <div
+ style={{
+ color: '#ffffff',
+ fontSize: '40px',
+ fontWeight: 800,
+ fontFamily: 'sans-serif',
+ display: 'flex',
+ }}
+ >
+ G
+ </div>
+ </div>
+
+ <div
+ style={{
+ fontSize: '72px',
+ fontWeight: 800,
+ color: '#047857',
+ fontFamily: 'sans-serif',
+ letterSpacing: '-2px',
+ display: 'flex',
+ }}
+ >
+ Grant
+ </div>
+ </div>
+
+ {/* Tagline */}
+ <div
+ style={{
+ fontSize: '32px',
+ fontWeight: 600,
+ color: '#1f2937',
+ fontFamily: 'sans-serif',
+ textAlign: 'center',
+ marginBottom: '20px',
+ display: 'flex',
+ }}
+ >
+ Every grant, ranked by fit.
+ </div>
+
+ {/* Sub-tagline */}
+ <div
+ style={{
+ fontSize: '22px',
+ fontWeight: 400,
+ color: '#6b7280',
+ fontFamily: 'sans-serif',
+ textAlign: 'center',
+ maxWidth: '800px',
+ lineHeight: 1.5,
+ display: 'flex',
+ }}
+ >
+ Federal, foundation, corporate, and local grants — one fit-scored feed for your mission.
+ </div>
+
+ {/* Pill badges */}
+ <div
+ style={{
+ display: 'flex',
+ gap: '12px',
+ marginTop: '48px',
+ }}
+ >
+ {['Federal', 'Foundation', 'Corporate', 'Local'].map((label) => (
+ <div
+ key={label}
+ style={{
+ backgroundColor: '#10b981',
+ color: '#ffffff',
+ fontSize: '18px',
+ fontWeight: 600,
+ fontFamily: 'sans-serif',
+ padding: '8px 20px',
+ borderRadius: '9999px',
+ display: 'flex',
+ }}
+ >
+ {label}
+ </div>
+ ))}
+ </div>
+
+ {/* Emerald accent bar bottom */}
+ <div
+ style={{
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: '4px',
+ backgroundColor: '#10b981',
+ display: 'flex',
+ }}
+ />
+
+ {/* Domain */}
+ <div
+ style={{
+ position: 'absolute',
+ bottom: '20px',
+ right: '40px',
+ fontSize: '16px',
+ color: '#9ca3af',
+ fontFamily: 'sans-serif',
+ display: 'flex',
+ }}
+ >
+ grant.agentabrams.com
+ </div>
+ </div>
+ ),
+ { ...size }
+ );
+}
diff --git a/app/robots.ts b/app/robots.ts
new file mode 100644
index 0000000..a9d7c40
--- /dev/null
+++ b/app/robots.ts
@@ -0,0 +1,25 @@
+import type { MetadataRoute } from 'next';
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: [
+ {
+ userAgent: '*',
+ allow: ['/', '/landing', '/_next/static/'],
+ disallow: [
+ '/api/',
+ '/dashboard',
+ '/grants',
+ '/proposals',
+ '/news',
+ '/collaborations',
+ '/outreach',
+ '/settings',
+ '/login',
+ '/river',
+ ],
+ },
+ ],
+ sitemap: 'https://grant.agentabrams.com/sitemap.xml',
+ };
+}
diff --git a/app/sitemap.ts b/app/sitemap.ts
new file mode 100644
index 0000000..a8c7b4e
--- /dev/null
+++ b/app/sitemap.ts
@@ -0,0 +1,21 @@
+import type { MetadataRoute } from 'next';
+
+const BASE = 'https://grant.agentabrams.com';
+const LAST_MODIFIED = '2026-05-30';
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [
+ {
+ url: BASE,
+ lastModified: LAST_MODIFIED,
+ changeFrequency: 'weekly',
+ priority: 1.0,
+ },
+ {
+ url: `${BASE}/landing`,
+ lastModified: LAST_MODIFIED,
+ changeFrequency: 'weekly',
+ priority: 0.9,
+ },
+ ];
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..57e98df
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,197 @@
+#!/usr/bin/env bash
+# deploy.sh — Mac2 → Kamatera deploy for grant-app (Next.js, port 7450)
+#
+# Usage:
+# ./deploy.sh # full deploy: rsync + npm install + build + reload + verify
+# ./deploy.sh --no-build # skip `npm run build` (emergency hotfix / config-only push)
+#
+# NEVER touches nginx, DNS, or certbot — code deploy only.
+# Safe to re-run (idempotent): rsync is incremental, pm2 reload is graceful.
+#
+# Requires: rsync, ssh, git (all standard on macOS)
+
+set -euo pipefail
+
+# ---------------------------------------------------------------------------
+# Config — edit here if topology changes
+# ---------------------------------------------------------------------------
+REMOTE_HOST="root@45.61.58.125"
+REMOTE_DIR="/root/Projects/Grant"
+PM2_PROC="grant-app"
+APP_PORT="7450"
+LOCAL_DIR="/Users/stevestudio2/Projects/Grant"
+
+# ---------------------------------------------------------------------------
+# Flag parsing
+# ---------------------------------------------------------------------------
+DO_BUILD=true
+for arg in "$@"; do
+ case "$arg" in
+ --no-build) DO_BUILD=false ;;
+ *) echo "Unknown flag: $arg"; exit 1 ;;
+ esac
+done
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+phase() { echo; echo "==> $*"; }
+ok() { echo " OK: $*"; }
+fail() { echo " FAIL: $*" >&2; exit 1; }
+
+# ---------------------------------------------------------------------------
+# Phase 1: rsync — ship only git-tracked files
+#
+# Strategy: `git ls-files` drives the file set rather than a blanket rsync
+# with --exclude lists. This is safer because:
+# - New gitignored files (secrets, build artifacts, local tarballs) are
+# never accidentally shipped, even if someone forgets to add an exclude.
+# - Only files the repo actually knows about land on prod.
+# - Explicit excludes below are belt-and-suspenders for edge cases.
+#
+# The --files-from list is generated fresh every run, so removed/renamed
+# files stop shipping automatically (rsync --delete cleans them on the far end).
+# ---------------------------------------------------------------------------
+phase "Phase 1: rsync tracked files → ${REMOTE_HOST}:${REMOTE_DIR}/"
+
+cd "$LOCAL_DIR"
+
+# Verify we're inside a git repo
+git rev-parse --git-dir > /dev/null 2>&1 || fail "Not a git repository: $LOCAL_DIR"
+
+# Build the file list from git (includes committed + staged; excludes untracked/ignored)
+# Use process substitution to avoid a temp file (BSD/macOS compatible, no mapfile needed)
+RSYNC_FILE_LIST="$(mktemp /tmp/grant-deploy-files.XXXXXX)"
+git ls-files > "$RSYNC_FILE_LIST"
+
+# Always include package-lock.json even if not tracked (needed for npm ci)
+# and ecosystem.config.js (pm2 manifest). Both are tracked, but explicit here.
+
+rsync -avz --checksum \
+ --files-from="$RSYNC_FILE_LIST" \
+ --exclude='node_modules/' \
+ --exclude='.next/' \
+ --exclude='.env*' \
+ --exclude='*.log' \
+ --exclude='tmp/' \
+ --exclude='.git/' \
+ --exclude='*.bak' \
+ --exclude='*.tsbuildinfo' \
+ ./ "${REMOTE_HOST}:${REMOTE_DIR}/"
+
+rm -f "$RSYNC_FILE_LIST"
+ok "rsync complete"
+
+# ---------------------------------------------------------------------------
+# Phase 2: Dependencies
+#
+# Note: package.json contains a local file: dependency
+# "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz"
+# That tarball does NOT exist on prod (it lives at /tmp on Mac2 only).
+# `npm ci` will hard-fail if the tarball is missing. We therefore:
+# - Skip `npm ci` and run `npm install --no-audit --no-fund` instead.
+# - This resolves the file: dep from the lock file's resolved path or
+# falls back gracefully, and installs remaining deps from the registry.
+# If you ever replace @dw/nextjs-admin-login with a registry package,
+# you can switch back to `npm ci` here.
+# ---------------------------------------------------------------------------
+phase "Phase 2: install dependencies on remote"
+
+ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+echo " Node: \$(node --version) npm: \$(npm --version)"
+
+# Install deps — skipping npm ci because of the file: tarball dep (see comment above)
+npm install --no-audit --no-fund --prefer-offline 2>&1 | tail -5
+
+echo " deps installed"
+ENDSSH
+
+ok "npm install complete"
+
+# ---------------------------------------------------------------------------
+# Phase 3: Build (Next.js production build)
+# ---------------------------------------------------------------------------
+if [ "$DO_BUILD" = true ]; then
+ phase "Phase 3: npm run build on remote (this takes ~60-120s)"
+
+ ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+# Ensure NODE_ENV=production for the build so Next.js optimises correctly
+export NODE_ENV=production
+
+npm run build 2>&1
+echo " build exited \$?"
+ENDSSH
+
+ ok "build complete"
+else
+ phase "Phase 3: SKIPPED (--no-build)"
+fi
+
+# ---------------------------------------------------------------------------
+# Phase 4: pm2 graceful reload
+# ---------------------------------------------------------------------------
+phase "Phase 4: pm2 reload ${PM2_PROC} --update-env"
+
+ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -euo pipefail
+cd "${REMOTE_DIR}"
+
+# --update-env picks up any new/changed env vars in the pm2 ecosystem or .env
+pm2 reload "${PM2_PROC}" --update-env
+
+# Brief settle window — Next.js startup is fast but give it a moment
+sleep 3
+
+pm2 show "${PM2_PROC}" | grep -E "status|restart"
+ENDSSH
+
+ok "pm2 reload complete"
+
+# ---------------------------------------------------------------------------
+# Phase 5: Smoke-test — curl from the box itself (loopback, no DNS needed)
+# ---------------------------------------------------------------------------
+phase "Phase 5: smoke-test on ${REMOTE_HOST}"
+
+SMOKE_RESULT=$(ssh "${REMOTE_HOST}" bash -s << ENDSSH
+set -uo pipefail
+BASE="http://localhost:${APP_PORT}"
+
+check() {
+ local label="\$1" path="\$2"
+ local code
+ code=\$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "\${BASE}\${path}")
+ if [ "\$code" = "200" ]; then
+ echo "PASS \$label → HTTP \$code"
+ else
+ echo "FAIL \$label → HTTP \$code"
+ fi
+}
+
+check "/" "/"
+check "/landing" "/landing"
+ENDSSH
+)
+
+echo "$SMOKE_RESULT"
+
+# Exit non-zero if any check failed
+if echo "$SMOKE_RESULT" | grep -q "^FAIL"; then
+ fail "One or more smoke tests failed — check pm2 logs: ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
+fi
+
+# ---------------------------------------------------------------------------
+# Done
+# ---------------------------------------------------------------------------
+phase "Deploy complete"
+echo
+echo " App : https://grant.agentabrams.com (once nginx + cert are live)"
+echo " Loopback check: ssh ${REMOTE_HOST} curl -s -o /dev/null -w '%{http_code}' http://localhost:${APP_PORT}/"
+echo " Logs : ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
+echo " Rollback: see DEPLOY.md § Rollback"
+echo
diff --git a/deploy/nginx/grant.agentabrams.com.conf b/deploy/nginx/grant.agentabrams.com.conf
new file mode 100644
index 0000000..ece2123
--- /dev/null
+++ b/deploy/nginx/grant.agentabrams.com.conf
@@ -0,0 +1,122 @@
+# nginx vhost: grant.agentabrams.com
+# Reverse-proxy to Next.js grant-app running on 127.0.0.1:7450.
+#
+# OPERATOR NOTES
+# --------------
+# 1. certbot will MANAGE this file once you run:
+# certbot --nginx -d grant.agentabrams.com
+# It will insert/update the ssl_certificate, ssl_certificate_key, and
+# redirect-to-https lines automatically. The ssl block below is the
+# hand-maintained reference; certbot may rewrite it — that is expected.
+#
+# 2. Place this file and symlink before running certbot:
+# cp deploy/nginx/grant.agentabrams.com.conf \
+# /etc/nginx/sites-available/grant.agentabrams.com
+# ln -sf /etc/nginx/sites-available/grant.agentabrams.com \
+# /etc/nginx/sites-enabled/grant.agentabrams.com
+# nginx -t && systemctl reload nginx
+#
+# 3. client_max_body_size covers file uploads (grant documents, org logos).
+# Raise to 50m if the app adds bulk-import features.
+
+# ---------------------------------------------------------------------------
+# HTTP → HTTPS redirect (port 80)
+# certbot will add its own redirect block here; this one is pre-flight.
+# ---------------------------------------------------------------------------
+server {
+ listen 80;
+ listen [::]:80;
+ server_name grant.agentabrams.com;
+
+ # ACME challenge for Let's Encrypt renewal (certbot manages this)
+ location /.well-known/acme-challenge/ {
+ root /var/www/html;
+ }
+
+ # Redirect all other HTTP traffic to HTTPS
+ location / {
+ return 301 https://$host$request_uri;
+ }
+}
+
+# ---------------------------------------------------------------------------
+# HTTPS — main app (port 443)
+# ---------------------------------------------------------------------------
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name grant.agentabrams.com;
+
+ # --- TLS ---
+ # Paths managed by certbot. Do not edit manually after certbot runs.
+ ssl_certificate /etc/letsencrypt/live/grant.agentabrams.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/grant.agentabrams.com/privkey.pem;
+
+ # Modern TLS config — certbot will append/update include line below
+ # include /etc/letsencrypt/options-ssl-nginx.conf;
+ # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+
+ # --- General ---
+ client_max_body_size 10m;
+
+ # --- Security headers ---
+ # Uncomment and tune after a security audit; HSTS in particular is
+ # sticky (browsers cache it) so only enable when TLS is confirmed stable.
+ #
+ # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
+ # add_header X-Frame-Options "SAMEORIGIN" always;
+ # add_header X-Content-Type-Options "nosniff" always;
+ # add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ # add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
+ # add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:;" always;
+
+ # --- Logging ---
+ access_log /var/log/nginx/grant.agentabrams.com.access.log;
+ error_log /var/log/nginx/grant.agentabrams.com.error.log;
+
+ # --- Proxy to Next.js ---
+ location / {
+ proxy_pass http://127.0.0.1:7450;
+
+ # Standard proxy headers — Next.js reads these for request.ip,
+ # req.headers.host, and canonical URL construction.
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket / streaming support — required for Next.js HMR in dev
+ # and Server-Sent Events / streaming responses in production.
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Timeouts — Next.js SSR + DB queries can be slow on first cold hit;
+ # 60s gives headroom without leaving browsers hanging indefinitely.
+ proxy_connect_timeout 10s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+
+ # Disable buffering for streaming responses (Next.js App Router uses
+ # React streaming / Suspense; buffering breaks the chunked delivery).
+ proxy_buffering off;
+ proxy_buffer_size 4k;
+ }
+
+ # --- Static asset caching ---
+ # Next.js content-hashes _next/static filenames, so they are safe to
+ # cache aggressively. Reduces round-trips for repeat visitors.
+ location /_next/static/ {
+ proxy_pass http://127.0.0.1:7450;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_cache_valid 200 1y;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ }
+
+ # --- Public assets ---
+ location /public/ {
+ proxy_pass http://127.0.0.1:7450;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
← f42a326 Serve fit-feed landing at / for logged-out visitors; AppShel
·
back to Grant
·
Auth-branch /: read cookie via cookies() API (headers().get 330e761 →