diff --git a/src/wp-includes/block-template.php b/src/wp-includes/block-template.php
index 85ad05cfc91ae..af2ed79fa6c30 100644
--- a/src/wp-includes/block-template.php
+++ b/src/wp-includes/block-template.php
@@ -233,6 +233,50 @@ function _block_template_render_title_tag() {
echo '
' . wp_get_document_title() . '' . "\n";
}
+/**
+ * Prints the opening of the document a block theme renders into.
+ *
+ * Block themes have no header.php, so the markup that opens their documents lives in the
+ * template canvas rather than in the theme. Printing it from here lets anything that has to
+ * open a document on a block theme's behalf produce exactly what the canvas produces.
+ *
+ * @since 7.1.0
+ * @access private
+ */
+function _wp_block_theme_document_start() {
+ echo '' . "\n";
+ echo '\n";
+ echo "\n";
+ echo "\t" . '' . "\n";
+ echo "\t";
+ wp_head();
+ echo "\n";
+ echo "\n";
+ echo '\n";
+ wp_body_open();
+}
+
+/**
+ * Prints the closing of the document a block theme renders into.
+ *
+ * The counterpart to _wp_block_theme_document_start().
+ *
+ * @since 7.1.0
+ * @access private
+ */
+function _wp_block_theme_document_end() {
+ echo "\n";
+ wp_footer();
+ echo "\n";
+ echo "\n";
+}
+
/**
* Returns the markup for the current template.
*
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index 29c30137e173d..c7c0df5cd9fb5 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -45,6 +45,19 @@ function get_header( $name = null, $args = array() ) {
$templates[] = 'header.php';
+ /*
+ * A block theme has no header.php by design, so locate_template() falls through to
+ * wp-includes/theme-compat/header.php: markup deprecated since 3.0.0 that opens the
+ * document with the old default theme's page frame and its site title and description.
+ * Print the same document opening the template canvas uses instead, so a block theme
+ * gets its own markup whether the page is rendered by a block template or by a caller
+ * of this function.
+ */
+ if ( wp_is_block_theme() && ! _wp_theme_has_template( $templates ) ) {
+ _wp_block_theme_document_start();
+ return;
+ }
+
if ( ! locate_template( $templates, true, true, $args ) ) {
return false;
}
@@ -89,6 +102,12 @@ function get_footer( $name = null, $args = array() ) {
$templates[] = 'footer.php';
+ // See get_header(): block themes close their document the way the canvas does.
+ if ( wp_is_block_theme() && ! _wp_theme_has_template( $templates ) ) {
+ _wp_block_theme_document_end();
+ return;
+ }
+
if ( ! locate_template( $templates, true, true, $args ) ) {
return false;
}
diff --git a/src/wp-includes/template-canvas.php b/src/wp-includes/template-canvas.php
index 8f631e5a77dfb..1631832b91fd6 100644
--- a/src/wp-includes/template-canvas.php
+++ b/src/wp-includes/template-canvas.php
@@ -10,18 +10,10 @@
* This needs to run before so that blocks can add scripts and styles in wp_head().
*/
$template_html = get_the_block_template_html();
-?>
->
-
-
-
-
->
-
+_wp_block_theme_document_start();
+?>
-
-
-
-
+assertNull( get_footer() );
}
+ /**
+ * Tests that a block theme opens its own document rather than the deprecated fallback.
+ *
+ * A block theme ships no header.php, so locate_template() reaches
+ * wp-includes/theme-compat/header.php, whose markup opens the page with the old default
+ * theme's frame, site title and description.
+ *
+ * @ticket 55023
+ *
+ * @covers ::get_header
+ */
+ public function test_get_header_uses_block_theme_document_when_theme_has_no_header() {
+ switch_theme( 'block-theme' );
+
+ $output = get_echo( 'get_header' );
+
+ $this->assertStringContainsString( '', $output, 'The document should be opened.' );
+ $this->assertStringContainsString( 'assertStringNotContainsString( 'id="page"', $output, 'The theme-compat page frame should not be printed.' );
+ $this->assertStringNotContainsString( 'id="header"', $output, 'The theme-compat header should not be printed.' );
+ }
+
+ /**
+ * Tests that a block theme closes the document it opened.
+ *
+ * @ticket 55023
+ *
+ * @covers ::get_footer
+ */
+ public function test_get_footer_uses_block_theme_document_when_theme_has_no_footer() {
+ switch_theme( 'block-theme' );
+
+ // As a page does: wp_head() is where the back-compat skip link action is unhooked.
+ get_echo( 'get_header' );
+ $output = get_echo( 'get_footer' );
+
+ $this->assertStringContainsString( '', $output, 'The body element should be closed.' );
+ $this->assertStringContainsString( '', $output, 'The document should be closed.' );
+ $this->assertStringNotContainsString( 'proudly powered by', $output, 'The theme-compat footer should not be printed.' );
+ $this->assertStringNotContainsString( 'id="footer"', $output, 'The theme-compat footer should not be printed.' );
+ }
+
+ /**
+ * Tests that the deprecation notice for a missing header.php is not raised for block themes,
+ * which are not expected to provide one.
+ *
+ * @ticket 55023
+ *
+ * @covers ::get_header
+ */
+ public function test_get_header_does_not_deprecate_for_block_theme() {
+ switch_theme( 'block-theme' );
+
+ // Would trigger the deprecation notice handled by WP_UnitTestCase and fail the test.
+ get_echo( 'get_header' );
+
+ $this->assertTrue( wp_is_block_theme(), 'The test should run on a block theme.' );
+ }
+
+ /**
+ * Tests that a classic theme without header.php keeps the deprecated fallback, so themes
+ * relying on it are unaffected. camelCase is a classic fixture theme with no header.php.
+ *
+ * @ticket 55023
+ *
+ * @expectedDeprecated Theme without header.php
+ *
+ * @covers ::get_header
+ */
+ public function test_get_header_still_falls_back_for_classic_theme() {
+ switch_theme( 'camelCase' );
+
+ $output = get_echo( 'get_header' );
+
+ $this->assertStringContainsString( 'id="page"', $output, 'The theme-compat markup should still be printed for classic themes.' );
+ }
+
/**
* @ticket 40969
*