=== Megop SEO – Bulk Editor, Internal Links, Broken Links & Search Console ===
Contributors: hamuo1372
Tags: bulk edit, internal links, persian, broken links, search console
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 8.3.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Bulk edit every post type and taxonomy in one grid, prices included. Persian-aware internal linking, broken-link repair, Search Console.

== Description ==

Internal linking on WordPress is a paid category. The tools people recommend are commercial and closed source, their licences are proprietary, and the ones that publish a price ask anywhere from a monthly subscription to several hundred dollars. All of them were built for English first. The most translated of them ships a dozen interface languages, and Persian is not one of them.

So for a Persian site the shelf has been empty. Not a free option, not an open one, and not one that knows a Persian phrase can be spelled four ways that look identical to a reader and are four different strings to a computer.

This plugin is the option that was missing. GPLv2 for every line, with no paid tier holding half the work back. Persian folding is not a feature added at the end; it is the layer the matching engine stands on. And the Search Console module is in the same plugin rather than being a second purchase, which in this category is the part that usually does not exist at all.

Megop SEO is five tools in one plugin: a spreadsheet for editing your whole site at once, an internal linking engine, a Search Console module that reads what Google already knows and writes what your pages are missing, a broken link scanner that repairs what it finds, and an analytics module that keeps your visitors on your own server.

If you have been looking for a bulk editor, a bulk price editor for WooCommerce, a bulk duplicator, a search and replace tool, a redirect chain checker or a self hosted analytics plugin, five of those are in here, and they share one index instead of sitting side by side.

