PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/seo-tools/class-jetpack-seo-admin-columns.php +258 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,258 @@
1 +<?php
2 +/**
3 + * Adds factual SEO columns to wp-admin post list tables.
4 + *
5 + * Surfaces the per-post SEO *state* at a glance — schema type, whether a meta
6 + * description is set, and search visibility — without grading it. Whether a
7 + * given setting should be configured depends on the post's purpose, so we
8 + * report facts and let the author decide.
9 + *
10 + * @package automattic/jetpack
11 + */
12 +
13 +/**
14 + * Registers read-only SEO columns on every public post-list table.
15 + */
16 +class Jetpack_SEO_Admin_Columns {
17 +
18 + /**
19 + * The column IDs this class registers on the post-list tables.
20 + *
21 + * @var string[]
22 + */
23 + const COLUMNS = array( 'jetpack_seo_schema', 'jetpack_seo_description', 'jetpack_seo_search' );
24 +
25 + /**
26 + * User meta listing the post-list screen IDs whose saved hidden-column set
27 + * has already been backfilled with the SEO columns.
28 + */
29 + const BACKFILL_USER_META_KEY = 'jetpack_seo_columns_backfilled_screens';
30 +
31 + /**
32 + * Wire all hooks.
33 + *
34 + * @return void
35 + */
36 + public static function init() {
37 + add_action( 'admin_init', array( __CLASS__, 'register_columns_for_post_types' ) );
38 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
39 + add_filter( 'default_hidden_columns', array( __CLASS__, 'default_hidden_columns' ), 10, 2 );
40 + add_action( 'current_screen', array( __CLASS__, 'backfill_hidden_columns' ) );
41 + }
42 +
43 + /**
44 + * Hide the SEO columns by default in Screen Options.
45 + *
46 + * Three extra always-on columns squeeze the title column unreadably narrow,
47 + * so we default them to hidden using core's `default_hidden_columns` filter —
48 + * the standard mechanism for choosing which columns start hidden in Screen
49 + * Options. It only reaches users who have never customized Screen Options for
50 + * the screen; everyone else is handled by self::backfill_hidden_columns().
51 + *
52 + * @param string[] $hidden Column IDs hidden by default.
53 + * @param WP_Screen $screen Current screen.
54 + * @return string[]
55 + */
56 + public static function default_hidden_columns( $hidden, $screen ) {
57 + if ( isset( $screen->base ) && 'edit' === $screen->base ) {
58 + $hidden = array_merge( $hidden, self::COLUMNS );
59 + }
60 + return $hidden;
61 + }
62 +
63 + /**
64 + * Hide the SEO columns for users whose Screen Options predate them.
65 + *
66 + * `default_hidden_columns` is a first-run default: core only consults it when
67 + * the user has no saved hidden-column set for the screen (`$use_defaults =
68 + * ! is_array( $hidden )` in wp-admin/includes/screen.php). Toggling any single
69 + * column checkbox writes that user meta, so a user who customized Screen
70 + * Options at any point — years before these columns existed — never gets the
71 + * default and sees all three columns crowding the title column.
72 + *
73 + * Backfill those users once per screen by merging the SEO columns into the set
74 + * they already have, the way core seeds its own nav-menu column defaults in
75 + * wp-admin/includes/nav-menu.php. Screens are recorded as they're visited, so a
76 + * post type registered later still gets handled the first time it's opened, and
77 + * a user who turns the columns back on afterwards keeps them.
78 + *
79 + * @param WP_Screen $screen Current screen.
80 + * @return void
81 + */
82 + public static function backfill_hidden_columns( $screen ) {
83 + if ( ! ( $screen instanceof WP_Screen ) || 'edit' !== $screen->base ) {
84 + return;
85 + }
86 +
87 + $user_id = get_current_user_id();
88 + if ( ! $user_id ) {
89 + return;
90 + }
91 +
92 + $backfilled = get_user_meta( $user_id, self::BACKFILL_USER_META_KEY, true );
93 + $backfilled = is_array( $backfilled ) ? $backfilled : array();
94 + if ( in_array( $screen->id, $backfilled, true ) ) {
95 + return;
96 + }
97 +
98 + $option_name = 'manage' . $screen->id . 'columnshidden';
99 + $hidden = get_user_option( $option_name, $user_id );
100 +
101 + /*
102 + * A non-array means the user has never customized this screen, so
103 + * `default_hidden_columns` already hides the columns for them and there is
104 + * nothing to backfill — just record the screen so we stop looking.
105 + */
106 + if ( is_array( $hidden ) ) {
107 + $missing = array_values( array_diff( self::COLUMNS, $hidden ) );
108 + if ( ! empty( $missing ) ) {
109 + /*
110 + * $is_global = true writes the unprefixed key — the same one core's
111 + * `hidden-columns` AJAX handler writes and that the get_user_option()
112 + * read above falls back to. Passing false would write a blog-prefixed
113 + * key that core itself never updates, so the two would diverge.
114 + */
115 + update_user_option( $user_id, $option_name, array_values( array_merge( $hidden, $missing ) ), true );
116 + }
117 + }
118 +
119 + $backfilled[] = $screen->id;
120 + update_user_meta( $user_id, self::BACKFILL_USER_META_KEY, $backfilled );
121 + }
122 +
123 + /**
124 + * Register columns + renderers for each supported post type.
125 + *
126 + * @return void
127 + */
128 + public static function register_columns_for_post_types() {
129 + foreach ( self::get_supported_post_types() as $post_type ) {
130 + add_filter( "manage_{$post_type}_posts_columns", array( __CLASS__, 'add_columns' ) );
131 + add_action( "manage_{$post_type}_posts_custom_column", array( __CLASS__, 'render_column' ), 10, 2 );
132 + }
133 + }
134 +
135 + /**
136 + * Post types that get the SEO columns.
137 + *
138 + * Defers to the SEO package so the columns cover exactly the post types the
139 + * Jetpack > SEO Content tab lists, rather than a second copy of the same query
140 + * that can drift from it. The guard covers older bundled snapshots of the
141 + * package that predate Post_Types.
142 + *
143 + * @return string[]
144 + */
145 + private static function get_supported_post_types() {
146 + if ( method_exists( '\Automattic\Jetpack\SEO\Post_Types', 'get_supported_content_types' ) ) {
147 + return \Automattic\Jetpack\SEO\Post_Types::get_supported_content_types();
148 + }
149 +
150 + $post_types = get_post_types(
151 + array(
152 + 'public' => true,
153 + 'show_ui' => true,
154 + 'show_in_rest' => true,
155 + ),
156 + 'names'
157 + );
158 + unset( $post_types['attachment'] );
159 +
160 + return array_values( $post_types );
161 + }
162 +
163 + /**
164 + * Insert the SEO columns just after the title column.
165 + *
166 + * @param array $columns Existing columns keyed by column name.
167 + * @return array
168 + */
169 + public static function add_columns( $columns ) {
170 + $new = array();
171 + foreach ( $columns as $key => $label ) {
172 + $new[ $key ] = $label;
173 + if ( 'title' === $key ) {
174 + $new['jetpack_seo_schema'] = __( 'Schema', 'jetpack' );
175 + $new['jetpack_seo_description'] = __( 'Meta description', 'jetpack' );
176 + $new['jetpack_seo_search'] = __( 'Search', 'jetpack' );
177 + }
178 + }
179 + return $new;
180 + }
181 +
182 + /**
183 + * Render a single cell — factual state only.
184 + *
185 + * @param string $column Column identifier.
186 + * @param int $post_id Current row post ID.
187 + * @return void
188 + */
189 + public static function render_column( $column, $post_id ) {
190 + if ( ! in_array( $column, self::COLUMNS, true ) ) {
191 + return;
192 + }
193 +
194 + $coverage = Jetpack_SEO_Posts::get_post_seo_coverage( $post_id );
195 +
196 + switch ( $column ) {
197 + case 'jetpack_seo_schema':
198 + $schema = Jetpack_SEO_Posts::get_post_schema_type( $post_id );
199 + echo esc_html( '' !== $schema ? self::schema_type_label( $schema ) : '—' );
200 + break;
201 +
202 + case 'jetpack_seo_description':
203 + // wp_kses_post() sanitizes the markup and signals the escaping to PHPCS;
204 + // the muted branch wraps its (already-escaped) label in a <span>.
205 + echo wp_kses_post(
206 + $coverage['has_description']
207 + ? esc_html__( 'Set', 'jetpack' )
208 + : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Not set', 'jetpack' ) . '</span>'
209 + );
210 + break;
211 +
212 + case 'jetpack_seo_search':
213 + echo wp_kses_post(
214 + $coverage['noindex']
215 + ? esc_html__( 'Hidden', 'jetpack' )
216 + : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Visible', 'jetpack' ) . '</span>'
217 + );
218 + break;
219 + }
220 + }
221 +
222 + /**
223 + * Display label for an allowed schema type.
224 + *
225 + * @param string $schema Schema type slug.
226 + * @return string
227 + */
228 + private static function schema_type_label( $schema ) {
229 + switch ( $schema ) {
230 + case 'article':
231 + return __( 'Article', 'jetpack' );
232 + case 'faq':
233 + return __( 'FAQ', 'jetpack' );
234 + default:
235 + return ucfirst( $schema );
236 + }
237 + }
238 +
239 + /**
240 + * Minimal column-width styling on edit.php only (no color-coding —
241 + * these columns report state, not a grade).
242 + *
243 + * @param string $hook_suffix Current admin hook suffix.
244 + * @return void
245 + */
246 + public static function enqueue_assets( $hook_suffix ) {
247 + if ( 'edit.php' !== $hook_suffix ) {
248 + return;
249 + }
250 + wp_register_style( 'jetpack-seo-admin-columns', false, array(), JETPACK__VERSION );
251 + wp_add_inline_style(
252 + 'jetpack-seo-admin-columns',
253 + '.column-jetpack_seo_schema,.column-jetpack_seo_description,.column-jetpack_seo_search{width:9em}' .
254 + '.jetpack-seo-col-muted{color:#787c82}'
255 + );
256 + wp_enqueue_style( 'jetpack-seo-admin-columns' );
257 + }
258 +}