Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 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
357ea8a
Add wp-block-template-skip-link to RTL styles
westonruter Jan 11, 2026
65fedcb
Remove unnecessary re-adding of action
westonruter Jan 11, 2026
3fceee0
Use HTML Tag Processor in tests
westonruter Jan 11, 2026
4d539a8
Account for omitting skip link when wp_enqueue_block_template_skip_li…
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
116 changes: 115 additions & 1 deletion src/wp-includes/block-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,121 @@ 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>';

return _block_template_skip_link_markup( $template_html );
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
}

/**
* Inserts the block template skip link into the template HTML.
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
*
* Uses the HTML API to ensure that the main content element has an ID and to
* inject the skip-link anchor before the block template wrapper.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

as much as I love this, I’m not sure we need to describe in the docblock how the function operates. we can probably clarify the goal, which is well-stated here, and give an illustrative example.

/**
 * ...
 *
 * When a `MAIN` element exists in the template, this function will ensure
 * that the element contains a `id` attribute and will insert a link to
 * that main element at the top of the first `DIV.wp-site-blocks` match.
 *
 * Example:
 *
 *     // Input.
 *     <main>
 *         <nav>...</nav>
 *         <div class="wp-site-blocks">
 *             <h2>...
 *
 *     // Output.
 *     <main id="wp--skip-link--target">
 *         <nav>...</nav>
 *         <div class="wp-site-blocks">
 *             <a href="#wp--skip-link--target" id="wp-skip-link" class="...">
 *             <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
*
* @global string $_wp_current_template_content
*
* @param string $template_html Block template markup.
* @return string Modified markup with skip link when applicable.
*/
function _block_template_skip_link_markup( $template_html ) {
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
global $_wp_current_template_content;
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated

// Back-compat for plugins that disable functionality by unhooking this action.
if ( ! has_action( 'wp_footer', 'the_block_template_skip_link' ) ) {
return $template_html;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like we could move this check up into get_the_block_template_html() which would leave this function considerably more declarative and stable. with the check here it’s leading us to create the awkward Closure-passing for test setup because the function is stateful with the system.


// Early exit if not a block theme.
if ( ! current_theme_supports( 'block-templates' ) ) {
return $template_html;
}

// Early exit if not a block template.
if ( ! $_wp_current_template_content ) {
return $template_html;
}

Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
// Ensure a skip-link target exists and has an ID.
$processor = new WP_HTML_Tag_Processor( $template_html );
$skip_link_target_id = null;

// Get the first <main> element.
while ( $processor->next_tag() ) {
if ( 'MAIN' !== $processor->get_tag() || $processor->is_tag_closer() ) {
continue;
}

$skip_link_target_id = $processor->get_attribute( 'id' );
if ( ! $skip_link_target_id ) {
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
$skip_link_target_id = 'wp--skip-link--target';
$processor->set_attribute( 'id', $skip_link_target_id );
}

// Only consider the first <main> element.
break;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if we’re going to default to a given value, we could set it first and only change if we find one already exists.

// Only add skip-links to templates with a MAIN element.
if ( ! $processor->next_tag( 'MAIN' ) ) {
	return $template_html;
}

$target_id = $processor->get_attribute( 'id' );
if ( ! is_string( $target_id ) && '' === $target_id ) {
	$target_id = 'wp--skip-link--target'
	$processor->set_attribute( 'id', $target_id );
}

...

Note too that I did not use trim() in my example. This is a secondary mention, but it’s not appropriate for us to trim() an id attribute’s value, as “Identifiers are opaque strings.” (HTML spec).

we can see differentiation between id values with different whitespace.

<p id="me">one</p>
<p id=" me">two</p>
<p id="me ">three</p>
<p id=" me ">four</p>
<style>
#me {
  border: 1px solid blue;
}

#\20me {
  border: 1px solid green;
}

#me\20 {
  border: 1px solid red;
}

#\20me\20 {
  border: 1px solid orange;
}
</style>

In other words, the trim() will cause WordPress to misidentify the id and break the skip-link any time it would ever make a difference in the code.


// Early exit if a skip-link target can't be located.
if ( ! $skip_link_target_id ) {
return $template_html;
}

// Apply any updates from setting the main ID.
$template_html = $processor->get_updated_html();

// If a skip link already exists, don't insert another one.
$existing = new WP_HTML_Tag_Processor( $template_html );
while ( $existing->next_tag() ) {
if ( 'A' === $existing->get_tag() && 'wp-skip-link' === $existing->get_attribute( 'id' ) ) {
return $template_html;
}
}

Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
// Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans
// so that text can be inserted before the current token.
$inserter = new class( $template_html ) extends WP_HTML_Tag_Processor {
/**
* Gets the span for the current token.
*
* @return WP_HTML_Span Current token span.
*/
private function get_span(): WP_HTML_Span {
// Note: This call will never fail according to the usage of this class, given it is always called after ::next_tag() is true.
$this->set_bookmark( 'here' );
return $this->bookmarks['here'];
}

/**
* Inserts text before the current token.
*
* @param string $text Text to insert.
*/
public function insert_before( string $text ) {
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text );
}
};
Comment thread
dmsnell marked this conversation as resolved.

while ( $inserter->next_tag() ) {
if ( $inserter->is_tag_closer() ) {
continue;
}

Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
if ( 'DIV' === $inserter->get_tag() && $inserter->has_class( 'wp-site-blocks' ) ) {
$skip_link = sprintf(
'<a class="skip-link screen-reader-text" id="wp-skip-link" href="#%s">%s</a>',
esc_attr( $skip_link_target_id ),
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
Comment thread
westonruter marked this conversation as resolved.
Outdated
/* translators: Hidden accessibility text. Do not use HTML entities (&nbsp;, etc.). */
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated
esc_html__( 'Skip to content' )
);
$inserter->insert_before( $skip_link );
break;
}
}
Comment thread
dmsnell marked this conversation as resolved.

return $inserter->get_updated_html();
}

/**
Expand Down
84 changes: 2 additions & 82 deletions src/wp-includes/theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ 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
Expand All @@ -125,34 +125,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;
}';
$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}';
Comment thread
rutviksavsani marked this conversation as resolved.
Outdated

$handle = 'wp-block-template-skip-link';

Expand All @@ -162,59 +135,6 @@ function wp_enqueue_block_template_skip_link() {
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 );
}

/**
Expand Down
Loading
Loading