WordPress and Gravity Forms integration package for React applications with static data generation
npm install @marvalt/wadapterStatic-first WordPress and Gravity Forms integration package for React applications. Provides build-time static data generation, runtime static data access, and secure form submission capabilities with Cloudflare Turnstile bot protection.
- Overview
- Installation
- WordPress Plugin Requirements
- Environment Configuration
- Build-Time Data Generation
- Runtime Usage
- Cloudflare Setup
- Security
- API Reference
- Examples
- Troubleshooting
@marvalt/wadapter provides a complete solution for integrating WordPress and Gravity Forms into React applications with a static-first architecture:
- Build-time generators: Fetch WordPress content and Gravity Forms schemas at build time, outputting static JSON files
- Runtime static access: Read-only access to pre-generated static data (no runtime API calls)
- Secure form submissions: Server-side proxy with multiple security layers including Turnstile bot protection
- React components: Ready-to-use components for rendering WordPress content and Gravity Forms
- TypeScript support: Full type definitions for all data structures
The package has three distinct entry points to separate browser-safe code from Node.js-only code:
1. @marvalt/wadapter (Main browser bundle)
- React components (GravityForm, WordPressContent, TurnstileWidget)
- React hooks (useWordPress, useGravityForms)
- API clients (WordPressClient, GravityFormsClient)
- Static data loaders and selectors
- Types and utilities
- No Node.js dependencies (browser-safe)
2. @marvalt/wadapter/generators (Node.js-only bundle)
- generateWordPressData() - Build-time WordPress data generation
- generateGravityFormsData() - Build-time Gravity Forms schema generation
- generateFormProtectionData() - Build-time form protection data
- Uses fs, path, and other Node.js modules
- Only for build scripts and SSG
3. @marvalt/wadapter/server (Cloudflare Pages Functions)
- handleGravityFormsProxy() - Server-side proxy handler
- verifyTurnstile() - Server-side Turnstile verification
- Only for serverless functions
``bash`
npm install @marvalt/wadapter
The package requires React (16.8.0+) and React DOM (16.8.0+). These should already be installed in your React project.
1. Gravity Forms (Premium or Basic)
- Must be installed and activated
- Required for form functionality
2. Gravity Forms API Endpoint (Custom Plugin)
- Required for proper form submissions with notification triggering
- Provides enhanced REST API endpoints at /wp-json/gf-api/v1/`
- Solves the critical issue where standard Gravity Forms REST API submissions don't trigger email notifications
- Installation:
bash`
# Clone or download the plugin
git clone https://github.com/ViBuNe-Pty-Ltd/gravity-forms-api-endpoint.git wp-content/plugins/gravity-forms-api-endpoint
Plugins → Installed Plugins
- Activate the plugin in WordPress admin: https://your-wordpress-site.com/wp-json/gf-api/v1/health
- Verify installation: Visit
The standard Gravity Forms REST API (/wp-json/gf/v2/forms/{id}/submissions) creates entries but does not trigger notifications. The custom plugin provides /wp-json/gf-api/v1/forms/{id}/submit which:
- ✅ Creates entries AND triggers notifications
- ✅ Fires all Gravity Forms hooks
- ✅ Returns proper confirmation messages
- ✅ Integrates with webhook automation for static data regeneration
These variables are used during build-time data generation and may be exposed to the browser (use .env for non-secrets, .env.local for secrets):
`envAuthentication Mode
VITE_AUTH_MODE=direct # or 'cloudflare_proxy'
$3
These are set in the Cloudflare Pages dashboard (Settings → Environment Variables) and are server-side only:
`env
WordPress Credentials (for Pages Function proxy)
VITE_WORDPRESS_API_URL=https://your-wordpress-site.com
VITE_WP_API_USERNAME=your-username
VITE_WP_APP_PASSWORD=your-app-passwordOrigin Validation (REQUIRED for production)
ALLOWED_ORIGINS=https://yourdomain.com,https://preview.yourdomain.comTurnstile Verification (optional but recommended)
TURNSTILE_SECRET_KEY=your-secret-keyCloudflare Access (if WordPress is behind Cloudflare Access)
VITE_CF_ACCESS_CLIENT_ID=your-client-id
VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret
`Important Notes:
-
ALLOWED_ORIGINS is required for the proxy to work in production. If not set, only localhost:8080 and localhost:5173 are allowed.
- TURNSTILE_SECRET_KEY is optional. If set, Turnstile verification will be enforced for form submissions.
- VITE_TURNSTILE_SITE_KEY must be set in both build-time and runtime environments for Turnstile to work.Build-Time Data Generation
$3
Create scripts to generate static data at build time. These scripts use the
/generators export which requires Node.js.Example:
scripts/generateWordPressData.ts`typescript
import dotenv from 'dotenv';
import { generateWordPressData } from '@marvalt/wadapter/generators';dotenv.config({ path: '.env' });
dotenv.config({ path: '.env.local' });
async function run() {
const config = {
authMode: 'direct' as const,
apiUrl: process.env.VITE_WORDPRESS_API_URL,
username: process.env.VITE_WP_API_USERNAME,
password: process.env.VITE_WP_APP_PASSWORD,
cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
};
if (!config.apiUrl || !config.username || !config.password) {
console.error('❌ Missing required environment variables');
process.exit(1);
}
const postTypes = process.env.VITE_ENABLED_POST_TYPES
? process.env.VITE_ENABLED_POST_TYPES.split(',').map(t => t.trim())
: ['posts', 'pages'];
const maxItems = parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '100', 10);
await generateWordPressData({
...config,
outputPath: './public/wordpress-data.json',
postTypes,
includeEmbedded: true, // Always true for full functionality
maxItems,
});
console.log('✅ WordPress data generation completed');
}
run();
`Example:
scripts/generateGravityFormsData.ts`typescript
import dotenv from 'dotenv';
import { generateGravityFormsData } from '@marvalt/wadapter/generators';dotenv.config({ path: '.env' });
dotenv.config({ path: '.env.local' });
async function run() {
const config = {
authMode: 'direct' as const,
apiUrl: process.env.VITE_GRAVITY_FORMS_API_URL ||
${process.env.VITE_WORDPRESS_API_URL}/wp-json/gf-api/v1,
username: process.env.VITE_WP_API_USERNAME || '',
password: process.env.VITE_WP_APP_PASSWORD || '',
useCustomEndpoint: true, // Force use of gf-api/v1
cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
}; if (!config.apiUrl || !config.username || !config.password) {
console.error('❌ Missing required environment variables');
process.exit(1);
}
await generateGravityFormsData({
...config,
outputPath: './public/gravityForms.json',
includeInactive: false,
});
console.log('✅ Gravity Forms data generation completed');
}
run();
`$3
Add to your
package.json:`json
{
"scripts": {
"generate:wp": "tsx scripts/generateWordPressData.ts",
"generate:gf": "tsx scripts/generateGravityFormsData.ts",
"generate:all": "npm run generate:wp && npm run generate:gf",
"build": "npm run generate:all && vite build"
}
}
`$3
The generators create static JSON files in your
public directory:-
public/wordpress-data.json - WordPress posts, pages, media, categories, tags, and custom post types
- public/gravityForms.json - Gravity Forms schemas with fields, notifications, and confirmationsWordPress Data Structure:
- Pages include parsed Gutenberg blocks (
content.raw is parsed and stored as blocks)
- Posts use rendered HTML (content.rendered)
- Embedded data (featured media, terms, etc.) is included when includeEmbedded: trueRuntime Usage
$3
Load and query static data at runtime (no API calls):
`typescript
import {
loadWordPressData,
getWordPressPosts,
getWordPressPages,
getWordPressMedia
} from '@marvalt/wadapter';import {
loadGravityFormsData,
getActiveForms,
getFormById
} from '@marvalt/wadapter';
// Load data (typically done once at app startup)
await loadWordPressData();
await loadGravityFormsData();
// Query data
const posts = getWordPressPosts();
const pages = getWordPressPages();
const media = getWordPressMedia();
const forms = getActiveForms();
const form = getFormById(1);
`$3
Use React hooks for convenient data access:
`typescript
import { useWordPress, useGravityForms } from '@marvalt/wadapter';function MyComponent() {
const { posts, pages, media, loading } = useWordPress();
const { form, loading: formLoading, submitForm } = useGravityForms(1, {
apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
authMode: 'direct',
});
// Use the data...
}
`$3
#### WordPressContent Component
Render WordPress content with automatic HTML sanitization and responsive images:
`typescript
import { WordPressContent } from '@marvalt/wadapter';function PostPage({ post }) {
return (
content={post.content.rendered}
className="prose prose-lg"
/>
);
}
`#### GravityForm Component
Render and handle Gravity Forms with automatic Turnstile integration:
`typescript
import { GravityForm } from '@marvalt/wadapter';function ContactForm() {
return (
formId={1}
config={{
apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
authMode: 'direct', // Uses Cloudflare Pages Function proxy
}}
onSubmit={(result) => {
console.log('Success:', result);
// result.confirmation contains confirmation message
}}
onError={(error) => {
console.error('Error:', error);
}}
/>
);
}
`Features:
- Automatic field rendering based on form schema
- Client-side validation
- Conditional logic support
- Turnstile bot protection (automatic if
VITE_TURNSTILE_SITE_KEY is set)
- Submit button disabled until Turnstile verification completes (if enabled)#### TurnstileWidget Component
Standalone Turnstile widget for custom implementations:
`typescript
import { TurnstileWidget } from '@marvalt/wadapter';function CustomForm() {
const [token, setToken] = useState(null);
return (
<>
onVerify={(token) => setToken(token)}
onError={() => setToken(null)}
/>
>
);
}
`$3
For programmatic access without React:
`typescript
import { GravityFormsClient } from '@marvalt/wadapter';const client = new GravityFormsClient({
apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
authMode: 'direct',
});
const result = await client.submitForm({
form_id: 1,
field_values: {
'1': 'John Doe',
'2': 'john@example.com',
},
});
`Cloudflare Setup
$3
The package automatically installs a Cloudflare Pages Function template during
npm install via a postinstall script. The function is located at:`
functions/api/gravity-forms-submit.ts
`If the file doesn't exist, create it manually:
`typescript
import { handleGravityFormsProxy } from '@marvalt/wadapter/server';export const onRequest = handleGravityFormsProxy;
`$3
`
┌─────────┐
│ Browser │ (Untrusted)
└────┬────┘
│ HTTPS + Turnstile Token (optional)
▼
┌─────────────────────┐
│ Pages Function │ ← Security Checkpoint
│ /api/gf-submit │ • Origin validation
│ │ • Turnstile verification (optional)
│ │ • Endpoint whitelist
│ │ • Basic Auth injection
│ │ • CF Access injection (optional)
└────┬────────────────┘
│ Authenticated Request
▼
┌─────────────────────┐
│ WordPress │ (Trusted)
│ /wp-json/gf-api/v1 │ • No auth needed from client
│ (Custom Plugin) │ • Processes submission
│ │ • Triggers notifications
│ │ • Returns confirmation
└─────────────────────┘
`$3
The proxy implements five security layers:
1. Origin Validation - Only authorized domains can submit (via
ALLOWED_ORIGINS)
2. Endpoint Whitelisting - Only /forms/{id}/submit endpoints allowed
3. Turnstile Verification - Bot protection (optional, requires TURNSTILE_SECRET_KEY)
4. Server-Side Auth - WordPress credentials never exposed to browser
5. CF Access Support - Optional additional security layer for WordPress behind Cloudflare Access$3
If your WordPress site is behind Cloudflare Access (Zero Trust), you need to:
1. Create a Cloudflare Access Service Token
2. Set
VITE_CF_ACCESS_CLIENT_ID and VITE_CF_ACCESS_CLIENT_SECRET in both:
- Build-time environment (for data generation)
- Cloudflare Pages environment (for proxy)The proxy will automatically inject CF Access headers when these credentials are present.
Security
$3
#### Direct Mode (
VITE_AUTH_MODE=direct)- Build-time: Uses WordPress Basic Auth directly (credentials in
.env.local)
- Runtime: Uses Cloudflare Pages Function proxy (credentials in Cloudflare Pages environment)
- WordPress: Uses standard REST API endpoints
- Gravity Forms: Uses custom gf-api/v1 endpoints (requires custom plugin)#### Cloudflare Proxy Mode (
VITE_AUTH_MODE=cloudflare_proxy)- Build-time: Uses WordPress Basic Auth directly
- Runtime: Uses Cloudflare Worker with
?endpoint= query parameter
- WordPress: Worker injects CF Access credentials
- Gravity Forms: Worker routes to gf-api/v1 endpointsNote: Most implementations use
direct mode with Cloudflare Pages Functions, which provides the same security benefits without requiring a separate Worker.$3
1. Never expose credentials to the browser: All WordPress credentials should be in
.env.local (not committed) or Cloudflare Pages environment variables
2. Always use the proxy for form submissions: Never call WordPress API directly from the browser
3. Enable Turnstile: Set VITE_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY for bot protection
4. Set ALLOWED_ORIGINS: Required for production deployments
5. Use HTTPS: Always use HTTPS in productionAPI Reference
$3
####
generateWordPressData(config)Generates static WordPress data.
Parameters:
-
config.apiUrl (string, required) - WordPress API URL
- config.username (string, required) - WordPress username
- config.password (string, required) - WordPress app password
- config.outputPath (string, required) - Output file path
- config.postTypes (string[], optional) - Post types to fetch (default: ['posts', 'pages'])
- config.includeEmbedded (boolean, optional) - Include embedded data (default: true)
- config.maxItems (number, optional) - Max items per post type (default: 100)
- config.cfAccessClientId (string, optional) - CF Access client ID
- config.cfAccessClientSecret (string, optional) - CF Access client secretReturns:
Promise####
generateGravityFormsData(config)Generates static Gravity Forms data.
Parameters:
-
config.apiUrl (string, required) - Gravity Forms API URL (should use gf-api/v1)
- config.username (string, required) - WordPress username
- config.password (string, required) - WordPress app password
- config.outputPath (string, required) - Output file path
- config.useCustomEndpoint (boolean, optional) - Use custom gf-api/v1 endpoint (default: true)
- config.includeInactive (boolean, optional) - Include inactive forms (default: false)
- config.cfAccessClientId (string, optional) - CF Access client ID
- config.cfAccessClientSecret (string, optional) - CF Access client secretReturns:
Promise$3
####
handleGravityFormsProxy(context)Cloudflare Pages Function handler for Gravity Forms proxy.
Usage:
`typescript
import { handleGravityFormsProxy } from '@marvalt/wadapter/server';export const onRequest = handleGravityFormsProxy;
`Required Environment Variables:
-
VITE_WORDPRESS_API_URL or WORDPRESS_API_URL
- VITE_WP_API_USERNAME or WP_API_USERNAME
- VITE_WP_APP_PASSWORD or WP_APP_PASSWORD
- ALLOWED_ORIGINS (required for production)Optional Environment Variables:
-
TURNSTILE_SECRET_KEY or VITE_TURNSTILE_SECRET_KEY
- VITE_CF_ACCESS_CLIENT_ID or CF_ACCESS_CLIENT_ID
- VITE_CF_ACCESS_CLIENT_SECRET or CF_ACCESS_CLIENT_SECRET$3
####
Props:
-
formId (number, required) - Gravity Forms form ID
- config (GravityFormsConfig, required) - Configuration object
- className (string, optional) - Additional CSS classes
- onSubmit (function, optional) - Success callback
- onError (function, optional) - Error callback####
Props:
-
content (string, required) - WordPress HTML content
- className (string, optional) - Additional CSS classes####
Props:
-
onVerify (function, required) - Called with token when verified
- onError (function, optional) - Called on verification error$3
####
useWordPress()Returns WordPress static data.
Returns:
`typescript
{
posts: WordPressPost[];
pages: WordPressPage[];
media: WordPressMedia[];
categories: WordPressCategory[];
tags: WordPressTag[];
loading: boolean;
error: Error | null;
}
`####
useGravityForms(formId, config)Returns Gravity Forms data and submission function.
Parameters:
-
formId (number, required) - Form ID
- config (GravityFormsConfig, required) - ConfigurationReturns:
`typescript
{
form: GravityForm | null;
loading: boolean;
error: Error | null;
submitting: boolean;
result: any | null;
submitForm: (data: GravityFormSubmission) => Promise;
}
`Examples
$3
See the
bnibrilliance-website implementation for a complete, production-ready example:- Build scripts:
scripts/generateWordPressData.ts, scripts/generateGravityFormsData.ts
- Cloudflare Pages Function: functions/api/gravity-forms-submit.ts
- React components using the package
- Environment variable configuration$3
`typescript
import { GravityForm } from '@marvalt/wadapter';function ContactPage() {
return (
Contact Us
formId={1}
config={{
apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
authMode: 'direct',
}}
onSubmit={(result) => {
alert(result.confirmation?.message || 'Thank you!');
}}
onError={(error) => {
alert('Error: ' + error.message);
}}
/>
);
}
`$3
`typescript
import { useGravityForms } from '@marvalt/wadapter';function CustomForm() {
const { form, loading, submitForm } = useGravityForms(1, {
apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
authMode: 'direct',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const fieldValues: Record = {};
formData.forEach((value, key) => {
fieldValues[key] = value.toString();
});
try {
const result = await submitForm({
form_id: 1,
field_values: fieldValues,
});
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}
};
if (loading) return
Loading form...;
if (!form) return Form not found; return (
);
}
`Troubleshooting
$3
Error: Module "fs" has been externalized for browser compatibility
Solution: You're importing generators from the main package. Use the
/generators export instead:`typescript
// ❌ Wrong
import { generateWordPressData } from '@marvalt/wadapter';// ✅ Correct
import { generateWordPressData } from '@marvalt/wadapter/generators';
`$3
Error: 403 Forbidden
Possible causes:
1.
ALLOWED_ORIGINS not set in Cloudflare Pages environment
2. Turnstile token missing or invalid (if Turnstile is enabled)
3. Endpoint pattern mismatchSolutions:
1. Set
ALLOWED_ORIGINS in Cloudflare Pages dashboard: Settings → Environment Variables
2. Verify VITE_TURNSTILE_SITE_KEY is set and Turnstile widget is rendering
3. Check that form ID matches the endpoint patternError: 404 Not Found
Possible causes:
1. Gravity Forms API Endpoint plugin not installed/activated
2. Incorrect API URL
Solutions:
1. Verify plugin is installed: Visit
https://your-site.com/wp-json/gf-api/v1/health
2. Check VITE_WORDPRESS_API_URL is correct
3. Verify WordPress REST API is enabled (Settings → Permalinks)$3
Error: 401 Unauthorized
Possible causes:
1. Incorrect WordPress credentials
2. Cloudflare Access credentials missing (if WordPress is behind CF Access)
Solutions:
1. Verify
VITE_WP_API_USERNAME and VITE_WP_APP_PASSWORD are correct
2. If using Cloudflare Access, set VITE_CF_ACCESS_CLIENT_ID and VITE_CF_ACCESS_CLIENT_SECRETError: Empty or incomplete data
Possible causes:
1.
includeEmbedded: false (should always be true for full functionality)
2. maxItems too low
3. Post types not enabledSolutions:
1. Always set
includeEmbedded: true in generator config
2. Increase VITE_DEFAULT_MAX_ITEMS if needed
3. Verify VITE_ENABLED_POST_TYPES includes all needed post types$3
Widget not rendering
Possible causes:
1.
VITE_TURNSTILE_SITE_KEY not set
2. Turnstile script not loadedSolutions:
1. Set
VITE_TURNSTILE_SITE_KEY in environment variables
2. The widget automatically loads the Turnstile script - verify no CSP blocks itVerification always fails
Possible causes:
1.
TURNSTILE_SECRET_KEY incorrect
2. Token expired (tokens expire after 5 minutes)Solutions:
1. Verify
TURNSTILE_SECRET_KEY matches your Cloudflare Turnstile configuration
2. Ensure form is submitted within 5 minutes of Turnstile verificationLicense
GPL-3.0-or-later
---
Need help? Check the implementation example in
bnibrilliance-website or review the source code in the src` directory.