npm.io
2.3.60 • Published 2d ago

@marvalt/wadapter

Licence
GPL-3.0-or-later
Version
2.3.60
Deps
1
Size
790 kB
Vulns
0
Weekly
0
Install scriptsThis package runs scripts during installation (preinstall/install/postinstall)

@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.

Documentation

Full handover docs ship with this package:

  • In repo: docs/
  • After install: node_modules/@marvalt/wadapter/docs/README.md

Start with docs/README.md (environment variables, integration guide, API reference).

Table of Contents

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:

  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

Installation

npm install @marvalt/wadapter
Peer 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
  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:
      # 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 type
Cloudflare 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-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

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 types
  • public/gravityForms.json - Gravity Forms schemas with fields, notifications, and confirmations

WordPress 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: 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_KEY is 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': 'john@example.com',
  },
});

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.ts

If 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:

  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
Cloudflare Access (Optional)

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

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/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 endpoints

Note: Most implementations use direct mode with Cloudflare Pages Functions, which provides the same security benefits without requiring a separate Worker.

Best Practices
  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 production

API Reference

Generators (@marvalt/wadapter/generators)
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 secret

Returns: Promise<WordPressStaticData>

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 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_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
React Components
<GravityForm />

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
<WordPressContent />

Props:

  • content (string, required) - WordPress HTML content
  • className (string, optional) - Additional CSS classes
<TurnstileWidget />

Props:

  • onVerify (function, required) - Called with token when verified
  • onError (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 ID
  • config (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:

  1. ALLOWED_ORIGINS not set in Cloudflare Pages environment
  2. Turnstile token missing or invalid (if Turnstile is enabled)
  3. Endpoint pattern mismatch

Solutions:

  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 pattern

Error: 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)
Data Generation Errors

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_SECRET

Error: Empty or incomplete data

Possible causes:

  1. includeEmbedded: false (should always be true for full functionality)
  2. maxItems too low
  3. Post types not enabled

Solutions:

  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
Turnstile Issues

Widget not rendering

Possible causes:

  1. VITE_TURNSTILE_SITE_KEY not set
  2. Turnstile script not loaded

Solutions:

  1. Set VITE_TURNSTILE_SITE_KEY in environment variables
  2. The widget automatically loads the Turnstile script - verify no CSP blocks it

Verification 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 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.

Keywords