PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / src / AI / GEO / Outputs / Posts.php
secure-custom-fields / src / AI / GEO / Outputs Last commit date
Blocks.php 2 months ago Posts.php 2 months ago
Posts.php
274 lines
1 <?php
2 /**
3 * ACF 6.8.0 feature port.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 // phpcs:disable -- Upstream ACF 6.8.0 feature-port files are kept close to source.
9
10 namespace SCF\AI\GEO\Outputs;
11
12 use SCF\AI\GEO\GEO;
13
14 // Exit if accessed directly.
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * SCF GEO Posts Output
19 *
20 * Handles JSON-LD structured data output for ACF fields on post types.
21 */
22 class Posts {
23
24 /**
25 * Constructor
26 *
27 * @since 6.8.0
28 */
29 public function __construct() {
30 $this->init();
31 }
32
33 /**
34 * Initialize the GEO Posts extension
35 *
36 * @since 6.8.0
37 */
38 public function init() {
39 // Add front-end JSON-LD output for posts.
40 add_action( 'wp_head', array( $this, 'output_jsonld_data' ) );
41 }
42
43 /**
44 * Output JSON-LD structured data for ACF fields on posts
45 *
46 * @since 6.8.0
47 */
48 public function output_jsonld_data() {
49 /**
50 * Filters whether to output debug comments in HTML
51 *
52 * @param bool $debug Whether to output debug comments. Default false.
53 */
54 $debug = apply_filters( 'acf/schema/debug', false );
55
56 // Only output on singular posts/pages.
57 if ( ! is_singular() ) {
58 if ( $debug ) {
59 echo "<!-- SCF AI JSON-LD: Not a singular post/page -->\n";
60 }
61 return;
62 }
63
64 global $post;
65 if ( ! $post ) {
66 if ( $debug ) {
67 echo "<!-- SCF AI JSON-LD: No post object found -->\n";
68 }
69 return;
70 }
71
72 $post_type = get_post_type( $post );
73 if ( ! $post_type ) {
74 if ( $debug ) {
75 echo "<!-- SCF AI JSON-LD: No post type found -->\n";
76 }
77 return;
78 }
79
80 if ( $debug ) {
81 echo '<!-- SCF AI JSON-LD: Checking post type: ' . esc_html( $post_type ) . " -->\n";
82 }
83
84 // Build list of post types with JSON-LD enabled.
85 $enabled_post_types = array();
86
87 // Get ACF custom post types with JSON-LD enabled.
88 $acf_post_types = acf_get_acf_post_types();
89 foreach ( $acf_post_types as $acf_post_type ) {
90 if ( ! empty( $acf_post_type['auto_jsonld'] ) && isset( $acf_post_type['post_type'] ) ) {
91 $enabled_post_types[] = $acf_post_type['post_type'];
92 }
93 }
94
95 /**
96 * Filters the list of post types that have JSON-LD output enabled.
97 *
98 * @param array $enabled_post_types Array of post type names with JSON-LD enabled.
99 */
100 $enabled_post_types = apply_filters( 'acf/schema/enabled_post_types', $enabled_post_types );
101
102 // Exit if current post type is not in the enabled list.
103 if ( ! in_array( $post_type, $enabled_post_types, true ) ) {
104 if ( $debug ) {
105 echo '<!-- SCF AI JSON-LD: Post type \'' . esc_html( $post_type ) . "' does not have JSON-LD enabled -->\n";
106 }
107 return;
108 }
109
110 /**
111 * Filters the field objects before retrieval, allowing posts to provide custom data.
112 *
113 * This is useful for posts that need custom field data handling.
114 * Return a non-null value to short-circuit the default get_field_objects() call.
115 *
116 * @since 6.8.0
117 *
118 * @param array|null $field_objects The field objects array, or null to use default behavior.
119 * @param WP_Post $post The post object.
120 * @param string $post_type The post type.
121 */
122 $field_objects = apply_filters( 'acf/schema/post_field_objects', null, $post, $post_type );
123
124 /**
125 * Filters the field objects for a specific post type.
126 *
127 * The dynamic portion of the hook name, `$post_type`, refers to the post type name.
128 * For example, 'acf/schema/post_field_objects/post_type=product' for the product post type.
129 *
130 * @since 6.8.0
131 *
132 * @param array|null $field_objects The field objects array, or null to use default behavior.
133 * @param WP_Post $post The post object.
134 */
135 $field_objects = apply_filters( 'acf/schema/post_field_objects/post_type=' . $post_type, $field_objects, $post );
136
137 // If no custom field objects were provided, get them from the post.
138 if ( null === $field_objects ) {
139 // Get all ACF field objects for this post without formatting to get raw ACF storage format.
140 $field_objects = get_field_objects( $post->ID, false );
141 }
142
143 if ( ! $field_objects || ! is_array( $field_objects ) ) {
144 if ( $debug ) {
145 echo '<!-- SCF AI JSON-LD: No ACF fields found for post ID ' . absint( $post->ID ) . " -->\n";
146 }
147 return;
148 }
149
150 if ( $debug ) {
151 echo '<!-- SCF AI JSON-LD: Found ' . count( $field_objects ) . " ACF fields -->\n";
152 }
153
154 // Process ACF fields and extract types from qualified properties.
155 // This handles schema_property mapping.
156 $processed_fields = GEO::process_fields( $field_objects );
157
158 // Get any explicitly set schema type for this post type.
159 $acf_post_type_object = null;
160 foreach ( $acf_post_types as $acf_post_type ) {
161 if ( isset( $acf_post_type['post_type'] ) && $acf_post_type['post_type'] === $post_type ) {
162 $acf_post_type_object = $acf_post_type;
163 break;
164 }
165 }
166
167 $provided_type = $acf_post_type_object['schema_type'] ?? null;
168 $field_types = $processed_fields['field_types'] ?? array();
169
170 // Determine the final @type based on provided type or field types from qualified properties.
171 $schema_type = GEO::determine_schema_type( $provided_type, $field_types, 'Article' );
172
173 // Remove internal type data from processed fields.
174 unset( $processed_fields['field_types'] );
175
176 // Build base JSON-LD structured data.
177 $jsonld_data = array(
178 '@context' => 'https://schema.org',
179 '@type' => $schema_type,
180 '@id' => get_permalink( $post ),
181 'url' => get_permalink( $post ),
182 'name' => get_the_title( $post ),
183 'headline' => get_the_title( $post ),
184 );
185
186 // Add publication date if available.
187 if ( $post->post_date ) {
188 $jsonld_data['datePublished'] = get_the_date( 'c', $post );
189 }
190
191 if ( $post->post_modified ) {
192 $jsonld_data['dateModified'] = get_the_modified_date( 'c', $post );
193 }
194
195 /**
196 * Filter whether to automatically add featured image to JSON-LD output
197 *
198 * @param bool $add_image Whether to add the featured image. Default true.
199 * @param WP_Post $post The post object.
200 * @param string $post_type The post type.
201 */
202 $add_image = apply_filters( 'acf/schema/auto_add_image', true, $post, $post_type );
203
204 // Add featured image if available and enabled.
205 if ( $add_image && has_post_thumbnail( $post ) ) {
206 $thumbnail_id = get_post_thumbnail_id( $post );
207 $thumbnail_url = wp_get_attachment_image_url( $thumbnail_id, 'full' );
208
209 if ( $thumbnail_url ) {
210 // Get image metadata for additional properties.
211 $image_meta = wp_get_attachment_metadata( $thumbnail_id );
212
213 $jsonld_data['image'] = array(
214 '@type' => 'ImageObject',
215 'url' => $thumbnail_url,
216 );
217 if ( isset( $image_meta['width'] ) ) {
218 $jsonld_data['image']['width'] = $image_meta['width'];
219 }
220 if ( isset( $image_meta['height'] ) ) {
221 $jsonld_data['image']['height'] = $image_meta['height'];
222 }
223 }
224 }
225
226 /**
227 * Filter whether to automatically add author to JSON-LD output
228 *
229 * @param bool $add_author Whether to add the author. Default true.
230 * @param WP_Post $post The post object.
231 * @param string $post_type The post type.
232 */
233 $add_author = apply_filters( 'acf/schema/auto_add_author', true, $post, $post_type );
234
235 // Add author if enabled.
236 if ( $add_author && $post->post_author ) {
237 $author = get_userdata( $post->post_author );
238
239 if ( $author ) {
240 $jsonld_data['author'] = array(
241 '@type' => 'Person',
242 'name' => $author->display_name,
243 );
244
245 // Add author URL if available.
246 $author_url = get_author_posts_url( $post->post_author );
247 if ( $author_url ) {
248 $jsonld_data['author']['url'] = $author_url;
249 }
250 }
251 }
252
253 // Merge processed fields into JSON-LD data.
254 $jsonld_data = array_merge( $jsonld_data, $processed_fields );
255
256 /**
257 * Filters the JSON-LD data before output for a post.
258 *
259 * @param array $jsonld_data The JSON-LD data array.
260 * @param WP_Post $post The post object.
261 * @param string $post_type The post type.
262 */
263 $jsonld_data = apply_filters( 'acf/schema/data', $jsonld_data, $post, $post_type );
264
265 // Only output if we have data after filtering.
266 if ( empty( $jsonld_data ) ) {
267 return;
268 }
269
270 // Output the JSON-LD using the shared helper.
271 GEO::render_jsonld_script( $jsonld_data );
272 }
273 }
274