A modern, accessible, and SEO-optimized political campaign website built with Next.js 14, TypeScript, and Tailwind CSS. This website is designed to help candidates connect with voters, collect signups, manage volunteers, and process yard sign requests.
This is a full-stack campaign website featuring:
- Responsive Design: Mobile-first approach with beautiful UI/UX
- Form Management: Email signups, volunteer registration, yard sign requests, and contact forms
- Security: Cloudflare Turnstile bot protection, rate limiting, and comprehensive security headers
- Email Automation: Automated welcome emails and confirmations via Resend
- Database Integration: Supabase for data storage and management
- SEO Optimized: Dynamic sitemaps, structured data, Open Graph images, and comprehensive metadata
- Analytics: Plausible Analytics integration for privacy-friendly tracking
- Accessibility: WCAG compliant with keyboard navigation and screen reader support
- Performance: Optimized images, lazy loading, and Lighthouse scores 90+
- Next.js 14 - React framework with App Router
- TypeScript - Type-safe development
- React 18 - UI library
- Tailwind CSS - Utility-first CSS framework
- React Hook Form - Form state management
- Zod - Schema validation
- @hookform/resolvers - Zod integration for React Hook Form
- Cloudflare Turnstile - Bot protection and CAPTCHA alternative
- Upstash Redis - Rate limiting and caching
- Supabase - PostgreSQL database and backend services
- Resend - Transactional email service
- Framer Motion - Animation library
- Lucide React - Icon library
- clsx & tailwind-merge - Conditional class utilities
- Playwright - End-to-end testing
- Axe-core - Accessibility testing
- Lighthouse CI - Performance auditing
- Node.js 20+ - Download
- npm or yarn - Package manager
- Supabase Account - Sign up (free tier available)
- Cloudflare Account - Sign up (free tier available)
- Resend Account - Sign up (free tier available)
- Upstash Account - Sign up (free tier available)
- Clone the repository (or download the project)
git clone <repository-url>
cd CountySite- Install dependencies
npm install- Copy environment variables
cp .env.example .env.local- Fill in environment variables in
.env.local:
# Site Configuration
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXT_PUBLIC_CANDIDATE_NAME=Jane Doe
NEXT_PUBLIC_OFFICE=County Commissioner
NEXT_PUBLIC_COUNTY=Springfield
NEXT_PUBLIC_STATE=IL
NEXT_PUBLIC_ELECTION_DATE=2024-11-05
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Cloudflare Turnstile
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-site-key
TURNSTILE_SECRET_KEY=your-secret-key
# Resend Email
RESEND_API_KEY=your-resend-api-key
EMAIL_FROM=noreply@yourdomain.com
EMAIL_TO=hello@yourdomain.com
EMAIL_NOTIFICATIONS_ENABLED=true
# Analytics
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
# Donations
NEXT_PUBLIC_ACTBLUE_URL=https://secure.actblue.com/donate/your-campaign
# Upstash Redis (for rate limiting)
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-redis-token- Run the development server
npm run devOpen http://localhost:3000 to see your site.
Run the following SQL in your Supabase SQL Editor to create the required tables:
-- Email signups table
CREATE TABLE IF NOT EXISTS signups (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
zip_code TEXT,
source TEXT,
utm_source TEXT,
utm_medium TEXT,
utm_campaign TEXT,
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Volunteers table
CREATE TABLE IF NOT EXISTS volunteers (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
phone TEXT,
zip_code TEXT,
interests TEXT[],
availability TEXT,
contacted BOOLEAN DEFAULT FALSE,
source TEXT,
utm_source TEXT,
utm_medium TEXT,
utm_campaign TEXT,
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Yard sign requests table
CREATE TABLE IF NOT EXISTS yard_sign_requests (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
address_line1 TEXT NOT NULL,
address_line2 TEXT,
city TEXT NOT NULL,
state TEXT NOT NULL,
zip_code TEXT NOT NULL,
quantity INTEGER DEFAULT 1 CHECK (quantity >= 1 AND quantity <= 5),
fulfilled BOOLEAN DEFAULT FALSE,
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Contact submissions table
CREATE TABLE IF NOT EXISTS contact_submissions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
subject TEXT,
message TEXT NOT NULL,
responded BOOLEAN DEFAULT FALSE,
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_signups_email ON signups(email);
CREATE INDEX IF NOT EXISTS idx_volunteers_email ON volunteers(email);
CREATE INDEX IF NOT EXISTS idx_volunteers_contacted ON volunteers(contacted);
CREATE INDEX IF NOT EXISTS idx_yard_sign_fulfilled ON yard_sign_requests(fulfilled);
CREATE INDEX IF NOT EXISTS idx_contact_responded ON contact_submissions(responded);For security, enable RLS on all tables. Since we're using the service role key in API routes, RLS can be restrictive:
-- Enable RLS on all tables
ALTER TABLE signups ENABLE ROW LEVEL SECURITY;
ALTER TABLE volunteers ENABLE ROW LEVEL SECURITY;
ALTER TABLE yard_sign_requests ENABLE ROW LEVEL SECURITY;
ALTER TABLE contact_submissions ENABLE ROW LEVEL SECURITY;
-- Deny all public access (API routes use service role key)
CREATE POLICY "Deny all public access" ON signups FOR ALL USING (false);
CREATE POLICY "Deny all public access" ON volunteers FOR ALL USING (false);
CREATE POLICY "Deny all public access" ON yard_sign_requests FOR ALL USING (false);
CREATE POLICY "Deny all public access" ON contact_submissions FOR ALL USING (false);CountySite/
├── public/ # Static assets
│ └── images/ # Campaign images (add your images here)
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── api/ # API routes
│ │ │ ├── contact/ # Contact form endpoint
│ │ │ ├── signup/ # Email signup endpoint
│ │ │ ├── volunteer/ # Volunteer signup endpoint
│ │ │ └── yard-sign/ # Yard sign request endpoint
│ │ ├── privacy/ # Privacy policy page
│ │ ├── terms/ # Terms of service page
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Homepage
│ │ ├── globals.css # Global styles
│ │ ├── robots.ts # Dynamic robots.txt
│ │ ├── sitemap.ts # Dynamic sitemap
│ │ └── opengraph-image.tsx # Dynamic OG image
│ ├── components/
│ │ ├── forms/ # Form components
│ │ │ ├── ContactForm.tsx
│ │ │ ├── SignupForm.tsx
│ │ │ ├── VolunteerForm.tsx
│ │ │ └── YardSignForm.tsx
│ │ ├── layout/ # Layout components
│ │ │ ├── AnnouncementBar.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Navbar.tsx
│ │ ├── sections/ # Page sections
│ │ │ ├── About.tsx
│ │ │ ├── Donate.tsx
│ │ │ ├── Endorsements.tsx
│ │ │ ├── Events.tsx
│ │ │ ├── GetInvolved.tsx
│ │ │ ├── Hero.tsx
│ │ │ ├── Issues.tsx
│ │ │ ├── News.tsx
│ │ │ ├── VideoQuote.tsx
│ │ │ └── YardSign.tsx
│ │ ├── ui/ # Reusable UI components
│ │ │ ├── AnimatedSection.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── OptimizedImage.tsx
│ │ │ └── SectionWrapper.tsx
│ │ ├── Analytics.tsx # Analytics component
│ │ ├── ErrorBoundary.tsx # Error handling
│ │ └── StructuredData.tsx # JSON-LD schema
│ ├── hooks/ # Custom React hooks
│ │ ├── useCountdown.ts
│ │ └── useScrollSpy.ts
│ ├── lib/
│ │ ├── constants/
│ │ │ └── images.ts # Image path constants
│ │ ├── integrations/
│ │ │ ├── ratelimit.ts # Rate limiting
│ │ │ ├── resend.ts # Email templates
│ │ │ └── supabase.ts # Supabase clients
│ │ ├── analytics.ts # Analytics utilities
│ │ ├── utils.ts # Utility functions
│ │ └── validations.ts # Zod schemas
│ └── types/ # TypeScript types
├── tests/
│ ├── accessibility/ # Accessibility tests
│ │ └── axe.spec.ts
│ └── e2e/ # End-to-end tests
│ └── forms.spec.ts
├── .github/
│ └── workflows/ # CI/CD workflows
│ ├── ci.yml
│ └── lighthouse.yml
├── next.config.ts # Next.js configuration
├── tailwind.config.ts # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies
Update environment variables in .env.local:
NEXT_PUBLIC_CANDIDATE_NAME- Candidate's full nameNEXT_PUBLIC_OFFICE- Office being soughtNEXT_PUBLIC_COUNTY- County nameNEXT_PUBLIC_STATE- State abbreviationNEXT_PUBLIC_ELECTION_DATE- Election date (YYYY-MM-DD format)
Edit src/components/sections/Issues.tsx to update:
- Issue titles
- Descriptions
- Icons (from Lucide React)
Edit src/components/sections/Endorsements.tsx to:
- Add/remove endorsements
- Update quotes
- Add endorsement photos to
/public/images/endorsements/
Edit src/components/sections/Events.tsx to:
- Add upcoming events
- Update event details
- Link to RSVP pages
Edit src/components/sections/About.tsx to:
- Update candidate biography
- Modify pull quotes
- Add credentials/experience
Edit tailwind.config.ts to customize the color palette:
colors: {
primary: {
// Main brand color (red)
50: '#FEF2F2',
// ... other shades
500: '#E92128',
},
accent: {
// Accent color (red; matches primary in this brand)
50: '#FEF2F2',
// ... other shades
500: '#E92128',
},
}Fonts are configured in src/app/layout.tsx:
- Display: Bebas Neue (from Google Fonts)
- Headings: Space Grotesk (from Google Fonts)
- Body: Inter (from Google Fonts)
To change fonts, update the font imports and CSS variables.
- Add images to
/public/images/following the structure insrc/lib/constants/images.ts - Update image paths in
src/lib/constants/images.tsif needed - Optimize images before adding:
- Use WebP or AVIF format
- Compress using tools like Squoosh
- Target file sizes: Hero < 200KB, Headshots < 50KB
Required images:
/public/images/candidate/hero.jpg(1200x1600px)/public/images/candidate/about.jpg(800x1000px)/public/images/candidate/action.jpg(1200x800px)/public/images/endorsements/*.jpg(600x600px each)/public/images/yard-sign-mockup.png(800x1000px)
IMPORTANT: Update the footer disclaimer in src/components/layout/Footer.tsx:
<p className="text-sm text-gray-400">
Paid for by {candidateName} for {office}
</p>Check your state's requirements for:
- Campaign finance disclosure language
- Authorized by statements
- Treasurer information
- Contact information requirements
Review and update src/app/privacy/page.tsx:
- Add actual campaign email address
- Update data collection practices
- Verify SMS opt-in language (if using text messaging)
- Add state-specific privacy requirements
Review src/app/terms/page.tsx:
- Update contact information
- Add state-specific terms
- Verify donation processing language
- Ensure all donation links go through ActBlue (or your compliant processor)
- Verify contribution limits are displayed
- Add required FEC disclaimers on donation pages
- Push your code to GitHub
git add .
git commit -m "Initial commit"
git push origin main-
Import project to Vercel
- Go to vercel.com
- Click "New Project"
- Import your GitHub repository
-
Configure environment variables
- Add all variables from
.env.localto Vercel - Use Vercel's environment variable interface
- Add all variables from
-
Deploy
- Vercel will automatically build and deploy
- Your site will be live at
your-project.vercel.app
-
Custom domain (optional)
- Add your domain in Vercel settings
- Update DNS records as instructed
- Update
NEXT_PUBLIC_SITE_URLin environment variables
Ensure all these are set in your hosting platform:
# Required
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXT_PUBLIC_CANDIDATE_NAME=Your Candidate Name
NEXT_PUBLIC_OFFICE=Office Name
NEXT_PUBLIC_COUNTY=County Name
NEXT_PUBLIC_STATE=ST
NEXT_PUBLIC_ELECTION_DATE=2024-11-05
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxx
SUPABASE_SERVICE_ROLE_KEY=xxx
# Cloudflare Turnstile
NEXT_PUBLIC_TURNSTILE_SITE_KEY=xxx
TURNSTILE_SECRET_KEY=xxx
# Resend
RESEND_API_KEY=xxx
EMAIL_FROM=noreply@yourdomain.com
EMAIL_TO=hello@yourdomain.com
EMAIL_NOTIFICATIONS_ENABLED=true
# Analytics (optional)
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
# Donations
NEXT_PUBLIC_ACTBLUE_URL=https://secure.actblue.com/donate/xxx
# Upstash Redis
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx- Netlify: Similar to Vercel, supports Next.js out of the box
- AWS Amplify: Full AWS integration
- Self-hosted: Requires Node.js server setup
- Update all placeholder content with real campaign information
- Add real candidate photos (hero, about, action shots)
- Add endorsement photos and quotes
- Update all social media links in Footer
- Verify all text for typos and accuracy
- Update video URL in VideoQuote section (if applicable)
- Configure ActBlue donation link
- Set up Supabase production database
- Run database migrations in production
- Configure Cloudflare Turnstile for production domain
- Set up Resend email domain verification
- Configure Upstash Redis for production
- Set up Plausible Analytics (or alternative)
- Test all forms (signup, volunteer, yard sign, contact)
- Verify email delivery (check spam folders)
- Review and update Privacy Policy
- Review and update Terms of Service
- Verify disclaimer language with campaign counsel
- Add required FEC disclaimers
- Check state-specific requirements
- Add treasurer information if required
- Run Lighthouse audit (target: 90+ in all categories)
- Test on mobile devices (iOS and Android)
- Test on different browsers (Chrome, Firefox, Safari, Edge)
- Run accessibility tests (
npm run test:a11y) - Run end-to-end tests (
npm run test:e2e) - Test all external links
- Verify all images load correctly
- Test form submissions end-to-end
- Verify sitemap is accessible at
/sitemap.xml - Verify robots.txt is accessible at
/robots.txt - Test Open Graph image generation
- Verify structured data with Google Rich Results Test
- Submit sitemap to Google Search Console
- Optimize all images (WebP/AVIF format)
- Test page load speeds
- Verify security headers are working
- Test rate limiting on forms
- Verify Turnstile is blocking bots
- Check that API routes are not publicly accessible
- Verify environment variables are not exposed
npm run dev- Start development servernpm run build- Build for productionnpm run start- Start production servernpm run lint- Run ESLintnpm run type-check- Run TypeScript type checkingnpm run test- Run all testsnpm run test:e2e- Run end-to-end testsnpm run test:a11y- Run accessibility tests
- Check browser console for errors
- Verify Turnstile keys are correct
- Check Supabase connection
- Verify rate limiting isn't blocking legitimate requests
- Ensure images are in
/public/images/directory - Check image paths in
src/lib/constants/images.ts - Verify Next.js Image optimization is working
- Verify Resend API key is correct
- Check Resend dashboard for errors
- Verify email domain is verified in Resend
- Check spam folders
- Verify Supabase credentials
- Check database tables exist
- Verify RLS policies allow service role access
- Check Supabase logs for errors
This project is for campaign use only. All rights reserved.
Note: This template is provided as-is. Campaigns are responsible for:
- Legal compliance with FEC and state regulations
- Content accuracy
- Data privacy compliance
- Accessibility requirements
- Framework: Next.js
- Styling: Tailwind CSS
- Icons: Lucide
- Animations: Framer Motion
For issues or questions:
- Check the troubleshooting section above
- Review Next.js documentation
- Check Supabase, Resend, and Cloudflare documentation
- Consult with your development team
Built with ❤️ for grassroots campaigns