Skip to content

Conversation

@rutviksavsani
Copy link

@rutviksavsani rutviksavsani commented Jan 1, 2026

Move the block template skip link from client-side injection to server-side HTML processing using the HTML API, while keeping the existing accessibility behaviour and minifying the CSS.

What this changes

  • Adds _block_template_skip_link_markup() to process the block template HTML:

    • Ensures the first <main> has an id (adds wp--skip-link--target if missing).
    • Inserts a single <a id="wp-skip-link" class="skip-link screen-reader-text"> before .wp-site-blocks.
    • Skips insertion when there is no <main> or when a skip link already exists.
  • Updates get_the_block_template_html() to run the rendered template through the new helper.

  • Refactors wp_enqueue_block_template_skip_link() to:

    • Remove the old JS-based DOM injection.
    • Minify the skip-link CSS as an inline style.
  • Preserves backward compatibility gating via the_block_template_skip_link and block-templates theme support.

  • Add tests for the new function as well.

Ticket

Trac ticket: https://core.trac.wordpress.org/ticket/64361


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Jan 1, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props rutviksavsani, westonruter, dmsnell.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

github-actions bot commented Jan 1, 2026

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Copy link
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see this!

rutviksavsani and others added 6 commits January 3, 2026 12:54
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
@westonruter westonruter requested a review from dmsnell January 3, 2026 08:26
@westonruter
Copy link
Member

Something is wrong with that Tests_Template::test_wp_hoist_late_printed_styles test. I'll have to debug it tomorrow.

Fixed in 61711e2. The problem is that some other test in the test suite is not cleaning up after itself. When the test runs in isolation, it's not a block template. But some other test is leaving the block template status in place. Previously the code was already ignoring wp-block-template-skip-link-inline-css for this reason. It was failing for wp-block-template-skip-link-css because now since the stylesheet is actually in the filesystem, it can be conditionally not inlined, as is being done in the tests.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the block template skip link implementation to move it from client-side JavaScript injection to server-side HTML processing using WordPress's HTML API. The skip link is now inserted during template rendering rather than dynamically added via JavaScript on page load.

Key changes:

  • Server-side skip link insertion via HTML API replaces JavaScript DOM manipulation
  • New CSS file created with skip link styles (to be minified by build process)
  • JavaScript removed from wp_enqueue_block_template_skip_link() function

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/wp-includes/block-template.php Adds _block_template_add_skip_link() function to insert skip links server-side and updates get_the_block_template_html() to call it
src/wp-includes/theme-templates.php Removes JavaScript insertion code and updates function to only enqueue CSS styles
src/wp-includes/script-loader.php Registers new CSS file for skip link styles with minification support
src/wp-includes/css/wp-block-template-skip-link.css New CSS file containing skip link styles (previously inline)
tests/phpunit/tests/block-template.php Adds tests for skip link insertion behavior in template HTML
tests/phpunit/tests/block-template-utils.php Adds comprehensive tests for various skip link edge cases
tests/phpunit/tests/template.php Updates ignored styles list to include new skip link CSS handle

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

westonruter and others added 2 commits January 9, 2026 12:07
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice to me from the HTML API perspective. Thanks @rutviksavsani and @westonruter for this work; should be a nice uplift for the initial page render!

@westonruter I‘ll leave the next step to you. It seems like things are fine now with the tests, but since I didn’t focus much on those I don’t want to eagerly merge anything.

@westonruter
Copy link
Member

westonruter commented Jan 11, 2026

I cURL'ed the Sample Page before and after the change and here's the diff (after running both the before and after through prettier):

--- before.html	2026-01-10 20:32:07
+++ after.html	2026-01-10 20:32:16
@@ -2478,7 +2478,8 @@
         width: auto;
         z-index: 100000;
       }
-      /*# sourceURL=wp-block-template-skip-link-inline-css */
+
+      /*# sourceURL=/wp-includes/css/wp-block-template-skip-link.css */
     </style>
     <style id="twentytwentyfive-style-inline-css">
       /*
@@ -2628,6 +2629,12 @@
   <body
     class="wp-singular page-template-default page page-id-2 wp-embed-responsive wp-theme-twentytwentyfive"
   >
+    <a
+      class="skip-link screen-reader-text"
+      id="wp-skip-link"
+      href="#wp--skip-link--target"
+      >Skip to content</a
+    >
     <div class="wp-site-blocks">
       <header class="wp-block-template-part">
         <div
@@ -2765,6 +2772,7 @@
       </header>
 
       <main
+        id="wp--skip-link--target"
         class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained"
         style="margin-top: var(--wp--preset--spacing--60)"
       >
@@ -3013,49 +3021,6 @@
       fetchpriority="low"
       data-wp-router-options='{"loadOnClientNavigation":true}'
     ></script>
-    <script id="wp-block-template-skip-link-js-after">
-      (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 = "Skip to content";
-
-        // Inject the skip link.
-        sibling.parentElement.insertBefore(skipLink, sibling);
-      })();
-
-      //# sourceURL=wp-block-template-skip-link-js-after
-    </script>
     <script id="wp-emoji-settings" type="application/json">
       {
         "baseUrl": "https://s.w.org/images/core/emoji/17.0.2/72x72/",

I just realized another benefit to doing this: the skip link works even when JavaScript is turned off.

@westonruter
Copy link
Member

I just noticed that RTL versions of the CSS files are being generated. Here are all the versions now after a build:

  • src/wp-includes/css/wp-block-template-skip-link-rtl.css
  • src/wp-includes/css/wp-block-template-skip-link-rtl.min.css
  • src/wp-includes/css/wp-block-template-skip-link.css
  • src/wp-includes/css/wp-block-template-skip-link.min.css

Previously there was no RTL variation in the CSS. So maybe this was a bug. But it doesn't seem the RTL version is currently getting registered so it is being unused. We need to double check that.

@westonruter
Copy link
Member

Indeed, this is an issue. The skip link is not getting positioned on the right as expected in Arabic:

English Arabic
Screenshot 2026-01-10 at 21 01 20 Screenshot 2026-01-10 at 20 59 37

@westonruter
Copy link
Member

OK, with 357ea8a I now get the skip link positioned on the right as expected, but only when styles_inline_size_limit is zero. I then get this stylesheet in the HTML:

<link rel='stylesheet' id='wp-block-template-skip-link-rtl-css' href='http://localhost:8000/wp-includes/css/wp-block-template-skip-link-rtl.min.css?ver=7.0-alpha-61215-src' media='all' />

But when SCRIPT_DEBUG is off and styles_inline_size_limit is at the default of 40K, I see:

<style id="wp-block-template-skip-link-inline-css">
.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;
}

/*# sourceURL=/wp-includes/css/wp-block-template-skip-link.css */
</style>

Note the erroneous left: 5px. It should have right: 5px; which I can see in wp-block-template-skip-link-rtl.css.

It turns out this is a known issue which doesn't have to be fixed in this PR: Core-61625. It's already something on my radar, so I'll follow up on that now that I've seen another example of how to reproduce it. At least it's fixed when stylesheet inlining is disabled.

@westonruter
Copy link
Member

The diff above in #10676 (comment) was with prettier applied. Here's the diff with SCRIPT_DEBUG off and no prettier formatting. Notice how the CSS is now minified:

--- /tmp/before.html	2026-01-10 21:41:27
+++ /tmp/after.html	2026-01-10 21:41:37
@@ -97,35 +97,9 @@
 /*# sourceURL=core-block-supports-inline-css */
 </style>
 <style id="wp-block-template-skip-link-inline-css">
-
-		.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;
-		}
-/*# sourceURL=wp-block-template-skip-link-inline-css */
+/*! 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}
+/*# sourceURL=/wp-includes/css/wp-block-template-skip-link.min.css */
 </style>
 <style id="twentytwentyfive-style-inline-css">
 a{text-decoration-thickness:1px!important;text-underline-offset:.1em}:where(.wp-site-blocks :focus){outline-width:2px;outline-style:solid}.wp-block-navigation .wp-block-navigation-submenu .wp-block-navigation-item:not(:last-child){margin-bottom:3px}.wp-block-navigation .wp-block-navigation-item .wp-block-navigation-item__content{outline-offset:4px}.wp-block-navigation .wp-block-navigation-item ul.wp-block-navigation__submenu-container .wp-block-navigation-item__content{outline-offset:0}blockquote,caption,figcaption,h1,h2,h3,h4,h5,h6,p{text-wrap:pretty}.more-link{display:block}:where(pre){overflow-x:auto}
@@ -147,7 +121,7 @@
 
 <body class="wp-singular page-template-default page page-id-2 wp-embed-responsive wp-theme-twentytwentyfive">
 
-<div class="wp-site-blocks"><header class="wp-block-template-part">
+<a class="skip-link screen-reader-text" id="wp-skip-link" href="#wp--skip-link--target">Skip to content</a><div class="wp-site-blocks"><header class="wp-block-template-part">
 <div class="wp-block-group alignfull is-layout-flow wp-block-group-is-layout-flow">
 	
 	<div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
@@ -198,7 +172,7 @@
 </header>
 
 
-<main class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="margin-top:var(--wp--preset--spacing--60)">
+<main id="wp--skip-link--target" class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="margin-top:var(--wp--preset--spacing--60)">
 	
 	<div class="wp-block-group alignfull has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)">
 		
@@ -298,49 +272,6 @@
 {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/twentytwentyfive/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}
 </script>
 <script type="module" src="http://localhost:8000/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js?ver=7437ed5c45ee57daf02c" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low" data-wp-router-options="{&quot;loadOnClientNavigation&quot;:true}"></script>
-<script id="wp-block-template-skip-link-js-after">
-	( 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 = 'Skip to content';
-
-		// Inject the skip link.
-		sibling.parentElement.insertBefore( skipLink, sibling );
-	}() );
-	
-//# sourceURL=wp-block-template-skip-link-js-after
-</script>
 <script id="wp-emoji-settings" type="application/json">
 {"baseUrl":"https://s.w.org/images/core/emoji/17.0.2/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/17.0.2/svg/","svgExt":".svg","source":{"concatemoji":"http://localhost:8000/wp-includes/js/wp-emoji-release.min.js?ver=7.0-alpha-61215-src"}}
 </script>

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

pento pushed a commit that referenced this pull request Jan 11, 2026
… 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 #10676

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

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


git-svn-id: https://develop.svn.wordpress.org/trunk@61469 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions
Copy link

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 61469
GitHub commit: 6bdb78e

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions bot closed this Jan 11, 2026
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Jan 11, 2026
… 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 WordPress/wordpress-develop#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
github-actions bot pushed a commit to platformsh/wordpress-performance that referenced this pull request Jan 11, 2026
… 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 WordPress/wordpress-develop#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: https://core.svn.wordpress.org/trunk@60781 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@rutviksavsani
Copy link
Author

Thanks @dmsnell for the final review and Thanks @westonruter for taking it over the line with all the necessary checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants