Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Last active March 15, 2026 06:02
Show Gist options
  • Select an option

  • Save arenagroove/2deceba3f09fde5ec732a5e6e5f9cefe to your computer and use it in GitHub Desktop.

Select an option

Save arenagroove/2deceba3f09fde5ec732a5e6e5f9cefe to your computer and use it in GitHub Desktop.
WordPress MU plugin — Admin Columns — Featured Image, URL Path, Modified Date, Post ID. Per-column enable/disable, role visibility, and URL path truncation — all configurable via Settings → LR Admin Columns.
<?php
/**
* Plugin Name: LR Admin Columns
* Description: Unified admin list columns — Featured Image, URL Path, Modified Date, Post ID. Per-column enable/disable, role visibility, and URL path truncation — all configurable via Settings → LR Admin Columns.
* Version: 1.1.2
* Author: Luis Martinez
* Author URI: https://www.lessrain.com
* Requires at least: 5.6
* Tested up to: 6.5
* Requires PHP: 7.4
*
* Changelog:
* 1.1.2 — 2026-03-11
* - Fixed fallback_image not persisting after save — array_replace_recursive
* was clobbering top-level string values; replaced with explicit merge
* - Fixed media uploader — moved JS to admin_footer hook, most reliable
* way to ensure wp.media is available before binding
* - Fallback image now correctly used in column output
* 1.1.1 — 2026-03-11
* - URL path word-break fix, media uploader via wp_add_inline_script
* 1.1.0 — 2026-03-11
* - Settings page with column toggles, role restrictions, truncation
* 1.0.0 — 2026-03-11
* - Initial unified release
*/
defined( 'ABSPATH' ) || exit;
if ( ! is_admin() ) {
return;
}
// =============================================================================
// DEFAULTS & SETTINGS
// =============================================================================
function lr_col_defaults(): array {
return [
'fallback_image' => defined( 'LR_TEMPLATE_DIR_URI' )
? trailingslashit( LR_TEMPLATE_DIR_URI ) . 'assets/images/brand-logo.svg'
: '',
'columns' => [
'lr_featured_image' => [
'enabled' => true,
'roles' => [],
],
'lr_slug' => [
'enabled' => true,
'roles' => [],
'truncate' => false,
'truncate_length' => 40,
],
'lr_modified' => [
'enabled' => true,
'roles' => [],
],
'lr_post_id' => [
'enabled' => true,
'roles' => [],
],
],
];
}
/**
* Returns merged settings — saved values win over defaults.
* Does NOT use array_replace_recursive to avoid string key collisions.
*/
function lr_col_settings(): array {
$saved = get_option( 'lr_admin_columns', [] );
$defaults = lr_col_defaults();
// Top-level scalar: fallback_image
$out = $defaults;
if ( ! empty( $saved['fallback_image'] ) ) {
$out['fallback_image'] = $saved['fallback_image'];
}
// Per-column settings
foreach ( array_keys( $defaults['columns'] ) as $key ) {
$saved_col = $saved['columns'][ $key ] ?? [];
$def_col = $defaults['columns'][ $key ];
$out['columns'][ $key ]['enabled'] = isset( $saved_col['enabled'] )
? (bool) $saved_col['enabled']
: $def_col['enabled'];
$out['columns'][ $key ]['roles'] = isset( $saved_col['roles'] ) && is_array( $saved_col['roles'] )
? $saved_col['roles']
: $def_col['roles'];
if ( $key === 'lr_slug' ) {
$out['columns'][ $key ]['truncate'] = isset( $saved_col['truncate'] )
? (bool) $saved_col['truncate']
: $def_col['truncate'];
$out['columns'][ $key ]['truncate_length'] = isset( $saved_col['truncate_length'] )
? max( 10, (int) $saved_col['truncate_length'] )
: $def_col['truncate_length'];
}
}
return $out;
}
function lr_col_user_can_see( array $col ): bool {
$roles = $col['roles'] ?? [];
if ( empty( $roles ) ) {
return true;
}
$user = wp_get_current_user();
foreach ( $roles as $role ) {
if ( in_array( $role, (array) $user->roles, true ) ) {
return true;
}
}
return false;
}
// =============================================================================
// HELPERS
// =============================================================================
function lr_columns_register( string $post_type, callable $register_cb, callable $output_cb ): void {
if ( 'post' === $post_type ) {
add_filter( 'manage_posts_columns', $register_cb );
add_action( 'manage_posts_custom_column', $output_cb, 10, 2 );
} elseif ( 'page' === $post_type ) {
add_filter( 'manage_pages_columns', $register_cb );
add_action( 'manage_pages_custom_column', $output_cb, 10, 2 );
} else {
add_filter( "manage_{$post_type}_posts_columns", $register_cb );
add_action( "manage_{$post_type}_posts_custom_column", $output_cb, 10, 2 );
}
}
// =============================================================================
// 1. FEATURED IMAGE
// =============================================================================
function lr_col_image_register( array $columns ): array {
$s = lr_col_settings();
if ( ! $s['columns']['lr_featured_image']['enabled'] || ! lr_col_user_can_see( $s['columns']['lr_featured_image'] ) ) {
return $columns;
}
$new = [];
foreach ( $columns as $key => $title ) {
if ( 'title' === $key ) {
$new['lr_featured_image'] = 'Image';
}
$new[ $key ] = $title;
}
return $new;
}
function lr_col_image_output( string $column, int $post_id ): void {
if ( 'lr_featured_image' !== $column ) {
return;
}
$s = lr_col_settings();
$image_src = $s['fallback_image'];
if ( function_exists( 'lr_get_enhanced_featured_img' ) ) {
$enhanced = lr_get_enhanced_featured_img( $post_id );
if ( $enhanced ) {
$image_src = isset( $enhanced['img'] )
? $enhanced['img']['sizes']['thumbnail']
: $enhanced['sizes']['thumbnail'];
}
} else {
$thumb_id = get_post_thumbnail_id( $post_id );
if ( $thumb_id ) {
$src = wp_get_attachment_image_src( $thumb_id, 'thumbnail' );
if ( $src ) {
$image_src = $src[0];
}
}
}
if ( ! $image_src ) {
return;
}
printf(
'<img src="%s" alt="%s" decoding="async" width="40" height="40">',
esc_url( $image_src ),
esc_attr( get_the_title( $post_id ) )
);
}
// =============================================================================
// 2. URL PATH
// =============================================================================
function lr_col_slug_register( array $columns ): array {
$s = lr_col_settings();
if ( ! $s['columns']['lr_slug']['enabled'] || ! lr_col_user_can_see( $s['columns']['lr_slug'] ) ) {
return $columns;
}
$screen = get_current_screen();
if ( ! $screen || ! is_post_type_viewable( $screen->post_type ) ) {
return $columns;
}
$columns['lr_slug'] = 'URL Path';
return $columns;
}
function lr_col_slug_output( string $column, int $post_id ): void {
if ( 'lr_slug' !== $column ) {
return;
}
static $home = null;
if ( null === $home ) {
$home = get_home_url();
}
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
$col_s = lr_col_settings()['columns']['lr_slug'];
$truncate = (bool) $col_s['truncate'];
$limit = max( 10, (int) $col_s['truncate_length'] );
$maybe_truncate = static function( string $path ) use ( $truncate, $limit ): string {
if ( $truncate && mb_strlen( $path ) > $limit ) {
return mb_substr( $path, 0, $limit ) . '…';
}
return $path;
};
if ( in_array( $post->post_status, [ 'draft', 'pending', 'future' ], true ) ) {
$sample = get_sample_permalink( $post_id );
if ( ! empty( $sample[0] ) ) {
$path = str_replace( [ $home, '%pagename%', '%postname%' ], [ '', $sample[1], $sample[1] ], $sample[0] );
printf(
'<span style="color:#999;" title="%1$s"><code>%2$s</code></span>',
esc_attr( $path ),
esc_html( $maybe_truncate( $path ) )
);
}
} else {
$full_url = get_permalink( $post_id );
$path = urldecode( str_replace( $home, '', $full_url ) );
printf(
'<a href="%1$s" target="_blank" title="%2$s"><code>%3$s</code></a>',
esc_url( $full_url ),
esc_attr( $path ),
esc_html( $maybe_truncate( $path ) )
);
}
}
// =============================================================================
// 3. MODIFIED DATE
// =============================================================================
function lr_col_modified_register( array $columns ): array {
$s = lr_col_settings();
if ( ! $s['columns']['lr_modified']['enabled'] || ! lr_col_user_can_see( $s['columns']['lr_modified'] ) ) {
return $columns;
}
$new = [];
foreach ( $columns as $key => $label ) {
$new[ $key ] = $label;
if ( 'date' === $key ) {
$new['lr_modified'] = 'Modified';
}
}
if ( ! isset( $new['lr_modified'] ) ) {
$new['lr_modified'] = 'Modified';
}
return $new;
}
function lr_col_modified_output( string $column, int $post_id ): void {
if ( 'lr_modified' !== $column ) {
return;
}
printf(
'<div class="lr-mod-date"><em>%s %s</em><br><small>by <strong>%s</strong></small></div>',
esc_html( get_post_modified_time( 'Y/m/d', false, $post_id ) ),
esc_html( get_post_modified_time( 'g:i a', false, $post_id ) ),
esc_html( lr_col_modified_get_author( $post_id ) )
);
}
function lr_col_modified_get_author( int $post_id ): string {
static $cache = [];
$resolve = static function( int $uid ) use ( &$cache ): ?string {
if ( ! $uid ) return null;
if ( ! isset( $cache[ $uid ] ) ) {
$u = get_userdata( $uid );
$cache[$uid] = $u ? $u->display_name : null;
}
return $cache[ $uid ];
};
$name = $resolve( (int) get_post_meta( $post_id, '_edit_last', true ) );
if ( $name ) return $name;
foreach ( wp_get_post_revisions( $post_id, [ 'posts_per_page' => 5, 'order' => 'DESC', 'orderby' => 'date' ] ) as $rev ) {
if ( str_contains( $rev->post_name, 'autosave' ) ) continue;
$name = $resolve( (int) $rev->post_author );
if ( $name ) return $name;
}
$post = get_post( $post_id );
if ( $post ) {
$name = $resolve( (int) $post->post_author );
if ( $name ) return $name;
}
return 'Unknown';
}
// Sortable
foreach ( [ 'post', 'page' ] as $_pt ) {
add_filter( "manage_edit-{$_pt}_sortable_columns", fn( $c ) => array_merge( $c, [ 'lr_modified' => 'modified' ] ) );
}
add_action( 'registered_post_type', function( string $pt ): void {
if ( ! in_array( $pt, [ 'post', 'page', 'attachment' ], true ) ) {
add_filter( "manage_edit-{$pt}_sortable_columns", fn( $c ) => array_merge( $c, [ 'lr_modified' => 'modified' ] ) );
}
} );
add_action( 'pre_get_posts', function( WP_Query $q ): void {
if ( ! is_admin() || wp_doing_ajax() || ! $q->is_main_query() ) return;
$screen = get_current_screen();
if ( ! $screen || ! in_array( $screen->base, [ 'edit', 'upload' ], true ) ) return;
if ( in_array( $q->get( 'orderby' ), [ 'modified', 'lr_modified' ], true ) ) {
$q->set( 'orderby', 'modified' );
$order = strtoupper( $q->get( 'order' ) );
$q->set( 'order', in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC' );
}
} );
// =============================================================================
// 4. POST ID
// =============================================================================
function lr_col_id_register( array $columns ): array {
$s = lr_col_settings();
if ( ! $s['columns']['lr_post_id']['enabled'] || ! lr_col_user_can_see( $s['columns']['lr_post_id'] ) ) {
return $columns;
}
$columns['lr_post_id'] = 'ID';
return $columns;
}
function lr_col_id_output( string $column, int $post_id ): void {
if ( 'lr_post_id' === $column ) {
echo absint( $post_id );
}
}
// =============================================================================
// REGISTER via current_screen
// =============================================================================
add_action( 'current_screen', function (): void {
$screen = get_current_screen();
if ( ! $screen || 'edit' !== $screen->base ) return;
$pt = $screen->post_type;
lr_columns_register( $pt, 'lr_col_image_register', 'lr_col_image_output' );
lr_columns_register( $pt, 'lr_col_slug_register', 'lr_col_slug_output' );
lr_columns_register( $pt, 'lr_col_modified_register', 'lr_col_modified_output' );
lr_columns_register( $pt, 'lr_col_id_register', 'lr_col_id_output' );
} );
// =============================================================================
// CSS
// =============================================================================
add_action( 'admin_head', function (): void {
$screen = get_current_screen();
if ( ! $screen || 'edit' !== $screen->base ) return;
echo '<style>
.wp-list-table .column-title,
.wp-list-table .column-post_title { min-width: 180px; }
.column-lr_featured_image { width: 58px !important; min-width: 58px; }
.column-lr_featured_image img {
display: block;
width: 40px !important; height: 40px !important;
max-width: 40px !important; max-height: 40px !important;
object-fit: cover;
}
.column-lr_featured_image img[src*=".png"],
.column-lr_featured_image img[src*=".svg"] {
background-image:
linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7),
linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7);
background-position: 0 0, 5px 5px;
background-size: 10px 10px;
}
.column-lr_featured_image::before { opacity: 0; }
.column-lr_slug { width: 180px; min-width: 120px; max-width: 220px; }
.column-lr_slug code { background: none; padding: 0; font-size: inherit; word-break: break-all; }
.column-lr_modified { width: 150px; min-width: 130px; max-width: 170px; white-space: nowrap; }
.lr-mod-date { line-height: 1.6; }
.column-lr_post_id { width: 70px; min-width: 60px; max-width: 80px; white-space: nowrap; }
@media screen and (max-width: 1280px) { .wp-list-table .column-lr_post_id { display: none; } }
@media screen and (max-width: 1100px) { .wp-list-table .column-lr_modified, .wp-list-table .column-author { display: none; } }
@media screen and (max-width: 960px) { .wp-list-table .column-lr_slug { display: none; } }
@media screen and (max-width: 782px) { .wp-list-table .column-lr_featured_image { display: none; } }
</style>';
} );
// =============================================================================
// SETTINGS
// =============================================================================
add_action( 'admin_menu', function (): void {
add_options_page( 'LR Admin Columns', 'LR Admin Columns', 'manage_options', 'lr-admin-columns', 'lr_admin_columns_settings_page' );
} );
add_action( 'admin_init', function (): void {
register_setting( 'lr_admin_columns', 'lr_admin_columns', [ 'sanitize_callback' => 'lr_admin_columns_sanitize' ] );
} );
function lr_admin_columns_sanitize( $input ): array {
$defaults = lr_col_defaults();
$out = $defaults;
// Fallback image — save whatever came in (already a URL from the form)
$out['fallback_image'] = isset( $input['fallback_image'] )
? esc_url_raw( trim( $input['fallback_image'] ) )
: $defaults['fallback_image'];
foreach ( array_keys( $defaults['columns'] ) as $key ) {
$col = $input['columns'][ $key ] ?? [];
$out['columns'][ $key ]['enabled'] = ! empty( $col['enabled'] );
$roles = $col['roles'] ?? [];
$out['columns'][ $key ]['roles'] = is_array( $roles )
? array_map( 'sanitize_key', array_values( $roles ) )
: [];
if ( $key === 'lr_slug' ) {
$out['columns'][ $key ]['truncate'] = ! empty( $col['truncate'] );
$out['columns'][ $key ]['truncate_length'] = max( 10, min( 200, (int) ( $col['truncate_length'] ?? 40 ) ) );
}
}
return $out;
}
add_action( 'admin_enqueue_scripts', function ( string $hook ): void {
if ( 'settings_page_lr-admin-columns' === $hook ) {
wp_enqueue_media();
}
} );
// Media uploader JS — admin_footer guarantees wp.media is fully loaded
add_action( 'admin_footer', function (): void {
$screen = get_current_screen();
if ( ! $screen || 'settings_page_lr-admin-columns' !== $screen->id ) return;
?>
<script>
(function($) {
$(function() {
// Fallback image picker
var mediaFrame;
$('#lr_fallback_image_btn').on('click', function(e) {
e.preventDefault();
if ( mediaFrame ) {
mediaFrame.open();
return;
}
mediaFrame = wp.media({
title: 'Select Fallback Image',
button: { text: 'Use this image' },
multiple: false,
library: { type: 'image' }
});
mediaFrame.on('select', function() {
var att = mediaFrame.state().get('selection').first().toJSON();
$('#lr_fallback_image').val(att.url);
$('#lr_fallback_preview').attr('src', att.url).show();
});
mediaFrame.open();
});
// Clear fallback image
$('#lr_fallback_image_clear').on('click', function(e) {
e.preventDefault();
$('#lr_fallback_image').val('');
$('#lr_fallback_preview').attr('src', '').hide();
});
// Truncation length toggle
$('#lr_truncate_toggle').on('change', function() {
var $row = $('#lr_truncate_length_row');
$row.css({ opacity: this.checked ? '1' : '.4', pointerEvents: this.checked ? 'auto' : 'none' });
});
});
}(jQuery));
</script>
<?php
} );
function lr_admin_columns_settings_page(): void {
$s = lr_col_settings();
$all_roles = wp_roles()->get_names();
$col_meta = [
'lr_featured_image' => [ 'icon' => '🖼️', 'label' => 'Featured Image' ],
'lr_slug' => [ 'icon' => '🔗', 'label' => 'URL Path' ],
'lr_modified' => [ 'icon' => '🕐', 'label' => 'Modified Date' ],
'lr_post_id' => [ 'icon' => '#', 'label' => 'Post ID' ],
];
?>
<div class="wrap">
<h1>LR Admin Columns</h1>
<p style="color:#666;">Configure which columns appear on post type list screens, who can see them, and how they behave.</p>
<form method="post" action="options.php">
<?php settings_fields( 'lr_admin_columns' ); ?>
<table class="widefat" style="margin-top:20px;border-radius:4px;overflow:hidden;">
<thead>
<tr style="background:#f0f0f1;">
<th style="padding:12px 16px;width:180px;">Column</th>
<th style="padding:12px 16px;width:80px;">Show</th>
<th style="padding:12px 16px;">Visible to roles <span style="font-weight:400;color:#888;">(empty&nbsp;=&nbsp;everyone)</span></th>
<th style="padding:12px 16px;">Options</th>
</tr>
</thead>
<tbody>
<?php foreach ( $col_meta as $key => $meta ) :
$col = $s['columns'][ $key ];
$checked_roles = $col['roles'] ?? [];
?>
<tr style="border-top:1px solid #e0e0e0;">
<td style="padding:14px 16px;font-weight:600;">
<?php echo esc_html( $meta['icon'] . ' ' . $meta['label'] ); ?>
</td>
<td style="padding:14px 16px;">
<label class="lr-toggle">
<input type="checkbox"
name="lr_admin_columns[columns][<?php echo esc_attr( $key ); ?>][enabled]"
value="1" <?php checked( ! empty( $col['enabled'] ) ); ?>>
<span class="lr-toggle-slider"></span>
</label>
</td>
<td style="padding:14px 16px;">
<div style="display:flex;flex-wrap:wrap;gap:8px 16px;">
<?php foreach ( $all_roles as $role_key => $role_name ) : ?>
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;">
<input type="checkbox"
name="lr_admin_columns[columns][<?php echo esc_attr( $key ); ?>][roles][]"
value="<?php echo esc_attr( $role_key ); ?>"
<?php checked( in_array( $role_key, $checked_roles, true ) ); ?>>
<?php echo esc_html( translate_user_role( $role_name ) ); ?>
</label>
<?php endforeach; ?>
</div>
</td>
<td style="padding:14px 16px;">
<?php if ( 'lr_slug' === $key ) :
$truncate = ! empty( $col['truncate'] );
$limit = (int) ( $col['truncate_length'] ?? 40 );
?>
<label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<input type="checkbox" id="lr_truncate_toggle"
name="lr_admin_columns[columns][lr_slug][truncate]"
value="1" <?php checked( $truncate ); ?>>
Truncate long paths
</label>
<label id="lr_truncate_length_row"
style="display:flex;align-items:center;gap:6px;<?php echo $truncate ? '' : 'opacity:.4;pointer-events:none;'; ?>">
Max characters:
<input type="number" min="10" max="200" style="width:70px;"
name="lr_admin_columns[columns][lr_slug][truncate_length]"
value="<?php echo esc_attr( $limit ); ?>">
</label>
<?php else : ?>
<span style="color:#aaa;">—</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2 style="margin-top:32px;">Fallback Image</h2>
<p style="color:#666;">Shown in the Image column when a post has no featured image.</p>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<input type="text" id="lr_fallback_image"
name="lr_admin_columns[fallback_image]"
value="<?php echo esc_attr( $s['fallback_image'] ); ?>"
class="regular-text" placeholder="https://…">
<button type="button" class="button" id="lr_fallback_image_btn">Select Image</button>
<button type="button" class="button" id="lr_fallback_image_clear">Clear</button>
<img id="lr_fallback_preview"
src="<?php echo esc_url( $s['fallback_image'] ); ?>"
style="width:40px;height:40px;object-fit:cover;border:1px solid #ddd;border-radius:2px;<?php echo $s['fallback_image'] ? '' : 'display:none;'; ?>">
</div>
<h2 style="margin-top:32px;">Responsive Behaviour</h2>
<p style="color:#666;">Columns hide in priority order as the viewport narrows:</p>
<table class="widefat" style="max-width:460px;border-radius:4px;overflow:hidden;">
<thead><tr style="background:#f0f0f1;">
<th style="padding:8px 14px;">Breakpoint</th>
<th style="padding:8px 14px;">Hidden</th>
</tr></thead>
<tbody>
<tr><td style="padding:8px 14px;">≤ 1280px</td><td style="padding:8px 14px;">Post ID</td></tr>
<tr style="background:#f9f9f9;"><td style="padding:8px 14px;">≤ 1100px</td><td style="padding:8px 14px;">Modified + Author</td></tr>
<tr><td style="padding:8px 14px;">≤ 960px</td><td style="padding:8px 14px;">URL Path</td></tr>
<tr style="background:#f9f9f9;"><td style="padding:8px 14px;">≤ 782px</td><td style="padding:8px 14px;">Featured Image</td></tr>
</tbody>
</table>
<p style="margin-top:24px;"><?php submit_button( 'Save Settings', 'primary', 'submit', false ); ?></p>
</form>
</div>
<style>
.lr-toggle { position:relative; display:inline-block; width:40px; height:22px; }
.lr-toggle input { opacity:0; width:0; height:0; }
.lr-toggle-slider { position:absolute; cursor:pointer; inset:0; background:#ccc; border-radius:22px; transition:.2s; }
.lr-toggle-slider:before { content:''; position:absolute; width:16px; height:16px; left:3px; bottom:3px; background:#fff; border-radius:50%; transition:.2s; }
.lr-toggle input:checked + .lr-toggle-slider { background:#2271b1; }
.lr-toggle input:checked + .lr-toggle-slider:before { transform:translateX(18px); }
</style>
<?php
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment