I have a CPT that gets its data by reading from an external server. The external server is the master of the data and my plugin stores a replication without an ability to update the master, therefor the ability to edit the content will just confuse the user, might even make him think that the master will be updated by the edits.

I basically wish to reuse the CPT admin framework but leave only the delete link (maybe not even that), even for administrators.

I can probably do it with CSS but looking for other options that feel less of a hack.

This is for esthetics of UI and not security, so I don't care that it is possible to work around this.

2 Answers 2


I did something similar recently for a simple email log (for diagnosing some problems). I don't have time to dissect the code, but hopefully it'll be clear enough for you.

It creates a CPT for storing data that can be viewed in the admin (only), and allows the user to view and delete each post. NB: delete, not trash -- no need for resurrection of deleted posts. Edit is changed to View, and metaboxes are replaced with a view template. This code is obviously missing the bits that write to the CPT and auto-purge old ones, but should be enough for your question.

* custom post type for email log
class LogEmailsPostTypeLog {

    * hooks
    public function __construct() {
        add_action('admin_init', array($this, 'init'));
        add_action('init', array($this, 'register'));

    * admin_init action
    public function init() {
        global $typenow;

        if (empty($typenow)) {
            // try to pick it up from the query string
            if (!empty($_GET['post'])) {
                $post = get_post($_GET['post']);
                $typenow = $post->post_type;

        if ($typenow == 'log_emails_log') {
            add_filter('display_post_states', '__return_false');
            add_action('edit_form_after_title', array($this, 'adminEditAfterTitle'), 100);
            add_filter('post_row_actions', array($this, 'adminPostRowActions'), 10, 2);
            add_filter('bulk_actions-edit-log_emails_log', array($this, 'adminBulkActionsEdit'));
            add_filter('manage_log_emails_log_posts_columns', array($this, 'adminManageColumns'));
            add_action('manage_log_emails_log_posts_custom_column', array($this, 'adminManageCustomColumn'), 10, 2);
            add_action('admin_print_footer_scripts', array($this, 'adminPrintFooterScripts'));

            add_action('in_admin_header', array($this, 'adminScreenLayout'));
            add_filter('views_edit-log_emails_log', array($this, 'adminViewsEdit'));

            if (is_admin()) {
                add_filter('gettext', array($this, 'adminGetText'), 10, 3);


    * register Custom Post Type
    public function register() {
        // register the post type
        register_post_type('log_emails_log', array(
            'labels' => array (
                'name' => __('Email Logs', 'log-emails'),
                'singular_name' => __('Email Log', 'log-emails'),
                'add_new_item' => __('Add New Email Log', 'log-emails'),
                'edit_item' => __('View Email Log', 'log-emails'),
                'new_item' => __('New Email Log', 'log-emails'),
                'view_item' => __('View Email Log', 'log-emails'),
                'search_items' => __('Search Email Log', 'log-emails'),
                'not_found' => __('No email logs found', 'log-emails'),
                'not_found_in_trash' => __('No email logs found in Trash', 'log-emails'),
                'parent_item_colon' => __('Parent email logs', 'log-emails'),
            'description' => __('Email Logs, as a custom post type', 'log-emails'),
            'exclude_from_search' => true,
            'publicly_queryable' => false,
            'public' => false,
            'show_ui' => true,
            'show_in_admin_bar' => false,
            'menu_position' => 75,
            'hierarchical' => false,
            'has_archive' => false,
            'supports' => array('nada'),
            'rewrite' => false,
            'can_export' => false,
            'capabilities' => array (
                'create_posts' => false,
                'edit_post' => 'manage_options',
                'read_post' => 'manage_options',
                'delete_post' => 'manage_options',
                'edit_posts' => 'manage_options',
                'edit_others_posts' => 'manage_options',
                'publish_posts' => 'manage_options',
                'read_private_posts' => 'manage_options',

    * change some text on admin pages
    * @param string $translation
    * @param string $text
    * @param string $domain
    * @return string
    public function adminGetText($translation, $text, $domain) {
        if ($domain == 'default') {
            if ($text == 'Edit “%s”') {
                $translation = 'View “%s”';

        return $translation;

    * remove views we don't need from post list
    * @param array $views
    * @return array
    public function adminViewsEdit($views) {

        return $views;

    * remove unwanted actions from post list
    * @param array $actions
    * @param WP_Post $post
    * @return array
    public function adminPostRowActions($actions, $post) {
        unset($actions['inline hide-if-no-js']);        // "quick edit"

        if ($post && $post->ID) {
            // add View link
            $actions['view'] = sprintf('<a href="%s" title="%s">%s</a>',
                __('View', 'log-emails'), __('View', 'log-emails'));

            // add Delete link
            $actions['delete'] = sprintf('<a href="%s" title="%s" class="submitdelete">%s</a>',
                get_delete_post_link($post->ID, '', true),
                __('Delete', 'log-emails'), __('Delete', 'log-emails'));

        return $actions;

    * change the list of available bulk actions
    * @param array $actions
    * @return array
    public function adminBulkActionsEdit($actions) {

        return $actions;

    * filter to add columns to post list
    * @param array $posts_columns
    * @return array
    public function adminManageColumns($posts_columns) {
        $posts_columns['title'] = 'Subject';
        $posts_columns['_log_emails_log_to'] = 'Recipients';

        return $posts_columns;

    * action to add custom columns to post list
    * @param string $column_name
    * @param int $post_id
    public function adminManageCustomColumn($column_name, $post_id) {
        switch ($column_name) {
            case '_log_emails_log_to':
                $post = get_post($post_id);
                if ($post) {
                    echo htmlspecialchars(get_post_meta($post_id, '_log_emails_log_to', true));

    * change the screen layout
    public function adminScreenLayout() {
        // set max / default layout as single column
        add_screen_option('layout_columns', array('max' => 1, 'default' => 1));

    * drop all the metaboxes and output what we want to show
    public function adminEditAfterTitle($post) {
        global $wp_meta_boxes;

        // remove all meta boxes
        $wp_meta_boxes = array('log_emails_log' => array(
            'advanced' => array(),
            'side' => array(),
            'normal' => array(),

        // show my admin form
        require LOG_EMAILS_PLUGIN_ROOT . 'views/log-detail.php';

    * replace Trash bulk actions with Delete
    * NB: WP admin already handles the delete action, it just doesn't expose it as a bulk action
    public function adminPrintFooterScripts() {

        jQuery("select[name='action'],select[name='action2']").find("option[value='trash']").each(function() {
            this.value = 'delete';
            jQuery(this).text("<?php esc_attr_e('Delete', 'log-emails'); ?>");


  • This is an amazing solution. With a few minor changes, I was able to use it with my user posted contact form. Aug 3, 2017 at 12:41

You can replace the editor with a read-only meta box.

// use the action to create a place for your meta box
function add_before_editor($post) {
  if ('post' == $post->post_type) {
    do_meta_boxes('post', 'read_only_content', $post);

// add a box the location just created
function read_only_content_box() {
    'read_only_content_box', // id, used as the html id att
    __( 'Post Content (Read Only)' ), // meta box title
    'read_only_cb', // callback function, spits out the content
    'post', // post type or page. This adds to posts only
    'read_only_content', // context, where on the screen
    'low' // priority, where should this go in the context
function read_only_cb($post) {
  echo apply_filters('the_content',$post->post_content);
add_action( 'add_meta_boxes', 'read_only_content_box' );

I am assuming that you are disabling "save" functionality for this post type. If not-- if you need some parts to save just not the main content-- you may need to hook into the form processing to prevent someone from hacking around this. As is, the post content is not overwritten but it would not be that hard to hack around that.

To completely remove edit capability, use the capability argument when you register your post type. Set that argument to read_post and the whole type become "read only". You will also want 'show_in_admin_bar' => false,. Sample argument array:

  $args = array(
    'labels'             => $labels,
    'public'             => true,
    'publicly_queryable' => true,
    'show_ui'            => true,
    'show_in_menu'       => true,
    'query_var'          => true,
    'rewrite'            => array( 'slug' => 'book' ),
    'capability_type'    => 'post',
    'capabilities'       => array('read_post'),
    'show_in_admin_bar'  => false,
    'has_archive'        => true,
    'hierarchical'       => false,
  • this is the "easy" part, I would actually prefer to block the ability to enter those pages at all. At the minimum if you cant edit the content there should not be an "edit" link for the "posts" table page. Nov 30, 2013 at 9:42
  • @MarkKaplun : see the edit. There is more to a post than just the body content. There is meta data, thumbnails, publication scheduling... That is why I thought you wanted to only limit the editing of the post body. I added a more aggressive solution.
    – s_ha_dum
    Nov 30, 2013 at 16:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.