Experiment: Add Internal link suggestions - #887
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #887 +/- ##
=============================================
+ Coverage 79.68% 80.38% +0.69%
- Complexity 2460 2581 +121
=============================================
Files 104 109 +5
Lines 9955 10501 +546
=============================================
+ Hits 7933 8441 +508
- Misses 2022 2060 +38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…tions Experiment in the block editor'
…gic in Internal_Links ability
…ery and update package-lock.json
… link generation and update lockfile
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
dkotter
left a comment
There was a problem hiding this comment.
Testing works though results aren't great (which I've left a comment around this). Probably worth a discussion on the approach here prior to proceeding.
In addition, adding documentation to match how our other experiments are documented would be great
| 'max_suggestions' => array( | ||
| 'type' => 'integer', | ||
| 'sanitize_callback' => 'absint', | ||
| 'description' => esc_html__( 'Maximum number of link suggestions to return (1–10).', 'ai' ), |
There was a problem hiding this comment.
If we limit this from 1 to 10, our schema should set that here with minimum and maximum attributes
| protected function permission_callback( $input ) { | ||
| $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; | ||
|
|
||
| if ( ! $post_id ) { |
There was a problem hiding this comment.
Our schema says that a post ID is required so I don't think this check will ever be needed, unless I'm missing something
| ); | ||
|
|
||
| $post_content = wp_kses_post( (string) $args['post_content'] ); | ||
| $post_id = absint( $args['post_id'] ); |
There was a problem hiding this comment.
I know we validate post ID in our permission callback but to match our other abilities, probably best to ensure this is a valid post ID here
| ->using_system_instruction( $this->get_system_instruction() ) | ||
| ->as_json_response( $this->suggestions_schema() ); | ||
|
|
||
| $prompt_builder = $this->set_provider_model_preference( $prompt_builder, Internal_Links_Experiment::class ); |
There was a problem hiding this comment.
We have two new methods we'll want to integrate here: $this->filter_prompt and $this->filter_prompt_builder.
The latter replaces this $this->set_provider_model_preference call
| 7. **Quality over quantity.** If fewer than <max-suggestions> high-quality links exist, return fewer. An empty array is valid if no good matches exist. | ||
| 8. **Skip already-linked text.** If an `<already-linked>` list is provided, do NOT suggest any anchor text that appears in that list. Those phrases are already hyperlinked in the post. | ||
|
|
||
| ## Output format |
There was a problem hiding this comment.
Is this needed? We already provide the output format we expect when making a request so seems like this is unnecessary and wastes tokens
| <p className="description ai-internal-links__suggestions-header"> | ||
| { sprintf( | ||
| /* translators: %d: number of suggestions found. */ | ||
| __( '%d suggestion(s) found.', 'ai' ), |
There was a problem hiding this comment.
Should use _n here for plurals
| const attributeKey = | ||
| 'content' in block.attributes ? 'content' : 'value'; | ||
|
|
||
| ( dispatch( blockEditorStore ) as any ).updateBlockAttributes( |
There was a problem hiding this comment.
I think we can remove the as any from here
| ( window as any ).aiInternalLinksData?.maxSuggestions ?? 5; | ||
|
|
||
| const { content, postId } = useSelect( ( selectStore ) => { | ||
| const editor = selectStore( editorStore ) as any; |
There was a problem hiding this comment.
I think we can remove the as any from here
|
|
||
| const acceptSuggestion = ( suggestion: LinkSuggestion ) => { | ||
| const blocks = ( | ||
| select( blockEditorStore ) as any |
There was a problem hiding this comment.
I think we can remove the as any from here
| } | ||
|
|
||
| // Build the list of linkable posts/pages from this site. | ||
| $site_index = $this->build_site_index( $post_id ); |
There was a problem hiding this comment.
In testing, I'm not getting very good results based on this approach. I think we may want to pause here to decide if this is an approach we should continue on or not. My suggestion would be to wait until we have embeddings support and we can use those to find relationships, which I think will scale better and give us much better results.
Closes #875
Internal Link Suggestions Experiment
Description
This PR implements the Internal Link Suggestions experiment, which uses AI to suggest contextual internal links within post content by analyzing the current draft and matching relevant phrases to published posts or pages on the site.
Testing Instructions
Editor Experimentsin/wp-admin/options-general.php?page=ai-wp-admin.Screenshots or screencast
Screen.Recording.2026-07-27.at.12.45.28.PM.mov
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 4.6
Used for: Validating bug, suggesting a fix.
Changelog Entry