OpenFields is a modern custom fields builder for WordPress. It provides an alternative to ACF (Advanced Custom Fields) with a React-based admin interface and REST API.
The plugin consists of two main parts:
- Backend Plugin (
/plugin) - WordPress PHP plugin that handles field storage, REST API, meta box registration - Frontend Admin (
/admin) - React/TypeScript SPA for managing fieldsets and fields
plugin/
├── openfields.php # Plugin entry point & initialization
├── includes/
│ ├── class-openfields.php # Main plugin class
│ ├── class-openfields-assets.php # Script/style enqueueing
│ ├── class-openfields-rest-api.php # REST API endpoints
│ ├── class-openfields-installer.php # Installation & db setup
│ ├── admin/
│ │ ├── class-openfields-admin.php # Admin page handler
│ │ └── class-openfields-meta-box.php # Meta box registration & rendering
│ ├── api/
│ │ └── functions.php # Public API functions for developers
│ ├── fields/
│ │ ├── class-openfields-field-registry.php # Field type registry
│ │ └── class-openfields-base-field.php # Base field class
│ ├── locations/
│ │ └── class-openfields-location-manager.php # Location rule matching
│ └── storage/
│ └── class-openfields-storage-manager.php # Meta storage abstraction
├── assets/
│ └── admin/
│ ├── css/
│ │ └── admin.css # Admin page styles
│ └── js/
│ └── admin.js # Admin page JS (entrypoint for React app)
└── languages/
└── openfields.pot # i18n translation template
admin/
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Root component
│ ├── api/
│ │ └── index.ts # REST API client
│ ├── types/
│ │ ├── index.ts # Core type definitions
│ │ └── fields.ts # Field type schemas
│ ├── components/
│ │ └── ui/ # Shadcn UI components
│ ├── fields/
│ │ ├── index.ts # Field registry
│ │ ├── TextFieldSettings.tsx # Text field config UI
│ │ ├── SelectFieldSettings.tsx # Select field config UI
│ │ ├── TextareaFieldSettings.tsx # Textarea field config UI
│ │ ├── NumberFieldSettings.tsx # Number field config UI
│ │ └── SwitchFieldSettings.tsx # Switch field config UI
│ ├── pages/
│ │ ├── FieldsetList.tsx # Main fieldset list page
│ │ ├── Tools.tsx # Tools page
│ │ └── FieldsetEditor/
│ │ ├── index.tsx # Fieldset editor page
│ │ └── components/
│ │ ├── FieldsSection.tsx # Fields management
│ │ ├── LocationsSection.tsx # Location rules editor
│ │ ├── SettingsSection.tsx # Fieldset settings
│ │ └── TypeSpecificSettings.tsx # Field-type specific settings
│ ├── stores/
│ │ ├── fieldset-store.ts # Zustand store for fieldsets & fields
│ │ ├── ui-store.ts # UI state (toasts, modals)
│ │ └── index.ts # Store exports
│ ├── lib/
│ │ ├── field-registry.ts # Field type definitions & metadata
│ │ └── utils.ts # Utility functions
│ └── styles/
│ └── main.css # Global styles
├── vite.config.ts # Vite build config
├── tsconfig.json # TypeScript config
└── package.json # Dependencies
- Frontend → User fills fieldset form in React UI
- API Client → Sends POST to
/wp-json/openfields/v1/fieldsets - Backend →
OpenFields_REST_API::create_fieldset()- Validates input
- Creates
wp_openfields_fieldsetsrow - Returns created fieldset with ID
- Store → Updates Zustand store with new fieldset
- UI → Redirects to edit page for new fieldset
- Frontend → User configures location rules in LocationsSection
location_groups = [ { id: '1', rules: [{ type: 'post_type', operator: '==', value: 'page' }] } ]
- API → Sends PUT with
settings: { location_groups: [...] } - Backend →
OpenFields_REST_API::update_fieldset()- Extracts
location_groupsfrom settings - Calls
save_location_groups()to persist to database - Converts frontend format to DB format:
Database: { fieldset_id, param, operator, value, group_id } Frontend: { id, rules: [{ type, operator, value }] }
- Extracts
- Location Manager → Stores individual rows in
wp_openfields_locationstable
- WordPress → Fires
add_meta_boxesaction - Meta Box Class →
OpenFields_Meta_Box::register_meta_boxes()- Gathers context: post_type, post_id, page_template, categories, format
- Calls
OpenFields_Location_Manager::get_fieldsets_for_context()
- Location Manager → Matches fieldsets:
- Queries
wp_openfields_fieldsetsWHEREstatus = 'active' - For each fieldset, fetches location rules from
wp_openfields_locations - Converts DB rows to grouped rules format
- Calls
match()to check if rules match context
- Queries
- Meta Box Class → For matched fieldsets:
- Calls
add_meta_box()to register meta box - Renders field HTML in
render_meta_box()
- Calls
- Gutenberg Block Editor → Shows meta boxes below content
- Classic Editor → Shows meta boxes in sidebar
- WordPress → Fires
save_posthook - Meta Box Class →
OpenFields_Meta_Box::save_post()- Iterates over POSTed field data
- Calls
update_field()for each field
- Storage Manager →
update_field()- Stores value in appropriate meta table:
- Posts:
wp_postmeta(key:of_FIELDNAME) - Users:
wp_usermeta - Terms:
wp_termmeta
- Posts:
- Stores value in appropriate meta table:
- WordPress → Meta value persisted
CREATE TABLE wp_openfields_fieldsets (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255),
field_key VARCHAR(255) UNIQUE,
description LONGTEXT,
status VARCHAR(20) DEFAULT 'active', -- 'active' or 'inactive'
custom_css LONGTEXT,
settings LONGTEXT, -- JSON: { location_groups: [...], position: 'normal', priority: 'high' }
menu_order INT DEFAULT 0,
created_at DATETIME,
updated_at DATETIME
);CREATE TABLE wp_openfields_fields (
id INT PRIMARY KEY AUTO_INCREMENT,
fieldset_id INT,
label VARCHAR(255),
name VARCHAR(255),
type VARCHAR(50), -- 'text', 'email', 'textarea', 'number', 'select', etc.
settings LONGTEXT, -- JSON field settings
menu_order INT DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
FOREIGN KEY (fieldset_id) REFERENCES wp_openfields_fieldsets(id)
);CREATE TABLE wp_openfields_locations (
id INT PRIMARY KEY AUTO_INCREMENT,
fieldset_id INT,
param VARCHAR(50), -- 'post_type', 'page_template', 'taxonomy', 'user_role', etc.
operator VARCHAR(10), -- '==' or '!='
value VARCHAR(255),
group_id INT DEFAULT 0, -- Groups rules with OR logic; rules in same group are AND
FOREIGN KEY (fieldset_id) REFERENCES wp_openfields_fieldsets(id)
);All endpoints require manage_options capability (admin access).
GET /wp-json/openfields/v1/fieldsets- List all fieldsetsPOST /wp-json/openfields/v1/fieldsets- Create fieldsetGET /wp-json/openfields/v1/fieldsets/{id}- Get single fieldsetPUT /wp-json/openfields/v1/fieldsets/{id}- Update fieldsetDELETE /wp-json/openfields/v1/fieldsets/{id}- Delete fieldsetPOST /wp-json/openfields/v1/fieldsets/{id}/duplicate- Duplicate fieldset
GET /wp-json/openfields/v1/fieldsets/{fieldset_id}/fields- List fields for fieldsetPOST /wp-json/openfields/v1/fieldsets/{fieldset_id}/fields- Create fieldPUT /wp-json/openfields/v1/fields/{id}- Update fieldDELETE /wp-json/openfields/v1/fields/{id}- Delete field
GET /wp-json/openfields/v1/field-types- Get available field typesGET /wp-json/openfields/v1/locations/types- Get location type optionsGET /wp-json/openfields/v1/debug/locations- Debug endpoint (returns all fieldsets & locations)
| Class | File | Responsibility |
|---|---|---|
OpenFields_Plugin |
class-openfields.php |
Main plugin initialization & hooks |
OpenFields_REST_API |
class-openfields-rest-api.php |
REST API route registration & handlers |
OpenFields_Meta_Box |
class-openfields-meta-box.php |
Meta box registration & rendering |
OpenFields_Location_Manager |
class-openfields-location-manager.php |
Location rule matching logic |
OpenFields_Storage_Manager |
class-openfields-storage-manager.php |
Meta value persistence |
OpenFields_Field_Registry |
class-openfields-field-registry.php |
Field type registration |
OpenFields_Assets |
class-openfields-assets.php |
Script & style enqueueing |
OpenFields_Admin |
class-openfields-admin.php |
Admin page menu & routing |
| Module | File | Responsibility |
|---|---|---|
useFieldsetStore |
stores/fieldset-store.ts |
Zustand store: fieldsets, fields, pending changes |
useUIStore |
stores/ui-store.ts |
Toast notifications & modal state |
fieldsetApi |
api/index.ts |
REST client for all endpoints |
FieldsetEditor |
pages/FieldsetEditor/index.tsx |
Main editor page layout |
LocationsSection |
Components section | Location rule builder UI |
FieldsSection |
Components section | Field list & drag-to-reorder |
| Field Settings | fields/*.tsx |
Type-specific configuration UIs |
// Core types in /admin/src/types/index.ts
interface Fieldset {
id: number;
title: string;
field_key: string;
description: string;
is_active: boolean;
settings: {
location_groups: LocationGroup[];
position?: 'normal' | 'side';
priority?: 'high' | 'low';
};
}
interface Field {
id: string | number;
label: string;
name: string;
type: FieldType;
settings: Record<string, any>;
}
interface LocationGroup {
id: string;
rules: LocationRule[];
}
interface LocationRule {
type: string; // 'post_type', 'page_template', 'taxonomy', etc.
operator: '==' | '!=';
value: string;
}
type FieldType = 'text' | 'email' | 'textarea' | 'number' | 'select' | 'switch' | ...;- Backend: Create field class extending
OpenFields_Base_Field - Frontend:
- Add type to
FieldTypeenum in types - Create settings component in
fields/ - Register in
lib/field-registry.ts - Export from
fields/index.ts
- Add type to
- Update
OpenFields_Field_Registry::get_field_types()
- Register in
OpenFields_Location_Manager::register_location_type() - Implement matching callback
- Implement options callback (for UI dropdowns)
- Add to location type options in admin
Extend OpenFields_Storage_Manager to support:
- Custom post types
- Custom meta contexts beyond posts/users/terms
- Custom meta key prefixes
OPENFIELDS_VERSION- Plugin versionOPENFIELDS_PLUGIN_FILE- Plugin main file pathOPENFIELDS_PLUGIN_DIR- Plugin directoryOPENFIELDS_PLUGIN_URL- Plugin URLOPENFIELDS_PLUGIN_BASENAME- Plugin basename
Meta key prefix: of_ (e.g., field name stored as of_name)
cd admin
npm install
npm run build # Production build
npm run dev # Development with HMRnpm run wp-env start # Start Docker containers
npm run wp-env stop # Stop containers
# Run CLI commands
npm run wp-env run cli wp db query "SELECT ..."- Fieldset Caching: Consider caching active fieldsets (currently re-queried per page load)
- Location Matching: Could be optimized with fieldset → post_type index
- Meta Box Rendering: Large numbers of fields benefit from pagination in UI
- REST API: Add pagination for field lists when > 100 fields
- All REST endpoints require
manage_optionscapability - Input sanitization via
sanitize_*functions - Output escaping via
esc_*functions - Nonce verification for meta box saves
- WPNONCE validation for REST requests