get_supported_post_types();
foreach ($post_types as $post_type) {
// Add column header
add_filter("manage_{$post_type}_posts_columns", [$this, 'add_column_header'], 10);
// Add column content
add_action("manage_{$post_type}_posts_custom_column", [$this, 'render_column_content'], 10, 2);
// Make column sortable
add_filter("manage_edit-{$post_type}_sortable_columns", [$this, 'make_column_sortable'], 10);
}
});
// Handle sorting
add_action('pre_get_posts', [$this, 'handle_column_sorting']);
// Enqueue admin styles for column
add_action('admin_enqueue_scripts', [$this, 'enqueue_column_styles']);
}
/**
* Get supported post types
*
* @return array Array of post type names
*/
private function get_supported_post_types(): array {
$post_types = get_post_types(['public' => true], 'names');
// Remove unnecessary post types
unset($post_types['attachment']);
unset($post_types['elementor_library']);
unset($post_types['oceanwp_library']);
unset($post_types['ae_global_templates']);
/**
* Filter supported post types for ThinkRank SEO Overview column
*
* @param array $post_types Array of post type names
*/
return apply_filters('thinkrank_seo_overview_post_types', $post_types);
}
/**
* Add column header
*
* @param array $columns Existing columns
* @return array Modified columns
*/
public function add_column_header(array $columns): array {
// Insert column before date column
$new_columns = [];
foreach ($columns as $key => $value) {
if ($key === 'date') {
$new_columns[self::COLUMN_ID] = __('ThinkRank SEO Overview', 'thinkrank');
}
$new_columns[$key] = $value;
}
// If date column doesn't exist, add at the end
if (!isset($columns['date'])) {
$new_columns[self::COLUMN_ID] = __('ThinkRank SEO Overview', 'thinkrank');
}
return $new_columns;
}
/**
* Render column content
*
* @param string $column_name Column name
* @param int $post_id Post ID
* @return void
*/
public function render_column_content(string $column_name, int $post_id): void {
if ($column_name !== self::COLUMN_ID) {
return;
}
// Get SEO data
$seo_data = $this->get_seo_data($post_id);
// Render the column content
$this->render_seo_overview($seo_data, $post_id);
}
/**
* Get SEO data for a post
*
* @param int $post_id Post ID
* @return array SEO data
*/
private function get_seo_data(int $post_id): array {
// Get SEO score and metrics from database table
$seo_metrics = $this->get_seo_metrics_from_database($post_id);
// Get focus keyword
$focus_keyword = get_post_meta($post_id, '_thinkrank_focus_keyword', true);
// Get meta title
$meta_title = get_post_meta($post_id, '_thinkrank_seo_title', true);
// Get meta description
$meta_description = get_post_meta($post_id, '_thinkrank_meta_description', true);
// Get content quality and readability from database columns
$content_quality = $seo_metrics['content_quality'] ?? 'Not Analyzed';
$readability = $seo_metrics['readability_score'] ?? 'N/A';
return [
'seo_score' => $seo_metrics['seo_score'],
'seo_grade' => $seo_metrics['seo_grade'],
'content_quality' => $content_quality,
'readability' => $readability,
'focus_keyword' => !empty($focus_keyword),
'focus_keyword_value' => $focus_keyword,
'meta_title' => !empty($meta_title),
'meta_description' => !empty($meta_description),
'pillar_content' => $this->is_link_suggestions_enabled(get_post_type($post_id)) && get_post_meta($post_id, '_thinkrank_pillar_content', true) === '1',
];
}
/**
* Check if link suggestions are enabled for a post type
*
* @param string $post_type Post type
* @return bool True if enabled
*/
private function is_link_suggestions_enabled(string $post_type): bool {
$settings = get_option('thinkrank_global_seo_settings', []);
if (isset($settings[$post_type]['link_suggestions'])) {
return (bool) $settings[$post_type]['link_suggestions'];
}
return true;
}
/**
* Get SEO metrics from database table
*
* @param int $post_id Post ID
* @return array SEO metrics (seo_score, content_quality, readability_score)
*/
private function get_seo_metrics_from_database(int $post_id): array {
global $wpdb;
$table_name = $wpdb->prefix . 'thinkrank_seo_scores';
// Default values
$default_metrics = [
'seo_score' => null,
'seo_grade' => null,
'content_quality' => null,
'readability_score' => null,
];
// Check if table exists
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO score retrieval requires direct database access
$table_exists = $wpdb->get_var($wpdb->prepare(
"SHOW TABLES LIKE %s",
$table_name
));
if (!$table_exists) {
return $default_metrics;
}
// Get the most recent metrics from the database
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO score retrieval requires direct database access
$result = $wpdb->get_row($wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly escaped
"SELECT overall_score, grade, content_quality, readability_score FROM `{$table_name}` WHERE post_id = %d ORDER BY created_at DESC LIMIT 1",
$post_id
), ARRAY_A);
if (!$result) {
return $default_metrics;
}
return [
'seo_score' => $result['overall_score'] !== null ? intval($result['overall_score']) : null,
'seo_grade' => $result['grade'] !== null ? $result['grade'] : null,
'content_quality' => $result['content_quality'],
'readability_score' => $result['readability_score'],
];
}
/**
* Render SEO overview content
*
* @param array $seo_data SEO data
* @param int $post_id Post ID
* @return void
*/
private function render_seo_overview(array $seo_data, int $post_id): void {
?>
= 90) {
return 'great';
} elseif ($score >= 70) {
return 'good';
} elseif ($score >= 50) {
return 'medium';
} else {
return 'poor';
}
}
/**
* Make column sortable
*
* @param array $columns Sortable columns
* @return array Modified sortable columns
*/
public function make_column_sortable(array $columns): array {
$columns[self::COLUMN_ID] = 'thinkrank_seo_score';
return $columns;
}
/**
* Handle column sorting
*
* @param \WP_Query $query WordPress query object
* @return void
*/
public function handle_column_sorting(\WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
$orderby = $query->get('orderby');
if ($orderby === 'thinkrank_seo_score') {
$query->set('meta_key', '_thinkrank_seo_score');
$query->set('orderby', 'meta_value_num');
}
}
/**
* Enqueue column styles
*
* @param string $hook_suffix Current admin page hook
* @return void
*/
public function enqueue_column_styles(string $hook_suffix): void {
// Only load on post list pages (edit.php for all post types)
$screen = get_current_screen();
if (!$screen || $screen->base !== 'edit') {
return;
}
// Add inline styles to common admin styles
wp_add_inline_style('common', $this->get_column_styles());
// Enqueue inline editing script
wp_enqueue_script(
'thinkrank-focus-keyword-inline-edit',
THINKRANK_PLUGIN_URL . 'assets/focus-keyword-inline-edit.js',
['jquery'],
THINKRANK_VERSION,
true
);
// Localize script with AJAX data
wp_localize_script(
'thinkrank-focus-keyword-inline-edit',
'thinkrankFocusKeyword',
[
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(\ThinkRank\Admin\Focus_Keyword_Ajax::get_nonce_action()),
'action' => \ThinkRank\Admin\Focus_Keyword_Ajax::get_ajax_action(),
'i18n' => [
'notSet' => __('Not Set', 'thinkrank'),
'clickToEdit' => __('Click to edit', 'thinkrank'),
'pressEnter' => __('Press Enter to save, Esc to cancel', 'thinkrank'),
'saving' => __('Saving...', 'thinkrank'),
'saved' => __('Saved!', 'thinkrank'),
'error' => __('Error updating focus keyword. Please try again.', 'thinkrank'),
]
]
);
}
/**
* Get column CSS styles
*
* @return string CSS styles
*/
private function get_column_styles(): string {
return '
.column-thinkrank_seo_overview {
width: 240px;
}
.thinkrank-seo-overview {
font-size: 13px;
line-height: 1.4;
display: flex;
flex-direction: column;
gap: 6px;
}
.thinkrank-seo-row {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.thinkrank-seo-separator {
margin: 0 6px;
color: #c3c4c7;
font-size: 10px;
}
/* Top Row: Badges */
.thinkrank-seo-badge {
border-radius: 12px;
padding: 2px 8px;
font-weight: 600;
font-size: 12px;
display: inline-flex;
align-items: center;
white-space: nowrap;
position: relative;
}
.thinkrank-seo-badge::after,
.thinkrank-seo-metric::after {
content: attr(title);
position: absolute;
bottom: calc(100% + 11px);
left: 50%;
transform: translateX(-50%);
background: #00043D;
border-radius: 8px;
padding: 4px 8px;
color: #fff;
font-weight: 400;
box-shadow: 0 1px 2px rgba(17, 17, 17, 0.1);
visibility: hidden;
white-space: nowrap;
z-index: 20;
}
.thinkrank-seo-badge::before,
.thinkrank-seo-metric::before {
content: "";
position: absolute;
bottom: calc(100% + 7px);
left: calc(50% - 5px);
transform: rotate(45deg);
background: #00043D;
width: 10px;
height: 10px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(17, 17, 17, 0.1);
visibility: hidden;
z-index: 19;
}
.thinkrank-seo-badge:hover::before,
.thinkrank-seo-badge:hover::after {
visibility: visible;
}
/* Apply Z-Index for metric too */
.thinkrank-seo-metric:hover::before,
.thinkrank-seo-metric:hover::after {
visibility: visible;
}
.thinkrank-score-great {
background-color: #d1fae5;
color: #065f46;
}
.thinkrank-score-good {
background-color: #dbeafe;
color: #1e40af;
}
.thinkrank-score-medium {
background-color: #fef3c7;
color: #92400e;
}
.thinkrank-score-poor {
background-color: #fee2e2;
color: #b91c1c;
}
.thinkrank-status-good {
background-color: #E5FFF6;
color: #117953;
}
.thinkrank-status-ok {
background-color: #EBF0FF;
color: #1B42C2;
}
.thinkrank-status-not-analyzed,
.thinkrank-seo-badge.thinkrank-not-set {
background-color: #E5EBF0;
color: #58677D;
border-radius: 12px;
padding: 2px 8px;
}
.thinkrank-status-needs-improvement {
background-color: #FFF7EB;
color: #865A13;
}
.thinkrank-status-no-content {
background-color: #FFEBEE;
color: #EC113A;
}
.thinkrank-pillar-icon {
background-color: #F0F0F1;
color: #2271b1;
padding: 4px;
line-height: 20px;
}
/* Middle Row: Focus Keyword */
.thinkrank-focus-keyword-item {
width: 100%;
position: relative;
display: flex;
align-items: center;
background-color: #ECEEEE;
border-radius: 4px;
border: 1px solid #E4E6EC;
padding: 0 8px;
min-height: 24px;
}
.thinkrank-focus-keyword-display {
cursor: pointer;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #1d2327;
font-weight: 500;
}
.thinkrank-focus-keyword-display.thinkrank-not-set {
color: #646970;
font-style: italic;
}
.thinkrank-edit-icon {
font-size: 16px;
width: 16px;
height: 16px;
pointer-events: none;
position: absolute;
right: 8px;
path {
stroke: #A5AAAC;
}
}
.thinkrank-focus-keyword-item .thinkrank-success-icon {
position: absolute;
right: 8px;
visibility: hidden;
}
.thinkrank-focus-keyword-item:hover .thinkrank-edit-icon path {
stroke: #4451FF;
}
.thinkrank-focus-keyword-input[type="text"] {
width: 100%;
padding: 0 8px;
border: none;
border-radius: 4px;
font-size: 13px;
margin: 0;
min-height: 24px;
line-height: normal;
}
.thinkrank-focus-keyword-item.is-editing {
padding: 0;
background: transparent;
border-color: #5C91FE;
}
.thinkrank-focus-keyword-item.is-editing .thinkrank-focus-keyword-display,
.thinkrank-focus-keyword-item.is-editing .thinkrank-edit-icon {
display: none;
}
.thinkrank-focus-keyword-item.is-editing .thinkrank-success-icon {
visibility: visible;
}
.thinkrank-focus-keyword-item.is-editing.is-saving .thinkrank-success-icon {
visibility: hidden;
}
/* Bottom Row: Metrics */
.thinkrank-seo-metric {
display: flex;
align-items: center;
gap: 4px;
color: #646970;
font-size: 12px;
line-height: 20px;
position: relative;
}
.thinkrank-seo-metric:hover::before,
.thinkrank-seo-metric:hover::after {
visibility: visible;
}
.thinkrank-metric-value {
white-space: nowrap;
color: #505272;
}
/* Animations */
.thinkrank-focus-keyword-display.thinkrank-feedback-success {
animation: thinkrank-success-flash 0.5s ease-in-out;
}
.thinkrank-focus-keyword-display.thinkrank-feedback-error {
animation: thinkrank-error-flash 0.5s ease-in-out;
}
@keyframes thinkrank-success-flash {
0%, 100% { background-color: transparent; }
50% { background-color: #d1fae5; }
}
@keyframes thinkrank-error-flash {
0%, 100% { background-color: transparent; }
50% { background-color: #fee2e2; }
}
.thinkrank-inline-error {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
color: #fff;
font-size: 11px;
margin-top: 4px;
padding: 4px 8px;
background-color: #d63638;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.thinkrank-inline-error::after {
content: "";
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #d63638 transparent;
}
';
}
}