Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.92% covered (warning)
56.92%
37 / 65
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_SEO_Admin_Columns
56.92% covered (warning)
56.92%
37 / 65
28.57% covered (danger)
28.57%
2 / 7
65.29
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 default_hidden_columns
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 register_columns_for_post_types
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 add_columns
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 render_column
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
8
 schema_type_label
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 enqueue_assets
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
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 */
16class Jetpack_SEO_Admin_Columns {
17
18    /**
19     * Wire all hooks.
20     *
21     * @return void
22     */
23    public static function init() {
24        add_action( 'admin_init', array( __CLASS__, 'register_columns_for_post_types' ) );
25        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
26        add_filter( 'default_hidden_columns', array( __CLASS__, 'default_hidden_columns' ), 10, 2 );
27    }
28
29    /**
30     * Hide the SEO columns by default in Screen Options.
31     *
32     * Three extra always-on columns squeeze the title column unreadably narrow,
33     * so we default them to hidden using core's `default_hidden_columns` filter —
34     * the standard mechanism for choosing which columns start hidden in Screen
35     * Options. It only applies to users who have never customized Screen Options
36     * for the screen, so anyone who explicitly enabled the columns keeps them.
37     *
38     * @param string[]  $hidden Column IDs hidden by default.
39     * @param WP_Screen $screen Current screen.
40     * @return string[]
41     */
42    public static function default_hidden_columns( $hidden, $screen ) {
43        if ( isset( $screen->base ) && 'edit' === $screen->base ) {
44            $hidden = array_merge(
45                $hidden,
46                array( 'jetpack_seo_schema', 'jetpack_seo_description', 'jetpack_seo_search' )
47            );
48        }
49        return $hidden;
50    }
51
52    /**
53     * Register columns + renderers for each public, visible post type.
54     *
55     * @return void
56     */
57    public static function register_columns_for_post_types() {
58        $post_types = get_post_types(
59            array(
60                'public'       => true,
61                'show_ui'      => true,
62                'show_in_rest' => true,
63            ),
64            'names'
65        );
66        unset( $post_types['attachment'] );
67
68        foreach ( $post_types as $post_type ) {
69            add_filter( "manage_{$post_type}_posts_columns", array( __CLASS__, 'add_columns' ) );
70            add_action( "manage_{$post_type}_posts_custom_column", array( __CLASS__, 'render_column' ), 10, 2 );
71        }
72    }
73
74    /**
75     * Insert the SEO columns just after the title column.
76     *
77     * @param array $columns Existing columns keyed by column name.
78     * @return array
79     */
80    public static function add_columns( $columns ) {
81        $new = array();
82        foreach ( $columns as $key => $label ) {
83            $new[ $key ] = $label;
84            if ( 'title' === $key ) {
85                $new['jetpack_seo_schema']      = __( 'Schema', 'jetpack' );
86                $new['jetpack_seo_description'] = __( 'Meta description', 'jetpack' );
87                $new['jetpack_seo_search']      = __( 'Search', 'jetpack' );
88            }
89        }
90        return $new;
91    }
92
93    /**
94     * Render a single cell — factual state only.
95     *
96     * @param string $column  Column identifier.
97     * @param int    $post_id Current row post ID.
98     * @return void
99     */
100    public static function render_column( $column, $post_id ) {
101        $columns = array( 'jetpack_seo_schema', 'jetpack_seo_description', 'jetpack_seo_search' );
102        if ( ! in_array( $column, $columns, true ) ) {
103            return;
104        }
105
106        $coverage = Jetpack_SEO_Posts::get_post_seo_coverage( $post_id );
107
108        switch ( $column ) {
109            case 'jetpack_seo_schema':
110                $schema = Jetpack_SEO_Posts::get_post_schema_type( $post_id );
111                echo esc_html( '' !== $schema ? self::schema_type_label( $schema ) : '—' );
112                break;
113
114            case 'jetpack_seo_description':
115                // wp_kses_post() sanitizes the markup and signals the escaping to PHPCS;
116                // the muted branch wraps its (already-escaped) label in a <span>.
117                echo wp_kses_post(
118                    $coverage['has_description']
119                        ? esc_html__( 'Set', 'jetpack' )
120                        : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Not set', 'jetpack' ) . '</span>'
121                );
122                break;
123
124            case 'jetpack_seo_search':
125                echo wp_kses_post(
126                    $coverage['noindex']
127                        ? esc_html__( 'Hidden', 'jetpack' )
128                        : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Visible', 'jetpack' ) . '</span>'
129                );
130                break;
131        }
132    }
133
134    /**
135     * Display label for an allowed schema type.
136     *
137     * @param string $schema Schema type slug.
138     * @return string
139     */
140    private static function schema_type_label( $schema ) {
141        switch ( $schema ) {
142            case 'article':
143                return __( 'Article', 'jetpack' );
144            case 'faq':
145                return __( 'FAQ', 'jetpack' );
146            default:
147                return ucfirst( $schema );
148        }
149    }
150
151    /**
152     * Minimal column-width styling on edit.php only (no color-coding —
153     * these columns report state, not a grade).
154     *
155     * @param string $hook_suffix Current admin hook suffix.
156     * @return void
157     */
158    public static function enqueue_assets( $hook_suffix ) {
159        if ( 'edit.php' !== $hook_suffix ) {
160            return;
161        }
162        wp_register_style( 'jetpack-seo-admin-columns', false, array(), JETPACK__VERSION );
163        wp_add_inline_style(
164            'jetpack-seo-admin-columns',
165            '.column-jetpack_seo_schema,.column-jetpack_seo_description,.column-jetpack_seo_search{width:9em}' .
166            '.jetpack-seo-col-muted{color:#787c82}'
167        );
168        wp_enqueue_style( 'jetpack-seo-admin-columns' );
169    }
170}