=== Web & SEO Autolinker - AI Internal Linking ===
Contributors: webandseofr
Tags: internal linking, seo, ai, openai, links
Requires at least: 6.2
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 0.1.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

AI-powered internal linking. OpenAI embeddings find the targets, GPT picks natural anchors, and the links are written into your posts.

== Description ==

Autolinker builds internal links between the posts you already have. It reads
each post, finds the most semantically related pages on your site, and asks a
language model to choose an anchor phrase that already exists, word for word, in
the source text. The link is then written into the post content and kept in a
registry so it can be tracked, repaired, or removed later.

There is no keyword list to maintain. Relevance comes from meaning, not from
matching strings.

= How it works =

1. **Embedding.** Each post is converted to a vector with an OpenAI embeddings
   model, stored locally, and refreshed only when the content changes.
2. **Candidate search.** Cosine similarity ranks every possible target above a
   configurable threshold.
3. **Filtering.** A first model pass drops candidates that are topically close but
   editorially wrong.
4. **Anchor selection.** A second model pass proposes an anchor phrase. The plugin
   verifies the phrase appears verbatim in the source; if it does not, the link is
   rejected rather than forced.
5. **Writing.** The link is inserted with a marker attribute so the plugin can find
   its own links again without guessing.

Every rejection is written to a journal with a reason, so an empty result is never
indistinguishable from a broken plugin.

= IMPORTANT: this plugin relies on a third-party service =

Autolinker does not work without an OpenAI API key that you supply. The plugin
sends data to the OpenAI API (api.openai.com), a third-party service that is not
affiliated with this plugin or with WordPress.org.

**What is sent:** the text content of the posts you choose to process, the titles
and excerpts of candidate link targets, and the prompts the plugin builds around
them. Your API key is stored encrypted in your WordPress database and is sent to
OpenAI to authenticate each request.

**When it is sent:** only when a post is processed, either automatically on publish
(if you enable that) or when you trigger a scan. Your post content is never sent to
OpenAI at any other time.

**What is not sent:** no data is sent to the plugin author, and no post content
leaves your site other than to OpenAI, as described above.

**One clarification about link health.** The plugin also checks whether the links in
your content still resolve. That check runs on a schedule and makes ordinary HTTP
requests to the URLs already present in your posts, and to their robots.txt, which it
honors. It sends none of your content and involves no third-party service: it only
visits the addresses you already link to. Those requests identify themselves with a
user agent naming the plugin and your site URL, so the hosts you link to can see that
the request came from your site.

Using this plugin means your content is processed under OpenAI's terms. Please read
them before installing:

* OpenAI Terms of Use: https://openai.com/policies/terms-of-use
* OpenAI Privacy Policy: https://openai.com/policies/privacy-policy
* OpenAI API data usage policies: https://openai.com/policies/api-data-usage-policies

You pay OpenAI directly for the tokens the plugin consumes. See the FAQ for cost
control.

= Features =

* Semantic candidate search with a configurable similarity threshold
* Verbatim anchor validation, so anchors are never invented
* A per-post link budget and anchor length bounds (default 2 to 6 words)
* Silo mode, to keep links inside the same category
* Per-post exclusions: exclude entirely, as a source only, or as a target only
* Link health: periodic checking, broken link detection with a failure threshold,
  and an orphaned state when a link disappears from the content
* Slug refresh: when a target's permalink changes, the links pointing at it are
  rewritten
* Elementor support, including posts stored in Elementor's own data structure
* A decision journal covering every link placed and every candidate rejected

= Multilingual =

Sites running WPML or Polylang are detected automatically and links are kept inside
a single language. A French post never links to its English translation.

Detection requires at least two configured languages. Translation layers that render
on the fly without duplicating posts (TranslatePress, Weglot, GTranslate) are treated
as monolingual, because only the original content exists in the database.

On a multilingual site the plugin switches to a larger embedding model for better
cross-language separation. Posts are re-embedded lazily, at their next scan, which
means a one-off token cost the first time each post is processed after you add a
second language.

== Installation ==

1. Install and activate the plugin.
2. Go to Autolinker > Settings and paste your OpenAI API key. You can create one
   at https://platform.openai.com/. Review the OpenAI terms linked in the description
   before you do.
3. Set your similarity threshold and per-post link budget, or keep the defaults.
4. Either enable automatic processing on publish, or run a scan by hand from the
   plugin screen.

Processing runs through WP-Cron. On a site with little traffic, consider a real
system cron calling wp-cron.php so scans do not stall.

== Frequently Asked Questions ==

= Do I need an OpenAI API key? =

Yes. The plugin has no bundled key and no free tier of its own. You create a key on
your own OpenAI account and pay OpenAI for what you use.

= How much does it cost to run? =

It depends on how many posts you process and how long they are. Two levers reduce
it: the per-post link budget caps how many placements are attempted, and embeddings
are cached, so a post that has not changed is never re-embedded.

= Does it modify my post content? =

Yes. Links are written into the post content, with a marker attribute so the plugin
can identify its own links later. This is deliberate: the links are real HTML, they
survive in exports, in feeds, and for search engines, and they do not depend on a
filter running at display time.

= What happens if I deactivate the plugin? =

The links stay in your content, because they are ordinary HTML. Link tracking,
health checks and repair stop until you reactivate.

= Can I stop it from touching a specific post? =

Yes. Each post has controls to exclude it entirely, to stop it being used as a link
source, or to stop it receiving links.

= Does it work with page builders? =

Elementor is supported, including content stored in Elementor's own data structure.

Builders that store their layout as shortcodes in the post content, such as Divi,
are linked inside their text, and their shortcodes are left untouched. Text that a
builder keeps in a shortcode attribute rather than in the body, such as a module
title, is never linked: writing there would break the module and the link would be
invisible. The journal records those as an anchor found only in a shortcode.

Builders that keep their content outside the post content field entirely, such as
SeedProd, are not supported yet. Nothing is written on those pages.

= Why did it place no links on a post? =

Open the journal. Every candidate that was considered and dropped is recorded with a
reason: below the similarity threshold, rejected by the filtering pass, or an anchor
that did not appear verbatim in the source. The last one is the most common, and it
is working as intended: the plugin will not invent an anchor that is not in your text.

= Does it send my content anywhere other than OpenAI? =

No. The only external service is the OpenAI API, and only for the posts you process.

== Changelog ==

= 0.1.2 =
* Fixed: on a page whose layout is stored as shortcodes in the post content, such as Divi, a link could be written inside a shortcode attribute instead of the visible text. The module broke and the link was invisible.
* Fixed: removing or repointing a link no longer rewrites a raw ampersand inside a shortcode attribute, and no longer lets a raw angle bracket there swallow the rest of the field.
* Text that a builder keeps in a shortcode attribute, such as a module title, is now never linked. The journal records those as an anchor found only in a shortcode.
* No settings or database changes.

= 0.1.1 =
* Performance: inbound link counts for several pages are now read in a single grouped query instead of one query per page.
* Performance: those counts are cached in the object cache and cleared whenever the link registry is written to. Sites without a persistent object cache still get the grouped query, which is the larger of the two gains.
* No settings, database or behaviour changes.

= 0.1.0 =
* Initial release.

== Upgrade Notice ==

= 0.1.2 =
Fixes content damage on page builders that store their layout as shortcodes, such as Divi. Recommended for those sites.

= 0.1.1 =
Performance only. No settings or data changes.

= 0.1.0 =
Initial release.
