@marvalt/wadapter 2.3.18 → 2.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,118 +1,802 @@
1
- # marvalt-wadapter
1
+ # @marvalt/wadapter
2
2
 
3
- Static-first WordPress and Gravity Forms integration for React apps.
3
+ 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.
4
4
 
5
- ## What it provides
5
+ ## Table of Contents
6
6
 
7
- - WordPress and Gravity Forms static-data generators (build-time)
8
- - Static data loaders and selectors (runtime reads from JSON only)
9
- - Gravity Forms submission client with two modes: `cloudflare_proxy` and `direct`
7
+ - [Overview](#overview)
8
+ - [Installation](#installation)
9
+ - [WordPress Plugin Requirements](#wordpress-plugin-requirements)
10
+ - [Environment Configuration](#environment-configuration)
11
+ - [Build-Time Data Generation](#build-time-data-generation)
12
+ - [Runtime Usage](#runtime-usage)
13
+ - [Cloudflare Setup](#cloudflare-setup)
14
+ - [Security](#security)
15
+ - [API Reference](#api-reference)
16
+ - [Examples](#examples)
17
+ - [Troubleshooting](#troubleshooting)
10
18
 
11
- ## Modes
19
+ ## Overview
12
20
 
13
- - `cloudflare_proxy`:
14
- - Frontend calls a Cloudflare Worker with `?endpoint=/wp-json/...`.
15
- - Worker injects Cloudflare Access credentials from its environment and forwards requests.
16
- - Gravity Forms submissions go to enhanced `gf-api/v1` endpoints via the worker.
21
+ `@marvalt/wadapter` provides a complete solution for integrating WordPress and Gravity Forms into React applications with a static-first architecture:
17
22
 
18
- - `direct`:
19
- - Frontend calls your backend directly (not behind Zero Trust).
20
- - WordPress uses `/wp-json/wp/v2/...`.
21
- - Gravity Forms uses official REST API v2 for submissions.
23
+ - **Build-time generators**: Fetch WordPress content and Gravity Forms schemas at build time, outputting static JSON files
24
+ - **Runtime static access**: Read-only access to pre-generated static data (no runtime API calls)
25
+ - **Secure form submissions**: Server-side proxy with multiple security layers including Turnstile bot protection
26
+ - **React components**: Ready-to-use components for rendering WordPress content and Gravity Forms
27
+ - **TypeScript support**: Full type definitions for all data structures
22
28
 
23
- ## Install
29
+ ### Package Structure
30
+
31
+ The package has three distinct entry points to separate browser-safe code from Node.js-only code:
32
+
33
+ 1. **`@marvalt/wadapter`** (Main browser bundle)
34
+ - React components (`GravityForm`, `WordPressContent`, `TurnstileWidget`)
35
+ - React hooks (`useWordPress`, `useGravityForms`)
36
+ - API clients (`WordPressClient`, `GravityFormsClient`)
37
+ - Static data loaders and selectors
38
+ - Types and utilities
39
+ - **No Node.js dependencies** (browser-safe)
40
+
41
+ 2. **`@marvalt/wadapter/generators`** (Node.js-only bundle)
42
+ - `generateWordPressData()` - Build-time WordPress data generation
43
+ - `generateGravityFormsData()` - Build-time Gravity Forms schema generation
44
+ - `generateFormProtectionData()` - Build-time form protection data
45
+ - Uses `fs`, `path`, and other Node.js modules
46
+ - **Only for build scripts and SSG**
47
+
48
+ 3. **`@marvalt/wadapter/server`** (Cloudflare Pages Functions)
49
+ - `handleGravityFormsProxy()` - Server-side proxy handler
50
+ - `verifyTurnstile()` - Server-side Turnstile verification
51
+ - **Only for serverless functions**
52
+
53
+ ## Installation
24
54
 
25
55
  ```bash
26
56
  npm install @marvalt/wadapter
27
57
  ```
28
58
 
29
- ## Configure
59
+ ### Peer Dependencies
60
+
61
+ The package requires React (16.8.0+) and React DOM (16.8.0+). These should already be installed in your React project.
62
+
63
+ ## WordPress Plugin Requirements
64
+
65
+ ### Required Plugins
66
+
67
+ 1. **Gravity Forms** (Premium or Basic)
68
+ - Must be installed and activated
69
+ - Required for form functionality
70
+
71
+ 2. **Gravity Forms API Endpoint** (Custom Plugin)
72
+ - **Required** for proper form submissions with notification triggering
73
+ - Provides enhanced REST API endpoints at `/wp-json/gf-api/v1/`
74
+ - Solves the critical issue where standard Gravity Forms REST API submissions don't trigger email notifications
75
+ - Installation:
76
+ ```bash
77
+ # Clone or download the plugin
78
+ git clone https://github.com/ViBuNe-Pty-Ltd/gravity-forms-api-endpoint.git wp-content/plugins/gravity-forms-api-endpoint
79
+ ```
80
+ - Activate the plugin in WordPress admin: `Plugins → Installed Plugins`
81
+ - Verify installation: Visit `https://your-wordpress-site.com/wp-json/gf-api/v1/health`
30
82
 
31
- Required environment variables:
83
+ ### Why the Custom Plugin is Required
84
+
85
+ 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:
86
+ - ✅ Creates entries AND triggers notifications
87
+ - ✅ Fires all Gravity Forms hooks
88
+ - ✅ Returns proper confirmation messages
89
+ - ✅ Integrates with webhook automation for static data regeneration
90
+
91
+ ## Environment Configuration
92
+
93
+ ### Build-Time Variables (`.env` or `.env.local`)
94
+
95
+ 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):
32
96
 
33
97
  ```env
34
- # Mode
35
- VITE_AUTH_MODE=cloudflare_proxy|direct
98
+ # Authentication Mode
99
+ VITE_AUTH_MODE=direct # or 'cloudflare_proxy'
36
100
 
37
- # WordPress (non-secret)
101
+ # WordPress Configuration
38
102
  VITE_WORDPRESS_API_URL=https://your-wordpress-site.com
39
- VITE_CLOUDFLARE_WORKER_URL=https://your-worker.your-subdomain.workers.dev # when cloudflare_proxy
40
103
 
41
- # Direct mode credentials (server-side build/generator only; do not ship to browser)
104
+ # WordPress Credentials (for build-time generation only - never exposed to browser)
105
+ VITE_WP_API_USERNAME=your-username
106
+ VITE_WP_APP_PASSWORD=your-app-password
107
+
108
+ # Gravity Forms Configuration
109
+ VITE_GRAVITY_FORMS_API_URL=https://your-wordpress-site.com/wp-json/gf-api/v1
110
+ # Or leave empty to auto-construct from VITE_WORDPRESS_API_URL
111
+
112
+ # Cloudflare Access (if WordPress is behind Cloudflare Access)
113
+ VITE_CF_ACCESS_CLIENT_ID=your-client-id
114
+ VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret
115
+
116
+ # Turnstile Bot Protection (optional)
117
+ VITE_TURNSTILE_SITE_KEY=your-site-key
118
+
119
+ # WordPress Data Generation Options
120
+ VITE_ENABLED_POST_TYPES=posts,pages,chapter_member # Comma-separated list
121
+ VITE_DEFAULT_MAX_ITEMS=100 # Max items per post type
122
+ ```
123
+
124
+ ### Cloudflare Pages Environment Variables
125
+
126
+ These are set in the Cloudflare Pages dashboard (Settings → Environment Variables) and are **server-side only**:
127
+
128
+ ```env
129
+ # WordPress Credentials (for Pages Function proxy)
130
+ VITE_WORDPRESS_API_URL=https://your-wordpress-site.com
42
131
  VITE_WP_API_USERNAME=your-username
43
- VITE_WP_API_APP_PASSWORD=your-app-password
132
+ VITE_WP_APP_PASSWORD=your-app-password
133
+
134
+ # Origin Validation (REQUIRED for production)
135
+ ALLOWED_ORIGINS=https://yourdomain.com,https://preview.yourdomain.com
44
136
 
45
- # Gravity Forms
46
- VITE_GRAVITY_FORMS_API_URL=https://your-wordpress-site.com # base site URL
47
- VITE_GRAVITY_FORMS_USERNAME=your-username # when direct
48
- VITE_GRAVITY_FORMS_PASSWORD=your-app-password # when direct
137
+ # Turnstile Verification (optional but recommended)
138
+ TURNSTILE_SECRET_KEY=your-secret-key
49
139
 
50
- # No client app headers are required; the Worker handles authentication
140
+ # Cloudflare Access (if WordPress is behind Cloudflare Access)
141
+ VITE_CF_ACCESS_CLIENT_ID=your-client-id
142
+ VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret
51
143
  ```
52
144
 
53
- ## Build-time generation
145
+ **Important Notes:**
146
+ - `ALLOWED_ORIGINS` is **required** for the proxy to work in production. If not set, only `localhost:8080` and `localhost:5173` are allowed.
147
+ - `TURNSTILE_SECRET_KEY` is optional. If set, Turnstile verification will be enforced for form submissions.
148
+ - `VITE_TURNSTILE_SITE_KEY` must be set in both build-time and runtime environments for Turnstile to work.
54
149
 
55
- The generators fetch WordPress and Gravity Forms data and write static JSON:
150
+ ## Build-Time Data Generation
56
151
 
57
- - WordPress JSON: `public/wordpress-data.json`
58
- - Gravity Forms JSON: `public/gravityForms.json`
152
+ ### Setup Generation Scripts
59
153
 
60
- You can wire them into npm scripts using your preferred runner. Example entry points are included in the docs.
154
+ Create scripts to generate static data at build time. These scripts use the `/generators` export which requires Node.js.
61
155
 
62
- ### Pages now include Gutenberg blocks
156
+ **Example: `scripts/generateWordPressData.ts`**
63
157
 
64
- - Pages are fetched with `context=edit` so `content.raw` is available.
65
- - We parse `content.raw` using `@wordpress/block-serialization-default-parser` and persist `pages[n].blocks`.
66
- - Posts remain unchanged (HTML `content.rendered`).
158
+ ```typescript
159
+ import dotenv from 'dotenv';
160
+ import { generateWordPressData } from '@marvalt/wadapter/generators';
67
161
 
68
- Proxy mode note: the Cloudflare worker can inject Basic Auth for `context=edit` if `WP_API_USERNAME` and `WP_API_APP_PASSWORD` are set in the worker env.
162
+ dotenv.config({ path: '.env' });
163
+ dotenv.config({ path: '.env.local' });
69
164
 
70
- ## Runtime access (static-only)
165
+ async function run() {
166
+ const config = {
167
+ authMode: 'direct' as const,
168
+ apiUrl: process.env.VITE_WORDPRESS_API_URL,
169
+ username: process.env.VITE_WP_API_USERNAME,
170
+ password: process.env.VITE_WP_APP_PASSWORD,
171
+ cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
172
+ cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
173
+ };
71
174
 
72
- - Loaders
73
- - `loadWordPressData(path = '/wordpress-data.json')`
74
- - `loadGravityFormsData(path = '/gravityForms.json')`
175
+ if (!config.apiUrl || !config.username || !config.password) {
176
+ console.error('❌ Missing required environment variables');
177
+ process.exit(1);
178
+ }
75
179
 
76
- - Selectors
77
- - WordPress: `getWordPressPosts`, `getWordPressPages`, `getWordPressMedia`, `getWordPressCategories`, `getWordPressTags`
78
- - Gravity Forms: `getActiveForms`, `getPublishedForms`, `getFormById`
180
+ const postTypes = process.env.VITE_ENABLED_POST_TYPES
181
+ ? process.env.VITE_ENABLED_POST_TYPES.split(',').map(t => t.trim())
182
+ : ['posts', 'pages'];
79
183
 
80
- Note: These selectors read only from static JSON. They do not fetch live content at runtime.
184
+ const maxItems = parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '100', 10);
81
185
 
82
- ## Exports and types
186
+ await generateWordPressData({
187
+ ...config,
188
+ outputPath: './public/wordpress-data.json',
189
+ postTypes,
190
+ includeEmbedded: true, // Always true for full functionality
191
+ maxItems,
192
+ });
83
193
 
84
- - Generators
85
- - `WordPressGenerator`, `generateWordPressData`
86
- - `GravityFormsGenerator`, `generateGravityFormsData`
87
- - Generator output type for GF: `GravityFormsStaticBundle`
194
+ console.log('✅ WordPress data generation completed');
195
+ }
88
196
 
89
- - Static runtime selectors
90
- - WordPress: `loadWordPressData`, `getWordPressPosts`, `getWordPressPages`, `getWordPressMedia`, `getWordPressCategories`, `getWordPressTags`
91
- - Gravity Forms: `loadGravityFormsData`, `hasGravityFormsStatic`, `getActiveForms`, `getPublishedForms`, `getFormById`
197
+ run();
198
+ ```
92
199
 
93
- > Note: The generator bundle type was renamed to avoid conflicts with runtime static selectors.
200
+ **Example: `scripts/generateGravityFormsData.ts`**
201
+
202
+ ```typescript
203
+ import dotenv from 'dotenv';
204
+ import { generateGravityFormsData } from '@marvalt/wadapter/generators';
205
+
206
+ dotenv.config({ path: '.env' });
207
+ dotenv.config({ path: '.env.local' });
208
+
209
+ async function run() {
210
+ const config = {
211
+ authMode: 'direct' as const,
212
+ apiUrl: process.env.VITE_GRAVITY_FORMS_API_URL ||
213
+ `${process.env.VITE_WORDPRESS_API_URL}/wp-json/gf-api/v1`,
214
+ username: process.env.VITE_WP_API_USERNAME || '',
215
+ password: process.env.VITE_WP_APP_PASSWORD || '',
216
+ useCustomEndpoint: true, // Force use of gf-api/v1
217
+ cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
218
+ cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
219
+ };
220
+
221
+ if (!config.apiUrl || !config.username || !config.password) {
222
+ console.error('❌ Missing required environment variables');
223
+ process.exit(1);
224
+ }
225
+
226
+ await generateGravityFormsData({
227
+ ...config,
228
+ outputPath: './public/gravityForms.json',
229
+ includeInactive: false,
230
+ });
231
+
232
+ console.log('✅ Gravity Forms data generation completed');
233
+ }
234
+
235
+ run();
236
+ ```
94
237
 
95
- ## Form submissions
238
+ ### Integration with Build Process
96
239
 
97
- Use the package Gravity Forms client. It routes per mode:
240
+ Add to your `package.json`:
98
241
 
99
- - `cloudflare_proxy`: worker `?endpoint=/wp-json/gf-api/v1/...`
100
- - `direct`: official Gravity Forms REST v2 endpoints
242
+ ```json
243
+ {
244
+ "scripts": {
245
+ "generate:wp": "tsx scripts/generateWordPressData.ts",
246
+ "generate:gf": "tsx scripts/generateGravityFormsData.ts",
247
+ "generate:all": "npm run generate:wp && npm run generate:gf",
248
+ "build": "npm run generate:all && vite build"
249
+ }
250
+ }
251
+ ```
101
252
 
102
- ## License
253
+ ### Generated Files
254
+
255
+ The generators create static JSON files in your `public` directory:
256
+
257
+ - `public/wordpress-data.json` - WordPress posts, pages, media, categories, tags, and custom post types
258
+ - `public/gravityForms.json` - Gravity Forms schemas with fields, notifications, and confirmations
259
+
260
+ **WordPress Data Structure:**
261
+ - Pages include parsed Gutenberg blocks (`content.raw` is parsed and stored as `blocks`)
262
+ - Posts use rendered HTML (`content.rendered`)
263
+ - Embedded data (featured media, terms, etc.) is included when `includeEmbedded: true`
264
+
265
+ ## Runtime Usage
266
+
267
+ ### Static Data Access
268
+
269
+ Load and query static data at runtime (no API calls):
270
+
271
+ ```typescript
272
+ import {
273
+ loadWordPressData,
274
+ getWordPressPosts,
275
+ getWordPressPages,
276
+ getWordPressMedia
277
+ } from '@marvalt/wadapter';
278
+
279
+ import {
280
+ loadGravityFormsData,
281
+ getActiveForms,
282
+ getFormById
283
+ } from '@marvalt/wadapter';
284
+
285
+ // Load data (typically done once at app startup)
286
+ await loadWordPressData();
287
+ await loadGravityFormsData();
288
+
289
+ // Query data
290
+ const posts = getWordPressPosts();
291
+ const pages = getWordPressPages();
292
+ const media = getWordPressMedia();
293
+ const forms = getActiveForms();
294
+ const form = getFormById(1);
295
+ ```
296
+
297
+ ### React Hooks
298
+
299
+ Use React hooks for convenient data access:
300
+
301
+ ```typescript
302
+ import { useWordPress, useGravityForms } from '@marvalt/wadapter';
303
+
304
+ function MyComponent() {
305
+ const { posts, pages, media, loading } = useWordPress();
306
+ const { form, loading: formLoading, submitForm } = useGravityForms(1, {
307
+ apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
308
+ authMode: 'direct',
309
+ });
310
+
311
+ // Use the data...
312
+ }
313
+ ```
314
+
315
+ ### React Components
316
+
317
+ #### WordPressContent Component
318
+
319
+ Render WordPress content with automatic HTML sanitization and responsive images:
320
+
321
+ ```typescript
322
+ import { WordPressContent } from '@marvalt/wadapter';
323
+
324
+ function PostPage({ post }) {
325
+ return (
326
+ <WordPressContent
327
+ content={post.content.rendered}
328
+ className="prose prose-lg"
329
+ />
330
+ );
331
+ }
332
+ ```
333
+
334
+ #### GravityForm Component
335
+
336
+ Render and handle Gravity Forms with automatic Turnstile integration:
337
+
338
+ ```typescript
339
+ import { GravityForm } from '@marvalt/wadapter';
340
+
341
+ function ContactForm() {
342
+ return (
343
+ <GravityForm
344
+ formId={1}
345
+ config={{
346
+ apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
347
+ authMode: 'direct', // Uses Cloudflare Pages Function proxy
348
+ }}
349
+ onSubmit={(result) => {
350
+ console.log('Success:', result);
351
+ // result.confirmation contains confirmation message
352
+ }}
353
+ onError={(error) => {
354
+ console.error('Error:', error);
355
+ }}
356
+ />
357
+ );
358
+ }
359
+ ```
360
+
361
+ **Features:**
362
+ - Automatic field rendering based on form schema
363
+ - Client-side validation
364
+ - Conditional logic support
365
+ - Turnstile bot protection (automatic if `VITE_TURNSTILE_SITE_KEY` is set)
366
+ - Submit button disabled until Turnstile verification completes (if enabled)
367
+
368
+ #### TurnstileWidget Component
369
+
370
+ Standalone Turnstile widget for custom implementations:
371
+
372
+ ```typescript
373
+ import { TurnstileWidget } from '@marvalt/wadapter';
374
+
375
+ function CustomForm() {
376
+ const [token, setToken] = useState<string | null>(null);
377
+
378
+ return (
379
+ <>
380
+ <TurnstileWidget
381
+ onVerify={(token) => setToken(token)}
382
+ onError={() => setToken(null)}
383
+ />
384
+ <button disabled={!token}>Submit</button>
385
+ </>
386
+ );
387
+ }
388
+ ```
389
+
390
+ ### API Clients
391
+
392
+ For programmatic access without React:
393
+
394
+ ```typescript
395
+ import { GravityFormsClient } from '@marvalt/wadapter';
396
+
397
+ const client = new GravityFormsClient({
398
+ apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
399
+ authMode: 'direct',
400
+ });
401
+
402
+ const result = await client.submitForm({
403
+ form_id: 1,
404
+ field_values: {
405
+ '1': 'John Doe',
406
+ '2': 'john@example.com',
407
+ },
408
+ });
409
+ ```
410
+
411
+ ## Cloudflare Setup
412
+
413
+ ### Cloudflare Pages Function
414
+
415
+ The package automatically installs a Cloudflare Pages Function template during `npm install` via a postinstall script. The function is located at:
416
+
417
+ ```
418
+ functions/api/gravity-forms-submit.ts
419
+ ```
420
+
421
+ **If the file doesn't exist**, create it manually:
422
+
423
+ ```typescript
424
+ import { handleGravityFormsProxy } from '@marvalt/wadapter/server';
425
+
426
+ export const onRequest = handleGravityFormsProxy;
427
+ ```
428
+
429
+ ### How the Proxy Works
430
+
431
+ ```
432
+ ┌─────────┐
433
+ │ Browser │ (Untrusted)
434
+ └────┬────┘
435
+ │ HTTPS + Turnstile Token (optional)
436
+
437
+ ┌─────────────────────┐
438
+ │ Pages Function │ ← Security Checkpoint
439
+ │ /api/gf-submit │ • Origin validation
440
+ │ │ • Turnstile verification (optional)
441
+ │ │ • Endpoint whitelist
442
+ │ │ • Basic Auth injection
443
+ │ │ • CF Access injection (optional)
444
+ └────┬────────────────┘
445
+ │ Authenticated Request
446
+
447
+ ┌─────────────────────┐
448
+ │ WordPress │ (Trusted)
449
+ │ /wp-json/gf-api/v1 │ • No auth needed from client
450
+ │ (Custom Plugin) │ • Processes submission
451
+ │ │ • Triggers notifications
452
+ │ │ • Returns confirmation
453
+ └─────────────────────┘
454
+ ```
455
+
456
+ ### Security Layers
457
+
458
+ The proxy implements five security layers:
459
+
460
+ 1. **Origin Validation** - Only authorized domains can submit (via `ALLOWED_ORIGINS`)
461
+ 2. **Endpoint Whitelisting** - Only `/forms/{id}/submit` endpoints allowed
462
+ 3. **Turnstile Verification** - Bot protection (optional, requires `TURNSTILE_SECRET_KEY`)
463
+ 4. **Server-Side Auth** - WordPress credentials never exposed to browser
464
+ 5. **CF Access Support** - Optional additional security layer for WordPress behind Cloudflare Access
465
+
466
+ ### Cloudflare Access (Optional)
467
+
468
+ If your WordPress site is behind Cloudflare Access (Zero Trust), you need to:
469
+
470
+ 1. Create a Cloudflare Access Service Token
471
+ 2. Set `VITE_CF_ACCESS_CLIENT_ID` and `VITE_CF_ACCESS_CLIENT_SECRET` in both:
472
+ - Build-time environment (for data generation)
473
+ - Cloudflare Pages environment (for proxy)
474
+
475
+ The proxy will automatically inject CF Access headers when these credentials are present.
476
+
477
+ ## Security
478
+
479
+ ### Authentication Modes
480
+
481
+ #### Direct Mode (`VITE_AUTH_MODE=direct`)
482
+
483
+ - **Build-time**: Uses WordPress Basic Auth directly (credentials in `.env.local`)
484
+ - **Runtime**: Uses Cloudflare Pages Function proxy (credentials in Cloudflare Pages environment)
485
+ - **WordPress**: Uses standard REST API endpoints
486
+ - **Gravity Forms**: Uses custom `gf-api/v1` endpoints (requires custom plugin)
487
+
488
+ #### Cloudflare Proxy Mode (`VITE_AUTH_MODE=cloudflare_proxy`)
489
+
490
+ - **Build-time**: Uses WordPress Basic Auth directly
491
+ - **Runtime**: Uses Cloudflare Worker with `?endpoint=` query parameter
492
+ - **WordPress**: Worker injects CF Access credentials
493
+ - **Gravity Forms**: Worker routes to `gf-api/v1` endpoints
494
+
495
+ **Note**: Most implementations use `direct` mode with Cloudflare Pages Functions, which provides the same security benefits without requiring a separate Worker.
496
+
497
+ ### Best Practices
498
+
499
+ 1. **Never expose credentials to the browser**: All WordPress credentials should be in `.env.local` (not committed) or Cloudflare Pages environment variables
500
+ 2. **Always use the proxy for form submissions**: Never call WordPress API directly from the browser
501
+ 3. **Enable Turnstile**: Set `VITE_TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` for bot protection
502
+ 4. **Set ALLOWED_ORIGINS**: Required for production deployments
503
+ 5. **Use HTTPS**: Always use HTTPS in production
504
+
505
+ ## API Reference
103
506
 
104
- GPL-3.0-or-later.
507
+ ### Generators (`@marvalt/wadapter/generators`)
105
508
 
509
+ #### `generateWordPressData(config)`
106
510
 
511
+ Generates static WordPress data.
107
512
 
513
+ **Parameters:**
514
+ - `config.apiUrl` (string, required) - WordPress API URL
515
+ - `config.username` (string, required) - WordPress username
516
+ - `config.password` (string, required) - WordPress app password
517
+ - `config.outputPath` (string, required) - Output file path
518
+ - `config.postTypes` (string[], optional) - Post types to fetch (default: `['posts', 'pages']`)
519
+ - `config.includeEmbedded` (boolean, optional) - Include embedded data (default: `true`)
520
+ - `config.maxItems` (number, optional) - Max items per post type (default: `100`)
521
+ - `config.cfAccessClientId` (string, optional) - CF Access client ID
522
+ - `config.cfAccessClientSecret` (string, optional) - CF Access client secret
108
523
 
524
+ **Returns:** `Promise<WordPressStaticData>`
109
525
 
526
+ #### `generateGravityFormsData(config)`
110
527
 
528
+ Generates static Gravity Forms data.
111
529
 
530
+ **Parameters:**
531
+ - `config.apiUrl` (string, required) - Gravity Forms API URL (should use `gf-api/v1`)
532
+ - `config.username` (string, required) - WordPress username
533
+ - `config.password` (string, required) - WordPress app password
534
+ - `config.outputPath` (string, required) - Output file path
535
+ - `config.useCustomEndpoint` (boolean, optional) - Use custom `gf-api/v1` endpoint (default: `true`)
536
+ - `config.includeInactive` (boolean, optional) - Include inactive forms (default: `false`)
537
+ - `config.cfAccessClientId` (string, optional) - CF Access client ID
538
+ - `config.cfAccessClientSecret` (string, optional) - CF Access client secret
112
539
 
540
+ **Returns:** `Promise<GravityFormsStaticBundle>`
113
541
 
542
+ ### Server Functions (`@marvalt/wadapter/server`)
543
+
544
+ #### `handleGravityFormsProxy(context)`
545
+
546
+ Cloudflare Pages Function handler for Gravity Forms proxy.
547
+
548
+ **Usage:**
549
+ ```typescript
550
+ import { handleGravityFormsProxy } from '@marvalt/wadapter/server';
551
+
552
+ export const onRequest = handleGravityFormsProxy;
553
+ ```
554
+
555
+ **Required Environment Variables:**
556
+ - `VITE_WORDPRESS_API_URL` or `WORDPRESS_API_URL`
557
+ - `VITE_WP_API_USERNAME` or `WP_API_USERNAME`
558
+ - `VITE_WP_APP_PASSWORD` or `WP_APP_PASSWORD`
559
+ - `ALLOWED_ORIGINS` (required for production)
560
+
561
+ **Optional Environment Variables:**
562
+ - `TURNSTILE_SECRET_KEY` or `VITE_TURNSTILE_SECRET_KEY`
563
+ - `VITE_CF_ACCESS_CLIENT_ID` or `CF_ACCESS_CLIENT_ID`
564
+ - `VITE_CF_ACCESS_CLIENT_SECRET` or `CF_ACCESS_CLIENT_SECRET`
565
+
566
+ ### React Components
567
+
568
+ #### `<GravityForm />`
569
+
570
+ **Props:**
571
+ - `formId` (number, required) - Gravity Forms form ID
572
+ - `config` (GravityFormsConfig, required) - Configuration object
573
+ - `className` (string, optional) - Additional CSS classes
574
+ - `onSubmit` (function, optional) - Success callback
575
+ - `onError` (function, optional) - Error callback
576
+
577
+ #### `<WordPressContent />`
578
+
579
+ **Props:**
580
+ - `content` (string, required) - WordPress HTML content
581
+ - `className` (string, optional) - Additional CSS classes
582
+
583
+ #### `<TurnstileWidget />`
584
+
585
+ **Props:**
586
+ - `onVerify` (function, required) - Called with token when verified
587
+ - `onError` (function, optional) - Called on verification error
588
+
589
+ ### React Hooks
590
+
591
+ #### `useWordPress()`
592
+
593
+ Returns WordPress static data.
594
+
595
+ **Returns:**
596
+ ```typescript
597
+ {
598
+ posts: WordPressPost[];
599
+ pages: WordPressPage[];
600
+ media: WordPressMedia[];
601
+ categories: WordPressCategory[];
602
+ tags: WordPressTag[];
603
+ loading: boolean;
604
+ error: Error | null;
605
+ }
606
+ ```
114
607
 
608
+ #### `useGravityForms(formId, config)`
115
609
 
610
+ Returns Gravity Forms data and submission function.
611
+
612
+ **Parameters:**
613
+ - `formId` (number, required) - Form ID
614
+ - `config` (GravityFormsConfig, required) - Configuration
615
+
616
+ **Returns:**
617
+ ```typescript
618
+ {
619
+ form: GravityForm | null;
620
+ loading: boolean;
621
+ error: Error | null;
622
+ submitting: boolean;
623
+ result: any | null;
624
+ submitForm: (data: GravityFormSubmission) => Promise<any>;
625
+ }
626
+ ```
627
+
628
+ ## Examples
629
+
630
+ ### Complete Setup Example
631
+
632
+ See the `bnibrilliance-website` implementation for a complete, production-ready example:
633
+
634
+ - Build scripts: `scripts/generateWordPressData.ts`, `scripts/generateGravityFormsData.ts`
635
+ - Cloudflare Pages Function: `functions/api/gravity-forms-submit.ts`
636
+ - React components using the package
637
+ - Environment variable configuration
638
+
639
+ ### Basic Form Implementation
640
+
641
+ ```typescript
642
+ import { GravityForm } from '@marvalt/wadapter';
643
+
644
+ function ContactPage() {
645
+ return (
646
+ <div>
647
+ <h1>Contact Us</h1>
648
+ <GravityForm
649
+ formId={1}
650
+ config={{
651
+ apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
652
+ authMode: 'direct',
653
+ }}
654
+ onSubmit={(result) => {
655
+ alert(result.confirmation?.message || 'Thank you!');
656
+ }}
657
+ onError={(error) => {
658
+ alert('Error: ' + error.message);
659
+ }}
660
+ />
661
+ </div>
662
+ );
663
+ }
664
+ ```
665
+
666
+ ### Custom Form with Manual Submission
667
+
668
+ ```typescript
669
+ import { useGravityForms } from '@marvalt/wadapter';
670
+
671
+ function CustomForm() {
672
+ const { form, loading, submitForm } = useGravityForms(1, {
673
+ apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
674
+ authMode: 'direct',
675
+ });
676
+
677
+ const handleSubmit = async (e: React.FormEvent) => {
678
+ e.preventDefault();
679
+ const formData = new FormData(e.target as HTMLFormElement);
680
+
681
+ const fieldValues: Record<string, string> = {};
682
+ formData.forEach((value, key) => {
683
+ fieldValues[key] = value.toString();
684
+ });
685
+
686
+ try {
687
+ const result = await submitForm({
688
+ form_id: 1,
689
+ field_values: fieldValues,
690
+ });
691
+ console.log('Success:', result);
692
+ } catch (error) {
693
+ console.error('Error:', error);
694
+ }
695
+ };
696
+
697
+ if (loading) return <div>Loading form...</div>;
698
+ if (!form) return <div>Form not found</div>;
699
+
700
+ return (
701
+ <form onSubmit={handleSubmit}>
702
+ {/* Render form fields based on form.fields */}
703
+ <button type="submit">Submit</button>
704
+ </form>
705
+ );
706
+ }
707
+ ```
708
+
709
+ ## Troubleshooting
710
+
711
+ ### Build Errors
712
+
713
+ **Error: Module "fs" has been externalized for browser compatibility**
714
+
715
+ **Solution**: You're importing generators from the main package. Use the `/generators` export instead:
716
+
717
+ ```typescript
718
+ // ❌ Wrong
719
+ import { generateWordPressData } from '@marvalt/wadapter';
720
+
721
+ // ✅ Correct
722
+ import { generateWordPressData } from '@marvalt/wadapter/generators';
723
+ ```
724
+
725
+ ### Form Submission Errors
726
+
727
+ **Error: 403 Forbidden**
728
+
729
+ **Possible causes:**
730
+ 1. `ALLOWED_ORIGINS` not set in Cloudflare Pages environment
731
+ 2. Turnstile token missing or invalid (if Turnstile is enabled)
732
+ 3. Endpoint pattern mismatch
733
+
734
+ **Solutions:**
735
+ 1. Set `ALLOWED_ORIGINS` in Cloudflare Pages dashboard: `Settings → Environment Variables`
736
+ 2. Verify `VITE_TURNSTILE_SITE_KEY` is set and Turnstile widget is rendering
737
+ 3. Check that form ID matches the endpoint pattern
738
+
739
+ **Error: 404 Not Found**
740
+
741
+ **Possible causes:**
742
+ 1. Gravity Forms API Endpoint plugin not installed/activated
743
+ 2. Incorrect API URL
744
+
745
+ **Solutions:**
746
+ 1. Verify plugin is installed: Visit `https://your-site.com/wp-json/gf-api/v1/health`
747
+ 2. Check `VITE_WORDPRESS_API_URL` is correct
748
+ 3. Verify WordPress REST API is enabled (Settings → Permalinks)
749
+
750
+ ### Data Generation Errors
751
+
752
+ **Error: 401 Unauthorized**
753
+
754
+ **Possible causes:**
755
+ 1. Incorrect WordPress credentials
756
+ 2. Cloudflare Access credentials missing (if WordPress is behind CF Access)
757
+
758
+ **Solutions:**
759
+ 1. Verify `VITE_WP_API_USERNAME` and `VITE_WP_APP_PASSWORD` are correct
760
+ 2. If using Cloudflare Access, set `VITE_CF_ACCESS_CLIENT_ID` and `VITE_CF_ACCESS_CLIENT_SECRET`
761
+
762
+ **Error: Empty or incomplete data**
763
+
764
+ **Possible causes:**
765
+ 1. `includeEmbedded: false` (should always be `true` for full functionality)
766
+ 2. `maxItems` too low
767
+ 3. Post types not enabled
768
+
769
+ **Solutions:**
770
+ 1. Always set `includeEmbedded: true` in generator config
771
+ 2. Increase `VITE_DEFAULT_MAX_ITEMS` if needed
772
+ 3. Verify `VITE_ENABLED_POST_TYPES` includes all needed post types
773
+
774
+ ### Turnstile Issues
775
+
776
+ **Widget not rendering**
777
+
778
+ **Possible causes:**
779
+ 1. `VITE_TURNSTILE_SITE_KEY` not set
780
+ 2. Turnstile script not loaded
781
+
782
+ **Solutions:**
783
+ 1. Set `VITE_TURNSTILE_SITE_KEY` in environment variables
784
+ 2. The widget automatically loads the Turnstile script - verify no CSP blocks it
785
+
786
+ **Verification always fails**
787
+
788
+ **Possible causes:**
789
+ 1. `TURNSTILE_SECRET_KEY` incorrect
790
+ 2. Token expired (tokens expire after 5 minutes)
791
+
792
+ **Solutions:**
793
+ 1. Verify `TURNSTILE_SECRET_KEY` matches your Cloudflare Turnstile configuration
794
+ 2. Ensure form is submitted within 5 minutes of Turnstile verification
795
+
796
+ ## License
116
797
 
798
+ GPL-3.0-or-later
117
799
 
800
+ ---
118
801
 
802
+ **Need help?** Check the implementation example in `bnibrilliance-website` or review the source code in the `src` directory.