Commit 87bb5634ca for wordpress.org

commit 87bb5634ca3781437a193d04d02cbfb05c35016e
Author: Weston Ruter <weston@xwp.co>
Date:   Sun Jan 11 06:36:49 2026 +0000

    Themes: Use `WP_HTML_Tag_Processor` to insert the block template skip link instead of JavaScript.

    * The skip link now works when JavaScript is turned off.
    * By removing the script, the amount of JavaScript sent to the client is reduced for a very marginal performance improvement.
    * A new `wp-block-template-skip-link` stylesheet is registered, with minification and `path` data for inlining.
    * The CSS for the skip link now has an RTL version generated, although it is not yet served when the styles are inlined. See #61625.
    * The `wp_enqueue_block_template_skip_link()` function now exclusively enqueues the stylesheet since the script is removed.
    * For backwards-compatibility, the skip link will continue to be omitted if `the_block_template_skip_link()` is unhooked from the `wp_footer` action or `wp_enqueue_block_template_skip_link()` is unhooked from `wp_enqueue_scripts`.

    Developed in https://github.com/WordPress/wordpress-develop/pull/10676

    Follow-up to [56932], [51003].

    Props rutviksavsani, westonruter, dmsnell, whiteshadow01, Slieptsov.
    See #59505, #53176.
    Fixes #64361.

    Built from https://develop.svn.wordpress.org/trunk@61469


    git-svn-id: http://core.svn.wordpress.org/trunk@60781 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/block-template.php b/wp-includes/block-template.php
