'{{post_title}} | {{site_name}}', 'description' => '{{post_excerpt}}', 'image' => '{{featured_image}}', 'twitter_image' => '' ]; public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public array $schemaTypes = [ 'WebSite' => 'Web Site', 'Organization' => 'Organization', 'LocalBusiness' => ' - Local Business', 'TattooParlor' => ' - - Tattoo Shop', 'HealthBusiness' => ' - - Health Business', 'FoodEstablishment' => ' - - Restaurant', 'WebPage' => 'Web Page', 'CollectionPage' => ' - Collection Page', 'FAQPage' => ' - FAQ Page', 'Person' => 'Person', 'CreativeWork' => 'Creative Work', 'DefinedTerm' => ' - Defined Term', 'VisualArtwork' => ' - Visual Artwork', 'Tattoo' => ' - - Tattoo', 'BeforeAfter' => ' - Before & After', 'Product' => 'Product', 'Event' => 'Event', ]; private function __construct() { $this->registerFieldDefinitions(); $this->registerTypeDefinitions(); $this->registerTypeGroups(); do_action(BASE . 'schema_registry_loaded', $this); } /** * Get field definition for a specific field */ public function getFieldDefinition(string $fieldName): ?array { $definitions = $this->getFieldDefinitions(); return $definitions[$fieldName] ?? null; } /** * Get all field definitions */ public function getFieldDefinitions(): array { return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions); } public function getMetaFields(): array { return $this->metaFields; } public function getDefaultMetaValues(): array { return $this->defaultMetaValues; } /** * Get type definition */ public function getTypeDefinition(string $type): ?array { $definitions = $this->getTypeDefinitions(); return $definitions[$type] ?? null; } /** * Get all type definitions */ public function getTypeDefinitions(): array { return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions); } /** * Get all fields for a specific type (with inheritance) */ public function getFieldsForType(string $type): array { $fields = []; $typeDefinition = $this->getTypeDefinition($type); if (!$typeDefinition) { return $fields; } $fields = array_merge($fields, $typeDefinition['fields'] ?? []); // Handle inheritance if (!empty($typeDefinition['extends'])) { $parentFields = $this->getFieldsForType($typeDefinition['extends']); $fields = array_unique(array_merge($parentFields, $fields)); } return $fields; } /** * Get MetaManager configuration for a schema type * This creates the form fields for the selected @type */ public function getMetaConfigForType(string $type): array { $fields = $this->getFieldsForType($type); $config = []; foreach ($fields as $fieldName) { $fieldDef = $this->getFieldDefinition($fieldName); if ($fieldDef) { // Use the field name as the key (this IS the schema property) $config[$fieldName] = $fieldDef; } } return $config; } /** * Get types organized by group for UI display */ public function getTypesByGroup(): array { $types = $this->getTypeDefinitions(); $grouped = []; foreach ($types as $typeName => $config) { $group = $config['group'] ?? 'general'; if (!isset($grouped[$group])) { $grouped[$group] = [ 'label' => $this->typeGroups[$group] ?? ucfirst($group), 'types' => [] ]; } $grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName; } return $grouped; } /** * Register all field definitions * Array key = schema property name */ private function registerFieldDefinitions(): void { $this->fieldDefinitions = [ 'type' => [ 'type' => 'select', 'label' => 'Type', 'options' => array_merge(['' => '-- Content Type'], $this->schemaTypes) ], /************************************************************** META FIELDS **************************************************************/ 'metaTitle' => [ 'type' => 'text', 'label' => 'Meta Title', 'hint' => 'Used in search results and when shared on social media. Leave blank to use default.', 'default' => '{{post_title}} | {{site_name}}' ], 'metaDescription' => [ 'type' => 'textarea', 'label' => 'Meta Description', 'hint' => 'Brief description shown in search results and social previews.', 'default' => '{{post_excerpt}}', 'rows' => 3 ], 'socialPreviewImage' => [ 'type' => 'upload', 'label' => 'Social Preview Image', 'hint' => 'Image shown when shared on social media. Recommended: 1200x630px.', 'transformer' => 'image_url' ], 'twitterImage' => [ 'type' => 'upload', 'label' => 'Twitter Card Image (Optional)', 'hint' => 'Separate image for Twitter. Falls back to main image if empty.', 'transformer' => 'image_url' ], /************************************************************** CORE IDENTITY FIELDS **************************************************************/ 'name' => [ 'type' => 'text', 'label' => 'Name', 'description' => 'The name of the item', 'transformer' => 'text', ], 'alternateName' => [ 'type' => 'repeater', 'label' => 'Alternate Name(s)', 'description' => 'Alternative names or nicknames', 'transformer' => 'text_array', 'fields' => [ 'name' => [ 'type' => 'text', 'label' => 'Name', ] ] ], 'legalName' => [ 'type' => 'text', 'label' => 'Legal Name', 'description' => 'The official legal name', 'transformer' => 'text', ], 'description' => [ 'type' => 'textarea', 'label' => 'Description', 'description' => 'A description of the item', 'transformer' => 'text', ], 'disambiguatingDescription' => [ 'type' => 'textarea', 'label' => 'Disambiguating Description', 'description' => 'Brief clarification to distinguish from similar items', 'transformer' => 'text', ], 'url' => [ 'type' => 'url', 'label' => 'URL', 'description' => 'Website URL', 'transformer' => 'url', ], 'slogan' => [ 'type' => 'text', 'label' => 'Slogan', 'description' => 'A slogan or tagline', 'transformer' => 'text', ], /************************************************************** Before/After **************************************************************/ 'about' => [ 'type' => 'reference', 'label' => 'About (Service/Topic)', 'transformer' => 'reference', ], 'temporalCoverage' => [ 'type' => 'text', 'label' => 'Time Period', 'description' => 'ISO 8601 format: 2024-01-10/2024-09-01', 'transformer' => 'text', ], 'associatedMedia' => [ 'type' => 'repeater', 'label' => 'Associated Media', 'transformer' => 'image_object_array', 'fields' => [ 'image' => ['type' => 'image', 'label' => 'Image'], 'caption' => ['type' => 'text', 'label' => 'Caption'], 'position' => ['type' => 'number', 'label' => 'Position'], ] ], 'additionalProperty' => [ 'type' => 'repeater', 'label' => 'Additional Properties', 'transformer' => 'property_value_array', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Property Name'], 'value' => ['type' => 'text', 'label' => 'Value'], ] ], /************************************************************** IMAGE FIELDS **************************************************************/ 'image' => [ 'type' => 'image', 'label' => 'Image', 'description' => 'Primary image', 'transformer' => 'image_object', ], 'logo' => [ 'type' => 'upload', 'label' => 'Logo', 'transformer' => 'image_object', ], 'photo' => [ 'type' => 'upload', 'label' => 'Photo of Location', 'transformer' => 'image_object', ], /************************************************************** LOCATION & CONTACT FIELDS **************************************************************/ 'location' => [ 'type' => 'location', 'label' => 'Location', 'description' => 'Physical location with address and coordinates', 'transformer' => 'location_complex', // Returns array with 'address' and 'geo' ], 'address' => [ 'type' => 'location', 'label' => 'Address', 'description' => 'Postal address', 'transformer' => 'postal_address', ], 'geo' => [ 'type' => 'group', 'label' => 'Geographic Coordinates', 'description' => 'Latitude and longitude', 'transformer' => 'geo_coordinates', 'fields' => [ 'latitude' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Latitude', ], 'longitude' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Longitude', ] ] ], 'telephone' => [ 'type' => 'text', 'subtype'=> 'tel', 'label' => 'Telephone', 'description' => 'Phone number', 'transformer' => 'text', ], 'faxNumber' => [ 'type' => 'text', 'subtype'=> 'tel', 'label' => 'Fax Number', 'transformer' => 'text', ], 'email' => [ 'type' => 'email', 'label' => 'Email', 'description' => 'Email address', 'transformer' => 'email', ], 'contactPoint' => [ 'type' => 'repeater', 'label' => 'Contact Points', 'description' => 'Additional contact methods', 'transformer' => 'contact_point_array', 'fields' => [ 'contactType' => [ 'type' => 'text', 'label' => 'Contact Type', 'description' => 'e.g., customer service, sales', ], 'telephone' => [ 'type' => 'text', 'label' => 'Phone', ], 'email' => [ 'type' => 'email', 'label' => 'Email', ] ] ], 'potentialAction' => [ 'type' => 'repeater', 'label' => 'Potential Actions', 'fields' => [ 'action' => [ 'type' => 'radio', 'label' => 'Action', 'options' => [ 'searchAction' => 'Search Action', 'communicateAction' => 'Contact Action', 'scheduleAction' => 'Reserve Action', 'applyAction' => 'Estimate Action' ] ], 'name' => [ 'type' => 'text', 'label' => 'Name', ], 'target' => [ 'type' => 'url', 'label' => 'Action URL', ], 'description' => [ 'type' => 'textarea', 'label' => 'Description' ] ], 'default' => [ [ 'action' => 'searchAction', 'target' => get_home_url(null,'/search/?s={query}') ] ], 'transformer' => 'potential_action_array' ], /************************************************************** HOURS & OPERATIONAL FIELDS **************************************************************/ 'openingHours' => [ 'type' => 'group', 'label' => 'Opening Hours', 'description' => 'Business hours specification', 'transformer' => 'opening_hours_specification', 'fields' => [ 'monday' => [ 'type' => 'group', 'label' => 'Monday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'tuesday' => [ 'type' => 'group', 'label' => 'Tuesday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'wednesday' => [ 'type' => 'group', 'label' => 'Wednesday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'thursday' => [ 'type' => 'group', 'label' => 'Thursday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'friday' => [ 'type' => 'group', 'label' => 'Friday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'saturday' => [ 'type' => 'group', 'label' => 'Saturday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], 'sunday' => [ 'type' => 'group', 'label' => 'Sunday', 'fields' => [ 'opens' => [ 'type' => 'time', 'label' => 'Opens' ], 'closes' => [ 'type' => 'time', 'label' => 'Closes' ] ] ], ] ], 'hasPart' => [ 'type' => 'repeater', 'label' => 'Site Navigation', 'description' => 'Main navigation menu items', 'transformer' => 'navigation_array', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Link Text'], 'url' => ['type' => 'url', 'label' => 'URL'], 'description' => ['type' => 'textarea', 'label' => 'Description (optional)'], ] ], 'priceRange' => [ 'type' => 'text', 'label' => 'Price Range', 'description' => 'e.g., $$, $100-$500', 'transformer' => 'text', ], 'currenciesAccepted' => [ 'type' => 'checkbox', 'label' => 'Currencies Accepted', 'options' => [ 'CAD' => 'CAD', 'USD' => 'USD', ], 'transformer' => 'text', ], 'paymentAccepted' => [ 'type' => 'checkbox', 'label' => 'Payment Methods', 'options' => [ 'Cash' => 'Cash', 'Credit Card' => 'Credit Card', 'Debit' => 'Debit', 'Google Pay' => 'Google Pay', 'Apple Pay' => 'Apple Pay', 'PayPal' => 'PayPal', 'Interac' => 'Interac', 'AMEX' => 'AMEX', ], 'transformer' => 'text', ], /************************************************************** ORGANIZATION & BUSINESS FIELDS **************************************************************/ 'foundingDate' => [ 'type' => 'date', 'label' => 'Founding Date', 'description' => 'Date the organization was founded', 'transformer' => 'date', ], 'dissolutionDate' => [ 'type' => 'date', 'label' => 'Dissolution Date', 'description' => 'Date the organization closed', 'transformer' => 'date', ], 'founders' => [ 'type' => 'repeater', 'label' => 'Founders', 'description' => 'Name of founder(s)', 'fields' => [ 'name' => [ 'type' => 'text', 'label' => 'Name', ], 'url' => [ 'type' => 'url', 'label' => 'URL', ] ], 'transformer' => 'founders', ], 'numberOfEmployees' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Number of Employees', 'transformer' => 'number', ], 'taxID' => [ 'type' => 'text', 'label' => 'Tax ID', 'description' => 'Tax identification number', 'transformer' => 'text', ], 'vatID' => [ 'type' => 'text', 'label' => 'VAT ID', 'description' => 'VAT registration number', 'transformer' => 'text', ], 'duns' => [ 'type' => 'text', 'label' => 'D-U-N-S Number', 'description' => 'Dun & Bradstreet number', 'transformer' => 'text', ], /************************************************************** SOCIAL & LINKS **************************************************************/ 'sameAs' => [ 'type' => 'repeater', 'label' => 'Social Media & Links', 'description' => 'URLs to social profiles and related pages', 'transformer' => 'url_array', 'fields' => [ 'url' => [ 'type' => 'url', 'label' => 'URL', ] ] ], /************************************************************** AREA & GEOGRAPHY **************************************************************/ 'areaServed' => [ 'type' => 'repeater', 'label' => 'Area Served', 'description' => 'Geographic areas served', 'transformer' => 'text_array', 'fields' => [ 'name' => [ 'type' => 'text', 'label' => 'Location Name', ], 'url' => [ 'type' => 'url', 'label' => 'Wikipedia Page', ] ] ], 'hasMap' => [ 'type' => 'url', 'label' => 'Map URL', 'description' => 'Link to a map (e.g., Google Maps)', 'transformer' => 'url', ], /************************************************************** AMENITIES & FEATURES **************************************************************/ 'amenityFeature' => [ 'type' => 'checkbox', 'label' => 'Amenity Features', 'description' => 'Available facilities and features', 'transformer' => 'text', 'options' => [ 'Wheelchair Accessible' => 'Wheelchair Accessible', 'Free Parking' => 'Free Parking', 'Private Rooms' => 'Private Rooms', 'Air Conditioning' => 'Air Conditioning', 'WiFi' => 'WiFi', 'Gender Neutral Restroom' => 'Gender Neutral Restroom', 'LGBTQ+ Friendly' => 'LGBTQ+ Friendly', 'Sterilization Room' => 'Sterilization Room', 'Refreshments Available' => 'Refreshments Available', 'Street Level Access' => 'Street Level Access', 'Single Use Needles' => 'Single Use Needles', 'Consultation Room' => 'Consultation Room', 'Aftercare Products Available' => 'Aftercare Products Available', 'Walk-Ins Welcome' => 'Walk-Ins Welcome', 'By Appointment' => 'By Appointment Only', ], ], /************************************************************** LANGUAGES **************************************************************/ 'availableLanguage' => [ 'type' => 'repeater', 'label' => 'Languages Available', 'description' => 'Languages spoken or supported', 'transformer' => 'language_array', 'fields' => [ 'language' => [ 'type' => 'text', 'label' => 'Language', ] ] ], 'knowsLanguage' => [ 'type' => 'repeater', 'label' => 'Languages Known', 'description' => 'Languages the person knows', 'transformer' => 'language_array', 'fields' => [ 'language' => [ 'type' => 'text', 'label' => 'Language', ] ] ], 'inLanguage' => [ 'type' => 'radio', 'label' => 'In Language', 'options' => [ 'en-CA' => 'English, Canadian', 'en-US' => 'English, American', 'fr-CA' => 'French, Canadian' ], 'transformer' => 'text', ], /************************************************************** RATINGS & REVIEWS **************************************************************/ 'aggregateRating' => [ 'type' => 'group', 'label' => 'Aggregate Rating', 'description' => 'Overall rating and review count', 'transformer' => 'aggregate_rating', 'fields' => [ 'ratingValue' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating Value', 'description' => 'Average rating (e.g., 4.5)', ], 'bestRating' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Best Rating', 'default' => 5, 'description' => 'Highest possible rating (e.g., 5)', ], 'worstRating' => [ 'default' => 1, 'type' => 'text', 'subtype' => 'number', 'label' => 'Worst Rating', 'description' => 'Lowest possible rating (e.g., 1)', ], 'ratingCount' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating Count', 'description' => 'Total number of ratings', ], 'reviewCount' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Review Count', 'description' => 'Total number of reviews', ] ] ], /************************************************************** KEYWORDS & CATEGORIZATION **************************************************************/ 'keywords' => [ 'type' => 'repeater', 'label' => 'Keywords', 'description' => 'Keywords or tags', 'transformer' => 'text_array', 'fields' => [ 'keyword' => [ 'type' => 'text', 'label' => 'Keyword', ] ] ], /************************************************************** PERSON FIELDS **************************************************************/ 'givenName' => [ 'type' => 'text', 'label' => 'First Name', 'transformer' => 'text', ], 'familyName' => [ 'type' => 'text', 'label' => 'Last Name', 'transformer' => 'text', ], 'honorificPrefix' => [ 'type' => 'text', 'label' => 'Honorific Prefix', 'description' => 'e.g., Dr., Mr., Ms.', 'transformer' => 'text', ], 'honorificSuffix' => [ 'type' => 'text', 'label' => 'Honorific Suffix', 'description' => 'e.g., PhD, MD', 'transformer' => 'text', ], 'jobTitle' => [ 'type' => 'text', 'label' => 'Job Title', 'transformer' => 'text', ], 'birthDate' => [ 'type' => 'date', 'label' => 'Birth Date', 'description' => 'For public figures', 'transformer' => 'date', ], 'gender' => [ 'type' => 'text', 'label' => 'Gender', 'transformer' => 'text', ], /************************************************************** CREATIVE WORK FIELDS **************************************************************/ 'author' => [ 'type' => 'text', 'label' => 'Author', 'description' => 'Author name or reference', 'transformer' => 'text', ], 'creator' => [ 'type' => 'text', 'label' => 'Creator', 'description' => 'Creator name or reference', 'transformer' => 'text', ], 'dateCreated' => [ 'type' => 'date', 'label' => 'Date Created', 'transformer' => 'date', ], 'datePublished' => [ 'type' => 'date', 'label' => 'Date Published', 'transformer' => 'date', ], 'dateModified' => [ 'type' => 'date', 'label' => 'Date Modified', 'transformer' => 'date', ], /************************************************************** VISUAL ARTWORK FIELDS **************************************************************/ 'artform' => [ 'type' => 'text', 'label' => 'Art Form', 'description' => 'e.g., Painting, Sculpture, Tattoo', 'transformer' => 'text', ], 'artMedium' => [ 'type' => 'text', 'label' => 'Art Medium', 'description' => 'e.g., Oil, Watercolor, Ink', 'transformer' => 'text', ], 'artworkSurface' => [ 'type' => 'text', 'label' => 'Artwork Surface', 'description' => 'e.g., Canvas, Paper, Skin', 'transformer' => 'text', ], 'width' => [ 'type' => 'text', 'label' => 'Width', 'description' => 'Width with unit (e.g., 10cm, 5in)', 'transformer' => 'dimension', ], 'height' => [ 'type' => 'text', 'label' => 'Height', 'description' => 'Height with unit (e.g., 15cm, 8in)', 'transformer' => 'dimension', ], /************************************************************** EVENT FIELDS **************************************************************/ 'startDate' => [ 'type' => 'datetime', 'label' => 'Start Date/Time', 'transformer' => 'datetime', ], 'endDate' => [ 'type' => 'datetime', 'label' => 'End Date/Time', 'transformer' => 'datetime', ], 'eventStatus' => [ 'type' => 'select', 'label' => 'Event Status', 'options' => [ 'https://schema.org/EventScheduled' => 'Scheduled', 'https://schema.org/EventCancelled' => 'Cancelled', 'https://schema.org/EventPostponed' => 'Postponed', 'https://schema.org/EventRescheduled' => 'Rescheduled', ], 'transformer' => 'text', ], 'eventAttendanceMode' => [ 'type' => 'select', 'label' => 'Attendance Mode', 'options' => [ 'https://schema.org/OfflineEventAttendanceMode' => 'In-Person', 'https://schema.org/OnlineEventAttendanceMode' => 'Online', 'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid', ], 'transformer' => 'text', ], /************************************************************** PRODUCT FIELDS **************************************************************/ 'brand' => [ 'type' => 'group', 'label' => 'Brand', 'transformer' => 'brand_object', 'fields' => [ 'type' => [ 'type' => 'select', 'label' => 'Brand Type', 'options' => [ 'text' => 'Text Only', 'organization' => 'Organization/Brand', ] ], 'name' => [ 'type' => 'text', 'label' => 'Brand Name', ], 'url' => [ 'type' => 'url', 'label' => 'Brand Website', 'condition' => [ 'field' => 'type', 'value' => 'organization' ] ], 'logo' => [ 'type' => 'upload', 'label' => 'Brand Logo', 'condition' => [ 'field' => 'type', 'value' => 'organization' ] ], ] ], 'sku' => [ 'type' => 'text', 'label' => 'SKU', 'description' => 'Stock Keeping Unit', 'transformer' => 'text', ], 'gtin' => [ 'type' => 'text', 'label' => 'GTIN', 'description' => 'Global Trade Item Number', 'transformer' => 'text', ], /************************************************************** SERVICES & OFFERS **************************************************************/ 'hasOfferCatalog' => [ 'type' => 'group', 'label' => 'Offer Catalog', 'transformer' => 'offer_catalog_from_posts', 'fields' => [ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => [ 'auto' => 'Auto from post type', 'manual' => 'Manual entry', ] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => $this->getContentPostTypes(), 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'group_by_taxonomy' => [ 'type' => 'true_false', 'label' => 'Group by category/taxonomy', 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'taxonomy' => [ 'type' => 'select', 'label' => 'Taxonomy', 'options' => $this->getContentTaxonomies(), 'condition' => [ 'field' => 'group_by_taxonomy', 'value' => '1' // or '1' depending on how checkbox stores ] ] ] ], 'knowsAbout' => [ 'type' => 'repeater', 'label' => 'Areas of Expertise', 'description' => 'Skills and specialties', 'transformer' => 'text_array', 'fields' => [ 'topic' => [ 'type' => 'text', 'label' => 'Topic', ] ] ], /************************************************************** CREDENTIALS & CERTIFICATIONS **************************************************************/ 'hasCredential' => [ 'type' => 'repeater', 'label' => 'Credentials / Certifications', 'description' => 'Professional certifications', 'transformer' => 'credential_array', 'fields' => [ 'credentialCategory' => [ 'type' => 'text', 'label' => 'Category', ], 'name' => [ 'type' => 'text', 'label' => 'Name', ], 'issuedBy' => [ 'type' => 'text', 'label' => 'Issued By', ] ] ], 'award' => [ 'type' => 'repeater', 'label' => 'Awards & Recognition', 'transformer' => 'text_array', 'fields' => [ 'award' => ['type' => 'text', 'label' => 'Award'], ] ], 'serviceArea' => [ 'type' => 'repeater', 'label' => 'Service Areas', 'description' => 'Geographic areas served (cities, neighborhoods, or radius)', 'transformer' => 'service_area_array', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Area Name'], 'type' => [ 'type' => 'select', 'label' => 'Type', 'options' => [ 'City' => 'City', 'AdministrativeArea' => 'Region/Province', 'GeoCircle' => 'Radius', ] ], 'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'], ] ], // Specialties 'makesOffer' => [ 'type' => 'group', 'label' => 'Featured Offerings', 'transformer' => 'offers_from_posts', 'fields' => [ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => [ 'auto' => 'Auto from post type', 'manual' => 'Manual entry', ] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => $this->getContentPostTypes(), 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'limit' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Featured Count', 'default' => 5, 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'manual_items' => [ 'type' => 'repeater', 'label' => 'Manual Offers', 'condition' => [ 'field' => 'source', 'value' => 'manual' ], 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Offer Name'], 'description' => ['type' => 'textarea', 'label' => 'Description'], 'price' => ['type' => 'text', 'label' => 'Price/Range'], ] ] ] ], 'hasMenu' => [ 'type' => 'group', 'label' => 'Menu Items', 'description' => 'Auto-populate from post type or enter manually', 'transformer' => 'menu_from_posts', 'fields' => [ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => [ 'auto' => 'Auto from post type', 'manual' => 'Manual entry', ], ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => $this->getContentPostTypes(), // Dynamic callback 'condition' => [ 'field' => 'source', 'value' => 'auto', 'operator' => '==' ] ], 'limit' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Number of items', 'default' => 10, 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'orderby' => [ 'type' => 'select', 'label' => 'Order By', 'options' => [ 'menu_order' => 'Menu Order', 'title' => 'Title', 'date' => 'Date', ], 'condition' => [ 'field' => 'source', 'value' => 'auto' ] ], 'manual_items' => [ 'type' => 'repeater', 'label' => 'Manual Items', 'condition' => [ 'field' => 'source', 'value' => 'manual' ], 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Item Name'], 'description' => ['type' => 'textarea', 'label' => 'Description'], 'price' => ['type' => 'text', 'label' => 'Price'], ] ] ] ], /************************************************************** FAQ FIELDS **************************************************************/ 'mainEntity' => [ 'type' => 'repeater', 'label' => 'FAQ Items', 'description' => 'Question and Answer pairs', 'transformer' => 'faq_array', 'fields' => [ 'question' => [ 'type' => 'text', 'label' => 'Question', ], 'answer' => [ 'type' => 'text', 'label' => 'Answer', ] ] ], /************************************************************** FOOD & CUISINE **************************************************************/ 'servesCuisine' => [ 'type' => 'repeater', 'label' => 'Cuisine Types', 'description' => 'Types of cuisine served', 'transformer' => 'text_array', 'fields' => [ 'cuisine' => [ 'type' => 'text', 'label' => 'Cuisine Type', 'description' => 'e.g., Italian, Mexican, Vegan' ] ] ], 'menu' => [ 'type' => 'url', 'label' => 'Menu URL', 'description' => 'Link to online menu', 'transformer' => 'url', ], /************************************************************** PRODUCT/OFFER FIELDS **************************************************************/ 'offers' => [ 'type' => 'group', 'label' => 'Offer Details', 'description' => 'Price and availability information', 'transformer' => 'offer_object', 'fields' => [ 'price' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Price', ], 'priceCurrency' => [ 'type' => 'text', 'label' => 'Currency', 'default' => 'USD', ], 'availability' => [ 'type' => 'select', 'label' => 'Availability', 'options' => [ 'InStock' => 'In Stock', 'PreOrder' => 'Pre-Order', 'SoldOut' => 'Sold Out', 'OutOfStock' => 'Out of Stock', 'Discontinued' => 'Discontinued', ] ], 'validFrom' => [ 'type' => 'date', 'label' => 'Valid From', ], 'validThrough' => [ 'type' => 'date', 'label' => 'Valid Through', ], ] ], 'mpn' => [ 'type' => 'text', 'label' => 'Manufacturer Part Number', 'transformer' => 'text', ], /************************************************************** BUSINESS POLICIES & FEATURES **************************************************************/ 'isAccessibleForFree' => [ 'type' => 'true_false', 'label' => 'Accessible For Free', 'description' => 'Is this service/location accessible without payment?', 'transformer' => 'boolean', ], 'smokingAllowed' => [ 'type' => 'true_false', 'label' => 'Smoking Allowed', 'transformer' => 'boolean', ], 'petsAllowed' => [ 'type' => 'select', 'label' => 'Pets Allowed', 'options' => [ '' => 'Not specified', 'yes' => 'Yes', 'no' => 'No', ], 'transformer' => 'boolean', ], /************************************************************** ORGANIZATION RELATIONSHIPS **************************************************************/ 'parentOrganization' => [ 'type' => 'group', 'label' => 'Parent Organization', 'description' => 'Organization this is a part of', 'transformer' => 'organization_reference', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Organization Name'], 'url' => ['type' => 'url', 'label' => 'Website'], ] ], 'subOrganization' => [ 'type' => 'repeater', 'label' => 'Sub-Organizations', 'description' => 'Child organizations or departments', 'transformer' => 'organization_reference_array', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Organization Name'], 'url' => ['type' => 'url', 'label' => 'Website'], ] ], 'employee' => [ 'type' => 'repeater', 'label' => 'Employees', 'transformer' => 'person_reference_array', 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Name'], 'jobTitle' => ['type' => 'text', 'label' => 'Job Title'], ] ], /************************************************************** HOSPITALITY (for hotels, etc.) **************************************************************/ 'checkinTime' => [ 'type' => 'time', 'label' => 'Check-in Time', 'transformer' => 'time', ], 'checkoutTime' => [ 'type' => 'time', 'label' => 'Check-out Time', 'transformer' => 'time', ], 'starRating' => [ 'type' => 'group', 'label' => 'Star Rating', 'transformer' => 'rating_object', 'fields' => [ 'ratingValue' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5, ], ] ], /************************************************************** REVIEW & RATING **************************************************************/ 'review' => [ 'type' => 'repeater', 'label' => 'Reviews', 'transformer' => 'review_array', 'fields' => [ 'author' => ['type' => 'text', 'label' => 'Reviewer Name'], 'reviewRating' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5, ], 'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'], 'datePublished' => ['type' => 'date', 'label' => 'Date'], ] ], /************************************************************** HEALTH & MEDICAL **************************************************************/ 'medicalSpecialty' => [ 'type' => 'repeater', 'label' => 'Medical Specialties', 'transformer' => 'text_array', 'fields' => [ 'specialty' => ['type' => 'text', 'label' => 'Specialty'] ] ], 'healthcareService' => [ 'type' => 'repeater', 'label' => 'Healthcare Services', 'transformer' => 'text_array', 'fields' => [ 'service' => ['type' => 'text', 'label' => 'Service'] ] ], ]; } /** * Register all type definitions * Each type lists the fields it uses */ private function registerTypeDefinitions(): void { $this->typeDefinitions = [ /************************************************************** GENERAL / SITE-WIDE **************************************************************/ 'WebSite' => [ 'label' => 'Website', 'group' => 'general', 'fields' => [ 'name', 'description', 'url', 'inLanguage', 'potentialAction', 'hasPart', 'creator', ], ], /************************************************************** PAGE TYPES **************************************************************/ 'WebPage' => [ 'label' => 'Web Page', 'group' => 'page', 'fields' => [ 'name', 'description', 'url', 'image', 'datePublished', 'dateModified', 'author', ], ], 'CollectionPage' => [ 'label' => 'Collection Page', 'group' => 'page', 'extends' => 'WebPage', ], 'FAQPage' => [ 'label' => 'FAQ Page', 'group' => 'page', 'extends' => 'WebPage', 'fields' => [ 'mainEntity', // FAQ items ], ], /************************************************************** ORGANIZATION & BUSINESS **************************************************************/ 'Organization' => [ 'label' => 'Organization', 'group' => 'business', 'fields' => [ 'name', 'legalName', 'alternateName', 'description', 'url', 'logo', 'image', 'email', 'telephone', 'sameAs', 'founders', 'foundingDate', 'numberOfEmployees', 'taxID', 'vatID', 'duns', 'slogan', 'disambiguatingDescription', ], ], 'LocalBusiness' => [ 'label' => 'Local Business', 'group' => 'business', 'extends' => 'Organization', 'fields' => [ 'location', 'openingHours', 'priceRange', 'currenciesAccepted', 'paymentAccepted', 'serviceArea', 'areaServed', 'hasMap', 'amenityFeature', 'availableLanguage', 'hasOfferCatalog', 'makesOffer', 'hasMenu', 'knowsAbout', 'hasCredential', 'aggregateRating', 'award', ], ], 'TattooParlor' => [ 'label' => 'Tattoo Parlor', 'group' => 'business', 'extends' => 'LocalBusiness', 'fields' => [ 'makesOffer', // Tattoo styles/services 'hasOfferCatalog', // Portfolio as catalog 'award', ], ], 'HealthBusiness' => [ 'label' => 'Health Business', 'group' => 'business', 'extends' => 'LocalBusiness', 'description' => 'Healthcare providers', ], 'FoodEstablishment' => [ 'label' => 'Food Establishment', 'group' => 'business', 'extends' => 'LocalBusiness', 'fields' => [ 'hasMenu', 'servesCuisine', ], ], 'FoodTruck' => [ 'label' => 'Food Truck', 'group' => 'business', 'extends' => 'FoodEstablishment', 'fields' => [ 'serviceArea', ], ], 'Store' => [ 'label' => 'Store / Shop', 'group' => 'business', 'extends' => 'LocalBusiness', 'fields' => [ 'hasOfferCatalog', 'makesOffer', ], ], 'ProfessionalService' => [ 'label' => 'Professional Service', 'group' => 'business', 'extends' => 'LocalBusiness', 'fields' => [ 'serviceArea', // Where they operate 'makesOffer', // Services offered 'award', // Professional recognition ], ], /************************************************************** PERSON **************************************************************/ 'Person' => [ 'label' => 'Person', 'group' => 'person', 'fields' => [ 'name', 'givenName', 'familyName', 'honorificPrefix', 'honorificSuffix', 'alternateName', 'description', 'image', 'url', 'email', 'telephone', 'sameAs', 'jobTitle', 'knowsLanguage', 'birthDate', 'gender', ], ], /************************************************************** CREATIVE WORKS **************************************************************/ 'CreativeWork' => [ 'label' => 'Creative Work', 'group' => 'creative', 'fields' => [ 'name', 'description', 'image', 'author', 'creator', 'dateCreated', 'datePublished', 'dateModified', 'keywords', ], ], 'DefinedTermSet' => [ 'label' => 'Defined Term', 'group' => 'creative', 'extends' => 'CreativeWork', 'fields' => [ 'DefinedTerm', ] ], 'BeforeAfter' => [ 'label' => 'Before & After Case', 'group' => 'creative', 'extends' => 'CreativeWork', 'fields' => [ 'about', // Service (Laser Tattoo Removal) 'temporalCoverage', // Treatment period 'hasPart', // Individual images (as references) 'associatedMedia', // Alternative to hasPart 'additionalProperty', // Sessions, treatment area ], ], 'VisualArtwork' => [ 'label' => 'Visual Artwork', 'group' => 'creative', 'extends' => 'CreativeWork', 'fields' => [ 'artform', 'artMedium', 'artworkSurface', 'width', 'height', ], ], 'Tattoo' => [ 'label' => 'Tattoo', 'group' => 'creative', 'extends' => 'VisualArtwork', 'description' => 'A tattoo artwork (custom extension)', ], 'Product' => [ 'label' => 'Product', 'group' => 'creative', 'fields' => [ 'name', 'description', 'image', 'brand', 'sku', 'gtin', 'offers', // Price, availability 'aggregateRating', // Reviews 'award', // Product awards ], ], /************************************************************** EVENTS **************************************************************/ 'Event' => [ 'label' => 'Event', 'group' => 'event', 'fields' => [ 'name', 'description', 'image', 'startDate', 'endDate', 'location', 'eventStatus', 'eventAttendanceMode', ], ], ]; } /** * Register type groups for UI organization */ private function registerTypeGroups(): void { $this->typeGroups = [ 'general' => 'General', 'page' => 'Page Types', 'business' => 'Business & Organization', 'person' => 'People', 'creative' => 'Creative Works', 'event' => 'Events', ]; } /** * Register a custom field definition */ public function registerField(string $fieldName, array $config): void { $this->fieldDefinitions[$fieldName] = $config; } /** * Register a custom type definition */ public function registerType(string $typeName, array $config): void { $this->typeDefinitions[$typeName] = $config; } /** * Register a type group */ public function registerGroup(string $key, string $label): void { $this->typeGroups[$key] = $label; } /** * Get post types for select options */ public static function getContentPostTypes(): array { $options = ['' => '-- Select Post Type --']; if (defined('JVB_CONTENT')) { foreach (JVB_CONTENT as $key => $config) { $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key); } } return $options; } /** * Get taxonomies for select options */ public static function getContentTaxonomies(): array { $options = ['' => '-- Select Taxonomy --']; if (defined('JVB_TAXONOMY')) { foreach (JVB_TAXONOMY as $key => $config) { $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key); } } return $options; } }