Skip to content

Experiment: Add Internal link suggestions - #887

Open
Infinite-Null wants to merge 15 commits into
WordPress:developfrom
Infinite-Null:feature/internal-link-suggestions
Open

Experiment: Add Internal link suggestions#887
Infinite-Null wants to merge 15 commits into
WordPress:developfrom
Infinite-Null:feature/internal-link-suggestions

Conversation

@Infinite-Null

@Infinite-Null Infinite-Null commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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

  1. Ensure an AI provider is connected and enabled under Settings → AI, and the Internal Link Suggestions experiment is active under Editor Experiments in /wp-admin/options-general.php?page=ai-wp-admin.
  2. Create or edit a post with several paragraphs of content. Ensure you have other published posts/pages on your site to link to.
  3. Open the Post sidebar and click Suggest Internal Links.
  4. Verify that relevant internal link suggestions appear with context snippets.
  5. Click Accept on a suggestion to verify that the suggestion is accepted.
  6. Click Dismiss on a suggestion to verify it is not accepted.

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

Added - Internal Link Suggestions Experiment

Open WordPress Playground Preview

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.12261% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.38%. Comparing base (abcd7c9) to head (4e614db).
⚠️ Report is 8 commits behind head on develop.

Files with missing lines Patch % Lines
...cludes/Abilities/Internal_Links/Internal_Links.php 86.78% 30 Missing ⚠️
...es/Abilities/Internal_Links/system-instruction.php 50.00% 1 Missing ⚠️
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     
Flag Coverage Δ
unit 80.38% <88.12%> (+0.69%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Infinite-Null
Infinite-Null marked this pull request as ready for review July 27, 2026 08:10
@Infinite-Null
Infinite-Null requested a review from a team July 27, 2026 08:10
@github-actions

Copy link
Copy Markdown

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: Infinite-Null <ankitkumarshah@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>

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

@dkotter dkotter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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' ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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'] );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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' ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should use _n here for plurals

const attributeKey =
'content' in block.attributes ? 'content' : 'value';

( dispatch( blockEditorStore ) as any ).updateBlockAttributes(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can remove the as any from here


const acceptSuggestion = ( suggestion: LinkSuggestion ) => {
const blocks = (
select( blockEditorStore ) as any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

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.

New Experiment: Suggest internal links within post content

2 participants