@marvalt/wadapter
v2.3.48
Published
WordPress and Gravity Forms integration package for React applications with static data generation
Maintainers
Readme
@marvalt/wadapter
Static-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.
Table of Contents
- Overview
- Installation
- WordPress Plugin Requirements
- Environment Configuration
- Build-Time Data Generation
- Runtime Usage
- Cloudflare Setup
- Security
- API Reference
- Examples
- Troubleshooting
Overview
@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
Package Structure
The package has three distinct entry points to separate browser-safe code from Node.js-only code:
@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)
- React components (
@marvalt/wadapter/generators(Node.js-only bundle)generateWordPressData()- Build-time WordPress data generationgenerateGravityFormsData()- Build-time Gravity Forms schema generationgenerateFormProtectionData()- Build-time form protection data- Uses
fs,path, and other Node.js modules - Only for build scripts and SSG
@marvalt/wadapter/server(Cloudflare Pages Functions)handleGravityFormsProxy()- Server-side proxy handlerverifyTurnstile()- Server-side Turnstile verification- Only for serverless functions
Installation
npm install @marvalt/wadapterPeer Dependencies
The package requires React (16.8.0+) and React DOM (16.8.0+). These should already be installed in your React project.
WordPress Plugin Requirements
Required Plugins
Gravity Forms (Premium or Basic)
- Must be installed and activated
- Required for form functionality
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:
# 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 - Activate the plugin in WordPress admin:
Plugins → Installed Plugins - Verify installation: Visit
https://your-wordpress-site.com/wp-json/gf-api/v1/health
Why the Custom Plugin is Required
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
Environment Configuration
Build-Time Variables (.env or .env.local)
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):
# Authentication Mode
VITE_AUTH_MODE=direct # or 'cloudflare_proxy'
# WordPress Configuration
VITE_WORDPRESS_API_URL=https://your-wordpress-site.com
# WordPress Credentials (for build-time generation only - never exposed to browser)
VITE_WP_API_USERNAME=your-username
VITE_WP_APP_PASSWORD=your-app-password
# Gravity Forms Configuration
VITE_GRAVITY_FORMS_API_URL=https://your-wordpress-site.com/wp-json/gf-api/v1
# Or leave empty to auto-construct from VITE_WORDPRESS_API_URL
# Cloudflare Access (if WordPress is behind Cloudflare Access)
VITE_CF_ACCESS_CLIENT_ID=your-client-id
VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret
# Turnstile Bot Protection (optional)
VITE_TURNSTILE_SITE_KEY=your-site-key
# WordPress Data Generation Options
VITE_ENABLED_POST_TYPES=posts,pages,chapter_member # Comma-separated list
VITE_DEFAULT_MAX_ITEMS=100 # Max items per post typeCloudflare Pages Environment Variables
These are set in the Cloudflare Pages dashboard (Settings → Environment Variables) and are server-side only:
# 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-password
# Origin Validation (REQUIRED for production)
ALLOWED_ORIGINS=https://yourdomain.com,https://preview.yourdomain.com
# Turnstile Verification (optional but recommended)
TURNSTILE_SECRET_KEY=your-secret-key
# Cloudflare Access (if WordPress is behind Cloudflare Access)
VITE_CF_ACCESS_CLIENT_ID=your-client-id
VITE_CF_ACCESS_CLIENT_SECRET=your-client-secretImportant Notes:
ALLOWED_ORIGINSis required for the proxy to work in production. If not set, onlylocalhost:8080andlocalhost:5173are allowed.TURNSTILE_SECRET_KEYis optional. If set, Turnstile verification will be enforced for form submissions.VITE_TURNSTILE_SITE_KEYmust be set in both build-time and runtime environments for Turnstile to work.
Build-Time Data Generation
Setup Generation Scripts
Create scripts to generate static data at build time. These scripts use the /generators export which requires Node.js.
Example: scripts/generateWordPressData.ts
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
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();Integration with Build Process
Add to your package.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"
}
}Generated Files
The generators create static JSON files in your public directory:
public/wordpress-data.json- WordPress posts, pages, media, categories, tags, and custom post typespublic/gravityForms.json- Gravity Forms schemas with fields, notifications, and confirmations
WordPress Data Structure:
- Pages include parsed Gutenberg blocks (
content.rawis parsed and stored asblocks) - Posts use rendered HTML (
content.rendered) - Embedded data (featured media, terms, etc.) is included when
includeEmbedded: true
Runtime Usage
Static Data Access
Load and query static data at runtime (no API calls):
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);React Hooks
Use React hooks for convenient data access:
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...
}React Components
WordPressContent Component
Render WordPress content with automatic HTML sanitization and responsive images:
import { WordPressContent } from '@marvalt/wadapter';
function PostPage({ post }) {
return (
<WordPressContent
content={post.content.rendered}
className="prose prose-lg"
/>
);
}GravityForm Component
Render and handle Gravity Forms with automatic Turnstile integration:
import { GravityForm } from '@marvalt/wadapter';
function ContactForm() {
return (
<GravityForm
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_KEYis set) - Submit button disabled until Turnstile verification completes (if enabled)
TurnstileWidget Component
Standalone Turnstile widget for custom implementations:
import { TurnstileWidget } from '@marvalt/wadapter';
function CustomForm() {
const [token, setToken] = useState<string | null>(null);
return (
<>
<TurnstileWidget
onVerify={(token) => setToken(token)}
onError={() => setToken(null)}
/>
<button disabled={!token}>Submit</button>
</>
);
}API Clients
For programmatic access without React:
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': '[email protected]',
},
});Cloudflare Setup
Cloudflare Pages Function
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.tsIf the file doesn't exist, create it manually:
import { handleGravityFormsProxy } from '@marvalt/wadapter/server';
export const onRequest = handleGravityFormsProxy;How the Proxy Works
┌─────────┐
│ 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
└─────────────────────┘Security Layers
The proxy implements five security layers:
- Origin Validation - Only authorized domains can submit (via
ALLOWED_ORIGINS) - Endpoint Whitelisting - Only
/forms/{id}/submitendpoints allowed - Turnstile Verification - Bot protection (optional, requires
TURNSTILE_SECRET_KEY) - Server-Side Auth - WordPress credentials never exposed to browser
- CF Access Support - Optional additional security layer for WordPress behind Cloudflare Access
Cloudflare Access (Optional)
If your WordPress site is behind Cloudflare Access (Zero Trust), you need to:
- Create a Cloudflare Access Service Token
- Set
VITE_CF_ACCESS_CLIENT_IDandVITE_CF_ACCESS_CLIENT_SECRETin 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
Authentication Modes
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/v1endpoints (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/v1endpoints
Note: Most implementations use direct mode with Cloudflare Pages Functions, which provides the same security benefits without requiring a separate Worker.
Best Practices
- Never expose credentials to the browser: All WordPress credentials should be in
.env.local(not committed) or Cloudflare Pages environment variables - Always use the proxy for form submissions: Never call WordPress API directly from the browser
- Enable Turnstile: Set
VITE_TURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEYfor bot protection - Set ALLOWED_ORIGINS: Required for production deployments
- Use HTTPS: Always use HTTPS in production
API Reference
Generators (@marvalt/wadapter/generators)
generateWordPressData(config)
Generates static WordPress data.
Parameters:
config.apiUrl(string, required) - WordPress API URLconfig.username(string, required) - WordPress usernameconfig.password(string, required) - WordPress app passwordconfig.outputPath(string, required) - Output file pathconfig.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 IDconfig.cfAccessClientSecret(string, optional) - CF Access client secret
Returns: Promise<WordPressStaticData>
generateGravityFormsData(config)
Generates static Gravity Forms data.
Parameters:
config.apiUrl(string, required) - Gravity Forms API URL (should usegf-api/v1)config.username(string, required) - WordPress usernameconfig.password(string, required) - WordPress app passwordconfig.outputPath(string, required) - Output file pathconfig.useCustomEndpoint(boolean, optional) - Use customgf-api/v1endpoint (default:true)config.includeInactive(boolean, optional) - Include inactive forms (default:false)config.cfAccessClientId(string, optional) - CF Access client IDconfig.cfAccessClientSecret(string, optional) - CF Access client secret
Returns: Promise<GravityFormsStaticBundle>
Server Functions (@marvalt/wadapter/server)
handleGravityFormsProxy(context)
Cloudflare Pages Function handler for Gravity Forms proxy.
Usage:
import { handleGravityFormsProxy } from '@marvalt/wadapter/server';
export const onRequest = handleGravityFormsProxy;Required Environment Variables:
VITE_WORDPRESS_API_URLorWORDPRESS_API_URLVITE_WP_API_USERNAMEorWP_API_USERNAMEVITE_WP_APP_PASSWORDorWP_APP_PASSWORDALLOWED_ORIGINS(required for production)
Optional Environment Variables:
TURNSTILE_SECRET_KEYorVITE_TURNSTILE_SECRET_KEYVITE_CF_ACCESS_CLIENT_IDorCF_ACCESS_CLIENT_IDVITE_CF_ACCESS_CLIENT_SECRETorCF_ACCESS_CLIENT_SECRET
React Components
<GravityForm />
Props:
formId(number, required) - Gravity Forms form IDconfig(GravityFormsConfig, required) - Configuration objectclassName(string, optional) - Additional CSS classesonSubmit(function, optional) - Success callbackonError(function, optional) - Error callback
<WordPressContent />
Props:
content(string, required) - WordPress HTML contentclassName(string, optional) - Additional CSS classes
<TurnstileWidget />
Props:
onVerify(function, required) - Called with token when verifiedonError(function, optional) - Called on verification error
React Hooks
useWordPress()
Returns WordPress static data.
Returns:
{
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 IDconfig(GravityFormsConfig, required) - Configuration
Returns:
{
form: GravityForm | null;
loading: boolean;
error: Error | null;
submitting: boolean;
result: any | null;
submitForm: (data: GravityFormSubmission) => Promise<any>;
}Examples
Complete Setup Example
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
Basic Form Implementation
import { GravityForm } from '@marvalt/wadapter';
function ContactPage() {
return (
<div>
<h1>Contact Us</h1>
<GravityForm
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);
}}
/>
</div>
);
}Custom Form with Manual Submission
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<string, string> = {};
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 <div>Loading form...</div>;
if (!form) return <div>Form not found</div>;
return (
<form onSubmit={handleSubmit}>
{/* Render form fields based on form.fields */}
<button type="submit">Submit</button>
</form>
);
}Troubleshooting
Build Errors
Error: Module "fs" has been externalized for browser compatibility
Solution: You're importing generators from the main package. Use the /generators export instead:
// ❌ Wrong
import { generateWordPressData } from '@marvalt/wadapter';
// ✅ Correct
import { generateWordPressData } from '@marvalt/wadapter/generators';Form Submission Errors
Error: 403 Forbidden
Possible causes:
ALLOWED_ORIGINSnot set in Cloudflare Pages environment- Turnstile token missing or invalid (if Turnstile is enabled)
- Endpoint pattern mismatch
Solutions:
- Set
ALLOWED_ORIGINSin Cloudflare Pages dashboard:Settings → Environment Variables - Verify
VITE_TURNSTILE_SITE_KEYis set and Turnstile widget is rendering - Check that form ID matches the endpoint pattern
Error: 404 Not Found
Possible causes:
- Gravity Forms API Endpoint plugin not installed/activated
- Incorrect API URL
Solutions:
- Verify plugin is installed: Visit
https://your-site.com/wp-json/gf-api/v1/health - Check
VITE_WORDPRESS_API_URLis correct - Verify WordPress REST API is enabled (Settings → Permalinks)
Data Generation Errors
Error: 401 Unauthorized
Possible causes:
- Incorrect WordPress credentials
- Cloudflare Access credentials missing (if WordPress is behind CF Access)
Solutions:
- Verify
VITE_WP_API_USERNAMEandVITE_WP_APP_PASSWORDare correct - If using Cloudflare Access, set
VITE_CF_ACCESS_CLIENT_IDandVITE_CF_ACCESS_CLIENT_SECRET
Error: Empty or incomplete data
Possible causes:
includeEmbedded: false(should always betruefor full functionality)maxItemstoo low- Post types not enabled
Solutions:
- Always set
includeEmbedded: truein generator config - Increase
VITE_DEFAULT_MAX_ITEMSif needed - Verify
VITE_ENABLED_POST_TYPESincludes all needed post types
Turnstile Issues
Widget not rendering
Possible causes:
VITE_TURNSTILE_SITE_KEYnot set- Turnstile script not loaded
Solutions:
- Set
VITE_TURNSTILE_SITE_KEYin environment variables - The widget automatically loads the Turnstile script - verify no CSP blocks it
Verification always fails
Possible causes:
TURNSTILE_SECRET_KEYincorrect- Token expired (tokens expire after 5 minutes)
Solutions:
- Verify
TURNSTILE_SECRET_KEYmatches your Cloudflare Turnstile configuration - Ensure form is submitted within 5 minutes of Turnstile verification
License
GPL-3.0-or-later
Need help? Check the implementation example in bnibrilliance-website or review the source code in the src directory.