index df3735b238..88f9c4384a 100644
--- a/wp-includes/block-template.php
+++ b/wp-includes/block-template.php
@@ -301,7 +301,102 @@ function get_the_block_template_html() {

 	// Wrap block template in .wp-site-blocks to allow for specific descendant styles
 	// (e.g. `.wp-site-blocks > *`).
-	return '<div class="wp-site-blocks">' . $content . '</div>';
+	$template_html = '<div class="wp-site-blocks">' . $content . '</div>';
+
+	// Back-compat for plugins that disable functionality by unhooking one of these actions.
+	if (
+		! has_action( 'wp_footer', 'the_block_template_skip_link' ) ||
+		! has_action( 'wp_enqueue_scripts', 'wp_enqueue_block_template_skip_link' )
+	) {
+		return $template_html;
+	}
+
+	return _block_template_add_skip_link( $template_html );
+}
+
+/**
+ * Inserts the block template skip-link into the template HTML.
+ *
+ * When a `MAIN` element exists in the template, this function will ensure
+ * that the element contains an `id` attribute, and it will insert a link to
+ * that `MAIN` element before the first `DIV.wp-site-blocks` element, which
+ * is the wrapper for all blocks in a block template as constructed by
+ * {@see get_the_block_template_html()}.
+ *
+ * Example:
+ *
+ *     // Input.
+ *     <div class="wp-site-blocks">
+ *         <nav>...</nav>
+ *         <main>
+ *             <h2>...
+ *
+ *     // Output.
+ *     <a href="#wp--skip-link--target" id="wp-skip-link" class="...">
+ *     <div class="wp-site-blocks">
+ *         <nav>...</nav>
+ *         <main id="wp--skip-link--target">
+ *             <h2>...
+ *
+ * When the `MAIN` element already contains a non-empty `id` value it will be
+ * used instead of the default skip-link id.
+ *
+ * @access private
+ * @since 7.0.0
+ *
+ * @param string $template_html Block template markup.
+ * @return string Modified markup with skip link when applicable.
+ */
+function _block_template_add_skip_link( string $template_html ): string {
+	// Anonymous subclass of WP_HTML_Tag_Processor to access protected bookmark spans.
+	$processor = new class( $template_html ) extends WP_HTML_Tag_Processor {
+		/**
+		 * Inserts text before the current token.
+		 *
+		 * @param string $text Text to insert.
+		 */
+		public function insert_before( string $text ) {
+			$this->set_bookmark( 'here' );
+			$this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->bookmarks['here']->start, 0, $text );
+		}
+	};
+
+	// Find and bookmark the first DIV.wp-site-blocks.
+	if (
+		! $processor->next_tag(
+			array(
+				'tag_name'   => 'DIV',
+				'class_name' => 'wp-site-blocks',
+			)
+		)
+	) {
+		return $template_html;
+	}
+	$processor->set_bookmark( 'skip_link_insertion_point' );
+
+	// Ensure the MAIN element has an ID.
+	if ( ! $processor->next_tag( 'MAIN' ) ) {
+		return $template_html;
+	}
+
+	$skip_link_target_id = $processor->get_attribute( 'id' );
+	if ( ! is_string( $skip_link_target_id ) || '' === $skip_link_target_id ) {
+		$skip_link_target_id = 'wp--skip-link--target';
+		$processor->set_attribute( 'id', $skip_link_target_id );
+	}
+
+	// Seek back to the bookmarked insertion point.
+	$processor->seek( 'skip_link_insertion_point' );
+
+	$skip_link = sprintf(
+		'<a class="skip-link screen-reader-text" id="wp-skip-link" href="%s">%s</a>',
+		esc_url( '#' . $skip_link_target_id ),
+		/* translators: Hidden accessibility text. */
+		esc_html__( 'Skip to content' )
+	);
+	$processor->insert_before( $skip_link );
+
+	return $processor->get_updated_html();
 }

 /**
diff --git a/wp-includes/css/dist/index.php b/wp-includes/css/dist/index.php
index c1720de876..1b98c542ab 100644
--- a/wp-includes/css/dist/index.php
+++ b/wp-includes/css/dist/index.php
@@ -42,16 +42,16 @@ return array(
 		'path' => 'patterns/style',
 		'dependencies' => array('wp-block-editor', 'wp-components'),
 	),
-	array(
-		'handle' => 'wp-components',
-		'path' => 'components/style',
-		'dependencies' => array(),
-	),
 	array(
 		'handle' => 'wp-format-library',
 		'path' => 'format-library/style',
 		'dependencies' => array('wp-block-editor', 'wp-components'),
 	),
+	array(
+		'handle' => 'wp-components',
+		'path' => 'components/style',
+		'dependencies' => array(),
+	),
 	array(
 		'handle' => 'wp-media-utils',
 		'path' => 'media-utils/style',
@@ -67,11 +67,6 @@ return array(
 		'path' => 'customize-widgets/style',
 		'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-preferences', 'wp-widgets'),
 	),
-	array(
-		'handle' => 'wp-block-library',
-		'path' => 'block-library/style',
-		'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'),
-	),
 	array(
 		'handle' => 'wp-edit-widgets',
 		'path' => 'edit-widgets/style',
@@ -82,19 +77,24 @@ return array(
 		'path' => 'edit-post/style',
 		'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-commands', 'wp-components', 'wp-editor', 'wp-preferences', 'wp-widgets'),
 	),
+	array(
+		'handle' => 'wp-block-library',
+		'path' => 'block-library/style',
+		'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'),
+	),
 	array(
 		'handle' => 'wp-editor',
 		'path' => 'editor/style',
 		'dependencies' => array('wp-block-editor', 'wp-commands', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences'),
 	),
-	array(
-		'handle' => 'wp-block-editor',
-		'path' => 'block-editor/style',
-		'dependencies' => array('wp-commands', 'wp-components', 'wp-preferences'),
-	),
 	array(
 		'handle' => 'wp-edit-site',
 		'path' => 'edit-site/style',
 		'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-commands', 'wp-components', 'wp-editor', 'wp-patterns', 'wp-preferences', 'wp-widgets'),
 	),
+	array(
+		'handle' => 'wp-block-editor',
+		'path' => 'block-editor/style',
+		'dependencies' => array('wp-commands', 'wp-components', 'wp-preferences'),
+	),
 );
diff --git a/wp-includes/css/wp-block-template-skip-link-rtl.css b/wp-includes/css/wp-block-template-skip-link-rtl.css
new file mode 100644
index 0000000000..17a074696b
--- /dev/null
+++ b/wp-includes/css/wp-block-template-skip-link-rtl.css
@@ -0,0 +1,28 @@
+/*! This file is auto-generated */
+.skip-link.screen-reader-text {
+	border: 0;
+	clip-path: inset(50%);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute !important;
+	width: 1px;
+	word-wrap: normal !important;
+}
+
+.skip-link.screen-reader-text:focus {
+	background-color: #eee;
+	clip-path: none;
+	color: #444;
+	display: block;
+	font-size: 1em;
+	height: auto;
+	right: 5px;
+	line-height: normal;
+	padding: 15px 23px 14px;
+	text-decoration: none;
+	top: 5px;
+	width: auto;
+	z-index: 100000;
+}
diff --git a/wp-includes/css/wp-block-template-skip-link-rtl.min.css b/wp-includes/css/wp-block-template-skip-link-rtl.min.css
new file mode 100644
index 0000000000..e7e1177b05
--- /dev/null
+++ b/wp-includes/css/wp-block-template-skip-link-rtl.min.css
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+.skip-link.screen-reader-text{border:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}.skip-link.screen-reader-text:focus{background-color:#eee;clip-path:none;color:#444;display:block;font-size:1em;height:auto;right:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}
\ No newline at end of file
diff --git a/wp-includes/css/wp-block-template-skip-link.css b/wp-includes/css/wp-block-template-skip-link.css
new file mode 100644
index 0000000000..4176599ad0
--- /dev/null
+++ b/wp-includes/css/wp-block-template-skip-link.css
@@ -0,0 +1,27 @@
+.skip-link.screen-reader-text {
+	border: 0;
+	clip-path: inset(50%);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute !important;
+	width: 1px;
+	word-wrap: normal !important;
+}
+
+.skip-link.screen-reader-text:focus {
+	background-color: #eee;
+	clip-path: none;
+	color: #444;
+	display: block;
+	font-size: 1em;
+	height: auto;
+	left: 5px;
+	line-height: normal;
+	padding: 15px 23px 14px;
+	text-decoration: none;
+	top: 5px;
+	width: auto;
+	z-index: 100000;
+}
diff --git a/wp-includes/css/wp-block-template-skip-link.min.css b/wp-includes/css/wp-block-template-skip-link.min.css
new file mode 100644
index 0000000000..0e7b5e49cd
--- /dev/null
+++ b/wp-includes/css/wp-block-template-skip-link.min.css
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+.skip-link.screen-reader-text{border:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}.skip-link.screen-reader-text:focus{background-color:#eee;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}
\ No newline at end of file
diff --git a/wp-includes/js/dist/script-modules/index.php b/wp-includes/js/dist/script-modules/index.php
index 1ebf6bd042..8497ba890e 100644
--- a/wp-includes/js/dist/script-modules/index.php
+++ b/wp-includes/js/dist/script-modules/index.php
@@ -17,6 +17,11 @@ return array(
 		'path' => 'interactivity-router/full-page',
 		'asset' => 'interactivity-router/full-page.min.asset.php',
 	),
+	array(
+		'id' => '@wordpress/a11y',
+		'path' => 'a11y/index',
+		'asset' => 'a11y/index.min.asset.php',
+	),
 	array(
 		'id' => '@wordpress/interactivity',
 		'path' => 'interactivity/index',
@@ -33,9 +38,9 @@ return array(
 		'asset' => 'latex-to-mathml/loader.min.asset.php',
 	),
 	array(
-		'id' => '@wordpress/a11y',
-		'path' => 'a11y/index',
-		'asset' => 'a11y/index.min.asset.php',
+		'id' => '@wordpress/core-abilities',
+		'path' => 'core-abilities/index',
+		'asset' => 'core-abilities/index.min.asset.php',
 	),
 	array(
 		'id' => '@wordpress/abilities',
@@ -52,11 +57,6 @@ return array(
 		'path' => 'edit-site-init/index',
 		'asset' => 'edit-site-init/index.min.asset.php',
 	),
-	array(
-		'id' => '@wordpress/core-abilities',
-		'path' => 'core-abilities/index',
-		'asset' => 'core-abilities/index.min.asset.php',
-	),
 	array(
 		'id' => '@wordpress/lazy-editor',
 		'path' => 'lazy-editor/index',
diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php
index 2d1f49ff95..2946f19656 100644
--- a/wp-includes/script-loader.php
+++ b/wp-includes/script-loader.php
@@ -1605,6 +1605,9 @@ function wp_default_styles( $styles ) {
 	$styles->add( 'wp-pointer', "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
 	$styles->add( 'customize-preview', "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) );
 	$styles->add( 'wp-empty-template-alert', "/wp-includes/css/wp-empty-template-alert$suffix.css" );
+	$skip_link_style_path = WPINC . "/css/wp-block-template-skip-link$suffix.css";
+	$styles->add( 'wp-block-template-skip-link', "/$skip_link_style_path" );
+	$styles->add_data( 'wp-block-template-skip-link', 'path', ABSPATH . $skip_link_style_path );

 	// External libraries and friends.
 	$styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.8' );
@@ -1800,6 +1803,7 @@ function wp_default_styles( $styles ) {
 		'media-views',
 		'wp-pointer',
 		'wp-jquery-ui-dialog',
+		'wp-block-template-skip-link',
 		// Package styles.
 		'wp-reset-editor-styles',
 		'wp-editor-classic-layout-styles',
diff --git a/wp-includes/theme-templates.php b/wp-includes/theme-templates.php
index eed0fb9b2b..301820f78f 100644
--- a/wp-includes/theme-templates.php
+++ b/wp-includes/theme-templates.php
@@ -99,10 +99,11 @@ function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_id
 }

 /**
- * Enqueues the skip-link script & styles.
+ * Enqueues the skip-link styles.
  *
  * @access private
  * @since 6.4.0
+ * @since 7.0.0 A script is no longer printed in favor of being added via {@see _block_template_add_skip_link()}.
  *
  * @global string $_wp_current_template_content
  */
@@ -125,96 +126,7 @@ function wp_enqueue_block_template_skip_link() {
 		return;
 	}

-	$skip_link_styles = '
-		.skip-link.screen-reader-text {
-			border: 0;
-			clip-path: inset(50%);
-			height: 1px;
-			margin: -1px;
-			overflow: hidden;
-			padding: 0;
-			position: absolute !important;
-			width: 1px;
-			word-wrap: normal !important;
-		}
-
-		.skip-link.screen-reader-text:focus {
-			background-color: #eee;
-			clip-path: none;
-			color: #444;
-			display: block;
-			font-size: 1em;
-			height: auto;
-			left: 5px;
-			line-height: normal;
-			padding: 15px 23px 14px;
-			text-decoration: none;
-			top: 5px;
-			width: auto;
-			z-index: 100000;
-		}';
-
-	$handle = 'wp-block-template-skip-link';
-
-	/**
-	 * Print the skip-link styles.
-	 */
-	wp_register_style( $handle, false );
-	wp_add_inline_style( $handle, $skip_link_styles );
-	wp_enqueue_style( $handle );
-
-	/**
-	 * Enqueue the skip-link script.
-	 */
-	ob_start();
-	?>
-	<script>
-	( function() {
-		var skipLinkTarget = document.querySelector( 'main' ),
-			sibling,
-			skipLinkTargetID,
-			skipLink;
-
-		// Early exit if a skip-link target can't be located.
-		if ( ! skipLinkTarget ) {
-			return;
-		}
-
-		/*
-		 * Get the site wrapper.
-		 * The skip-link will be injected in the beginning of it.
-		 */
-		sibling = document.querySelector( '.wp-site-blocks' );
-
-		// Early exit if the root element was not found.
-		if ( ! sibling ) {
-			return;
-		}
-
-		// Get the skip-link target's ID, and generate one if it doesn't exist.
-		skipLinkTargetID = skipLinkTarget.id;
-		if ( ! skipLinkTargetID ) {
-			skipLinkTargetID = 'wp--skip-link--target';
-			skipLinkTarget.id = skipLinkTargetID;
-		}
-
-		// Create the skip link.
-		skipLink = document.createElement( 'a' );
-		skipLink.classList.add( 'skip-link', 'screen-reader-text' );
-		skipLink.id = 'wp-skip-link';
-		skipLink.href = '#' + skipLinkTargetID;
-		skipLink.innerText = '<?php /* translators: Hidden accessibility text. Do not use HTML entities (&nbsp;, etc.). */ esc_html_e( 'Skip to content' ); ?>';
-
-		// Inject the skip link.
-		sibling.parentElement.insertBefore( skipLink, sibling );
-	}() );
-	</script>
-	<?php
-	$skip_link_script = wp_remove_surrounding_empty_script_tags( ob_get_clean() );
-	$script_handle    = 'wp-block-template-skip-link';
-	wp_register_script( $script_handle, false, array(), false, array( 'in_footer' => true ) );
-	wp_add_inline_script( $script_handle, $skip_link_script );
-	wp_enqueue_script( $script_handle );
+	wp_enqueue_style( 'wp-block-template-skip-link' );
 }

 /**
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 2d03a6b1ca..6fb6079e7f 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.0-alpha-61468';
+$wp_version = '7.0-alpha-61469';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.