===================================================================
--- a/src/wp-admin/edit-form-advanced.php	
+++ b/src/wp-admin/edit-form-advanced.php	
@@ -113,7 +113,7 @@
 
 $preview_url = get_preview_post_link( $post );
 
-$viewable = is_post_type_viewable( $post_type_object );
+$viewable = is_post_type_viewable( $post_type_object ) && $post_type_object->has_single;
 
 if ( $viewable ) {
 

===================================================================
--- a/src/wp-admin/includes/class-wp-posts-list-table.php	
+++ b/src/wp-admin/includes/class-wp-posts-list-table.php	
@@ -1580,7 +1580,7 @@
 			}
 		}
 
-		if ( is_post_type_viewable( $post_type_object ) ) {
+		if ( is_post_type_viewable( $post_type_object ) && $post_type_object->has_single ) {
 			if ( in_array( $post->post_status, array( 'pending', 'draft', 'future' ), true ) ) {
 				if ( $can_edit_post ) {
 					$preview_link    = get_preview_post_link( $post );

===================================================================
--- a/src/wp-admin/includes/meta-boxes.php	
+++ b/src/wp-admin/includes/meta-boxes.php	
@@ -62,7 +62,7 @@
 		</div>
 
 		<?php
-		if ( is_post_type_viewable( $post_type_object ) ) :
+		if ( is_post_type_viewable( $post_type_object ) && $post_type_object->has_single ) :
 			?>
 			<div id="preview-action">
 				<?php

===================================================================
--- a/src/wp-includes/class-wp-post-type.php	
+++ b/src/wp-includes/class-wp-post-type.php	
@@ -247,6 +247,16 @@
 	public $has_archive = false;
 
 	/**
+	 * Whether there should be post type singles.
+	 *
+	 * Will generate the proper rewrite rules if $rewrite is enabled. Default true.
+	 *
+	 * @since 6.9.0
+	 * @var bool $has_single
+	 */
+	public $has_single = true;
+
+	/**
 	 * Sets the query_var key for this post type.
 	 *
 	 * Defaults to $post_type key. If false, a post type cannot be loaded at `?{query_var}={post_slug}`.
@@ -546,6 +556,7 @@
 			'register_meta_box_cb'            => null,
 			'taxonomies'                      => array(),
 			'has_archive'                     => false,
+			'has_single'                      => true,
 			'rewrite'                         => true,
 			'query_var'                       => true,
 			'can_export'                      => true,
@@ -716,10 +727,12 @@
 		}
 
 		if ( false !== $this->rewrite && ( is_admin() || get_option( 'permalink_structure' ) ) ) {
-			if ( $this->hierarchical ) {
-				add_rewrite_tag( "%$this->name%", '(.+?)', $this->query_var ? "{$this->query_var}=" : "post_type=$this->name&pagename=" );
-			} else {
-				add_rewrite_tag( "%$this->name%", '([^/]+)', $this->query_var ? "{$this->query_var}=" : "post_type=$this->name&name=" );
+			if ( $this->has_single ) {
+				if ( $this->hierarchical ) {
+					add_rewrite_tag( "%$this->name%", '(.+?)', $this->query_var ? "{$this->query_var}=" : "post_type=$this->name&pagename=" );
+				} else {
+					add_rewrite_tag( "%$this->name%", '([^/]+)', $this->query_var ? "{$this->query_var}=" : "post_type=$this->name&name=" );
+				}
 			}
 
 			if ( $this->has_archive ) {

===================================================================
--- a/src/wp-includes/link-template.php	
+++ b/src/wp-includes/link-template.php	
@@ -319,7 +319,7 @@
  * @param int|WP_Post $post      Optional. Post ID or post object. Default is the global `$post`.
  * @param bool        $leavename Optional. Whether to keep post name. Default false.
  * @param bool        $sample    Optional. Is it a sample permalink. Default false.
- * @return string|false The post permalink URL. False if the post does not exist.
+ * @return string|false The post permalink URL. False if the post does not exist or has no single.
  */
 function get_post_permalink( $post = 0, $leavename = false, $sample = false ) {
 	global $wp_rewrite;
@@ -332,12 +332,16 @@
 
 	$post_link = $wp_rewrite->get_extra_permastruct( $post->post_type );
 
-	$slug = $post->post_name;
-
 	$force_plain_link = wp_force_plain_post_permalink( $post );
 
 	$post_type = get_post_type_object( $post->post_type );
 
+	if ( ! $post_type->has_single ) {
+		return false;
+	}
+
+	$slug = $post->post_name;
+
 	if ( $post_type->hierarchical ) {
 		$slug = get_page_uri( $post );
 	}
@@ -1413,7 +1417,7 @@
 	}
 
 	$post_type_object = get_post_type_object( $post->post_type );
-	if ( is_post_type_viewable( $post_type_object ) ) {
+	if ( is_post_type_viewable( $post_type_object ) && $post_type_object->has_single ) {
 		if ( ! $preview_link ) {
 			$preview_link = set_url_scheme( get_permalink( $post ) );
 		}
@@ -4187,6 +4191,10 @@
 	if ( ! empty( $post_id ) ) {
 		$post_type = get_post_type_object( $post->post_type );
 
+		if ( false === $post_type->has_single ) {
+			return '';
+		}
+
 		if ( 'page' === $post->post_type
 			&& 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID
 		) {

===================================================================
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php	
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php	
@@ -669,7 +669,8 @@
 		$data     = $this->prepare_item_for_response( $post, $request );
 		$response = rest_ensure_response( $data );
 
-		if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
+		$post_type_object = get_post_type_object( $post->post_type );
+		if ( is_post_type_viewable( $post_type_object ) && $post_type_object->has_single ) {
 			$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
 		}
 
@@ -2115,7 +2116,7 @@
 		}
 
 		$post_type_obj = get_post_type_object( $post->post_type );
-		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
+		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public && $post_type_obj->has_single ) {
 			$permalink_template_requested = rest_is_field_included( 'permalink_template', $fields );
 			$generated_slug_requested     = rest_is_field_included( 'generated_slug', $fields );
 
@@ -2507,7 +2508,7 @@
 		);
 
 		$post_type_obj = get_post_type_object( $this->post_type );
-		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
+		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public && $post_type_obj->has_single ) {
 			$schema['properties']['permalink_template'] = array(
 				'description' => __( 'Permalink template for the post.' ),
 				'type'        => 'string',

===================================================================
--- a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php	
+++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php	
@@ -37,7 +37,12 @@
 		$post_types = get_post_types( array( 'public' => true ), 'objects' );
 		unset( $post_types['attachment'] );
 
-		$post_types = array_filter( $post_types, 'is_post_type_viewable' );
+		$post_types = array_filter(
+			$post_types,
+			static function ( $post_type ) {
+				return is_post_type_viewable( $post_type ) && $post_type->has_single;
+			}
+		);
 
 		/**
 		 * Filters the list of post object sub types available within the sitemap.

