PluginProbe ʕ •ᴥ•ʔ
Advanced Custom Fields (ACF®) / 6.8.7
Advanced Custom Fields (ACF®) v6.8.7
6.8.9 6.8.8 6.8.7 6.8.6 6.8.5 6.8.4 6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.6.1 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10.2 6.3.11 6.3.12 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.6.1 6.4.0 6.4.0.1 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.8.0 trunk 1.0.0 1.0.2 1.0.3 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.1 2.1.3 2.1.4 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.6 3.0.7 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.8 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4.0 4.4.1 4.4.10 4.4.11 4.4.12 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 5.10 5.10.1 5.10.2 5.11 5.11.1 5.11.2 5.11.3 5.11.4 5.12 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.6.10 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.7.0 5.7.1 5.7.10 5.7.12 5.7.13 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.8.0 5.8.1 5.8.10 5.8.11 5.8.12 5.8.13 5.8.14 5.8.2 5.8.3 5.8.4
advanced-custom-fields / src / AI / GEO / GEO.php
advanced-custom-fields / src / AI / GEO Last commit date
Outputs 5 months ago data 5 months ago FieldSettings.php 5 months ago GEO.php 5 months ago Schema.php 5 months ago SchemaData.php 5 months ago
GEO.php
425 lines
1 <?php
2 /**
3 * @package ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 namespace ACF\AI\GEO;
13
14 // Exit if accessed directly.
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ACF GEO Extension
19 *
20 * Extends ACF admin interface to add AI-related settings and functionality.
21 */
22 class GEO {
23
24 /**
25 * Constructs the GEO class.
26 *
27 * @since 6.8.0
28 *
29 * @return void
30 */
31 public function __construct() {
32 $this->init();
33 }
34
35 /**
36 * Initialize the GEO extension,
37 *
38 * @since 6.8.0
39 *
40 * @return void
41 */
42 public function init() {
43 // Add hooks for ACF admin interface extensions for post types.
44 add_filter( 'acf/post_type/additional_settings_tabs', array( $this, 'add_schema_tab' ) );
45 add_action( 'acf/post_type/render_settings_tab/schema', array( $this, 'render_post_type_schema_tab' ) );
46
47 // Initialize GEO submodules.
48 // Field Settings.
49 new FieldSettings();
50
51 // JSON-LD Outputs.
52 new Outputs\Posts();
53 // Note: Blocks output is initialized separately in PRO (see acf-pro.php).
54 }
55
56 /**
57 * Adds the "Schema" settings tab for post types.
58 *
59 * @since 6.8.0
60 *
61 * @param array $tabs An array of the existing tabs.
62 * @return array
63 */
64 public function add_schema_tab( $tabs ) {
65 $tabs['schema'] = __( 'Schema', 'acf' );
66 return $tabs;
67 }
68
69 /**
70 * Render "Schema" tab content for post types
71 *
72 * @since 6.8.0
73 *
74 * @param array $acf_post_type The ACF post type data.
75 */
76 public function render_post_type_schema_tab( $acf_post_type ) {
77 ?>
78 <span class="acf-experimental-badge acf-field"><?php esc_html_e( 'Experimental', 'acf' ); ?></span>
79 <?php
80 // Add post-type-specific field: auto JSON-LD.
81 acf_render_field_wrap(
82 array(
83 'type' => 'true_false',
84 'name' => 'auto_jsonld',
85 'key' => 'auto_jsonld',
86 'prefix' => 'acf_post_type',
87 'value' => $acf_post_type['auto_jsonld'] ?? 0,
88 'label' => __( 'Automatically add JSON-LD data for fields on this post type', 'acf' ),
89 'instructions' => __( 'When enabled, ACF field data will be automatically included as JSON-LD structured data in the page head for better SEO and semantic markup.', 'acf' ),
90 'ui' => true,
91 'default' => 0,
92 )
93 );
94
95 // Add post-type-specific field: schema type.
96 acf_render_field_wrap(
97 array(
98 'type' => 'select',
99 'name' => 'schema_type',
100 'key' => 'schema_type',
101 'prefix' => 'acf_post_type',
102 'value' => $acf_post_type['schema_type'] ?? '',
103 'label' => __( 'Schema.org Type', 'acf' ),
104 'instructions' => __( 'The Schema.org @type for JSON-LD output. By default, the type is automatically detected based on the schema properties assigned to your fields. You can assign additional types here. Select multiple types if needed (e.g., Recipe + Article).', 'acf' ),
105 'choices' => $this->get_schema_types(),
106 'default' => '',
107 'allow_null' => 1,
108 'multiple' => 1,
109 'ui' => 1,
110 ),
111 'div',
112 'field'
113 );
114 }
115
116 /**
117 * Get available Schema.org types for selection
118 *
119 * @since 6.8.0
120 *
121 * @return array A hierarchical array of Schema.org types grouped by category.
122 */
123 public function get_schema_types() {
124 $types = array();
125
126 // Get all types from schema hierarchy.
127 $all_types = array_keys( SchemaData::get_type_hierarchy() );
128 // Add 'Thing' which is the root and doesn't have a parent.
129 $all_types[] = 'Thing';
130 sort( $all_types );
131
132 // Get priority types from Schema class.
133 $priority_types_list = Schema::get_priority_types();
134 $common_types_cat = __( 'Common Types', 'acf' );
135 $all_types_cat = __( 'All Types', 'acf' );
136
137 // Add priority types under "Common Types" group.
138 $types[ $common_types_cat ] = array();
139 foreach ( $priority_types_list as $type ) {
140 if ( in_array( $type, $all_types, true ) ) {
141 $types[ $common_types_cat ][ $type ] = $type;
142 }
143 }
144
145 // Add remaining types under "All Types".
146 $remaining_types = array_diff( $all_types, $priority_types_list );
147 if ( ! empty( $remaining_types ) ) {
148 $types[ $all_types_cat ] = array();
149 foreach ( $remaining_types as $type ) {
150 $types[ $all_types_cat ][ $type ] = $type;
151 }
152 }
153
154 /**
155 * Filter the available Schema.org types
156 *
157 * Allows developers to add custom Schema.org types or modify existing ones.
158 *
159 * @param array $types The Schema.org type mappings grouped by category.
160 */
161 return apply_filters( 'acf/schema/schema_types', $types );
162 }
163
164 /**
165 * Process ACF fields and map them to Schema.org structure
166 *
167 * Takes an array of field objects and processes them based on their schema_property setting.
168 * Fields with a schema_property are mapped to core Schema.org properties. Properties that
169 * expect objects (like 'author' or 'publisher') automatically get proper "@type" added.
170 * Fields without a schema_property are skipped.
171 *
172 * @since 6.8.0
173 *
174 * @param array $field_objects Array of ACF field objects with values.
175 * @return array Processed data with core properties, with 'field_types' key containing types from qualified properties.
176 */
177 public static function process_fields( $field_objects ) {
178 $data = array();
179 $field_types = array();
180
181 foreach ( $field_objects as $field_name => $field_object ) {
182 // Skip empty values.
183 if ( null === $field_object['value'] || '' === $field_object['value'] ) {
184 continue;
185 }
186
187 // Check if this field has a schema property mapping.
188 $schema_property = $field_object['schema_property'] ?? '';
189
190 if ( ! empty( $schema_property ) ) {
191 // Parse qualified property (e.g., "Offer.price" -> type: "Offer", property: "price").
192 $parsed = Schema::parse_qualified_property( $schema_property );
193 $property_name = $parsed['property'];
194
195 // Collect field types from qualified properties.
196 if ( $parsed['type'] ) {
197 $field_types[] = $parsed['type'];
198 }
199
200 $formatted_value = self::format_field_value_for_jsonld( $field_object['value'], $field_object );
201
202 // Field has a schema property - map to core property using just the property name.
203 $data[ $property_name ] = $formatted_value;
204 }
205 }
206
207 // Add @type to nested objects based on schema.org ranges.
208 $data = self::add_types_to_nested_objects( $data );
209
210 // Include field types from qualified properties.
211 if ( ! empty( $field_types ) ) {
212 $data['field_types'] = array_values( array_unique( $field_types ) );
213 }
214
215 return $data;
216 }
217
218 /**
219 * Determine the final "@type" value for JSON-LD output
220 *
221 * Merges provided types (from post type/block settings) with field types
222 * (from qualified properties like "Recipe.prepTime"). Falls back to the
223 * default type if neither source provides any types.
224 *
225 * @since 6.8.0
226 *
227 * @param string|array|null $provided_types Types explicitly set in settings (can be string, array, or null).
228 * @param array $field_types Types extracted from qualified properties.
229 * @param string $default_type Fallback type if no types provided.
230 * @return string|array Final @type value (string for single type, array for multiple).
231 */
232 public static function determine_schema_type( $provided_types, $field_types, $default_type = 'Thing' ) {
233 $types = array();
234
235 // Add provided types from post type/block settings.
236 if ( ! empty( $provided_types ) ) {
237 $types = is_array( $provided_types ) ? $provided_types : array( $provided_types );
238 }
239
240 // Merge in field types from qualified properties.
241 if ( ! empty( $field_types ) && is_array( $field_types ) ) {
242 $types = array_merge( $types, $field_types );
243 }
244
245 $types = array_values( array_unique( $types ) );
246
247 if ( empty( $types ) ) {
248 return $default_type;
249 }
250
251 return count( $types ) === 1 ? $types[0] : $types;
252 }
253
254 /**
255 * Add "@type" to nested objects based on schema.org property ranges
256 *
257 * Examines each property in the data and if it expects an object type
258 * (like Person, Organization, etc.), automatically adds the appropriate @type.
259 *
260 * For example, if 'author' contains { 'name': 'John' }, it becomes:
261 * { '@type': 'Person', 'name': 'John' }
262 *
263 * @since 6.8.0
264 *
265 * @param array $data The data array to process.
266 * @return array The data with @type added to nested objects.
267 */
268 private static function add_types_to_nested_objects( $data ) {
269 foreach ( $data as $property => $value ) {
270 // Skip if value is not an array (can't be a nested object).
271 if ( ! is_array( $value ) ) {
272 continue;
273 }
274
275 // Skip if already has @type.
276 if ( isset( $value['@type'] ) ) {
277 continue;
278 }
279
280 // Check if this is a sequential array (list) vs associative array (object).
281 // Sequential arrays are for properties that accept multiple values.
282 // We only add @type to associative arrays (objects).
283 $is_list = array_keys( $value ) === range( 0, count( $value ) - 1 );
284
285 if ( $is_list ) {
286 // This is a list/array, not a single object. Skip adding @type.
287 continue;
288 }
289
290 // Check if this property expects an object type.
291 if ( Schema::property_expects_object( $property ) ) {
292 // Get the preferred object type for this property.
293 $object_type = Schema::get_preferred_object_type( $property );
294
295 if ( $object_type ) {
296 // Add @type at the beginning of the array.
297 $data[ $property ] = array_merge(
298 array( '@type' => $object_type ),
299 $value
300 );
301 }
302 }
303 }
304
305 return $data;
306 }
307
308 /**
309 * Render a JSON-LD script tag with the provided data
310 *
311 * Shared helper method for outputting JSON-LD structured data.
312 *
313 * @since 6.8.0
314 *
315 * @param array $jsonld_data The JSON-LD data array to output.
316 */
317 public static function render_jsonld_script( $jsonld_data ) {
318 if ( empty( $jsonld_data ) ) {
319 return;
320 }
321
322 /**
323 * Action fired before rendering JSON-LD script tag
324 *
325 * Allows developers to output custom schemas or capture the data.
326 *
327 * @param array $jsonld_data The JSON-LD data array.
328 */
329 do_action( 'acf/schema/render_script', $jsonld_data );
330
331 /**
332 * Filter to disable ACF's default JSON-LD output
333 *
334 * Return true to prevent ACF from outputting the JSON-LD script tag.
335 * Useful if you want to handle the output yourself via the action above.
336 *
337 * @param bool $disable Whether to disable default output. Default false.
338 * @param array $jsonld_data The JSON-LD data that would be output.
339 */
340 $disable_output = apply_filters( 'acf/schema/disable_output', false, $jsonld_data );
341
342 if ( $disable_output ) {
343 return;
344 }
345
346 // Output the JSON-LD script tag.
347 echo "<script type=\"application/ld+json\">\n";
348 echo wp_json_encode( $jsonld_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_UNESCAPED_UNICODE );
349 echo "\n</script>\n";
350 }
351
352 /**
353 * Format ACF field value for JSON-LD output
354 *
355 * Shared helper method for formatting field values consistently.
356 * Checks for field-type-specific formatting methods in this order:
357 * 1. Pre-filter to allow complete bypass of formatting logic
358 * 2. format_value_for_jsonld() - custom method for JSON-LD formatting (if field type implements it)
359 * 3. Field-type-specific formatting, defaulting to format_value_for_rest() for most types
360 * 4. Post-filter on the final formatted value
361 *
362 * @since 6.8.0
363 *
364 * @param mixed $value The field value.
365 * @param array $field_object The ACF field object.
366 * @return mixed Formatted value.
367 */
368 public static function format_field_value_for_jsonld( $value, $field_object ) {
369 $field_type_name = $field_object['type'] ?? '';
370 $field_name = $field_object['name'] ?? '';
371
372 /**
373 * Filter to bypass the default formatting logic entirely
374 *
375 * Return a non-null value to bypass all default formatting.
376 * This runs before any other formatting logic.
377 *
378 * @param mixed|null $pre_value Return non-null to bypass default formatting.
379 * @param mixed $value The raw field value.
380 * @param array $field_object The ACF field object.
381 * @param string $field_type_name The field type name.
382 */
383 $pre_value = apply_filters( 'acf/schema/format_value/pre', null, $value, $field_object, $field_type_name );
384 $pre_value = apply_filters( "acf/schema/format_value/pre/type={$field_type_name}", $pre_value, $value, $field_object );
385 $pre_value = apply_filters( "acf/schema/format_value/pre/name={$field_name}", $pre_value, $value, $field_object );
386
387 if ( null !== $pre_value ) {
388 return $pre_value;
389 }
390
391 // Get the field type class instance.
392 $field_type = acf_get_field_type( $field_type_name );
393
394 // First priority: Check if field type has a custom format_value_for_jsonld method.
395 if ( $field_type && method_exists( $field_type, 'format_value_for_jsonld' ) ) {
396 $formatted_value = $field_type->format_value_for_jsonld( $value, null, $field_object );
397 } else {
398 // Second priority: Use format_value_for_rest or return as-is.
399 if ( $field_type && method_exists( $field_type, 'format_value_for_rest' ) ) {
400 $formatted_value = $field_type->format_value_for_rest( $value, null, $field_object );
401 } else {
402 // Final fallback: return value as-is.
403 // Arrays are valid JSON-LD (e.g., multi-select values).
404 $formatted_value = $value;
405 }
406 }
407
408 /**
409 * Filter the formatted value before returning
410 *
411 * Allows modification of the value after all default formatting has been applied.
412 *
413 * @param mixed $formatted_value The formatted value.
414 * @param mixed $value The raw field value.
415 * @param array $field_object The ACF field object.
416 * @param string $field_type_name The field type name.
417 */
418 $formatted_value = apply_filters( 'acf/schema/format_value', $formatted_value, $value, $field_object, $field_type_name );
419 $formatted_value = apply_filters( "acf/schema/format_value/type={$field_type_name}", $formatted_value, $value, $field_object );
420 $formatted_value = apply_filters( "acf/schema/format_value/name={$field_name}", $formatted_value, $value, $field_object );
421
422 return $formatted_value;
423 }
424 }
425