Written and maintained by [محمد قاسمی (Mohammad Ghasemi)](https://megop.org/), Tehran, Iran.

What writing for Persian first actually means, in code. A Persian phrase can be spelled several ways that look identical to a reader and completely different to a computer: an Arabic character where a Persian one belongs, a zero width non joiner instead of a space, a Persian digit where Google reports a Latin one. Every plugin that matches text literally treats those as different phrases. This one folds them together before matching, using an offset map so your content is read, never rewritten.

= How it is tested =

Every release runs against a live Persian WooCommerce site with several thousand posts before it is packaged, not only against a clean install. That is deliberate, and it is where the findings come from.

One example, because it is the kind of thing a checker cannot see. A template in the linking screen declared a small helper function at file level. PHP registers a function declared in an included file globally, whatever scope the include ran in, so the second time that template was rendered inside one request the whole screen died with `Cannot redeclare`. It is a compile time error, so `try/catch` cannot reach it, and the site's own error log had recorded it three times on real page loads. No static analyser flagged it and no clean install smoke test triggered it, because rendering the template twice is what triggers it. The helper is a static method now, and no template in this plugin declares a function at all.

A second example, from the same site and the same kind of blindness. A crawl reported twenty internal 301 links inside published posts; Link Nurse had reported none of them, on any scan, ever. The cause was one boolean. Redirect handling sat behind a setting whose own label reads "Rewrite 301/302 to final 200 destination", a repair switch, wired as a detection gate. With it off the scanner never resolved where a 301 went, so the classifier had no destination to judge and returned healthy for every redirected link on the site. Nothing errored, nothing was logged, and the dashboard was confidently wrong. Detection and repair are separate now, and the classifier is covered by a test that runs every case with the switch in both positions.

A third example, and the largest. The same crawl showed forty links Link Nurse had never reported. Not a classification problem this time: the extractor collected every href and src on the page and then dropped anything that did not begin with http, one line later. A link written as `/product/name/`, the form the editor produces when you pick a page from the link dialog, never reached the checker at all. Counted the way the scanner reads content, 332 of the 1,373 links on that site were stored that way, spread across 188 of its 364 published items. A quarter of every link and half of every page, invisible on every scan since the module was written, with nothing on the dashboard to suggest it. Relative links are resolved against the page they sit on now, and the repair keeps the form the author wrote.

The fourth example is the one that shaped this release, and I would rather tell it than hide it. Before this version went to the repository, the whole codebase was read once more, file by file, against this very page, with one question: does the page claim anything the code does not do? It did. The price fields in the bulk grid were drawn, the server was ready for them, the rule about empty fields was written and tested, and the browser never sent the two values. You typed a price, the row turned green, nothing was written. Three more claims failed the same reading: the Undo button on a title or description edit looked for its backup under the wrong key, the control group was never actually drawn from the queue, and switching the linking module off took the settings screen down with it. All four are fixed in this build, and the reading also turned up two ways the grid could quietly damage content on save, a backslash lost on every save and a Rank Math variable such as `%date%` corrupted by the sanitiser. Both are fixed. Each is in the changelog with what caused it.

The same habit produced the rest of the list: a settings form that reported success while saving nothing because the browser stringified an object into the request; a Jalali date reaching a SQL comparison and returning zero rows on a site with 66,000 recorded visits; a bulk editor whose first version emptied variable product prices because an empty field was read as clear rather than as leave alone.

= Works alongside Rank Math =

This plugin does not replace your SEO plugin and does not try to. Rank Math owns the SEO title, the meta description and the redirect table; Megop SEO reads and writes those fields rather than inventing its own. Where Rank Math holds a redirect, Link Nurse resolves it from the table instead of making an HTTP request, which is both exact and instant, and the bulk manager's delete with redirect tool records its rules in that same table when Rank Math's redirections module is on. If you use Yoast, SEOPress or All in One SEO instead, the sitemap tools detect those too.

= The five modules =

Each module can be switched off from the settings screen. A module that is off registers no hooks at all: no cost, no menu entry, no route. Its data is never touched by switching it off, and the settings screen with the switches belongs to the plugin itself, so it is always there to switch a module back on.

**Seo Bulk Manager** is a spreadsheet view of your whole site: every public post type and every public taxonomy, WooCommerce products and product categories included. The H1, the SEO title, the meta description, the featured image, the publication status, the category, the parent of a term, and the regular and sale price of a product are all edited across many rows at once. The character counters count the finished title rather than the template, search and replace shows its count before you commit, and the grid sorts by clicks and impressions from Search Console. One screen is the bulk editor, the bulk price editor and the bulk duplicator.

**Seo Auto Link** turns a keyword into a link everywhere that phrase appears, from a prebuilt index, with Persian folding applied before matching, and it finds the links you wrote by hand years ago and brings them under the same management.

**Search Console** is the largest part of the plugin. It reads what Google already knows about your site, tells you which of those searches your pages do not actually contain, decides which page each search belongs to, checks what Google did with each URL, and can write the missing text under guardrails with a control group watching.

**Link Nurse** finds broken and redirected links in content, in Elementor data, in term descriptions and in your sitemap, and repairs them in batches with undo. A redirect chain is followed to its end before the link is judged, so a 301 that lands on a 404 is reported as broken rather than healthy.

**Internal Analytics** is self hosted analytics: it records visits and events on your own server, with no third party anywhere in the path.

= Inside Seo Bulk Manager =

This is the screen most people open first, and the one they keep open. Everything below is what the code does, checked line by line before this page was written.

**Everything on the site that has a URL, in the same grid.** The grid enumerates every public post type and every public taxonomy the site has, whatever registered them: posts, pages, WooCommerce products, a portfolio, a custom post type a plugin created this morning. Categories, tags, product categories, product tags, brands, and any custom taxonomy. Nothing is special cased and nothing is hard coded. Attachments and post formats are left out, because neither is a page anyone edits. You pick post types or taxonomies, then one object at a time, and the grid lists published, scheduled, draft, pending and private items; the trash is never shown.

**What a row edits.** A post row carries the H1 (the post title), the Rank Math SEO title, the Rank Math meta description, the featured image, a Published or Draft switch, and a category dropdown. A product row carries all of that plus the regular price and the sale price. A term row carries the name, the SEO title, the meta description, and a parent dropdown for hierarchical taxonomies. The body of a post and the description of a term are in the grid too, but you do not type into them: they are reached through find and replace and through the headings tool, because a body edited blind in a one line box is how paragraphs get destroyed.

**The counter counts what Google sees, not what you typed.** This is the detail I would point at first. A Rank Math title is usually a template, something like `%title% %sep% %sitename%`, which is 24 characters as text and perhaps 61 characters once it renders. Every bulk editor I have used counts the template, which means the number under the field is fiction. Here the variables are resolved against that row before the count: `%title%` and `%seo_title%` take the row's own H1 as you type it, `%sitename%`, `%sitedesc%`, `%sep%`, `%currentyear%`, `%currentdate%`, `%currentmonth%`, `%currentday%` and `%id%` are filled in, and any other variable counts as empty. The limit is 55 characters for the title and 155 for the description, the H1 is uncounted, and the number turns red past the limit. Tick "Show a preview of Rank Math variables" and the resolved title is printed under the field; leave it off if you know your templates by heart.

**Find and replace tells you the answer before you commit.** Tick "Show the replacer tool" and each row grows its own. You tick which of the four fields it should touch, H1, title, description, content, you type what to look for, and before you have typed the replacement the box already says: found 7 times in the selected fields, 5 of them inside the content. A second line states exactly what is about to happen, with both strings and both counts. Matching is literal and case sensitive, every occurrence is replaced, and the row is saved the moment you confirm. Because the replace is scoped to one row, a phrase can be corrected on one product without a site wide operation you then have to audit. When you do want the site wide operation, tick the rows and use Bulk replace in the floating bar: one pair, every ticked row, one confirmation with the match count and the row count.

**Elementor is written in step.** When the content box is ticked, the same find and replace pair is applied to the page's Elementor data on the server, the page's generated stylesheet is thrown away so Elementor rebuilds it, and the row reports how many Elementor items changed. The replace never touches an Elementor key that holds an id, a URL or a link, and never a value that looks like a URL, so a phrase replacement cannot break a button.

**You save a row, not a page.** Press Save on a row and that row alone is saved, and it tells you what happened inline, in place, with no reload and no green bar at the top of the screen that could belong to any of the fifty rows below it. Tick several rows and "Save the selected rows" saves them together. A row with an unsaved change gets an orange bar on its edge and the floating bar counts them, so a row you edited and forgot is visible rather than lost, and leaving the page with unsaved rows asks first. A few things do not wait for Save, and it is worth knowing which: the status switch, the category and parent dropdowns, the featured image, the keyword chips, a heading edit and a deletion all write the moment you use them.

**Prices, including variable products.** Regular and sale, in the row, for every WooCommerce product kind. Two rules keep this safe on a live shop. Only a price you actually changed is sent, so editing a title never rewrites a catalogue's variations behind your back. And an empty field means leave this alone rather than clear it: a sale is ended in the product editor, never by a blank cell in a batch of twenty rows. On a variable product the value you type is written to every variation, the note above the field says how many, and the row confirms it after the save. The first version of this screen did not make the empty field distinction, on a live shop, which is how the rule was learned.

**Category and parent, with a lock where a URL is at stake.** Whenever the post type has a public taxonomy, the row carries a dropdown for it: category for posts, product category for products. Choosing one saves at once and replaces every term of that taxonomy with the one chosen, and the row warns you when a post had several. A term row carries a parent dropdown for the same reason. Both are locked, with the reason printed where the control would be, on sites where the change would change the URL: a permalink structure that contains `%category%`, a product base that contains `%product_cat%`, or a taxonomy whose URLs are hierarchical. Any URL change throws away whatever authority the old address had earned, and that is a decision to make deliberately in one place, not something to do by accident while editing a description.

**The featured image is set from inside the grid.** The WordPress media library opens over the row, you pick an image, the thumbnail in the cell updates.

**Sort by what Google actually shows.** With the Search Console module on, a card in the grid header offers "Show Search Console figures" with a range from 7 days to 16 months. Tick it and every row gains clicks and impressions with a trend arrow against the previous window, and the Order by list gains two entries, clicks and impressions, with sort arrows in the column header. The choice is remembered per user. This is where the five modules stop being five plugins wearing one coat: the numbers come from the Search Console tables the same plugin fills, so you can order the whole grid by the pages Google puts in front of people most often and start editing at the top.

**Filters, paging and columns.** Search, status, author, a date range, and the first taxonomy of the post type filter the posts; a parent term and hide empty filter the terms. Sort by date, last edited, title or id for posts, by name, slug, count or id for terms. Twenty rows per page by default, up to 200. Every column and every tool panel can be shown or hidden, and the choices are remembered per browser.

**The duplicator** takes one row and produces many. Tick "Show the advanced duplicate tool" and each post row grows a panel where you list pairs of a word being replaced and its replacements, one page per line, and tick which fields the replacement touches: H1, title, description, content including Elementor data, and the automatic linking keywords. The number of pages is the longest list; with no pairs you get plain copies titled with a number. Each copy is a draft owned by you, with the same terms, the same featured image, the same Elementor data and the same meta apart from editor locks and caches, and a slug built from its new title. Up to fifty copies per run. A product line with twelve near identical descriptions stops being twelve editing sessions.

**Deletion is two step, and a redirect is a separate decision.** Tick "Show the delete tool" and each post row gains Delete, which asks twice and then moves the post to the trash, where it can be restored. Tick several rows and the floating bar offers Delete, or Delete with a redirect: a dialog where you pick the destination by searching a page title or typing any URL, choose 301, 302 or 307, and the rows are trashed and one rule is written per row from the address each page had before it moved. The rules go into Rank Math's redirect table when its redirections module is on, and the dialog offers to switch that module on if it is not; without Rank Math they are written as `Redirect` lines into `.htaccess`, and the dialog tells you which happened. A term has no trash in WordPress, so a term row offers Delete permanently, with a confirmation that says what happens to its content and its children.

**The headings tool** lists the H1 to H6 headings of a page, read from the Elementor heading widgets and from any heading in the body, and lets you edit their text inline, with bold, italic, underline and a link. Adding or removing a heading is deliberately impossible here, and a heading that changed on the server since you opened the panel is refused rather than overwritten.

**Refresh sitemap** sits in the grid header. Publishing a post clears the sitemap cache on its own; bulk work does not, and the grid is exactly where a hundred rows change without anyone pressing Publish. The button clears the cache of Rank Math, Yoast or All in One SEO, reads the sitemap index back over HTTP the way a crawler would, and then reads up to sixty child sitemaps inside a twenty second budget, so the files are rebuilt before Googlebot asks for them. With SEOPress or the WordPress core sitemap there is no cache to clear, so the button only reads the files back.

**Who can use it, and in which language.** The grid is for administrators. Each person picks their own language for these screens, English, Persian or Arabic, and their own light or dark theme, and both are remembered per user without touching the rest of the dashboard.

= Inside Seo Auto Link =

Give a post, a page, a custom post type or a category archive a keyword, and the engine links every mention of that phrase across the site. Keywords are typed one per Enter in the post editor's side box, on a category's edit screen, or in the grid row, up to a hundred per page, and a page can also carry a second list of phrases that must never be linked inside its own text.

**It starts in shadow mode.** The index is built from the linking screen, in batches of forty pages per request while you watch the bar, and the screen shows the counts, the statistics and every row of the index while not a single link reaches a visitor. The live setting stays disabled until the index holds at least one row, so switching on an empty engine cannot silently remove the links you have. You switch to live when you are satisfied, not before.

**What links, and what gets linked.** Pages that give links are the post types and taxonomies you tick in the settings, posts and pages by default, with categories as archives. Pages that receive links are any published post that carries a keyword, whatever its type, any ticked term archive, and custom targets: a URL outside WordPress with a title, keywords, and its own nofollow and new tab switches.

**Links are injected at render time from the index.** The page view is handed the anchors already known for that page and searches the delivered HTML for them, longest first; no folding, no scoring and no database lookup beyond one cached query of at most 200 rows. On the development site the heaviest page of about 300 costs about seven milliseconds. Links are never placed inside headings, existing links, code, pre, script, style, textarea, button, select, option, SVG, canvas, iframe, form or label, twenty tags in all, nor inside HTML comments, shortcodes or attributes. That protection lives in the tokeniser, not in a setting, so it cannot be switched off by accident.

**Matching rules.** Longest phrase first, with range locking so two keywords can never overlap, a word boundary required on both sides, and a page never linking to itself. A phrase is linked once per page, the Wikipedia rule, and a phrase you have linked by hand anywhere on a page is left to your link: the engine drops it as a candidate for that whole page, so a hand written link and a generated one never share an anchor. Both rules are always on. Caps per page, per target, per paragraph and on incoming links are there when you want them, and all of them are off on a fresh install; a page can also carry its own caps in the editor's side box. Folding of the zero width non joiner and of Arabic yeh and kaf is on by default on a Persian site and switchable on any other.

**What a generated link looks like.** A plain `<a href>` with the class `megop-auto-link`, no title, no tracking attribute. Custom targets can carry nofollow and open in a new tab; internal links carry neither.

**It also finds the links you wrote by hand years ago.** A manual link blocks the engine, so those phrases sit outside every report and never update when a destination moves. The Manual patterns screen scans your published pages for hand written internal links, groups them by anchor and target, filters out navigation anchors such as read more or view products with a stoplist of about 800 Persian, English and Arabic words you can extend, and shows whether each pattern already has a keyword, has none, or conflicts with a keyword owned by another page. One click converts a pattern: the keyword is added to the target and the manual tags are removed with their text kept. Bulk conversion runs in twelve second slices from a queue held in an option, so a closed tab loses at most the pattern in flight, and it never touches a blacklisted page, because a blacklisted page is usually full of deliberate hand written links.

**Reports and tools.** Anchor conflicts, where one phrase is registered on two targets, resolved in place by picking the right one. Pages with keywords that nothing links to. Dead rows left behind by the previous plugin's index. Link and anchor statistics, sortable, with every count a link to the rows behind it. An anchor manager that removes a keyword and its links everywhere with one click and a five minute undo. A blacklist screen with a type ahead picker. An admin bar item that shows the live link count and, during a rebuild, that the old index is still serving. Blacklisted pages keep receiving links and stop giving them out, which is deliberate: a landing page should hold its authority rather than spend it.

**Coming from Internal Link Juicer.** The migration wizard copies the keywords, the per page blacklists and the per page caps from posts, the keywords from categories, the custom links, and the settings, and leaves the source untouched. Going live re-runs the self test, and retiring the old plugin needs the capability to manage plugins, not merely to edit keywords.

**Nothing is lost when you leave.** While the engine is live, deactivating the plugin writes every link permanently into your content, under three guards: a twenty check self test has to pass, the tag balance has to be exactly right, and the plain text before and after has to be identical. A plugin update is detected and skipped, so the site is not rewritten twice for nothing. Reactivating takes the links back out and hands control to the engine again. A Bake manually button does the same on demand, Restore all reverses it, and uninstalling keeps the backups that make that reversible.

= Inside Search Console =

This is the part of the plugin that took the longest to build and is hardest to describe in one line, so here it is in the order you would meet it. It is one screen with six tabs: Connection and data, Keyword coverage, Writing engine, Effect report, Mismatches and gaps, Index status. A strip at the top of every tab shows the four steps, connection, coverage, writing and measurement, ticks each one off from live numbers, and a guided tour opens by itself the first time you open each tab and stays out of the way afterwards. The module is for administrators.

**Connecting.** You upload your own Google service account key as a file, and the browser checks that it holds a client email and a private key before it is even sent. There is no OAuth dance, no refresh token to revoke, no vendor server in the middle, and no account of ours anywhere in the path. The token is signed on your server with `openssl_sign`, the scope asked for is read only, and Test connection finds your property by itself, the domain property first. Pulling in Google's SDK for two endpoints would have been the lazier choice and a much larger attack surface.

**Two syncs, on purpose.** Google's data is fetched at two granularities, because one cannot serve both jobs. Query level, meaning date and page and query, is what the coverage engine reads; it is kept for ninety days only, because a single sixty day slice of one real site is 170,000 rows and sixteen months of that would be gigabytes of query text for a question nobody asks at that depth. Page level, meaning date and page, has no query text, so a whole year is a few tens of thousands of rows and any range from 7 days to 16 months is one sum away. History goes back 3, 6, 12 or 16 months as you choose, Google's newest day is always three days old because Google's own data lags, and the first sync runs in twelve second slices while the page is open, then continues once a day.

**Keyword coverage, and the yellow state.** The question this screen answers: Google is already showing this page for these searches, does the page actually contain them? The page's text for this purpose is its title, its body, and its Rank Math title and description, or a term's name and description. Green means the folded phrase is in that text, word for word. Red means at least one of its words is missing altogether. **Yellow means every word is present but never together**, which is an editing job rather than a writing one, and it is the state no other tool reports. Yoast, Rank Math and All in One SEO all answer a two state question, present or absent. Scattered is a different problem with a different fix, and it is usually the cheapest win on the page. Each page is scored on up to 300 of its queries, Persian and Latin digits are folded so the two spellings of a year are one query, a page is re-analysed only when its text changes, and every query gets an opportunity number: its impressions, halved for yellow, zero for green and zero when the page already averages better than position 3.5 or worse than 20, because the first needs no help and the second is a different problem. The queue is ranked by that number.

**Which page a search belongs to.** A plain word overlap ratio treats every word as equally telling, which on a site about one subject is close to useless: the subject word is on every page and carries no information at all. Three signals are combined instead, and all three are printed next to the score so you can see why a number came out as it did.

*Overlap*, weighted 45 percent, is rarity weighted against the page's own identity, its title, its headings and its slug. A word that appears on every page weighs almost nothing; a word on three pages weighs a lot.

*Share*, weighted 45 percent, is what Google already decided: this page's impressions for that query as a fraction of the whole site's impressions for it.

*Head*, weighted 10 percent, is whether the query's rarest word is in the page's title.

The threshold is yours, 30 percent by default. A brand new page with no impressions has no share and can score at most 55 percent, so it is handicapped but not shut out at the default; an established page gets Google's own verdict as almost half its score, and I would defend that weighting hardest of anything in this codebase.

**Two questions about the site rather than about one page.** *Mismatch* is Google showing page A for a search that plainly belongs to page B, judged on titles: the page Google lands the search on matches under a third of the query's words while another page matches at least seventy percent. Writing that phrase into A would put two of your own pages into the same auction, so the fix is to route it, not to write it, and routing here means the phrase is added to B's linking keywords, so from then on both the linker and the writer treat B as its owner. *Gap* is a search the whole site is shown for and nobody ranks: average position worse than 20 and no page's title close. That is not an editing job at all, it is a page that does not exist yet, and New draft creates one, titled with the query, as a draft that nothing publishes.

**Index status.** This asks Google, one URL at a time, what it knows about each of your pages, and groups the answers the way Search Console groups them, with fifteen of Google's coverage states translated. It does not request indexing, and it is important not to pretend otherwise: Google's Indexing API is documented as accepting JobPosting and BroadcastEvent markup, pointing it at ordinary articles is outside what it is for, and the account that does it is yours. The opposite question can be asked honestly with the same credentials, and it turns out to be the more useful half. A page quietly dropped from the index is invisible in every other report here, because it has no impressions to appear under, so the coverage screen never lists it and the writing engine never queues it.

Two details make this work at all, both learned painfully. WordPress builds percent escapes in lower case and Google's index matches them in upper case, so on a site with Persian, Arabic or Cyrillic slugs, sending `get_permalink()` straight through returns URL is unknown to Google for every single page, and the feature looks broken in a way that never points at its cause. And each call takes about twelve seconds from a shared host, measured rather than guessed, so the screen runs three lanes side by side while it is open, prints a time estimate from the measured pace, and counts the daily quota, 1,000 by default against Google's 2,000, because running into it silently would be worse than stopping short. On a normal run, pages already indexed, pages excluded by noindex and pages blocked in robots.txt are not asked again within the re-check interval; a full review asks everything again, oldest answer first. Once a day, at the hour the key was first saved, twenty pages are checked without anyone at a desk, and every newly published post is queued for its first check three days after publishing, which is when Google has usually had a look.

**Writing, with the site holding the decisions.** The site owns the decisions; the writer, whether a connected agent or a model over HTTP, owns only the prose. The queue is built here, every guardrail is enforced here when the text arrives, and if a submission breaks a rule the rule wins. The gates, with their defaults, every one a setting: only post types and taxonomies you ticked; never a blacklisted page, a frozen page, an unpublished page, or a page excluded from linking; at most one edit per page per rolling week and three per page in ninety days; at most five edits site wide per rolling day; a query has to clear a minimum of impressions and a page a minimum of opportunity; the intent fit has to clear your threshold; the page may not grow past 20 percent, with a small tolerance so a section is not rejected for one sentence; and only paragraphs, headings, emphasis, lists and tables may be inserted, no links and no images, whatever the model returns. And a query that belongs to another page according to the linker's keyword map is skipped, because feeding it here would set two of your own pages against each other. That last guard has no equivalent anywhere I know of: no other plugin declines to write a phrase on the grounds that the phrase belongs to a different page of yours. Every skipped page is counted with its reason on the screen, never silently dropped.

The new section goes in after the heading whose words best match the query and before the next heading, or at the end when no heading matches, never inside a list or a shortcode. It is applied to the published page at once, wrapped in a marker that is stored in the content and stripped at render, so the database knows which paragraphs came from the engine and the page source does not. Every edit keeps a full snapshot of the page as it was, so undo is exact rather than reconstructed, and undo restores the whole page to that snapshot, which is stated on the button.

**Rewriting rather than appending.** Bolting a new section onto the end of a page is the easy way and the worse one, but a full body rewrite would be the single most destructive thing this plugin could do, because one bad pass takes the shortcodes, the builder markup and the author's voice with it. So rewriting is paragraph level and address based. The writer is handed specific paragraphs, each with a short hash of its exact current text, and may return a replacement only for a hash it was given. The server finds that exact block and swaps it as plain text in a fresh paragraph. Anything it was not handed cannot be touched, if the page changed in between the hash no longer matches and the edit is refused, and the page may not lose more than a sixth of its words, because a rewrite that shrinks a page that much is a deletion. Titles and descriptions are a separate button, written into Rank Math's fields, capped at 55 and 155 characters, switchable per post type, with their own backup and their own undo.

**Two ways of not fooling yourself.** Set a control group percentage and, every time the queue is built, that share of it is drawn as a holdout by a deterministic rule, so rebuilding the queue never reshuffles who is in it. Whatever happens to those pages over the same weeks is what would have happened anyway: seasonality, a Google update, the site as a whole moving. Subtract it and what remains is the part the writing actually caused, and the effect report shows both sides and the net. Without a holdout, a good season reads as a triumph and a bad one as a disaster, and neither is true. The second is the freezer: an edit whose page then falls by at least a quarter, measured over at least seven days so it is not noise, takes that page out of the queue and flags it. The engine's own answer to a bad outcome is to stop touching that page, not to write more. Unfreezing is deliberately manual.

**Measuring, with two honesty rules.** Every edit is dated, and the page's clicks over the 28 days before that date are compared with what the page does afterwards. A window shorter than seven days is reported as waiting, never as a result, and a change within ten percent either way is reported as unchanged. Both sides are compared as per day rates, because the after window is almost always shorter and raw totals would flatter or punish an edit for no reason of its own. Page level asks did this page get better; query level asks did the exact words we wrote for actually move, with a before and after dot per query, which is far sharper and is the only view that can tell a real win from a page that happened to rise for unrelated reasons. Query level is limited to the query table's 95 day depth, which is a real cost and is stated rather than hidden. A site wide threshold watches the whole property rather than one page: when the site as a whole drops past it, the report says so, and that is the moment to stop and look.

**Writing without an operator.** The normal path is an agent connected through Novamira, which means your own machine has to be awake. There is a second path: the site calls a model over HTTP itself. It is deliberately provider agnostic, base URL and key and model, and anything speaking the OpenAI chat completions shape works. That is the point. Where the first party APIs are unreachable, you point it at whatever gateway you can actually use and nothing else in the plugin changes. With it filled in, the daily run can write a number of pages you choose, zero by default, and the chain is the same each day: sync, freeze check, twenty index checks, then writing. It rides on WP-Cron, so it runs when your site is visited, at the hour the key was first saved. Left blank, nothing here runs and the screen says so plainly rather than looking busy while doing nothing.

= Inside Link Nurse =

Link Nurse began as a separate plugin, and it still has the habits of one: it keeps its own settings, its own results and its own undo, and it stands down by itself if the old standalone plugin is still installed.

It scans every published item of every public post type, the Elementor header, footer and popup templates that carry display conditions, because a broken footer link repeats on every page, and every term of every public taxonomy. From each it reads the body, the excerpt or WooCommerce short description, the Elementor data when the page is built with Elementor, and the term description; links come from `href`, `src`, `data-src` and `poster` attributes, from block markup, and from Elementor's JSON, decoded so an escaped slash or a Persian file name comes out whole. It groups what it finds by cause, repairs it in batches of 25 rows per request, and keeps a backup so a repair can be undone.

Two of the three hardest problems it solves are invisible until you meet them. The first is that most internal links are not written as full URLs. When you pick a page from the editor's link dialog, what gets stored is `/product/name/`, and a scanner that only looks for `http` never sees it. The second is that a redirect that lands on an error page is not a redirect at all, it is a broken link wearing a 301, which is exactly the shape a deleted WooCommerce product leaves behind. Both are handled now, and both were found by a crawler rather than by the plugin, which is why the third problem matters.

The third is the sitemap. The sitemap is the one list of URLs a search engine reads before it reads anything else, and it was the one list Link Nurse could not see, because it walks content and a URL that appears only in the sitemap is linked from nowhere. A category whose last post was deleted, a product archive that no longer resolves: those are precisely the URLs Google is being handed on a plate. So the sitemap button does two things in order. It clears the cached sitemap of whichever generator you use, Rank Math, Yoast, SEOPress, All in One SEO or the WordPress core one, and then it follows the rebuilt file, up to sixty child sitemaps and five thousand addresses, and checks every address it lists. A sitemap is a promise that every address in it answers 200, so anything else is reported, a redirect included. On one real site the first run found nine dead category URLs and twenty one redirects that no content scan could have reached.

**How a link is checked.** Your own uploads are checked on disk, not over HTTP, which removes hundreds of self requests per scan that can trip a CDN's anti bot wall and lock real visitors out. An internal link that Rank Math already redirects is resolved from Rank Math's table, chains and all, and the destination is trusted only after the plugin verifies it exists. Everything else gets a HEAD request, then a GET when HEAD is refused, with a 150 millisecond pause before each external request, a chain of up to ten redirects followed, and a user agent that names the plugin and carries your site's address so the sites you link to can see who is asking.

**Reporting is conservative by design.** A 401, 403, 404 or 429 is re checked with a browser user agent before it is reported, because ScienceDirect, Healthline and several journal publishers answer a crawler with 403 and a person with the page, and a bot wall should not become a false alarm on your dashboard. By default only 404 and 410 count as broken, and you can add others. Links that do something rather than go somewhere, cart, wishlist, login, logout, nonce carrying links, are recognised by query key and never even listed, and neither are share buttons, feeds, admin URLs, stylesheets, scripts, fonts and preview links. A bare domain that redirects to its own home page is listed as ignored rather than fixed. And a finding on a post is only reported if the URL actually appears in the page a visitor receives, checked by fetching the rendered page with a browser user agent; if the page cannot be fetched at all, the finding is kept, because a failed check must never hide a real issue.

**Repairs keep your house style.** A broken link is unwrapped and its text kept; a broken image can be removed with its Elementor widget if you switch that on. A redirect is rewritten to its verified destination, in the form you wrote it: a link you wrote as `/path/` stays site relative, a link you wrote in full stays absolute. Elementor data is edited in place with a guard that stops `example.com` from being rewritten inside `example.com/v/...`, the Elementor stylesheet and the WP Rocket cache for that page are cleared, and a blacklisted domain is removed entirely, link and text in content, URL only in an Elementor widget, always through a before and after preview.

**Undo, by field.** Before the first repair to a page's body, excerpt, Elementor data or term description, the original is copied to that page's meta. Undo on any row restores that whole field, so every repair in the same field of the same page comes back with it, and Undo fixes restores everything at once. Backups survive a new scan and the Reset button.

The scan runs while your browser tab is open, four items and eight seconds per request by default, and it stops itself after four failed requests. Results export to XLSX with ten columns, the five newest reports are kept, and twenty four built in tests run against in memory data so you can confirm nothing regressed without touching your site; every one of those tests is a bug that actually happened.

= Inside Internal Analytics =

The question this module answers is narrow and worth stating plainly: what is happening on this site, without telling anyone else about it.

No data leaves your server and no third party is contacted. Here is exactly what is recorded, because a privacy claim that hides its own list is worthless. Per visit: the page landed on and the page left from, with URL and title, the referrer and its domain, the source type, the five UTM values, the browser, the operating system and the device type, the raw user agent when that setting is on, the IP address with its last octet removed when anonymisation is on, and for a logged in visitor the user id and display name. Per page view: the URL, the title, the time on page and the seconds of real engagement. Per event: its name, its label and a small piece of metadata. The visitor identifier is a hash salted with your site's own auth salt, so it cannot be brute forced back into an address; for a guest it is built from the session, the address and the browser, so it stays the same while those do. Anonymised IP and raw user agent are both switches, both on by default, and a tool clears every stored user agent in one click.

**The cookie, and the reason it is set where it is.** One cookie, `megop_sid`, holding a random session id for 30 days. It is set client side, in JavaScript, and never with `setcookie()` in PHP, because a `Set-Cookie` header on every page makes every response uncacheable behind Varnish or a CDN full page cache, and a plugin that silently turns off your caching in exchange for a session id is a bad trade. The endpoint is REST rather than admin-ajax for the same family of reasons: admin-ajax loads the full admin bootstrap, and a nonce embedded in cached HTML dies after twelve to twenty four hours behind a cache and takes the tracking with it, silently. Instead there is a same origin check, a 60 kilobyte body cap, 120 requests a minute per address, a per visit token issued on init, and a bot filter of thirty five user agent signatures with an empty user agent counted as a bot. Administrators are not tracked by default, and the list of untracked roles is yours.

**What counts as engagement.** Seconds accrue only while the tab is visible and the visitor moved, scrolled, typed or clicked within the last thirty seconds; a visit counts as engaged with two page views or ten engaged seconds, and the bounce rate is the rest. Someone is online for five minutes after their last activity. The numbers are settings.

**Events.** Out of the box: outbound clicks, file downloads, tel and mailto links, form start and submit, CTA clicks, copying, HTML5 video play and scroll depth at 25, 50, 75 and 90 percent, each switchable. Your own: a `data-svs-event` attribute on any element, a binding you define on the Events screen by picking a page and typing a CSS selector, or a call to `window.megopTrack(name, label, meta)` from your own script.

**Screens.** A dashboard with visits, unique visitors, page views, engagement and bounce rates, average time and engagement, search, social and direct visits, daily charts drawn by the plugin's own six kilobyte canvas renderer, top pages, referrers, devices, browsers, operating systems and events, and three CSV exports of the visits, page views and events for the range, with a byte order mark so Excel opens Persian text. Online users, refreshed every thirty seconds. Page analytics with a detail page per URL. Source analytics with referrer domains, full referrer URLs and UTM combinations. Events with the top fifty and the binding form. Who may see them is a setting, administrators by default, down to anyone who can edit posts.

**Retention.** Raw rows are kept for 365 days by default and a daily total for twice that, an hourly job clears the online table, a daily job prunes and rebuilds the last three days of totals, and the settings screen has one click tools to rebuild every daily total, run the retention clean up now, or reset every figure.

One bug from this module is worth repeating because it will bite any Persian site. On a site with a Jalali calendar plugin, `wp_date()` returns a Persian date, and a Persian date string in a SQL comparison silently matches nothing. The dashboard showed zeros on a site with 66,000 recorded visits, and every card and chart looked like a plugin that had never worked. Every date that reaches a query in this plugin is now built calendar neutral. Only what you read on screen is localised.

= Persian and Arabic =

Every screen reads in English, Persian or Arabic, and **each person chooses for themselves**. The choice is stored per user and applies only to this plugin. WordPress, your theme and every other plugin keep the language the site set, and the direction of the screen follows the dashboard, never the chosen language, so choosing English on a Persian site translates the words and leaves every control where it was. The JavaScript is translated too, so confirmations and live counters follow the same choice.

The linking engine folds the zero width non joiner to a space and Arabic yeh and kaf to their Persian forms, on by default on a Persian site and switchable on any other, and strips Arabic diacritics always; matching uses an offset map, so your content is read and never rewritten. Search Console coverage folds the same characters and, on top of them, Persian and Arabic Indic digits to Latin, because a page titled with 1405 and a search typed with ۱۴۰۵ are the same thing to a reader and to Google, and treating them as different characters was marking a third of one site's impressions as missing content that was already on the page word for word. The Search Console writer can also correct Arabic yeh and kaf in the text it writes, and use the zero width non joiner where the language calls for it, both as settings.

= What it writes to your database =

Twelve tables of its own, all prefixed `megop_`: one for the link index, five for Search Console, six for analytics. Keywords and per page settings in post and term meta. Options for settings, queues and state. Nothing is written to the content of a page except by the actions this page describes: a bulk edit or heading edit you saved, a manual link you removed from the grid, a link repair, a pattern conversion, a Search Console edit, and the permanent write of links when the engine is live and the plugin is deactivated. Every one of those except a bulk edit keeps a backup in meta, and a bulk edit is what you typed.

Uninstalling removes nothing by default, so deleting the plugin and installing it again finds everything where it was. One switch on the settings screen, off by default, removes every table, option, transient, keyword, per user preference and Search Console backup the plugin created when it is uninstalled. The analytics form has a switch of its own that removes only that module's tables. The one thing neither switch removes is the backups behind a permanent link write, because without them the links now living in your content could never be undone.

= External services =

This plugin contacts nothing unless you configure it to or press a button that says it will, and here is the whole list.

**Google Search Console API**, used only when you upload your own Google service account key on the Search Console screen. The plugin then sends your site's property name and date ranges to `www.googleapis.com` to read your search data, sends individual page URLs of your own site to `searchconsole.googleapis.com` to ask what Google decided about each one, and sends a signed token request to `oauth2.googleapis.com` to authenticate. The URL inspection runs when you press Check on the index status screen, and also unattended: twenty pages once a day after the sync, and each newly published post three days after publishing, both switchable. No visitor data and no page content is sent. Remove the key and the calls stop.
Google API Terms of Service: https://developers.google.com/terms
Google Privacy Policy: https://policies.google.com/privacy

**An AI endpoint of your choosing**, optional, and off unless you fill in a base URL, key and model name on the Search Console settings screen. When enabled, the plugin sends to the endpoint you named: for a new section, the page's title, its content, the site language and the queries to cover; for a paragraph rewrite, the paragraphs offered; for a title and description, the top queries with their numbers and the start of the page; and for a new draft, the search, up to eighteen related searches with their numbers, the site name, and the opening of up to three of your own published posts on the subject. Sections, rewrites and titles are applied to the published page at once and every one can be undone; a draft stays a draft. The plugin ships with no endpoint, no key and no default provider; whichever service you point it at, its own terms and privacy policy apply. Leave the fields blank and no request is ever made.

**The sites you link to**, when you press Scan in Link Nurse or follow the sitemap. Every external link found in your content is requested from the site it points to, with a user agent that names the plugin and carries your site's address, so a site owner can see who checked. Your own pages and your own sitemap are fetched too, to verify that a reported link really appears in the rendered page. Nothing about your visitors is sent; the request is the same one a browser would make to open the link.

No other outbound request is made by this plugin, and there is no telemetry of any kind.

= Author =

Megop SEO is written and maintained by [محمد قاسمی (Mohammad Ghasemi)](https://megop.org/) in Tehran, Iran.

It began as three separate plugins built for real Persian sites over several years, internal linking, broken link repair and analytics, which were merged once it became clear they were solving one problem badly in three places instead of one problem well in one. The Persian matching rules in it come from mistakes made on live sites, not from a specification.

* Web: [megop.org](https://megop.org/)
* About the author: [megop.org/founder-bio](https://megop.org/founder-bio/)
* Telegram: [@sanjesh1](https://t.me/sanjesh1)
* Email: jobshiveh@gmail.com
* Consulting: +98 912 643 4367

Bug reports and feature requests are welcome on the support forum here, in English or Persian. If you run this plugin on a site with real traffic and want a second pair of eyes on your internal linking, your Search Console coverage or a WooCommerce catalogue that needs editing at scale, the phone number above is for exactly that.

== Installation ==

1. Upload the plugin folder to `/wp-content/plugins/`, or install it through the Plugins screen.
2. Activate it. The linking engine starts in shadow mode, so nothing on your site changes yet.
3. Open Megop SEO. The bulk grid is the first screen; the settings screen shows the five module switches.
4. For automatic linking, add a keyword to a page from its editor or from the grid, open Seo Auto Link, press Full index rebuild, review what the engine would do, and switch the engine to live when you are satisfied.
5. For Search Console, open the Search Console screen, upload a Google service account key file, press Test connection, then Sync now.

For the SEO title and meta description columns in the bulk manager to appear in your page source, install Rank Math, because those are its fields.

The Search Console features are optional and need a Google service account of your own. The screen walks you through creating one and accepts the key file as an upload. Link Nurse's XLSX export needs PHP's ZipArchive extension, which nearly every host provides.

== Frequently Asked Questions ==

= Do I have to use all five modules? =

No. Each one can be switched off from the settings screen, and a module that is off registers no hooks at all: no menu entry, no route, no cost on any request. Switching a module off never touches its data. The settings screen and the switches belong to the plugin itself rather than to any module, so the screen is always there to switch a module back on.

= Do I need Rank Math? =

Only for the SEO title and meta description to reach your page source. Those two are Rank Math's own fields, and this plugin reads and writes them rather than inventing a second copy; without Rank Math the grid still saves them, but nothing prints them. The bulk manager's delete with redirect tool writes into Rank Math's redirect table when its redirections module is on and into `.htaccess` otherwise. Everything else works without it. If you use Yoast, SEOPress or All in One SEO instead, the sitemap tools detect those too.

= Will it conflict with my SEO plugin? =

It is built not to. It does not output meta tags and does not manage sitemaps of its own. It writes to Rank Math's redirect table only when you use the delete with redirect tool, one rule per deleted page, in the same shape Rank Math's own screen writes.

= Who can use these screens? =

The bulk grid, the settings, Link Nurse and Search Console are for administrators. Keywords for automatic linking can be opened to editors or authors with one setting, which only ever widens access. Who may open the analytics screens is a setting of its own, from administrators down to anyone who can edit posts.

= Which language will I see? =

Whichever you choose. English, Persian and Arabic are built in, the choice is stored per user, and it applies only to this plugin: WordPress, your theme and every other plugin keep the language the site set. The direction of the screen follows the dashboard, not the chosen language, so nothing jumps to the other side. The JavaScript is translated too.

= Does it work with Persian and Arabic text? =

That is what it was built for. In the linking engine, the zero width non joiner and a space are one character and Arabic yeh and kaf are folded to Persian, on by default on a Persian site and switchable elsewhere, and the folding uses an offset map so your content is read rather than rewritten. Search Console coverage folds the same characters plus Persian and Arabic Indic digits, so a year typed either way is one query. The linking engine does not fold digits, and that is deliberate: a keyword is a phrase you chose, and 1405 and ۱۴۰۵ are two spellings you can register.

= What does the bulk manager actually do that other bulk editors do not? =

Four things I have not found elsewhere together. The counter under the SEO title counts the finished title with Rank Math's variables resolved, not the template. Every row has its own find and replace with a live count before you commit. Prices, sale prices and variable products are edited in the same grid as titles, under a rule that only a changed price is written and an empty field is never a deletion. And deletion is two step and reversible, with a separate delete with redirect tool that records one rule per removed page.

= Is bulk editing safe on a live shop? =

It is built for one. A row is written only when you press Save on it, a replace shows its counts and asks before it runs, only a price you changed is sent, an empty price field never clears anything, and Elementor data is written alongside the body so what you edit is what renders. Six things do write at once, and the screen makes each of them a deliberate click: the status switch, the category dropdown, the parent dropdown, the featured image, a keyword chip and a heading edit.

= Which post types can I edit in the grid? =

Every public one the site has. The list is not hard coded: it is read from WordPress at load, minus attachments, so a custom post type registered by any plugin or theme appears on its own without anyone adding support for it. Posts, pages, WooCommerce products, a portfolio, a documentation type, whatever exists.

= Can I edit taxonomies too, including WooCommerce categories and brands? =

Yes, and the same way it reads post types: every public taxonomy, minus post formats. Categories, tags, product categories, product tags, brands, and any custom taxonomy. A term row edits the name, the SEO title and the meta description, with the same counters; the term description is reached through find and replace, and hierarchical taxonomies get a parent dropdown. A term row has no featured image, no status switch and no duplicator, because WordPress terms have none of those.

= Can I edit WooCommerce prices in bulk? =

Yes, the regular price and the sale price, in the same row as the title and the description, for every product kind including variable products. On a variable product the value you type is written to all of its variations and the row tells you how many. Two rules: only a price you changed is sent, so a title edit never touches a catalogue; and an empty field means leave this alone, never clear it. To end a sale, clear the sale price in the product editor.

= Can I type into the body of a post from the grid? =

No, on purpose. The body is in the grid so that find and replace and the headings tool can reach it, but a paragraph edited blind inside a one line box is how content gets destroyed. Open the post for prose. For a phrase, use the replacer.

= Can I move posts or products into a different category from the grid? =

Yes. Whenever the post type has a public taxonomy, the row carries a dropdown for it: category for posts, product category for products. Choosing one saves at once and replaces every term of that taxonomy with the one chosen, and the row warns you when a post had several. Moving forty products into the right category is forty clicks on one screen rather than forty page loads.

= Why is the category or parent dropdown locked on my site? =

Because on your site the change would change the URL: a permalink structure that contains `%category%`, a WooCommerce product base that contains `%product_cat%`, or a taxonomy whose URLs are hierarchical. Changing a URL throws away whatever authority the old one had earned, and that is a decision worth making deliberately in one place. The screen says so where the control would have been. On a site whose URLs do not depend on the category, the dropdown is live.

= Does the character counter show the length of the template or of the finished title? =

The finished title. A Rank Math title is usually something like `%title% %sep% %sitename%`, which is 24 characters as text and perhaps 61 once it renders. Counting the template tells you nothing, so the variables are resolved against that row first and the count is of the result. The variables it knows are `%title%`, `%seo_title%`, `%sitename%`, `%sitedesc%`, `%sep%`, `%currentyear%`, `%currentdate%`, `%currentmonth%`, `%currentday%` and `%id%`; any other variable counts as empty. An empty title field counts as zero, because the grid does not know which of Rank Math's default templates applies. Tick the preview box to see the resolved title under the field.

= Can I use Rank Math variables like %date% and %category% in the grid? =

Yes, and this build is the first in which they survive a save. WordPress's standard text sanitiser strips any `%` followed by two hexadecimal characters, so `%date%` came back as `te%` and `%category%` lost its first letters. The grid now cleans those two fields without touching percent signs.

= What does the live count in find and replace actually tell me? =

How many times your search term occurs in the fields you ticked, and how many of those are inside the body, before you have typed the replacement. Then a second line states exactly what is about to happen, with both strings and both counts, and a confirmation repeats them. Matching is literal and case sensitive, and every occurrence is replaced. You are not agreeing to something and finding out afterwards.

= Can I replace text in only some fields of a row? =

Yes. You tick the fields the replace should touch, per row: H1, title, description, content. A phrase can be corrected in the meta description without touching the body, or in one product without a site wide operation you then have to audit.

= Can I replace a phrase across many rows at once? =

Yes. Tick the rows and press Bulk replace in the floating bar. You are asked for the search term and the replacement, shown how many matches and how many rows are about to change, and the rows are saved together.

= Do I have to save the whole page? =

No. You save a row, that row saves, and that row reports what happened inline, in place, with no reload. Tick several and Save the selected rows saves them together. Rows with unsaved changes are marked with an orange bar and counted in the floating bar, so a row you edited and forgot is visible rather than lost, and leaving the page with unsaved rows asks first.

= Does a save rewrite the whole post? =

A save writes the H1, the body as it stands in the row, and both Rank Math fields, whether or not each changed. For an administrator the body goes in exactly as it was; a user without the unfiltered HTML capability gets the same filtering the block editor would apply. Backslashes survive, which was not true before this build.

= Can I change the featured image without opening the post? =

Yes. The WordPress media library opens over the row, you choose an image, and the thumbnail in the cell updates at once.

= Can I change the slug, the author or the date from the grid? =

No. The grid edits the fields a search engine reads and the fields a shop lives on; the slug, the author and the date stay in the editor, and the slug in particular is left alone for the same reason the category lock exists.

= Can I sort the grid by what Google actually shows? =

Yes, by clicks and by impressions, once you tick Show Search Console figures in the grid header and pick a range. The option is per user and off by default, so a colleague who does not need the numbers does not pay for them. Those columns are read from the Search Console tables the same plugin fills. That is the point of five modules sharing one plugin rather than five plugins sharing a menu.

= Why is the Search Console column missing from my grid? =

Either the Search Console module is off, or the column is: it is a per user toggle, Show Search Console figures, in the card at the top of the grid.

= Are WooCommerce variable products safe to edit here? =

Yes, and the rules that make them safe are worth knowing. Only a price field you changed is sent to the server, so editing a title or a description never rewrites the variations. An empty price field means leave this alone, never clear it. Writing an empty value to every variation of every product wipes a catalogue in one click, and the first version of this screen did exactly that on a live shop. That is why the rule exists rather than the default.

= Does the grid work on Elementor pages? =

Yes, for what it edits. A find and replace with the content box ticked is applied to the page's Elementor data as well as the body, and the headings tool reads and writes Elementor heading widgets; after either, the page's generated stylesheet is thrown away so Elementor rebuilds it. The replace never touches an Elementor key that holds an id, a URL or a link. A plain save of the H1 or the SEO fields writes only those fields, which is all a plain save should touch.

= What happens when I delete rows in bulk? =

Two steps and reversibly. Delete moves posts to the trash, where they can be restored; it records no redirect. Delete with a redirect, next to it in the floating bar, opens a dialog where you pick the destination by searching a page title or typing any URL and choose 301, 302 or 307; the rows are trashed and one rule is written per row from the address each page had before it moved. Terms have no trash in WordPress, so a term row offers Delete permanently with a confirmation that says what happens to its content and its children.

= Where do the redirects go? =

Into Rank Math's redirect table when Rank Math is installed and its redirections module is on; the dialog offers to switch the module on if it is not. Without Rank Math they are written as `Redirect` lines into `.htaccess` between marked comments, and the dialog says so, because rules that live in `.htaccess` are not visible from the dashboard.

= What is the duplicator for? =

Taking one row and producing many, applying a list of find and replace pairs to each copy. Each pair is a word being replaced and either one fixed replacement or one replacement per line, one page per line; the pairs can touch the H1, the title, the description, the content including Elementor data, and the automatic linking keywords. Every copy is a draft owned by you, with the same terms, featured image and meta, up to fifty per run. A product line with twelve near identical descriptions stops being twelve editing sessions.

= What does the headings tool do? =

It lists a page's H1 to H6 headings, from the Elementor heading widgets and from any heading in the body, and lets you edit their text inline with bold, italic, underline and a link. It will not add or remove a heading, on purpose, and it refuses to save a heading that changed on the server after you opened the panel.

= What does the sitemap refresh button do? =

It clears the cached sitemap of Rank Math, Yoast or All in One SEO, reads the index back over HTTP the way a crawler would, and then reads up to sixty child sitemaps inside a twenty second budget, so the files are rebuilt before a crawler asks. SEOPress and the WordPress core sitemap keep no cache, so for those it only reads the files back. Following the sitemap and checking every address in it is Link Nurse's own button.

= Are my column and tool choices saved? =

Per browser, yes: which columns show, which tool panels are open, whether previews are on. Language and the dark theme are the two choices stored per user.

= What is internal linking, and why does it matter? =

A link from one of your pages to another tells a search engine that the second page exists, that it is about the words in the link, and that you consider it worth reading. On a site of any size, most pages are discovered and understood through internal links rather than through the sitemap. The usual failure is not that a site has too few links, it is that the links it has were placed by hand once and never updated when the destinations moved.

= Where do I type keywords? =

In the post editor's side box, on a category's edit or add screen, or in the grid row when the linking panel is shown. One phrase per Enter, up to a hundred per page, and on the category screen a pasted comma separated list is split into chips. The side box also takes a second list: phrases that must never be linked inside that page's own text.

= Which pages give links, and which receive them? =

Giving: the post types and taxonomies ticked in the settings, posts and pages by default with categories as archives. Receiving: any published post that carries a keyword whatever its type, any ticked term archive, and custom targets. A keyword on a draft or a private post does nothing until it is published.

= Will the linking engine slow my site down? =

No. All the matching happens when the index is built, in the admin. A page view is handed the anchors already known for that page and searches the delivered HTML for them, longest first, with no folding and no scoring. On the development site the heaviest page of about 300 costs about seven milliseconds.

= Do links appear in feeds, widgets or content loaded by AJAX? =

No. Links are added to the main content of a page and to the description of a category archive, in the main loop only. Feeds, the REST API, admin screens, previews, the customizer and the Elementor editor are all left alone on purpose.

= Do I have to rebuild the index after editing keywords? =

Saving a post re-indexes that page at once. Changing a page's keywords, adding a custom link or resolving a conflict queues a full rebuild, which runs when you press Full index rebuild on the linking screen; until then the old index keeps serving, and the admin bar says so. Nothing rebuilds on its own in the background, because a rebuild of a large site is work you should see happen.

= Will it improve my rankings? =

Nobody can promise that and this plugin does not. What it can do is make your internal linking consistent, keep it consistent as the site changes, and show you which searches Google already associates with a page. What you do with that is editorial work.

= Does the plugin change my content? =

Not while the engine is live. Links are injected as the page renders and your stored content is untouched, which is why switching the engine off makes them vanish cleanly. Content is written only by the actions that say so: deactivating the plugin while the engine is live, the Bake manually button, converting a hand written link, removing a manual link from the grid, a bulk edit, a link repair, and a Search Console edit.

= What happens if I delete the plugin entirely? =

Deactivating while the engine is live writes every engine link permanently into your content, so nothing disappears from your pages. WordPress always deactivates before it deletes, and the uninstaller repeats the write as a last line of defence if the first one did not finish. The backups that make that reversible are kept, whatever else you choose to remove.

= Does it work with content built by shortcodes or a page builder? =

Yes. Shortcodes are treated as forbidden ground, so a keyword inside one is never linked. On an Elementor page the engine links the rendered content as visitors see it, because it runs after Elementor has rendered the page; the index is built from the page's stored text.

= Can I stop the engine linking on particular pages? =

Two ways, and they mean different things. A tick in the editor excludes that one page. The blacklist screen excludes a list of them, with a type ahead picker. Either way the page keeps receiving links and stops giving them out, which is deliberate: a landing page should hold its authority rather than spend it.

= Can I control how many links appear? =

Yes, per page, per target, per paragraph and incoming per target, in the settings, and per page in the editor's side box. All four are off on a fresh install. The right number is whatever your content needs, not a figure someone picked for a blog post in 2015. If you imported settings from Internal Link Juicer, its per page and per target caps come across switched on, because that is how that plugin ran.

= What stops it linking a phrase inside a heading or another link? =

The tokeniser, not a setting. Before matching runs, the HTML is split into the ranges where text may be linked, and twenty tag types are excluded from that set: headings, existing links, code, pre, script, style, textarea, button, select, option, SVG, canvas, iframe, form and label, plus HTML comments, shortcodes and everything inside an attribute. Because the protection is structural rather than optional, no combination of settings can switch it off.

= Will a keyword match inside a longer word? =

No. A word boundary is required on both sides, and the zero width non joiner counts as a boundary, so a phrase matches at the edge of a Persian compound but never inside one.

= Can one phrase point at two pages? =

Not at once. The last page registered wins silently in the map, and the Anchor conflict screen lists every phrase on two or more targets so you can pick the right one; the others lose it and a rebuild is queued.

= In what order are keywords applied? =

Longest phrase first by default, so a longer phrase is never split by a shorter one inside it. First in the text, as entered, and random are the other orders, and whichever you pick, overlapping ranges are refused and each phrase is linked once per page.

= I have hand written internal links from years ago. What happens to them? =

They keep working, and they block the engine: a phrase you linked by hand on a page is dropped as a candidate for that whole page, so the two never share an anchor. That means those phrases sit outside every report and never update when a destination moves. The Manual patterns screen lists them, tells you whether each one agrees with your keyword map, and converts them one at a time or in bulk so they come under the same management as everything else.

= Why is a "read more" link never offered as a pattern? =

Because call to action anchors are navigation, not content. A stoplist of about 800 Persian, English and Arabic words filters them: an anchor is dropped when every one of its words is on the list and at least one of them signals an action, so "exam consulting" stays and "view products" goes. You can add words in the settings and remove a default with a leading minus.

= Can I stop a bulk conversion, and is it undoable? =

You can stop it, and everything already converted stays. Each converted page keeps its original content in its backup meta, but there is no one click that restores a conversion, and the confirmation says so rather than promising an undo.

= Can a link end up nested inside another link? =

No, and not because of a setting. The engine cannot see text inside an `<a>` tag at all, and a phrase you linked by hand is withdrawn from the whole page, so the situation cannot arise. On a real site this was verified both ways: on pages that had a manual link the engine had placed none, and on the others the reverse.

= Can I link a phrase to an address outside WordPress? =

Yes. The Custom links screen takes a title, an address, nofollow and open in a new tab switches, and keywords, and the engine treats it as a target like any page.

= Can I remove one keyword everywhere without opening pages? =

Yes. On the statistics card, the keyword count of a page opens a drawer of its keywords with live incoming link counts and a cross on each; the removal is confirmed in place and can be undone for five minutes. The anchor statistics table also takes ticks and removes the selected pairs together.

= I have hundreds of posts. Do I have to type keywords one at a time? =

If you are coming from Internal Link Juicer, the migration wizard copies keywords, per page blacklists and caps from posts, keywords from categories, custom links and settings in one step, and leaves the source untouched. Otherwise, yes, keywords are typed per page, one phrase per Enter, and the grid's linking panel is the fastest place to do it for many pages at once.

= Does it work with translation plugins? =

The keyword map is global: it does not know about WPML or Polylang languages. A keyword is a phrase, so a Persian keyword only ever matches Persian text and an English one English text, which keeps the two apart in practice, but a phrase shared between languages would link across them. It does not translate keywords for you.

= Will my links disappear if I deactivate the plugin? =

No, while the engine is live. Deactivation writes every engine link permanently into your content, and reactivation takes them back out and hands control to the engine again. Before that write happens a twenty check self test has to pass, the tag balance has to be exactly right, and the plain text before and after has to be identical; a plugin update is detected and skipped so the site is not rewritten twice. In shadow mode there are no live links, so nothing is written.

= What happens to pages I blacklist? =

They keep receiving links and stop giving them out. That is the intended behaviour rather than a limitation: a landing page should hold the authority it receives rather than spend it. Because a blacklisted page is often full of deliberate hand written links, the pattern converter never touches its content and the Search Console writer never queues it.

= Can I see what it would do before it does anything? =

That is the default. The linking engine starts in shadow mode, where the whole index is built and every link it would place is counted, listed in the link details, and served to nobody. Link repairs on a blacklisted domain show a before and after preview, a replace states its counts before it runs, and the writing engine keeps a snapshot of every edit.

= Do I need a Google account for the Search Console features? =

You need a Google service account of your own, which is free, and the screen walks you through creating one. You upload the key file and nothing else; the browser checks the file before it is sent. There is no OAuth flow, no token to refresh, no account of ours in the path, and no server of ours ever sees your data. The scope requested is read only.

= Why a service account instead of signing in with Google? =

Because sign in flows need a registered application and a redirect back through somebody's server, and that somebody would have been me. A service account keeps the credential on your own site, signed locally with `openssl_sign`, and removable by deleting one option.

= How long does the first sync take, and do I have to keep the page open? =

The first sync walks back through your history in 120 day windows, 5,000 rows per call, in twelve second slices while the page is open, so on a large site keep the tab open until the tiles stop saying not complete. After that, a daily run tops up the newest days by itself. Google's newest day is always three days old, because that is how far Google's own data lags.

= What is an orphan URL on the sync tiles? =

An address Google reported that the plugin could not match to any post or term of yours: a feed, a paginated archive, a deleted page, a URL with a query string that maps nowhere. Its data is kept but it cannot be written for, because there is no page to write.

= What is the yellow state in keyword coverage? =

Green means the search phrase itself is on the page, word for word after folding. Red means at least one of its words is missing. Yellow means every word of it is present but never together, which is a different problem with a different fix and is usually the cheapest win on the page. Yoast, Rank Math and All in One SEO all answer a two state question, present or absent. Scattered is the state that tells you an edit rather than an article is needed. The page's text for this purpose includes its Rank Math title and description, so a phrase that lives only in the meta description counts as present.

= What is opportunity? =

The number the queue is ranked by. For a red query it is the query's impressions; for a yellow one, half of them; for a green one, zero. It is also zero when the page already averages better than position 3.5 for that query, because it needs no help, or worse than 20, because that is a different problem. The Minimum opportunity setting is the floor a page has to clear to be queued.

= How does it decide which page a search belongs to? =

Three signals, each printed next to the score. Overlap, 45 percent of the score, is a rarity weighted match against the page's own title, headings and slug, so a word that appears on every page of your site weighs almost nothing. Share, 45 percent, is your page's impressions for that query as a fraction of the whole site's impressions for it. Head, 10 percent, is whether the query's rarest word is in the title. The threshold is 30 percent by default, and a fit above 60 shows green, above 35 yellow, below that red.

= What is the share signal, and why does it matter so much? =

Share is what Google already decided. If Google is giving most of that query's impressions to this page, that verdict is almost half the score, which is more than any wording heuristic of mine gets. A brand new page with no impressions has no share and can score at most 55 percent, so it is handicapped but not shut out at the default threshold; set the threshold above 55 and new pages can never qualify, which is a fair reading of that setting. Every part of the score is shown, so you set your threshold by looking rather than by trusting.

= What is a mismatch, and what is a gap? =

A mismatch is Google showing page A for a search that plainly belongs to page B: A's title matches under a third of the query's words and B's title matches at least seventy percent. Writing that phrase into A would put two of your own pages into the same auction, so the fix is to route it, not to write the text. A gap is a search the whole site is shown for and nobody ranks, average position worse than 20 with no page's title close. That is not an editing job at all, it is a page that does not exist yet.

= What does routing a phrase actually change? =

It adds the phrase to the chosen page's automatic linking keywords. From then on the linking engine links that phrase to that page, and the writing engine treats that page as its owner and refuses to write it anywhere else. One decision, two modules.

= What happens when I press New draft on a gap? =

A draft post is created, titled with the search, and the phrase is routed to it. If an AI endpoint is configured the draft's body is written at once from the search, its neighbouring searches and what your site already says on the subject; otherwise it waits on the drafts list until you press Content generation. Nothing is ever published by the plugin.

= Can it ask Google to index a page? =

No, and any plugin that says it can is describing something else. Google's Indexing API is documented as accepting JobPosting and BroadcastEvent markup, the button for anything else lives inside Search Console itself, and the account making the request would be yours. What this does instead is ask the opposite question, what Google decided about each URL, which turns out to be the more useful half.

= Why does an index check take so long, and why must the page stay open? =

Each call to Google's inspection endpoint takes about twelve seconds from a shared host; the plugin measures its own pace rather than guessing and prints a time estimate from it. The screen runs three lanes side by side while it is open, because the calls, not the quota, decide how long a full review takes; close the tab and the lanes stop, and the daily run picks up twenty pages a day on its own. A short lease per page stops two lanes spending quota on the same URL.

= Which pages are checked automatically, and why are my categories never checked? =

The daily run checks twenty published posts of the ticked types, in this order: posts whose first check has come due, three days after publishing by default; then pages Google declined, oldest answer first, because that is the answer most likely to have changed; then pages never checked. Pages Google indexed are asked again only after the re-check interval, thirty days by default. Term archives are never queued automatically, because their status changes so rarely that spending quota on them would be waste.

= Does a failed index check count against the quota? =

Yes, because Google counted it. The plugin's own cap is 1,000 a day by default against Google's 2,000, and it stops before a call it cannot finish.

= What is Mismatched canonical on the index status screen? =

A page where Google chose a different canonical address from the one your page declares. The row shows both, and the filter lists only those pages, because that is a signal that Google is indexing a different URL than you think.

= My index status said every page was unknown to Google. Why? =

That was a real bug and it is fixed, and it is worth knowing about because it will affect any non Latin site. WordPress builds percent escapes in lower case and Google's index matches them in upper case. On a site with Persian, Arabic or Cyrillic slugs, sending the permalink straight through returns URL is unknown to Google for every single page, and nothing about the failure points at its cause.

= Can the writing engine damage my content? =

The guardrails are on the site's side, not the writer's, and they are enforced when the submission arrives rather than requested in advance. Only post types and taxonomies you ticked. Never a blacklisted, frozen, unpublished or linker excluded page. At most one edit per page per rolling week and three per page in ninety days, five site wide per rolling day, all settings. The page may not grow past 20 percent. Only paragraphs, headings, emphasis, lists and tables may go in, never a link or an image. Every edit keeps a full snapshot, so undo is exact.

= Where does the new text go? =

After the heading whose words best match the first query, just before the next heading; at the end of the page when no heading matches; never inside a list or a shortcode. It is wrapped in a marker stored in the content and stripped at render, so the database knows which paragraphs the engine wrote and the page source does not.

= If I undo an edit, do I lose changes I made to the page afterwards? =

Yes. Undo restores the page to the snapshot taken before that edit, whole, which is what makes it exact. Undo the newest edit first, and if you have edited a page by hand since the engine did, expect that hand edit to go with it.

= How can it rewrite a paragraph without wrecking the page? =

By address rather than by instruction. The writer is handed specific paragraphs, each with a short hash of its exact current text, and may return a replacement only for a hash it was given. The server finds that exact block and swaps it as plain text in a fresh paragraph, so bold, italics and links inside a rewritten paragraph do not survive the rewrite. Anything it was not handed cannot be touched, and if the page changed in between, the hash no longer matches and the edit is refused rather than applied to the wrong place. The page may not lose more than a sixth of its words. A full body rewrite would be the single most destructive thing this plugin could do, so it does not offer one.

= Does the title and description button need Rank Math? =

It writes Rank Math's title and description fields, capped at 55 and 155 characters, and keeps the previous pair as a backup with its own undo. Without Rank Math the fields are written and nothing prints them.

= Why does it refuse to write a phrase that would help the page? =

Because the phrase belongs to another of your pages. The writing engine checks the linker's keyword map, and a query owned by a different page is skipped rather than written, because feeding it here would set two of your own pages against each other in the same result. The skip is counted and named on the screen. No other plugin I know of declines an edit on those grounds.

= My page was skipped. What do the reasons mean? =

Each skip is counted with its cause on the writing screen: blacklisted, frozen, not published, a type you have not ticked, excluded from linking, the page lifetime cap, the weekly cap, the daily cap, no paragraph long enough to rewrite, a malformed model reply, a query that belongs to another page, or a fit below your threshold. Change the setting the reason names, or wait for the window the cap counts over to slide.

= What is the control group for? =

For telling a real improvement from a good season. Set a percentage and every time the queue is built that share of it is held out by a deterministic rule, so a rebuild never reshuffles who is in it. Whatever happens to those pages over the same weeks is what would have happened anyway. Subtract it and what remains is the part the writing actually caused, and the effect report shows written, held out and the net. Without a holdout, a good month reads as a triumph and a bad one as a disaster, and neither is true.

= What is the freezer? =

An edit whose page then falls by at least a quarter, measured over at least seven days so it is not noise, takes that page out of the queue and lists it as frozen. The engine's answer to a bad outcome is to stop touching that page rather than to write more. Unfreezing is deliberately manual, one button per page, so nothing quietly resumes.

= Why does the effect report say waiting instead of giving me a number? =

Because the window is shorter than seven days, and a number from four days is a coin toss dressed as a result. Both sides are compared as per day rates rather than as totals, because the after window is almost always shorter and raw totals would flatter or punish an edit for no reason of its own, and a change within ten percent either way is reported as unchanged.

= What does the site wide warning threshold do? =

It puts a warning on the effect report when the whole property has dropped past that percentage over the last four weeks against the four before. It does not stop the engine; it tells you that this is the moment to look before anything else is written.

= When does the daily run happen? =

Once a day, on WP-Cron, at the hour the key was first saved. WP-Cron runs when your site is visited, so on a quiet site it can slip; a real cron job that calls `wp-cron.php` makes it exact. The chain is sync, freeze check, twenty index checks, then writing if an endpoint is configured.

= Do I need an AI provider? =

No. The writing engine's normal path is an agent connected through Novamira, and the AI endpoint is a separate optional path for running unattended. It ships with no endpoint, no key and no default provider. Leave the fields blank and no request is ever made, and the screen says so plainly rather than looking busy.

= Which AI providers are supported? =

Anything that speaks the OpenAI chat completions shape, configured with a base URL, a key and a model name, with a Test model button that sends one word and shows the reply. That is deliberate rather than lazy: where the first party APIs are unreachable, you point it at whatever gateway you can actually use, and nothing else in the plugin changes.

= Where is my AI key stored? =

In the plugin's settings option, as typed, and it is shown back in the field so you can check it. Treat it the way you treat any key in a WordPress option: a database backup contains it.

= Which PHP extensions does Search Console need? =

OpenSSL, to sign the token, and multibyte string support, which every current host provides. Link Nurse's XLSX export needs ZipArchive.

= Do I have to keep the browser tab open while Link Nurse scans? =

Yes. The scan runs one batch at a time while the tab is open, four items and eight seconds per request by default, and stops itself after four failed requests. There is no background scan on purpose: a scan makes requests to other people's sites, and it should happen when someone is watching it.

= I pressed Stop, then Scan. Why did it start from zero? =

Because Scan always starts a fresh scan and clears the previous results. Stop leaves the partial results on screen; Scan replaces them.

= Which links does Link Nurse deliberately never list? =

Links that do something rather than go somewhere, recognised by query key: cart, wishlist, login, logout, nonce carrying links and their kin. Share buttons for Facebook, X, LinkedIn, Telegram, WhatsApp, Pinterest and Reddit. Feeds, the REST API, admin URLs, the comment form, stylesheets, scripts, fonts, preview and moderation links. Internal filter and pagination links when that setting is on. And anything to a domain on your exclude list.

= Why does Link Nurse report a link my browser opens fine? =

Usually because the site on the other end blocks robots. ScienceDirect, Healthline and several journal publishers answer a crawler with 403 while answering a person with the page. A 401, 403, 404 or 429 is re checked with a browser user agent before it is reported, and by default only 404 and 410 count as broken, so a bot wall does not become a false alarm. You can add other codes in the settings if you want them.

= Why is a link with an expired certificate not reported as broken? =

Because the check does not verify certificates. A status code is the question being asked, and a certificate error would hide the answer behind a different problem; a link on an expired certificate answers 200 and is listed as fine.

= What do the sites I link to see? =

One HEAD request, then a GET if HEAD is refused, from a user agent that names the plugin and carries your site's address, with a pause before each external request. Nothing about your visitors.

= My internal links are written as /page-name/ rather than full URLs. Are they checked? =

Yes. They are resolved against the page they sit on, exactly as a browser resolves them, and checked like any other link. That includes protocol relative links such as `//example.com/x`. Fragments and non http targets such as `mailto:` and `tel:` are recognised and skipped, because none of them has a status code to check. When a repair runs, a site relative link stays site relative and an absolute one stays absolute.

= Why is a redirect shown as Manual instead of being fixed? =

Because reporting a redirect and rewriting it are two different permissions, and only the second one is optional. Link Nurse always tells you a link is redirected. Whether it rewrites the link for you is decided by the Rewrite 301/302 setting, and while that is off the row is listed as Manual with its destination shown. A row is also Manual when the destination could not be established: a chain still redirecting after ten hops, a loop, or a hop the plugin could not reach. A redirect that ends on an error page is neither Manual nor a redirect: it is a broken link wearing a 301, reported as broken with both codes.

= What does Ignored mean? =

A bare domain that redirects to a page of its own home, `aparat.com` to `aparat.com/home` for example, when the homepage setting is on. Rewriting it would change nothing a reader notices, so it is listed and left alone.

= What happens to an Elementor button whose domain I blacklisted? =

The URL is removed and the button and its text stay, because restructuring Elementor's JSON to remove a widget is how pages break. In ordinary content the whole link is removed, text included. Every blacklist repair goes through a before and after preview first.

= Can I undo a repair? =

Yes. Before the first repair to a page's body, excerpt, Elementor data or term description, the original of that field is copied to the page's meta. Undo on a row restores that whole field, so every repair made in the same field of the same page comes back with it, and Undo fixes restores everything at once. The backups survive a new scan and the Reset button.

= Why does Link Nurse only report a link if it appears on the rendered page? =

Because a link in stored content that never reaches a visitor is not a problem worth your attention, and Elementor sites are full of them: `post_content` on an Elementor page is a generated cache that can hold links the page has not shown for months. Before a finding on a post is reported, the page a visitor would receive is fetched with a browser user agent and checked for that URL. If the page cannot be fetched at all, the finding is kept rather than dropped, because a failed check must never hide a real issue. The check is a setting, on by default, and it does not apply to term archives.

= Where is the XLSX report saved, and who can download it? =

In `uploads/megop-link-nurse/`, under a name with a random twelve character suffix, with the five newest reports kept. Like anything in uploads, anyone who has the exact link can open it, which is why the name is not guessable.

= Does the analytics module set a cookie? =

Yes, one: `megop_sid`, a random session id, for 30 days, set client side in JavaScript. PHP never sets one, because a `Set-Cookie` header on every page makes every response uncacheable behind Varnish or a CDN full page cache, and a plugin that silently turns off your caching in exchange for a session id is a bad trade. Whether your jurisdiction wants a banner for a first party analytics cookie is a question for your lawyer, not for a readme.

= Does analytics store IP addresses? =

With anonymisation on, which is the default, the address is stored with its last octet removed for IPv4 and its last 80 bits removed for IPv6, on the visit row and the online row, and that truncated form is shown on the Online users screen. With anonymisation off, the full address is stored. Behind Cloudflare or a proxy, switch on Trust proxy headers or every visitor will share the proxy's address.

= Does analytics record who a logged in visitor is? =

Yes, the user id and the display name, and the Online users screen shows the name. Administrators are not tracked at all by default, and the untracked roles are a setting.

= Does it honour Do Not Track? =

No. Nothing is sent anywhere and no advertiser is involved, but the browser signal is not read, and I would rather say so than let a privacy page imply otherwise.

= Does the tracking still work behind a full page cache? =

Yes, and that is why it is a REST endpoint rather than admin-ajax. A nonce embedded in cached HTML dies after twelve to twenty four hours behind a cache and takes the tracking with it, silently. Instead there is a same origin check, a body size cap, a rate limit per address, a per visit token issued on init, and bot filtering.

= What counts as an engaged visit, and how is the bounce rate calculated? =

Seconds count as engaged only while the tab is visible and the visitor has moved, scrolled, typed or clicked within the idle threshold, thirty seconds by default. A visit is engaged with two or more page views or ten or more engaged seconds. The bounce rate is one hundred minus the engagement rate.

= How do I track a click on one specific button? =

Three ways. Put a `data-svs-event` attribute, and optionally `data-svs-label`, on the element. Or define a binding on the Events screen: pick a page, type a CSS selector, and every click inside anything matching it is recorded. Or call `window.megopTrack(name, label, meta)` from your own script. Bindings can be deleted but not edited or paused.

= Why do today's numbers differ from what the chart showed yesterday? =

The charts read the daily totals, which are rebuilt for the last three days each night, while today and any missing day are computed live. The cards and tables always read the raw rows. A day settles once the rollup has run over it.

= Can I export the analytics? =

Three CSV files from the dashboard, for the range you are viewing: visits, page views and events, with a byte order mark so Excel opens Persian text. The IP address and the user agent are deliberately left out of the export.

= Does it send my content or my visitors anywhere? =

No. Analytics data stays in your database. Search Console sends your property name, date ranges and your own page URLs to Google, only when you have uploaded a key. The optional AI endpoint sends page text only if you configure it. Link Nurse requests the links in your content from the sites they point to when you press Scan. There is no telemetry of any kind.

= Why is date handling mentioned so often? =

Because it broke this plugin once, on a real site, in a way that looked like the plugin had simply never worked. On a site with a Jalali calendar plugin, `wp_date()` returns a Persian date, and a Persian date string in a SQL comparison silently matches nothing. Every card read zero on a site with 66,000 recorded visits. Every date that reaches a query is now built calendar neutral, and only what you read on screen is localised.

= What does the plugin leave behind when I remove it? =

Everything, by default, so a reinstall finds your keywords, your history and your settings where they were. One switch on the settings screen removes every table, option, transient, keyword, per user preference and Search Console backup when the plugin is uninstalled, and the analytics form has a switch that removes only that module's tables. One thing is never removed: the backups that make a permanent link write reversible. Losing those would make the write one way, which would be the wrong kind of tidy.

== Screenshots ==

1. Seo Bulk Manager: every post type and taxonomy in one grid, with resolved character counters, per row find and replace, prices and the Search Console column.
2. Keyword coverage: which searches each page is shown for, in green, yellow and red, with the opportunity behind each.
3. The automatic linking overview in shadow mode, with the engine status, the permanence card and the statistics.
4. Link Nurse results, grouped by cause, with the destination of every redirect.
5. Index status, with the reasons Google gave for each page it did not index.
6. The effect report, with the control group card and the before and after dots per query.

== Changelog ==

= 8.3.0 =
* The whole codebase was read once more, file by file, against this readme, and four claims the page made were not true of the code. All four are fixed in this build.
* Fixed: prices typed in the bulk grid were never saved. The fields were drawn, the server was ready for them and the empty field rule was written and tested, but the browser sent only the title, the SEO fields and the body. A price now goes to the server only when you changed it, so a title edit never touches a catalogue, an empty field is left alone for every product kind, a variable product's value is written to all of its variations, and the row confirms what was written.
* Fixed: the Undo button on a Search Console title or description edit answered "No backup was found" for every one of them, because a meta edit keeps its backup under a different key from a content edit and the button only knew the content key. Undo now reaches both.
* Fixed: the control group was never actually drawn. Setting a percentage stored the number and the holdout was only ever formed by an operation no button called, so the effect report read "Control group (0)" whatever you set. The holdout is drawn every time the queue is built.
* Fixed: switching the linking module off made the settings screen fail to render, and because the endpoint that switches modules on lived inside that module, the module could not be switched back on from the screen. The switches and their endpoint belong to the plugin shell now, and the linking settings simply hide while the module is off.
* Fixed: every save from the bulk grid stripped backslashes from the title, the SEO fields and the body, because the values were unslashed once at the request boundary and once more by WordPress on the way in.
* Fixed: a Rank Math variable such as `%date%` or `%category%` typed into the grid was corrupted on save. WordPress's standard sanitiser removes any `%` followed by two hexadecimal characters, so `%date%` came back as `te%`. The two Rank Math fields are cleaned without touching percent signs now.
* Fixed: the whole body of a post was passed through the content filter on every grid save, even for an administrator, so an iframe or a form embedded in a page vanished when its H1 was edited. The body is filtered exactly as the block editor would filter it for the user saving it.
* The plugin wide delete on uninstall switch exists now, on the settings screen under the module switches, off by default. Before this build the option was read by the uninstaller and set by nothing. When on, every table this plugin created is dropped, including the eleven Search Console and analytics tables the old uninstaller left behind, and the analytics form's own switch removes only that module's tables.
* Fixed: the uninstaller's last chance write of the links died on a missing class when it was needed, because two of the files it depends on were never loaded there.
* The migration from Internal Link Juicer now copies category keywords and custom links as well as post keywords; both routines existed and neither was called.
* Fixed: keeping a category archive as the right target on the anchor conflict screen stripped the phrase from that archive instead, because a term reference is negative and the handler dropped the sign.
* Fixed: a purely numeric keyword such as 1405 lost its phrase when the post and term keyword maps were merged, because PHP renumbers integer keys on merge.
* Fixed: the keyword chips on the Custom links screen had no handler, so a keyword could not be typed for a custom link.
* Fixed: on the Add category screen, the keyword chips of the term just added survived the form reset, so the next term silently inherited them.
* Fixed: removing several anchors at once from the anchor statistics table threw before the confirmation appeared.
* Fixed: a per page outgoing cap set in the editor's side box was ignored unless the site wide cap was also on.
* Fixed: while another index build held the lock, the screen re-requested the batch in a tight loop; it waits five seconds now.
* Fixed: the two step confirmation on Delete permanently for a term was skipped, because the post handler armed the button first.
* Fixed: the success message after a delete with redirect printed the status code where the count belongs, so it read 5 rows moved to trash and 301 redirects recorded.
* Fixed: the character counter added two spaces around `%sep%`, so the usual template counted two characters long.
* The Minimum impressions setting in Search Console now does what its label says: a query below it is never written for. Before this build only the gaps screen read it.
* The "Correct Arabic yeh and kaf" setting in Search Console is applied to the text the engine writes, in sections, rewrites, titles and drafts. Before this build nothing read it.
* Two switches that no code read are gone from the linking settings, Respect existing links and Link each phrase once per page, and the screen says instead that both rules are always on, which they always were.
* A dead Links button and its toggle are gone from the grid; the linking panel that does that job is the Show automatic linking toggle. The open page button is labelled View rather than Impressions. The headings tool's description says H1 to H6, which is what it edits. The Link Nurse self test tooltip says 24 tests, which is how many there are. Several settings hints and tour steps in Search Console now describe what the code does rather than what an earlier design intended.
* The delete with redirect success message and the price note carry new text, and every new or changed string is translated into Persian and Arabic.
* Every table and column name that reaches a query comes from one small class, `Megop_SEO_Db`, which builds the name itself from a literal list and hands back an empty string for anything it does not recognise. The three table helpers in the linking, Search Console and analytics modules all delegate to it, and the CSV exporter's column list and date column pass through the same check before they are used.
* Fixed a query shape that was safe and indefensible at the same time. The index status screen built a WHERE clause with `$wpdb->prepare()` and then concatenated that prepared fragment into a string handed straight to `get_results()`. Each of the five cases is a whole statement now, prepared once, with its own bound values and a bound limit.
* The `phpcs:ignore` reasons are rewritten. There were 136 copies of one 300 character paragraph explaining why an interpolated table name was safe. That explanation lives in `Megop_SEO_Db` now, and each suppression carries one short reason that is true of the line it sits on. Plugin Check reports no errors and no warnings.
* The donate link is removed. It pointed at the site's front page rather than at a donation page.
* This readme was rewritten from that reading of the code. Every capability described on this page was checked against the file that implements it, and several claims that were more than the code did have been cut or corrected.

= 8.0.2 =
* Fixed, and this is the one that made the scanner look like it was working: Link Nurse could only see links written as full URLs. The extractor collected every href and src on the page and then discarded anything that did not begin with http, one line later. A link written as `/product/name/`, the form the editor produces when you pick a page from the link dialog, was gathered, thrown away, and never checked. On the site this plugin is developed against, 332 of the 1,373 links in published content were stored that way, across 188 of its 364 items: a quarter of every link and half of every page, invisible on every scan since the module was written. Ten of them were internal 301s that a crawler had listed and Link Nurse never had. Relative links are now resolved against the page they sit on, exactly as a browser resolves them, and checked like every other link.
* A repair now rewrites the link in the form the author wrote it. Both forms are kept for each finding: the absolute one, because that is what a status code belongs to, and the stored one, because that is what has to be found and replaced in the content. A redirect fixed on a site-relative link stays site-relative afterwards, because absolute against relative is a house style and a repair has no business changing it.
* Fragments (`#section`) and non-http targets (`mailto:`, `tel:`, `javascript:`) are recognised and skipped instead of being swept up. Protocol-relative links (`//example.com/x`) are resolved and checked.
* Fixed: the XLSX export always ended on a 404. The report was written into `uploads/megop-seo/` while the browser was sent to `uploads/megop-link-nurse/`, one folder apart, with the file correct on disk the whole time. Both sides read one constant now, and if the file is genuinely missing the screen says so instead of navigating away from the results.
* Report filenames carry a random suffix. A report holds the site's own link data, and a timestamp-only name in a public uploads folder is guessable.

= 8.0.1 =
* Fixed, and this is the one worth reading: Link Nurse could not see a single redirected link. Redirect handling sat behind a setting whose own label reads "Rewrite 301/302 to final 200 destination", a repair switch that had been wired as a detection gate. With it off, the scanner never resolved where a 301 went, the classifier had no destination to judge, and every redirected link on the site was recorded as healthy. A crawl of one real site listed twenty internal 301s inside published posts that Link Nurse had passed over on every previous scan. Detection now always runs, and the setting decides only whether the link is rewritten for you. A redirect found while rewriting is off is listed as Manual, with its destination and a note naming the checkbox that unlocks it.
* Fixed: a redirect that lands on an error page was reported as healthy. The old branch would only speak if the chain ended in 200, so a 301 into a deleted product, the exact shape WooCommerce leaves behind, fell through as though nothing were wrong. It is reported as broken now, with both codes, for example HTTP 301 to 404.
* Fixed: a redirect whose destination could not be established, whether a chain still redirecting after ten hops, a loop, or a follow request that failed, was treated as fine, and one fallback path assigned a 200 to a destination it had never reached. Those are reported as Manual with the reason stated, and no link is ever rewritten to a target the plugin did not verify.
* The results table shows the destination on every row that has one, not only on rows the plugin intends to rewrite.
* A finding is no longer dropped because the theme prints the link in a different form. Front-page verification now also matches on path and query, so a link rendered with a different scheme, without www., protocol-relative, or as a host-relative href is still recognised.
* Link Nurse's heading no longer carries a version number of its own. The unified header was still appending it; only the standalone fallback header had been corrected in 8.0.0.

= 8.0.0 =
* Fixed, and this is the one that mattered: the Link Nurse settings form never saved anything. Two separate faults, one on each side. In the browser, the whole settings object was handed to URLSearchParams, which stringified it, so PHP received the literal text "[object Object]" where it expected an array, fell back to defaults for every key and answered "Settings saved", which was true of nothing. And the module enqueued its own JavaScript with a frozen version constant instead of the file's modification time, so the corrected file sat on the server while every browser kept running the copy it had cached months earlier. The first fault made saving impossible; the second made fixing it invisible. Both are gone.
* Saving settings now says so. A green tick on success, a red line with the reason on failure, and the form is repainted from what the server actually stored rather than from what was typed, so if a value was clamped the field shows the stored one and the confirmation is telling the truth.
* Link Nurse can refresh the sitemap, and then follow it. Clearing a sitemap cache tells you nothing on its own, so the button does the second half too: every address the rebuilt sitemap lists is fetched with Link Nurse's own checker, and anything that does not answer 200 is reported. This reaches a class of problem no content scan can find: a URL listed for search engines but linked from nowhere on the site, such as a category whose last post was deleted. On one real site it found nine dead category URLs and twenty-one redirects the first time it ran. On by default, switchable in the scan settings.
* Fixed: opening the Link Nurse settings panel pushed the progress bar and every counter a screenful down the page, exactly while a scan was running and they were the thing worth watching. The progress bar sits above the panel now.
* Link Nurse no longer prints a version number of its own in its heading. It was a plugin once and it is a module now; the only version that means anything is the plugin's.
* Persian and Arabic catalogues cover the new strings, so the sitemap screens read in the site's language from the first run rather than after the next translation round.
* Deactivating the previous internal-linking plugin, offered at the end of the migration, now requires the capability to manage plugins. It was reachable by anyone the keyword-editor setting allowed, which an administrator can widen to author level, correct for editing keywords and far too wide for switching a plugin off.
* The bulk duplicator's find-and-replace pairs are sanitised before they reach a post title or body.
* Every remaining inline script tag is printed through WordPress rather than echoed by hand, including the tour's JSON data block, so nothing that filters, defers or adds a nonce to script output is bypassed.
* The uninstaller now also removes Link Nurse's own options. Its prefix differs from the plugin's by one letter, so the existing sweep never reached them.
* The plugin page has been rewritten with a full FAQ covering internal linking, bulk editing, broken links and the sitemap check.

= 7.5.0 =
* New: Link Nurse can refresh the sitemap, and then follow it. Clearing a sitemap cache tells you nothing on its own, so the button does the second half too: every address the rebuilt sitemap lists is fetched with Link Nurse's own checker, and anything that does not answer 200 is reported. This reaches a class of problem no content scan can find: a URL that is listed for search engines but linked from nowhere on the site, such as a category whose last post was deleted. On one real site this found nine dead category URLs and twenty-one redirects that every previous scan had passed over. It is on by default and can be switched off in the scan settings.
* Fixed: Link Nurse settings were never saved. The browser sent the whole settings object as one form value, so PHP received the text "[object Object]" where it expected an array, fell back to the defaults for every key, and answered "Settings saved", which was true of nothing. Values are now sent in PHP's own bracket notation and arrive as the array the sanitiser expects.
* Fixed: opening the Link Nurse settings panel pushed the progress bar and every counter a screenful down the page, exactly while a scan was running and they were the thing worth watching. The progress bar sits above the panel now.
* Deactivating the previous internal-linking plugin, offered at the end of the migration, now requires the capability to manage plugins. It was reachable by anyone the keyword-editor setting allowed, which can be widened to author level, correct for editing keywords and far too wide for switching a plugin off.
* The bulk duplicator's find-and-replace pairs are sanitised before they reach a post title or body.
* Two remaining inline script tags are printed through WordPress instead of being echoed by hand, so anything that filters or defers script output can see them.
* The uninstaller now also removes Link Nurse's own options. Its prefix differs from the plugin's by one letter, so the existing sweep never reached them.

= 7.4.3 =
* Fixed: a fatal "cannot redeclare" error on the automatic-linking screen. The link statistics template declared a helper function at file level, and PHP registers a function declared in an included file globally even when the include runs inside a method, so any request that rendered that template twice stopped the whole screen. The helper is a static method on the report class now, templates declare no functions at all, and the sort links lost a double escaping along the way: the URL is built once and escaped at the point of output.

= 7.4.2 =
* New: a Refresh sitemap button on the bulk manager header. Publishing a post clears the sitemap cache on its own; bulk work does not, and the bulk editor is exactly where a hundred rows change without anyone pressing Publish. The button clears whatever the active generator caches, whether Rank Math, Yoast SEO or All in One SEO, and then reads the sitemap back over HTTP the way a crawler would, the index and every child file, so they are already rebuilt before a crawler asks for them. It reports how many sitemaps were rebuilt and how many URLs they list. It can be switched off under Settings, and it is hidden on a site with no sitemap at all.
* Dark mode is a saved preference now, and it is offered only where it is real. It used to live in the address bar, so it lasted exactly one click: opening the next module dropped the parameter and the screen snapped back to light. It is stored per person now and it stays. The toggle is printed on the bulk manager alone, because the analytics and broken-link modules declare their palettes on :root, where no wrapper class can reach them, and a switch that darkens the frame while every card inside stays white is worse than no switch at all. Those two modules need a stylesheet rewrite of their own, and that is its own release.
* The header no longer turns round when you change language. Direction is taken from the dashboard, so reading these screens in English on a Persian site translates the words and leaves the logo and the controls exactly where they were.
* The settings screen carried a second row of tabs above the form. The Internal Analytics settings are shown underneath the linking settings now, on the one page, with one navigation instead of two.
* The plugin page now describes all five modules in full, states plainly which parts use Rank Math's fields and which do not, and names the author. Documentation only, and no code changed.

= 7.4.1 =
* Fixed: Link Nurse and Internal Analytics both registered their admin page under the same slug. WordPress runs every callback registered on a slug, so opening Internal Analytics also printed the whole Link Nurse screen above it. Link Nurse has its own slug now, and each screen prints only its own content.
* Plugin Check reports nothing at all, no errors and no warnings. Input from the bulk editor, the keyword fields and the Search Console screens is unslashed and sanitized at the point it is read, and every remaining notice is a direct query against one of this plugin's own tables that now carries a written reason a reviewer can check line by line.
* load_plugin_textdomain() is gone. WordPress has loaded the catalogue by itself since 4.6, and the per-user language switch is unaffected.

= 7.4.0 =
* Every string with a placeholder now carries a translators comment, so a translator can see what each one refers to before guessing.
* One message had its placeholders in a fixed order, which meant a translation that reordered them would swap the two numbers. Both are now positional.
* Output escaping reviewed at every point of output; inline scripts now go through wp_print_inline_script_tag().
* unlink(), is_writable() and parse_url() replaced with their WordPress equivalents.

= 7.3.0 =
* Every screen can now be read in English, Persian or Arabic, and each person chooses for themselves, because the choice is stored per user and applies only to this plugin.
* The JavaScript is translated too. Confirmations, live counters and inline messages follow the same choice as the rest of the screen.
* The Link Nurse module folded into the plugin's own text domain.
* The Analytics module is translatable for the first time; its screens were hard-coded in Persian.
* Catalogues rebuilt with the standard WordPress tooling, complete in Persian and Arabic.

= 7.2.0 =
* Index status: ask Google what it knows about each page, grouped by reason.
* A guided tour on every Search Console screen, switchable off per user.
* Every stylesheet rule now lives in one enqueued file.

= 7.1.0 =
* Search Console module: per-page keyword coverage, query-level effect measurement, control group and automatic freezing of pages that fall after an edit.
* Persian and Arabic-Indic digits are now folded during matching. On a mixed-script site this alone corrected about a third of the coverage verdicts.
* WooCommerce price is editable in the bulk manager, including variable products.
* Blacklists and whitelists are no longer cleared when settings are saved from an unrelated screen.

== Upgrade Notice ==

= 8.3.0 =
Prices in the bulk grid are saved now, they never were. Search Console meta undo works, the control group is really drawn, the settings screen survives switching the linking module off, and a grid save no longer strips backslashes or corrupts Rank Math variables. Plugin Check clean. No data changes.

= 8.0.2 =
Link Nurse only saw links written as full URLs; anything stored as /path/ was discarded unchecked. On one real site that was a quarter of every link and half of every page. Relative links are now resolved and checked, repairs keep the author's link style, and the XLSX export no longer 404s.

= 8.0.1 =
Link Nurse could not see redirected links at all: the rewrite setting had been wired as a detection gate. Redirects are always reported now, a redirect landing on a 404 counts as broken, and no link is rewritten to an unverified target. No data changes.

= 8.0.0 =
Link Nurse settings really save now. The previous fix could not reach the browser, because the module served a cached copy of its own JavaScript. Saving shows a green confirmation. Sitemap refresh added. No data changes.

= 7.5.0 =
Link Nurse can now refresh the sitemap and check every address in it. Two Link Nurse bugs are fixed: settings were never saved, and the progress bar jumped down the page when the settings panel opened. No data changes.

= 7.4.3 =
Fixes a fatal error on the automatic-linking screen when its statistics template was rendered twice in one request. No behaviour changes and no data changes.

= 7.4.2 =
New: a Refresh sitemap button on the bulk manager header, which clears the sitemap cache and rebuilds the files before a crawler asks. Dark mode is remembered per person. The header no longer flips when you change language. No data changes.

= 7.4.1 =
Fixes an admin-page slug collision that printed the Link Nurse screen on top of Internal Analytics, and clears every remaining Plugin Check notice. No behaviour changes elsewhere and no data changes.

= 7.4.0 =
Repository compliance pass: translator comments, escaping at the point of output and a written reason on every direct query. No behaviour changes and no data changes.
