Amply Public Website
Server-Rendered Public-Facing Site
The public website at amply-impact.org (dev: dev.amply-impact.org) serves as the marketing site, organization directory, and public transparency interface.
Overview
| Aspect | Details |
|---|---|
| Repository | StayMeaty/amply-web |
| Framework | Next.js 16 (App Router, Turbopack) |
| React | 19 |
| Styling | Tailwind CSS 4 |
| Rendering | Server-side (RSC) + static pages |
| Hosting | Netlify (via @netlify/plugin-nextjs) |
| Font | Lexend (Google Fonts, next/font) |
URL Structure
amply-impact.org/
├── / # Landing page (marketing + live stats)
├── /explore # Organization directory (search, SDG filter)
├── /orgs/:slug # Organization profile
├── /orgs/:slug/campaigns # Org's campaigns (paginated)
├── /orgs/:slug/campaigns/:campaignSlug # Campaign detail
├── /orgs/:slug/ledger # Org's public ledger
├── /donate/:slug # Donate to org (4-step Stripe flow)
├── /about # About Amply
├── /pricing # Fee transparency
├── /for-donors # For individual donors
├── /for-organizations # For nonprofits
├── /for-businesses # For businesses (dual role)
├── /sitemap.xml # Dynamic sitemap (static + org pages)
└── /robots.txt # Crawler rules (blocks non-production)
Project Structure
amply-web/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── layout.tsx # Root layout (Navbar, Footer, font)
│ │ ├── page.tsx # Landing page (live stats from API)
│ │ ├── explore/page.tsx # Organization directory
│ │ ├── orgs/[slug]/
│ │ │ ├── page.tsx # Organization profile
│ │ │ ├── campaigns/
│ │ │ │ ├── page.tsx # Paginated campaign list
│ │ │ │ └── [campaignSlug]/page.tsx # Campaign detail
│ │ │ └── ledger/page.tsx # Public ledger with chain verification
│ │ ├── donate/[slug]/
│ │ │ ├── page.tsx # Donation page (SSR org fetch + DonationFlow)
│ │ │ └── actions.ts # Server action: createDonation (proxies to backend)
│ │ ├── about/page.tsx # Static: about
│ │ ├── pricing/page.tsx # Static: fees
│ │ ├── for-donors/page.tsx # Static: for donors
│ │ ├── for-organizations/page.tsx # Static: for organizations
│ │ ├── for-businesses/page.tsx # Static: for businesses
│ │ ├── sitemap.ts # Dynamic sitemap generation
│ │ └── robots.ts # Robots.txt (blocks dev envs)
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Navbar.tsx # Global navigation
│ │ │ └── Footer.tsx # Global footer
│ │ ├── org/
│ │ │ ├── OrgCard.tsx # Org summary card (explore grid)
│ │ │ ├── OrgHeader.tsx # Banner, logo, badges, SDGs, donate CTA
│ │ │ └── OrgStats.tsx # 4-stat grid (donated, donations, campaigns, funds)
│ │ ├── campaign/
│ │ │ └── CampaignCard.tsx # Campaign summary card
│ │ ├── explore/
│ │ │ ├── SearchBar.tsx # Search input
│ │ │ ├── SDGFilter.tsx # SDG filter dropdown
│ │ │ └── Pagination.tsx # Reusable pagination (basePath prop)
│ │ ├── donate/
│ │ │ ├── DonationFlow.tsx # Orchestrator: 4-step wizard (Amount→Details→Payment→Confirmation)
│ │ │ ├── AmountStep.tsx # Preset amounts, custom input, fee breakdown
│ │ │ ├── DetailsStep.tsx # Email, name, display preference, message
│ │ │ ├── PaymentStep.tsx # Stripe Payment Element + confirmPayment
│ │ │ ├── ConfirmationStep.tsx # Success screen
│ │ │ └── StripeProvider.tsx # Stripe Elements wrapper (singleton loadStripe)
│ │ ├── ledger/
│ │ │ └── LedgerTable.tsx # Ledger entries table
│ │ └── ui/
│ │ ├── DonateButton.tsx # Styled donate CTA
│ │ ├── ProgressBar.tsx # Campaign progress bar
│ │ ├── SDGBadge.tsx # SDG pill badge
│ │ └── VerificationBadge.tsx # Verified/unverified badge
│ └── lib/
│ ├── api.ts # Server-side API client (fetch + revalidate)
│ ├── constants.ts # SITE_URL, IS_PRODUCTION
│ ├── types.ts # TypeScript types matching backend schemas
│ └── utils.ts # formatCurrency, formatDate, etc.
├── netlify.toml # Netlify build config
├── tailwind.config.ts # Brand colors, fonts
└── package.json
Architecture
Server-Side Rendering
All API calls happen server-side in React Server Components. The public site never exposes API URLs to the browser, and CORS configuration is not required.
Browser → Netlify Edge → Next.js Server Component → Backend API → Response → HTML
API Client
The apiFetch helper prepends /public to all paths and uses Next.js revalidate for ISR caching:
// lib/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/v1";
export async function apiFetch<T>(
path: string,
options?: { revalidate?: number; searchParams?: Record<string, string> },
): Promise<T> {
const url = new URL(`${API_URL}/public${path}`);
// ...
const res = await fetch(url.toString(), {
next: { revalidate: options?.revalidate ?? 60 },
});
// ...
}
Backend Endpoints Consumed
All calls go to /v1/public/* (no authentication required):
| Frontend Page | Backend Endpoint | Revalidate |
|---|---|---|
| Landing (stats) | GET /public/stats | 5 min |
| Explore | GET /public/organizations | 60s |
| Org Profile | GET /public/organizations/:slug | 60s |
| Org Campaigns | GET /public/organizations/:slug/campaigns | 60s |
| Campaign Detail | GET /public/organizations/:slug/campaigns/:slug | 60s |
| Org Ledger | GET /public/organizations/:slug/ledger | 60s |
| Donate (create) | POST /donations/ (via server action) | N/A |
| Sitemap | GET /public/organizations | 1 hour |
Donation Flow
The /donate/[slug] page is a hybrid: the page component is a Server Component that fetches org data, but the DonationFlow client component manages the interactive 4-step wizard.
1. Amount → Client: AmountStep (preset buttons, custom input, cover-fees toggle)
2. Details → Client: DetailsStep (email, name, display preference, message)
3. Payment → Server Action: createDonation() → POST /donations/ → returns client_secret
Client: StripeProvider wraps PaymentStep → Stripe Payment Element → confirmPayment
4. Confirm → Client: ConfirmationStep (success screen)
The createDonation server action proxies the donation creation to the backend API, keeping the API URL server-side. The backend creates a Stripe PaymentIntent on the organization's connected account and returns the client_secret. The frontend then mounts the Stripe Payment Element and confirms payment client-side.
SEO
- Metadata:
generateMetadata()on org/campaign pages fetches data server-side for dynamic<title>and<meta description> - JSON-LD: Organization structured data on org profile pages
- Sitemap: Dynamic
sitemap.tsincludes static pages + all org profile URLs - Robots: Environment-aware
robots.ts— blocks all crawlers on non-production (IS_PRODUCTIONcheck) - noindex meta tag: Root layout adds
<meta name="robots" content="noindex, nofollow">on non-production as safety net
Static vs Dynamic Pages
| Type | Pages | Rendering |
|---|---|---|
| Static | Landing, About, Pricing, For Donors/Orgs/Businesses | ISR (5min for landing, static for others) |
| Dynamic | Explore, Org Profile, Campaigns, Ledger | SSR per request with 60s revalidation |
| Dynamic + Client | Donate | SSR org fetch + client-side Stripe Payment Element |
| Generated | Sitemap, Robots | Generated at build + ISR |
Environment Configuration
| Variable | Purpose | Dev Value |
|---|---|---|
NEXT_PUBLIC_API_URL | Backend API base URL | https://api.dev.amply-impact.org/v1 |
NEXT_PUBLIC_SITE_URL | Canonical site URL (used for SEO, sitemap) | https://dev.amply-impact.org |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe publishable key for Payment Element | pk_test_... |
The IS_PRODUCTION flag is derived from SITE_URL === "https://amply-impact.org" and controls:
- Robots.txt behavior (allow vs disallow)
- Meta robots tag (index vs noindex)
Deployment
Hosting: Netlify
The site runs on Netlify using @netlify/plugin-nextjs for full Next.js support (SSR, ISR, middleware).
# netlify.toml
[build]
command = "npm run build"
publish = ".next"
[[plugins]]
package = "@netlify/plugin-nextjs"
DNS
Route 53 CNAME:
dev.amply-impact.org → amply-web.netlify.app
SSL provisioned automatically by Netlify (Let's Encrypt).
Deploy Process
Currently manual CLI deploys (auto-deploy from GitHub pending OAuth setup):
# Build with production env vars
NEXT_PUBLIC_API_URL=https://api.dev.amply-impact.org/v1 \
NEXT_PUBLIC_SITE_URL=https://dev.amply-impact.org \
npx next build
# Deploy
npx netlify deploy --prod --dir=.next
Dependencies
| Package | Purpose |
|---|---|
next 16.1.6 | Framework (App Router, Turbopack) |
react 19.2.3 | UI library |
tailwindcss 4 | Styling |
lucide-react | Icons |
@radix-ui/* | Accessible UI primitives (Dialog, Popover, Select, Tooltip) |
@stripe/stripe-js | Stripe.js loader |
@stripe/react-stripe-js | React components for Stripe Elements |
clsx + tailwind-merge | Conditional class utilities |
@netlify/plugin-nextjs | Netlify deployment adapter |
Local Development
cd amply-web
npm install
npm run dev # → http://localhost:3001
Requires the backend running at http://localhost:8000 (or set NEXT_PUBLIC_API_URL).
Related: