Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8e7719a
Insert block template skip link via HTML API, minify CSS, remove JS.
rutviksavsani Jan 1, 2026
7e29956
remove extra checks for block template.
rutviksavsani Jan 3, 2026
908f6a9
Add type and return type to the function.
rutviksavsani Jan 3, 2026
d6278d3
Update translators comment.
rutviksavsani Jan 3, 2026
9a73e67
Add extra checks for get_attribute link target.
rutviksavsani Jan 3, 2026
a62dffb
update function doc comment.
rutviksavsani Jan 3, 2026
43c4b01
Remove the duplication check and tag closers check.
rutviksavsani Jan 3, 2026
ce8885a
css concat but make it still readable.
rutviksavsani Jan 3, 2026
cd9e029
Remove single-use private method
westonruter Jan 5, 2026
a194ca8
Add since tag to wp_enqueue_block_template_skip_link()
westonruter Jan 5, 2026
6b7841c
address feedback.
rutviksavsani Jan 5, 2026
0bb3ec4
Add missing path data for stylesheet
westonruter Jan 5, 2026
c340b2c
Leverage assertEqualHTML
westonruter Jan 5, 2026
eff4489
Add test case for removal of the_block_template_skip_link from wp_footer
westonruter Jan 5, 2026
b00b244
Tweak comment
westonruter Jan 5, 2026
1ae2bb2
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Jan 5, 2026
d179f23
Fix swapped test names
westonruter Jan 5, 2026
540b526
Replace while loop with if statement
westonruter Jan 5, 2026
746e686
Use data provider
westonruter Jan 5, 2026
45c26b8
Add test cases for malformed IDs
westonruter Jan 5, 2026
c565ff9
Remove unnecessary variable
westonruter Jan 5, 2026
ce5b88e
Use esc_url() instead of esc_attr()
westonruter Jan 7, 2026
8b83d4e
Block template skip link handling
rutviksavsani Jan 8, 2026
d066dc4
Add tests for block template skip link
rutviksavsani Jan 8, 2026
64ff9d1
Remove the extra processor loop and use seek to insert link.
rutviksavsani Jan 9, 2026
64a8b4a
Merge branch 'WordPress:trunk' into refact/insert-skip-link-via-html
rutviksavsani Jan 9, 2026
350b57c
Fix URL for wp-block-template-skip-link
westonruter Jan 9, 2026
b1afb70
Debug: Reduce phpunit-tests for debugging
westonruter Jan 9, 2026
3ed9039
Revert "Debug: Reduce phpunit-tests for debugging"
westonruter Jan 9, 2026
61711e2
Add wp-block-template-skip-link-css to $ignored_styles
westonruter Jan 9, 2026
999acca
Update docs for _block_template_add_skip_link()
westonruter Jan 9, 2026
0db64ac
Fix comment
westonruter Jan 9, 2026
b219ac5
Merge branch 'trunk' into refact/insert-skip-link-via-html
westonruter Jan 10, 2026
9f62b62
Merge branch 'trunk' into refact/insert-skip-link-via-html
westonruter Jan 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion src/wp-includes/block-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,99 @@ 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 this action.
if ( ! has_action( 'wp_footer', 'the_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();
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/wp-includes/css/wp-block-template-skip-link.css
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down
94 changes: 3 additions & 91 deletions src/wp-includes/theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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' );
}

/**
Expand Down
81 changes: 81 additions & 0 deletions tests/phpunit/tests/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,87 @@ public function data_remove_theme_attribute_in_block_template_content() {
);
}

/**
* Tests that a skip link is added and a MAIN element without an ID receives the default ID.
*
* @ticket 64361
*
* @covers ::_block_template_add_skip_link
*/
public function test_block_template_add_skip_link_inserts_link_and_adds_main_id_when_missing() {
$template_html = '<div class="wp-site-blocks"><main>Content</main></div>';
$expected = '
<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"><main id="wp--skip-link--target">Content</main></div>
';

$this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
}

/**
* Tests that an existing MAIN ID is reused for the skip link.
*
* @ticket 64361
*
* @covers ::_block_template_add_skip_link
*/
public function test_block_template_add_skip_link_uses_existing_main_id() {
$template_html = '<div class="wp-site-blocks"><main id="custom-id">Content</main></div>';
$expected = '
<a class="skip-link screen-reader-text" id="wp-skip-link" href="#custom-id">Skip to content</a>
<div class="wp-site-blocks"><main id="custom-id">Content</main></div>
';

$this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
}

/**
* Tests that a boolean MAIN ID is treated as missing and replaced with the default.
*
* @ticket 64361
*
* @covers ::_block_template_add_skip_link
*/
public function test_block_template_add_skip_link_handles_boolean_main_id() {
$template_html = '<div class="wp-site-blocks"><main id>Content</main></div>';
$expected = '
<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"><main id="wp--skip-link--target">Content</main></div>
';

$this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
}

/**
* Tests that a MAIN ID containing whitespace is preserved and used for the skip link.
*
* @ticket 64361
*
* @covers ::_block_template_add_skip_link
*/
public function test_block_template_add_skip_link_preserves_whitespace_main_id() {
$template_html = '<div class="wp-site-blocks"><main id=" my-id ">Content</main></div>';
$expected = '
<a class="skip-link screen-reader-text" id="wp-skip-link" href="#%20my-id%20">Skip to content</a>
<div class="wp-site-blocks"><main id=" my-id ">Content</main></div>
';

$this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) );
}

/**
* Tests that no changes are made when there is no MAIN element.
*
* @ticket 64361
*
* @covers ::_block_template_add_skip_link
*/
public function test_block_template_add_skip_link_does_not_modify_when_main_missing() {
$template_html = '<div class="wp-site-blocks"><div>Content</div></div>';

$this->assertSame( $template_html, _block_template_add_skip_link( $template_html ) );
}

/**
* Should retrieve the template from the theme files.
*/
Expand Down
51 changes: 51 additions & 0 deletions tests/phpunit/tests/block-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,57 @@ public function test_get_the_block_template_html_skips_singular_query_loop_when_
$this->assertSame( array( false ), $in_the_loop_logs, 'Main query loop was triggered despite a custom block template outside the current theme being used' );
}

/**
* Tests that `get_the_block_template_html()` adds a skip link when a MAIN element is present.
*
* @ticket 64361
* @covers ::get_the_block_template_html
*/
public function test_get_the_block_template_html_adds_skip_link_when_main_present() {
global $_wp_current_template_id, $_wp_current_template_content;

$_wp_current_template_id = get_stylesheet() . '//index';
$_wp_current_template_content = '<main>Content</main>';

$output = get_the_block_template_html();

$this->assertStringContainsString(
'<a class="skip-link screen-reader-text" id="wp-skip-link"',
$output,
'Expected skip link was not added to the block template HTML.'
);
}

/**
* Tests that `get_the_block_template_html()` does not add a skip link when the skip-link action is unhooked.
*
* @ticket 64361
* @covers ::get_the_block_template_html
*/
public function test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked() {
global $_wp_current_template_id, $_wp_current_template_content;

$_wp_current_template_id = get_stylesheet() . '//index';
$_wp_current_template_content = '<main>Content</main>';

$was_hooked = (bool) has_action( 'wp_footer', 'the_block_template_skip_link' );
remove_action( 'wp_footer', 'the_block_template_skip_link' );

try {
$output = get_the_block_template_html();
} finally {
if ( $was_hooked ) {
add_action( 'wp_footer', 'the_block_template_skip_link' );
}
}

$this->assertStringNotContainsString(
'<a class="skip-link screen-reader-text" id="wp-skip-link"',
$output,
'Unexpected skip link was added to the block template HTML when the action was unhooked.'
);
}

/**
* @ticket 58319
*
Expand Down
Loading
Loading