Skip to main content

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

AspectDetails
RepositoryStayMeaty/amply-web
FrameworkNext.js 16 (App Router, Turbopack)
React19
StylingTailwind CSS 4
RenderingServer-side (RSC) + static pages
HostingNetlify (via @netlify/plugin-nextjs)
FontLexend (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 PageBackend EndpointRevalidate
Landing (stats)GET /public/stats5 min
ExploreGET /public/organizations60s
Org ProfileGET /public/organizations/:slug60s
Org CampaignsGET /public/organizations/:slug/campaigns60s
Campaign DetailGET /public/organizations/:slug/campaigns/:slug60s
Org LedgerGET /public/organizations/:slug/ledger60s
Donate (create)POST /donations/ (via server action)N/A
SitemapGET /public/organizations1 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.ts includes static pages + all org profile URLs
  • Robots: Environment-aware robots.ts — blocks all crawlers on non-production (IS_PRODUCTION check)
  • noindex meta tag: Root layout adds <meta name="robots" content="noindex, nofollow"> on non-production as safety net

Static vs Dynamic Pages

TypePagesRendering
StaticLanding, About, Pricing, For Donors/Orgs/BusinessesISR (5min for landing, static for others)
DynamicExplore, Org Profile, Campaigns, LedgerSSR per request with 60s revalidation
Dynamic + ClientDonateSSR org fetch + client-side Stripe Payment Element
GeneratedSitemap, RobotsGenerated at build + ISR

Environment Configuration

VariablePurposeDev Value
NEXT_PUBLIC_API_URLBackend API base URLhttps://api.dev.amply-impact.org/v1
NEXT_PUBLIC_SITE_URLCanonical site URL (used for SEO, sitemap)https://dev.amply-impact.org
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe publishable key for Payment Elementpk_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

PackagePurpose
next 16.1.6Framework (App Router, Turbopack)
react 19.2.3UI library
tailwindcss 4Styling
lucide-reactIcons
@radix-ui/*Accessible UI primitives (Dialog, Popover, Select, Tooltip)
@stripe/stripe-jsStripe.js loader
@stripe/react-stripe-jsReact components for Stripe Elements
clsx + tailwind-mergeConditional class utilities
@netlify/plugin-nextjsNetlify 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: