Make WordPress Core

Opened 10 years ago

Last modified 3 days ago

#37376 new feature request

Make it possible for custom post type to have an archive but no single

Reported by: jason_the_adams's profile jason_the_adams Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version: 4.6
Component: Posts, Post Types Keywords: needs-unit-tests has-patch
Focuses: Cc:

Description

Very commonly we'll have testimonials, faqs, and such custom post types, which warrant an archive, but don't make sense to have a single. It's possible to have a post type with no archive and a single, but not the other way around. Funnily enough, I find that we far more often could use what isn't available versus what is.

There are workarounds. One option is to make it a non-publicly-queryable post type and use a page template or something to display the results. Another option I've seen around is to hook into template_redirect on the single and redirect the user. Chances are there's no links on the front-end, so this really only occurs when users click on the link from edit-post admin-side.

Another thing to consider here is SEO plugins and the like which look to the definition of the CPT to build the sitemap. The redirect method ends up putting useless links in the sitemap. The page template method works fine, but obviously means the CPT has to be queried separately.

Would there be a downside to adding a sibling has_single option? If it's true by default then it's backwards compatible, and the single rewrites simply reflect the option.

Attachments (2)

37376.diff (4.4 KB) - added by jason_the_adams 8 years ago.
37376.2.patch (7.0 KB) - added by bacoords 3 days ago.
Updated patch based on the latest trunk changes

Download all attachments as: .zip

Change History (8)

#1 @swissspidy
10 years ago

  • Component changed from General to Posts, Post Types

#2 @jason_the_adams
8 years ago

  • Keywords has-unit-tests added

@swissspidy Added a first pass patch to better clarify what I'm thinking. Also added string support for has_single, similar to has_archive, to make it possible to explicitly set the root url for singles. With that it would be possible to have a unique post_name, archive url, and single url.

Please take a look at this and let me know if this is in the right direction. If it is, I'll start to put together tests.

#3 @jason_the_adams
8 years ago

  • Keywords needs-unit-tests added; has-unit-tests removed

#4 @jason_the_adams
8 years ago

  • Keywords has-patch added

This ticket was mentioned in Slack in #core by peterwilsoncc. View the logs.


5 years ago

@bacoords
3 days ago

Updated patch based on the latest trunk changes

#6 @bacoords
3 days ago

I've been working on refreshing this patch and wanted to document some findings about the broader implications.

Core Approach

The patch adds a has_single property (boolean, default true) to register_post_type(). When false:

  • No rewrite rules are generated for single post views
  • get_permalink() / get_post_permalink() return false
  • wp_get_shortlink() returns empty string

The is_post_type_viewable() Problem

A key decision: is_post_type_viewable() intentionally does NOT check has_single.

This function is used for two distinct purposes:

  1. "Can users view this post type on the front-end?" (REST API, Query Loop, archives)
  2. "Does this post type have single post views?" (View links, sitemaps, permalinks)

If we made is_post_type_viewable() return false for has_single => false post types, it would break:

  • REST API public access
  • Query Loop blocks
  • Archive pages
  • Any code checking general "publicness"

Instead, places that specifically care about single views must check both is_post_type_viewable() AND has_single:

if ( is_post_type_viewable( $post_type ) && $post_type->has_single ) {
    // Show view link, add to sitemap, etc.
}

An alternate approach is a new helper function that works like is_post_type_viewable() maybe.

The Permalink Assumption

WordPress core assumes get_permalink() always returns a URL string. This patch changes that - it can now return false. Places generating links must check:

$permalink = get_permalink( $post );
if ( $permalink ) {
    echo '<a href="' . esc_url( $permalink ) . '">View</a>';
}

The issue here is that this feels like a breaking change, so maybe just the default behavior (returning an empty string) is more realistic, and less fragile.

Files Changed in This Patch

Post type registration:

  • class-wp-post-type.php - Adds property, skips single rewrite rules

Permalink functions:

  • link-template.php - get_post_permalink(), wp_get_shortlink(), get_preview_post_link() return false/empty

Admin UI:

  • edit-form-advanced.php - Hides preview/view links in editor
  • meta-boxes.php - Hides Preview button
  • class-wp-posts-list-table.php - Hides View/Preview row actions

REST API:

  • class-wp-rest-posts-controller.php - No alternate link header, no permalink_template/generated_slug fields

Sitemaps:

  • class-wp-sitemaps-posts.php - Excludes post types with has_single => false

Gutenberg PR Required

Many core blocks also assume permalinks exist for all post types. These blocks need updates in the Gutenberg repo:

  • post-title - Link option
  • post-excerpt - "Read more" link
  • post-featured-image - Link option
  • post-date - Link option
  • read-more - Entire block purpose is linking
  • latest-posts - Post title links
  • breadcrumbs - Ancestor links

The fix is straightforward - check if get_the_permalink() returns a value before wrapping in <a>. I'll open a companion PR in Gutenberg.

Testing

Register a test post type:

register_post_type( 'no_single', array(
    'label'       => 'No Single',
    'public'      => true,
    'has_archive' => true,
    'has_single'  => false,
    'show_in_rest' => true,
) );

Verify:

  • Archive page works
  • REST API returns posts
  • Query Loop displays posts
  • No "View" links in admin
  • Not in sitemaps
  • Single URLs 404 (no rewrite rules)
Last edited 3 days ago by bacoords (previous) (diff)
Note: See TracTickets for help on using tickets.