@silverassist/recaptcha
v0.2.1
Published
Google reCAPTCHA v3 integration for Next.js applications with Server Actions support
Maintainers
Readme
@silverassist/recaptcha
Google reCAPTCHA v3 integration for Next.js applications with Server Actions support.
Features
- ✅ Client Component:
RecaptchaWrapperfor automatic token generation - ✅ Server Validation:
validateRecaptchafunction for Server Actions - ✅ TypeScript Support: Full type definitions included
- ✅ Next.js Optimized: Works with App Router and Server Actions
- ✅ Auto Token Refresh: Tokens refresh automatically before expiration
- ✅ Graceful Degradation: Works in development without credentials
- ✅ Configurable Thresholds: Custom score thresholds per form
- ✅ Lazy Loading: Optional lazy loading for better performance
- ✅ Singleton Script Loading: Prevents duplicate script loads across multiple forms
Installation
npm install @silverassist/recaptcha
# or
yarn add @silverassist/recaptcha
# or
pnpm add @silverassist/recaptchaSetup
1. Get reCAPTCHA Keys
- Go to Google reCAPTCHA Admin
- Create a new site with reCAPTCHA v3
- Get your Site Key (public) and Secret Key (private)
2. Add Environment Variables
# .env.local
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key_here
RECAPTCHA_SECRET_KEY=your_secret_key_hereUsage
Client Component
Add RecaptchaWrapper inside your form:
"use client";
import { RecaptchaWrapper } from "@silverassist/recaptcha";
export function ContactForm() {
return (
<form action={submitForm}>
<RecaptchaWrapper action="contact_form" />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}Server Action
Validate the token in your Server Action:
"use server";
import { validateRecaptcha, getRecaptchaToken } from "@silverassist/recaptcha/server";
export async function submitForm(formData: FormData) {
// Get and validate reCAPTCHA token
const token = getRecaptchaToken(formData);
const recaptcha = await validateRecaptcha(token, "contact_form");
if (!recaptcha.success) {
return { success: false, message: recaptcha.error };
}
// Process form data...
const email = formData.get("email");
const message = formData.get("message");
// Your form processing logic here
return { success: true };
}⚠️ Important: Custom FormData
RecaptchaWrapper injects a hidden input field containing the reCAPTCHA token. If your form handler creates a custom FormData object, you must ensure the hidden token is included.
❌ This will fail (token is missing):
"use client";
import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action
export function ContactForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ❌ Creating empty FormData - hidden reCAPTCHA input is NOT included!
const formData = new FormData();
formData.set("email", "[email protected]");
formData.set("message", "Hello");
await submitForm(formData);
};
return (
<form onSubmit={handleSubmit}>
<RecaptchaWrapper action="contact_form" />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}✅ Recommended: Pass form element to capture all inputs
"use client";
import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action
export function ContactForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ✅ Pass form element - captures ALL inputs including hidden reCAPTCHA token
const formData = new FormData(e.currentTarget);
await submitForm(formData);
};
return (
<form onSubmit={handleSubmit}>
<RecaptchaWrapper action="contact_form" />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}✅ Alternative: Start with form element, then modify
"use client";
import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action
export function ContactForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ✅ Start with form element (includes hidden token)
const formData = new FormData(e.currentTarget);
// Then add/override specific fields
formData.set("customField", "customValue");
formData.set("timestamp", Date.now().toString());
await submitForm(formData);
};
return (
<form onSubmit={handleSubmit}>
<RecaptchaWrapper action="contact_form" />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}Performance Optimization: Lazy Loading
The lazy prop enables lazy loading of the reCAPTCHA script, which defers loading until the form becomes visible in the viewport. This significantly improves initial page load performance.
Performance Benefits
Note: These metrics are approximate values measured on production websites using Google reCAPTCHA v3. Actual performance improvements will vary based on network conditions, device capabilities, and page complexity.
| Metric | Without Lazy Loading | With Lazy Loading | Improvement | |--------|---------------------|-------------------|-------------| | Initial JS | 320KB+ | 0 KB (until visible) | -320KB | | TBT (Total Blocking Time) | ~470ms | ~0ms (deferred) | -470ms | | TTI (Time to Interactive) | +2-3s | Minimal impact | -2-3s |
Basic Lazy Loading
Enable lazy loading by adding the lazy prop:
"use client";
import { RecaptchaWrapper } from "@silverassist/recaptcha";
export function ContactForm() {
return (
<form action={submitForm}>
{/* Script loads only when form is near viewport */}
<RecaptchaWrapper action="contact_form" lazy />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}Custom Root Margin
Control when the script loads with the lazyRootMargin prop (default: "200px"):
// Load script earlier (400px before entering viewport)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="400px" />
// Load script later (load only when fully visible)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="0px" />
// Load with negative margin (load only after scrolling past)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="-100px" />Best Practices
1. Use lazy loading for below-the-fold forms
// Hero form (above the fold) - load immediately
<RecaptchaWrapper action="hero_signup" />
// Footer form (below the fold) - lazy load
<RecaptchaWrapper action="footer_contact" lazy />2. Multiple forms on the same page
The package automatically uses singleton script loading, so the script is only loaded once even with multiple forms:
export function MultiFormPage() {
return (
<>
{/* First form triggers script load */}
<RecaptchaWrapper action="newsletter" lazy />
{/* Second form reuses the same script */}
<RecaptchaWrapper action="contact" lazy />
{/* Third form also reuses the script */}
<RecaptchaWrapper action="feedback" lazy />
</>
);
}3. Adjust root margin based on form position
// Form near top - smaller margin for faster load
<RecaptchaWrapper action="signup" lazy lazyRootMargin="100px" />
// Form far down page - larger margin to load in advance
<RecaptchaWrapper action="newsletter" lazy lazyRootMargin="500px" />API Reference
RecaptchaWrapper
Client component that loads reCAPTCHA and generates tokens.
<RecaptchaWrapper
action="contact_form" // Required: action name for analytics
inputName="recaptchaToken" // Optional: hidden input name (default: "recaptchaToken")
inputId="recaptcha-token" // Optional: hidden input id
siteKey="..." // Optional: override env variable
refreshInterval={90000} // Optional: token refresh interval in ms (default: 90000)
onTokenGenerated={(token) => {}} // Optional: callback when token is generated
onError={(error) => {}} // Optional: callback on error
lazy={false} // Optional: enable lazy loading (default: false)
lazyRootMargin="200px" // Optional: IntersectionObserver rootMargin (default: "200px")
/>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| action | string | Required | Action name for reCAPTCHA analytics (e.g., "contact_form", "signup") |
| inputName | string | "recaptchaToken" | Name attribute for the hidden input field |
| inputId | string | "recaptcha-token" | ID attribute for the hidden input field |
| siteKey | string | process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY | Override the site key from environment variable |
| refreshInterval | number | 90000 | Token refresh interval in milliseconds (90 seconds) |
| onTokenGenerated | (token: string) => void | undefined | Callback invoked when a new token is generated |
| onError | (error: Error) => void | undefined | Callback invoked when an error occurs |
| lazy | boolean | false | Enable lazy loading to defer script until form is visible |
| lazyRootMargin | string | "200px" | IntersectionObserver rootMargin (used when lazy is true) |
validateRecaptcha
Server-side token validation function.
const result = await validateRecaptcha(
token, // Token from form
"contact_form", // Expected action (optional)
{
scoreThreshold: 0.5, // Minimum score (default: 0.5)
secretKey: "...", // Override env variable
debug: true, // Enable debug logging
}
);
// Result type:
// {
// success: boolean,
// score: number,
// error?: string,
// skipped?: boolean,
// rawResponse?: RecaptchaVerifyResponse
// }isRecaptchaEnabled
Check if reCAPTCHA is configured.
import { isRecaptchaEnabled } from "@silverassist/recaptcha/server";
if (isRecaptchaEnabled()) {
// Validate token
} else {
// Skip validation (development)
}getRecaptchaToken
Extract token from FormData.
import { getRecaptchaToken } from "@silverassist/recaptcha/server";
const token = getRecaptchaToken(formData);
const token = getRecaptchaToken(formData, "customFieldName");Score Thresholds
reCAPTCHA v3 returns a score from 0.0 to 1.0:
| Score | Meaning | |-------|---------| | 1.0 | Very likely human | | 0.7+ | Likely human | | 0.5 | Default threshold | | 0.3- | Suspicious | | 0.0 | Very likely bot |
Adjust threshold based on form sensitivity:
// Standard forms
await validateRecaptcha(token, "contact", { scoreThreshold: 0.5 });
// Sensitive forms (payments, account creation)
await validateRecaptcha(token, "payment", { scoreThreshold: 0.7 });
// Low-risk forms (newsletter signup)
await validateRecaptcha(token, "newsletter", { scoreThreshold: 0.3 });Subpath Imports
You can import from specific subpaths for better tree-shaking:
// Main exports (client + server + types)
import { RecaptchaWrapper, validateRecaptcha } from "@silverassist/recaptcha";
// Client only
import { RecaptchaWrapper } from "@silverassist/recaptcha/client";
// Server only
import { validateRecaptcha, getRecaptchaToken, isRecaptchaEnabled } from "@silverassist/recaptcha/server";
// Types only
import type { RecaptchaValidationResult, RecaptchaWrapperProps } from "@silverassist/recaptcha/types";
// Constants only
import { DEFAULT_SCORE_THRESHOLD, RECAPTCHA_CONFIG } from "@silverassist/recaptcha/constants";Development
In development, when RECAPTCHA_SECRET_KEY is not set, validation is skipped and forms work normally. This allows testing without reCAPTCHA credentials.
const result = await validateRecaptcha(token, "test");
// Returns: { success: true, score: 1, skipped: true }TypeScript
Full TypeScript support with exported types:
import type {
RecaptchaWrapperProps,
RecaptchaValidationResult,
RecaptchaVerifyResponse,
RecaptchaConfig,
RecaptchaValidationOptions,
} from "@silverassist/recaptcha";License
Polyform Noncommercial License 1.0.0
